diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 440af194b..0a83160bd 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1 @@ -# Update the VARIANT arg to pick an Elixir version: latest, 1.11.4, etc. -ARG VARIANT=latest - -FROM ghcr.io/processone/elixir:${VARIANT} +FROM ghcr.io/processone/devcontainer:latest diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1e05cfed4..c216cd0c0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,48 +1,7 @@ { "name": "ejabberd", - // "dockerComposeFile": "docker-compose.yml", - "build": { - "dockerfile": "Dockerfile", - "args": { - "VARIANT": "latest" // 1.11.4 - } - }, - "workspaceFolder": "/workspace", - - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.defaultProfile.linux": "/bin/zsh", - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": ["pgourlain.erlang", "jakebecker.elixir-ls"], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [5222, 5280, 5269], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "sh .devcontainer/post-create.sh", - - // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode", - "portsAttributes": { - "1883": { - "label": "MQTT" - }, - "5222": { - "label": "XMPP C2S" - }, - "5223": { - "label": "Legacy XMPP C2S" - }, - "5269": { - "label": "XMPP S2S" - }, - "5280": { - "label": "ejabberd HTTP" - }, - "5443": { - "label": "ejabberd HTTPS" - } - } + "build": {"dockerfile": "Dockerfile"}, + "extensions": ["erlang-ls.erlang-ls"], + "postCreateCommand": ".devcontainer/prepare-container.sh", + "remoteUser": "vscode" } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index d3d6ff4b2..000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -ejabberd: - image: ejabberd/ecs - ports: - - 5222:5222 - - 5223:5223 - - 5269:5269 - - 5280:5280 - - 1883:1883 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 index b825bcd1f..6abcb6de0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -43,4 +43,4 @@ Mnesia.nonode@nohost/ /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 index 9b0b529e5..d984ed09d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,8 +6,7 @@ assignees: '' --- -Before creating a ticket, please consider if this should fit the discussion forum better: -https://github.com/processone/ejabberd/discussions +Before creating a ticket, please consider if this should fit the [discussion forum](https://github.com/processone/ejabberd/discussions) better. ## Environment diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2bca321a2..0ac588c37 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,17 +7,20 @@ assignees: '' --- -Before creating a ticket, please consider if this should fit the discussion forum better: -https://github.com/processone/ejabberd/discussions +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 [...] + +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/container/Dockerfile b/.github/container/Dockerfile index a60446c32..1d238339a 100644 --- a/.github/container/Dockerfile +++ b/.github/container/Dockerfile @@ -1,55 +1,98 @@ -FROM alpine:3.15.4 AS build -ARG VERSION=master +#' 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' -RUN apk upgrade --update musl \ - && apk add \ - autoconf \ - automake \ - bash \ - build-base \ - curl \ - elixir \ - erlang-odbc \ - erlang-reltool \ - expat-dev \ - file \ - gd-dev \ - git \ - jpeg-dev \ - libpng-dev \ - libwebp-dev \ - linux-pam-dev \ - openssl \ - openssl-dev \ - sqlite-dev \ - yaml-dev \ - zlib-dev +################################################################################ +#' 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 -COPY . ./ejabberd - -WORKDIR ejabberd +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 -RUN cp -r _build/prod/rel/ejabberd/ /opt/ejabberd-$VERSION \ - && mkdir -p /opt/ejabberd \ - && mv /opt/ejabberd-$VERSION/conf /opt/ejabberd/conf +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 BINPATH=$(dirname $(find /opt -name msgs))/bin/ \ - && mkdir -p $BINPATH \ - && cp tools/captcha*.sh $BINPATH +RUN cp -p $BUILD_DIR/tools/captcha*.sh $HOME-$VERSION/lib -RUN [ ! -d .ejabberd-modules ] || cp -r .ejabberd-modules /opt/ejabberd/ +RUN find "$HOME-$VERSION/bin" -name 'ejabberd' -delete \ + && find "$HOME-$VERSION/releases" -name 'COOKIE' -delete -RUN export PEM=/opt/ejabberd/conf/server.pem \ - && curl -o "/opt/ejabberd/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' \ +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 \ @@ -57,63 +100,107 @@ RUN export PEM=/opt/ejabberd/conf/server.pem \ -keyout $PEM \ -out $PEM \ -days 3650 \ - -subj "/CN=localhost" \ - && sed -i '/^loglevel:/a \ \ - \nca_file: /opt/ejabberd/conf/cacert.pem \ - \ncertfiles: \ - \n - /opt/ejabberd/conf/server.pem' "/opt/ejabberd/conf/ejabberd.yml" + -subj "/CN=localhost" -FROM alpine:3.15.4 -ENV HOME=/opt/ejabberd -ARG VERSION=master +RUN sed -i 's|^#CTL_OVER_HTTP=|CTL_OVER_HTTP=../|' "$HOME/conf/ejabberdctl.cfg" -RUN apk upgrade --update musl \ - && apk add \ - expat \ - freetds \ - gd \ - jpeg \ - libgd \ - libpng \ - libstdc++ \ - libwebp \ - linux-pam \ - ncurses-libs \ - openssl \ - sqlite \ - sqlite-libs \ - unixodbc \ - yaml \ - zlib \ - && ln -fs /usr/lib/libtdsodbc.so.0 /usr/lib/libtdsodbc.so \ - && rm -rf /var/cache/apk/* +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 -COPY --from=build /opt /opt -RUN echo -e \ - "#!/bin/sh \ - \n[ -z \$ERLANG_NODE_ARG ] && export ERLANG_NODE_ARG=ejabberd@localhost \ - \nexport CONFIG_DIR=/opt/ejabberd/conf \ - \nexport LOGS_DIR=/opt/ejabberd/logs \ - \nexport SPOOL_DIR=/opt/ejabberd/database \ - \nexec /opt/ejabberd-$VERSION/bin/ejabberdctl \"\$@\"" > /usr/local/bin/ejabberdctl \ - && chmod +x /usr/local/bin/ejabberdctl +ARG UID +RUN chown -R $UID:$UID $HOME -RUN addgroup ejabberd -g 9000 \ - && adduser -s /bin/sh -D -G ejabberd ejabberd -u 9000 \ - && mkdir -p $HOME/conf $HOME/database $HOME/logs $HOME/upload \ - && chown -R ejabberd:ejabberd $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 /usr/local/bin/ejabberdctl status + CMD ejabberdctl status -WORKDIR $HOME -USER ejabberd -VOLUME ["$HOME/conf", "$HOME/database", "$HOME/logs", "$HOME/upload"] -EXPOSE 1883 4369-4399 5210 5222 5269 5280 5443 +WORKDIR /$HOME +USER $USER +VOLUME ["/$HOME"] +EXPOSE 1880 1883 4369-4399 5210 5222 5269 5280 5443 -ENTRYPOINT ["/usr/local/bin/ejabberdctl"] +ENTRYPOINT ["/sbin/tini","--","ejabberdctl"] CMD ["foreground"] 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 index de3826a46..b1f1d2179 100755 --- a/.github/container/ejabberdctl.template +++ b/.github/container/ejabberdctl.template @@ -15,10 +15,10 @@ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" # shellcheck disable=SC2034 ERTS_VSN="{{erts_vsn}}" ERL="{{erl}}" -IEX="{{bindir}}/iex" EPMD="{{epmd}}" -[ -z "$ERLANG_COOKIE" ] && ERL_OPTIONS="-setcookie $(cat "${SCRIPT_DIR%/*}/releases/COOKIE")" -[ -n "$ERLANG_COOKIE" ] && [ ! -f "$HOME"/.erlang.cookie ] && echo "$ERLANG_COOKIE" > "$HOME"/.erlang.cookie && chmod 400 "$HOME"/.erlang.cookie +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 @@ -60,7 +60,6 @@ done # define ejabberd variables if not already defined from the command line : "${CONFIG_DIR:="{{config_dir}}"}" : "${LOGS_DIR:="{{logs_dir}}"}" -: "${SPOOL_DIR:="{{spool_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 @@ -69,16 +68,24 @@ done [ -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" -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) + 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 @@ -90,11 +97,12 @@ 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="$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="\ +$(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" @@ -129,8 +137,8 @@ run_cmd() exec_cmd() { case $EXEC_CMD in - as_install_user) su -s /bin/sh -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; as_current_user) exec "$@" ;; + as_install_user) su -s /bin/sh -c 'exec "$0" "$@"' "$INSTALLUSER" -- "$@" ;; esac } run_erl() @@ -162,9 +170,11 @@ 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" @@ -185,8 +195,10 @@ 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:" @@ -197,6 +209,39 @@ livewarning() 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 "" @@ -225,16 +270,34 @@ help() } # dynamic node name helper -uid() -{ - uuid=$(uuidgen 2>/dev/null) - random=$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null) - [ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid) - [ -z "$uuid" ] && uuid=$(printf "%X" "${random:-$$}$(date +%M%S)") - uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/') - [ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}" - [ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}" - [ $# -eq 2 ] && echo "${uuid}-${1}@${2}" +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 @@ -248,6 +311,8 @@ stop_epmd() # 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 && { @@ -281,14 +346,32 @@ post_waiter_loop() LIST=$@ HEAD=${LIST%% ; *} TAIL=${LIST#* ; } - echo ":> ejabberdctl $HEAD" - $0 $HEAD + 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" @@ -299,14 +382,71 @@ wait_status() if [ $timeout -eq 0 ] ; then status="$1" else - run_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null + 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" @@ -315,6 +455,103 @@ cd "$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) @@ -342,24 +579,31 @@ case $1 in ;; etop) set_dist_client - exec_erl "$(uid top)" -hidden -node "$ERLANG_NODE" -s etop \ - -s erlang halt -output text + 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" --app ejabberd + 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 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ + -noinput -hidden \ + -eval 'net_kernel:connect_node('"'$PEER'"')' \ + -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ -s erlang halt -output text ;; started) @@ -370,18 +614,14 @@ case $1 in 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 - run_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" - result=$? - case $result in - 2|3) help;; - *) :;; - esac - exit $result + exec_other_command "$@" ;; esac diff --git a/.github/workflows/ci-19.3.yml b/.github/workflows/ci-19.3.yml deleted file mode 100644 index c65a26050..000000000 --- a/.github/workflows/ci-19.3.yml +++ /dev/null @@ -1,229 +0,0 @@ -name: CI (19.3) - -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: ['19.3'] - runs-on: ubuntu-18.04 - services: - redis: - image: redis - ports: - - 6379:6379 - - steps: - - - uses: actions/checkout@v3 - - - name: Get specific Erlang/OTP - uses: erlef/setup-beam@v1 - with: - otp-version: ${{ matrix.otp }} - - - name: Get a compatible Rebar3 - run: | - rm rebar3 - wget https://github.com/processone/ejabberd/raw/21.12/rebar3 - chmod +x rebar3 - - - name: Prepare databases - run: | - sudo systemctl start mysql.service - sudo systemctl start postgresql.service - mysql -u root -proot -e "CREATE USER 'ejabberd_test'@'localhost' - IDENTIFIED BY 'ejabberd_test';" - mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" - mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* - TO 'ejabberd_test'@'localhost';" - mysql -u root -proot ejabberd_test < sql/mysql.sql - pg_isready - sudo -u postgres psql -c "CREATE USER ejabberd_test - WITH PASSWORD 'ejabberd_test';" - sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" - sudo -u postgres psql ejabberd_test -f sql/pg.sql - sudo -u postgres psql -c "GRANT ALL PRIVILEGES - ON DATABASE ejabberd_test TO ejabberd_test;" - sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL - TABLES IN SCHEMA public - TO ejabberd_test;" - sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL - SEQUENCES IN SCHEMA public - TO ejabberd_test;" - - - 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: Prepare rebar - run: | - echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} - ]}.' >>rebar.config - echo '{xref_checks, [deprecated_function_calls, deprecated_functions, - locals_not_used, undefined_function_calls, undefined_functions]}. - % Disabled: exports_not_used,' >>rebar.config - echo '{dialyzer, [{get_warnings, true}, {plt_extra_apps, [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]} ]}.' >>rebar.config - echo '{ct_extra_params, "-verbosity 20"}.' >>rebar.config - echo "{ct_opts, [{verbosity, 20}, {keep_logs, 20}]}." >>rebar.config - - - name: Remove syntax_tools from release - run: sed -i 's|, syntax_tools||g' src/ejabberd.app.src.script - - - name: Cache rebar - uses: actions/cache@v3 - 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 update - make - - - run: make install -s - - run: make hooks - - run: make options - - run: make xref - - run: make dialyzer - - - 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: Check Development Release - run: | - make dev - RE=_build/dev/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: Run tests - id: ct - run: | - (cd priv && ln -sf ../sql) - COMMIT=`echo $GITHUB_SHA | cut -c 1-7` - DATE=`date +%s` - REF_NAME=`echo $GITHUB_REF_NAME | tr "/" "_"` - NODENAME=$DATE@$GITHUB_RUN_NUMBER-$GITHUB_ACTOR-$REF_NAME-$COMMIT - LABEL=`git show -s --format=%s | cut -c 1-30` - ./rebar3 ct --name $NODENAME --label "$LABEL" - ./rebar3 cover - - - name: Check results - if: always() && (steps.ct.outcome != 'skipped' || steps.ct2.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: Upload test logs - if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' - uses: peaceiris/actions-gh-pages@v3 - with: - publish_dir: _build/test - exclude_assets: '.github,lib,plugins' - external_repository: processone/ecil - deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} - keep_files: true - - - name: View ECIL address - if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' - run: | - CTRUN=`ls -la _build/test/logs/last | sed 's|.*-> ||'` - echo "::notice::View CT results: https://processone.github.io/ecil/logs/$CTRUN/" - - - name: Prepare new schema - run: | - [[ -d logs ]] && rm -rf logs - [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true - mysql -u root -proot -e "DROP DATABASE ejabberd_test;" - sudo -u postgres psql -c "DROP DATABASE ejabberd_test;" - mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" - mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* - TO 'ejabberd_test'@'localhost';" - mysql -u root -proot ejabberd_test < sql/mysql.new.sql - sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" - sudo -u postgres psql ejabberd_test -f sql/pg.new.sql - sudo -u postgres psql -c "GRANT ALL PRIVILEGES - ON DATABASE ejabberd_test TO ejabberd_test;" - sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL - TABLES IN SCHEMA public - TO ejabberd_test;" - sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL - SEQUENCES IN SCHEMA public - TO ejabberd_test;" - sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl - - run: CT_BACKENDS=mysql,pgsql make test - id: ctnewschema - - name: Check results - if: always() && steps.ctnewschema.outcome != 'skipped' - run: | - [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true - ln `find logs/ -name suite.log` logs/suite.log - grep 'TEST COMPLETE' logs/suite.log - grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log - test $(find logs/ -empty -name error.log) - - name: View logs failures - if: failure() && steps.ctnewschema.outcome != 'skipped' - run: | - cat logs/suite.log | awk \ - 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' - find logs/ -name error.log -exec cat '{}' ';' - find logs/ -name exunit.log -exec cat '{}' ';' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba236f9ec..ebf9da68c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,20 +25,20 @@ jobs: strategy: fail-fast: false matrix: - otp: ['20.0', '21.3', '24.3', '25'] - runs-on: ubuntu-20.04 + otp: ['25', '26', '27', '28'] + runs-on: ubuntu-24.04 services: redis: - image: redis + image: public.ecr.aws/docker/library/redis ports: - 6379:6379 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Test shell scripts - if: matrix.otp == 25 + if: matrix.otp == '27' run: | shellcheck test/ejabberd_SUITE_data/gencerts.sh shellcheck tools/captcha.sh @@ -46,35 +46,38 @@ jobs: shellcheck -x ejabberdctl.template - name: Get specific Erlang/OTP - if: matrix.otp != 25 uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} - - name: Get a compatible Rebar3 - if: matrix.otp <= '21.3' + - name: Install MS SQL Server run: | - rm rebar3 - wget https://github.com/processone/ejabberd/raw/21.12/rebar3 - chmod +x rebar3 + 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 "CREATE DATABASE ejabberd_test;" mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* TO 'ejabberd_test'@'localhost';" - mysql -u root -proot ejabberd_test < sql/mysql.sql 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 "CREATE DATABASE ejabberd_test;" - sudo -u postgres psql ejabberd_test -f sql/pg.sql 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;" @@ -89,39 +92,16 @@ jobs: sudo apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ libsqlite3-dev libwebp-dev libyaml-dev - - name: Prepare rebar - run: | - echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} - ]}.' >>rebar.config - echo '{xref_checks, [deprecated_function_calls, deprecated_functions, - locals_not_used, undefined_function_calls, undefined_functions]}. - % Disabled: exports_not_used,' >>rebar.config - echo '{dialyzer, [{get_warnings, true}, {plt_extra_apps, [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]} ]}.' >>rebar.config - echo '{ct_extra_params, "-verbosity 20"}.' >>rebar.config - echo "{ct_opts, [{verbosity, 20}, {keep_logs, 20}]}." >>rebar.config - - name: Remove syntax_tools from release run: sed -i 's|, syntax_tools||g' src/ejabberd.app.src.script - - name: Cache rebar - uses: actions/cache@v3 + - name: Cache Hex.pm + uses: actions/cache@v4 with: path: | ~/.cache/rebar3/ key: ${{matrix.otp}}-${{hashFiles('rebar.config')}} - - name: Download test logs - if: matrix.otp == 25 && github.repository == 'processone/ejabberd' - continue-on-error: true - run: | - mkdir -p _build/test - curl -sSL https://github.com/processone/ecil/tarball/gh-pages | - tar -C _build/test --strip-components=1 --wildcards -xzf - - rm -rf _build/test/logs/last/ - - name: Compile run: | ./autogen.sh @@ -131,7 +111,6 @@ jobs: --disable-elixir \ --disable-mssql \ --disable-odbc - make update make - run: make install -s @@ -139,6 +118,9 @@ jobs: - 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: | @@ -151,12 +133,30 @@ jobs: cat $RE/logs/ejabberd.log grep -q "is stopped in" $RE/logs/ejabberd.log - - name: Check Development Release + - 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 @@ -166,6 +166,7 @@ jobs: 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 "/" "_"` @@ -175,7 +176,7 @@ jobs: ./rebar3 cover - name: Check results - if: always() && (steps.ct.outcome != 'skipped' || steps.ct2.outcome != 'skipped') + if: always() && (steps.ct.outcome != 'skipped') id: ctresults run: | [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true @@ -193,7 +194,7 @@ jobs: find logs/ -name exunit.log -exec cat '{}' ';' - name: Send to coveralls - if: matrix.otp == 25 + if: matrix.otp == '27' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -205,36 +206,60 @@ jobs: "payload":{"build_num":$GITHUB_RUN_ID, "status":"done"}}' - - name: Upload test logs - if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' - uses: peaceiris/actions-gh-pages@v3 + - name: Check for changes to trigger schema upgrade test + uses: dorny/paths-filter@v3 + id: filter with: - publish_dir: _build/test - exclude_assets: '.github,lib,plugins' - external_repository: processone/ecil - deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} - keep_files: true + filters: | + sql: + - 'sql/**' + - 'src/mod_admin_update_sql.erl' - - name: View ECIL address - if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' + - name: Prepare for schema upgrade test + id: prepupgradetest + if: ${{ steps.filter.outputs.sql == 'true' }} run: | - CTRUN=`ls -la _build/test/logs/last | sed 's|.*-> ||'` - echo "::notice::View CT results: https://processone.github.io/ecil/logs/$CTRUN/" + [[ -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';" - mysql -u root -proot ejabberd_test < sql/mysql.new.sql sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" - sudo -u postgres psql ejabberd_test -f sql/pg.new.sql sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ejabberd_test TO ejabberd_test;" + sudo -u postgres psql -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;" @@ -242,7 +267,8 @@ jobs: SEQUENCES IN SCHEMA public TO ejabberd_test;" sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl - - run: CT_BACKENDS=mysql,pgsql make test + - 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' @@ -259,3 +285,17 @@ jobs: '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 index d36965d97..0bb169ccc 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -17,24 +17,23 @@ env: jobs: container: name: Container - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 permissions: packages: write steps: - - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Checkout ejabberd-contrib - uses: actions/checkout@v3 + 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@v1.14.1 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -42,11 +41,11 @@ jobs: - name: Get git describe id: gitdescribe - run: echo "::set-output name=ver::$(git describe --tags)" + run: echo "ver=$(git describe --tags)" >> $GITHUB_OUTPUT - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v3.8.0 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} labels: | @@ -55,13 +54,13 @@ jobs: org.opencontainers.image.vendor=ProcessOne - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Build and push Docker image - uses: docker/build-push-action@v2.10.0 + uses: docker/build-push-action@v6 with: build-args: | VERSION=${{ steps.gitdescribe.outputs.ver }} diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index 5fe622411..37c8983b4 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -21,13 +21,13 @@ on: jobs: binaries: name: Binaries - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Cache build directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/build/ - key: ${{runner.os}}-ct-ng-1.25.0 + key: ${{runner.os}}-ct-ng-1.27.0 - name: Install prerequisites run: | sudo apt-get -qq update @@ -39,9 +39,9 @@ jobs: - name: Install FPM run: | gem install --no-document --user-install fpm - echo $HOME/.gem/ruby/*/bin >> $GITHUB_PATH + echo $HOME/.local/share/gem/ruby/*/bin >> $GITHUB_PATH - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Build binary archives @@ -55,7 +55,7 @@ jobs: mkdir ejabberd-packages mv ejabberd_*.deb ejabberd-*.rpm ejabberd-*.run ejabberd-packages - name: Upload packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ejabberd-packages # @@ -70,15 +70,15 @@ jobs: release: name: Release needs: [binaries] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: github.ref_type == 'tag' steps: - name: Download packages - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v5 with: name: ejabberd-packages - name: Draft Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: draft: true files: ejabberd-packages/* diff --git a/.github/workflows/runtime.yml b/.github/workflows/runtime.yml index b991e8b1a..cd599fe83 100644 --- a/.github/workflows/runtime.yml +++ b/.github/workflows/runtime.yml @@ -31,132 +31,197 @@ jobs: strategy: fail-fast: false matrix: - otp: ['19.3', '20.3', '24.3', '25'] + otp: ['24', '25', '26', '27', '28'] rebar: ['rebar', 'rebar3'] - runs-on: ubuntu-latest + exclude: + - otp: '24' + rebar: 'rebar' + - otp: '27' + rebar: 'rebar' + - otp: '28' + rebar: 'rebar' + runs-on: ubuntu-24.04 container: - image: erlang:${{ matrix.otp }} + image: public.ecr.aws/docker/library/erlang:${{ matrix.otp }} steps: - - uses: actions/checkout@v3 + - 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 + 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=`which ${{ matrix.rebar }}` \ + ./configure --with-rebar=./${{ matrix.rebar }} \ --prefix=/tmp/ejabberd \ + --with-min-erlang=9.0.5 \ --enable-all \ --disable-elixir \ + --disable-tools \ --disable-odbc - make update make - - name: Prepare rebar - run: | - echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} - ]}.' >>rebar.config - echo '{xref_checks, [deprecated_function_calls, deprecated_functions, - locals_not_used, undefined_function_calls, undefined_functions]}. - % Disabled: exports_not_used,' >>rebar.config - - run: make xref - - name: Test rel (rebar2) + - run: make dialyzer + + - name: Prepare rel (rebar2) if: matrix.rebar == 'rebar' run: | - make rel - rel/ejabberd/bin/ejabberdctl start \ - && rel/ejabberd/bin/ejabberdctl started - rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass - rel/ejabberd/bin/ejabberdctl registered_users localhost - cat rel/ejabberd/logs/* + mkdir -p _build/prod && ln -s `pwd`/rel/ _build/prod/rel + mkdir -p _build/dev && ln -s `pwd`/rel/ _build/dev/rel - - name: Test rel - if: matrix.rebar != 'rebar' + - 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 + _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 - cat _build/prod/rel/ejabberd/logs/* - - name: Test dev - if: matrix.rebar != 'rebar' + - 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 user1 localhost s0mePass - _build/dev/rel/ejabberd/bin/ejabberdctl registered_users localhost + _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 - cat _build/dev/rel/ejabberd/logs/* - mix: - name: Mix + - 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: - otp: ['21.3', '22.0', '25.0'] - elixir: ['1.10.3', '1.11.4', '1.12.3', '1.13.0'] - exclude: - - otp: '21.3' - elixir: '1.12.3' - - otp: '21.3' - elixir: '1.13.0' - - otp: '25.0' - elixir: '1.10.3' - - otp: '25.0' - elixir: '1.11.4' - - otp: '25.0' - elixir: '1.12.3' - runs-on: ubuntu-latest + 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@v3 - - - name: Get specific Erlang/OTP - uses: erlef/setup-beam@v1 - with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} + - uses: actions/checkout@v5 - 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 + 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 + - name: Enable Module.Example and an Elixir dependency 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::" + 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=mix \ + ./configure --with-rebar=./rebar3 \ --prefix=/tmp/ejabberd \ --enable-all \ - --disable-elixir \ --disable-odbc - mix deps.get make - run: make xref @@ -181,17 +246,46 @@ jobs: _build/dev/rel/ejabberd/bin/ejabberdctl stop \ && _build/dev/rel/ejabberd/bin/ejabberdctl stopped - - name: Check rel + - 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() @@ -200,3 +294,139 @@ jobs: 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 6bfafc069..0f69a0aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ \#*# .#* .edts +.tool-versions *.dump /Makefile +/doc /config.log /config.status /config/releases.exs @@ -32,12 +34,14 @@ /priv/bin/captcha*sh /priv/sql /rel/ejabberd +/recompile.log /_build /database/ /.rebar -/rebar.lock /log/ Mnesia.nonode@nohost/ +/TAGS +/tags # Binaries created with tools/make-{binaries,installers,packages}: /ejabberd_*.deb /ejabberd-*.rpm 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 index bcf2d32db..cadfc1c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,911 @@ -# Version 22.10 +## 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: -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)) @@ -18,7 +923,8 @@ Core: - `mod_shared_roster_ldap`: Update roster_get hook to use `#roster_item{}` - `prosody2ejabberd`: Fix parsing of scram password from prosody -MIX: +#### MIX: + - Fix MIX's filter_nodes - Return user jid on join - `mod_mix_pam`: Add new MIX namespaces to disco features @@ -33,12 +939,13 @@ MIX: - `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: +#### 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)) -- Don't set affiliation to `none` if it's already `none` in `mod_muc_room:process_item_change/3` +- 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 @@ -46,7 +953,8 @@ MUC: - Export `mod_muc_admin:get_room_pid/2` - Export function for getting room diagnostics -SQL: +#### 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 @@ -58,7 +966,8 @@ SQL: - Update mysql library - Catch mysql connection being close earlier -Build: +#### 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)) @@ -71,7 +980,8 @@ Build: - Remove unused macro definitions detected by rebar3_hank - Remove unused header files which content is already in xmpp library -Container: +#### Container: + - Get ejabberd-contrib sources to include them - Copy `.ejabberd-modules` directory if available - Do not clone repo inside container build @@ -81,7 +991,8 @@ Container: - 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: +#### Installers: + - make-binaries: Include CAPTCHA script with release - make-binaries: Edit rebar.config more carefully - make-binaries: Fix linking of EIMP dependencies @@ -95,57 +1006,59 @@ Installers: - make-installers: Override code on upgrade - make-installers: Apply cosmetic changes -External modules: +#### 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: +#### 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 +## Version 22.05 -Core +#### 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 +#### 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 +#### 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 +#### 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 +#### 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 +#### 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 +#### 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 @@ -158,7 +1071,7 @@ Other Modules - `mod_shared_roster`: Normalize JID on unset_presence (#3752) - `mod_stun_disco`: Fix parsing of IPv6 listeners -Dependencies +#### 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 @@ -168,7 +1081,7 @@ Dependencies - rebar3: Updated binary to work from Erlang/OTP 22 to 25 - `make update`: Fix when used with rebar 3.18 -Compile +#### 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 @@ -177,7 +1090,7 @@ Compile - `tools/make-binaries`: New script for building Linux binaries - `tools/make-installers`: New script for building command line installers -Start +#### 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` @@ -185,7 +1098,7 @@ Start - `mix.exs`: Add `-boot` and `-boot_var` in `ejabberdctl` instead of adding `vm.args` - `tools/captcha.sh`: Fix some warnings detected by ShellCheck -Commands +#### 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 @@ -197,7 +1110,7 @@ Commands - `stop|restart`: Terminate ejabberd_sm before everything else to ensure sessions closing (#3641) - `subscribe_room_many`: New command -Translations +#### Translations - Updated Catalan - Updated French - Updated German @@ -205,7 +1118,7 @@ Translations - Updated Portuguese (Brazil) - Updated Spanish -Workflows +#### 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 @@ -213,14 +1126,14 @@ Workflows - Installers: New workflow to build binary packages - Runtime: New workflow to test compilation, rel, starting and ejabberdctl -# Version 21.12 +## Version 21.12 -Commands +#### 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 +#### 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 @@ -235,7 +1148,7 @@ Modules - mod_register_web: Handle unknown host gracefully - mod_register_web: Use mod_register configured restrictions -PubSub +#### PubSub - Add `delete_expired_pubsub_items` command - Add `delete_old_pubsub_items` command - Optimize publishing on large nodes (SQL) @@ -246,7 +1159,7 @@ PubSub - node_flat: Avoid catch-all clauses for RSM - node_flat_sql: Avoid catch-all clauses for RSM -SQL +#### 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 @@ -254,15 +1167,15 @@ SQL - PgSQL: Add missing SQL migration for table `push_session` - PgSQL: Fix `vcard_search` definition in pgsql new schema -Other +#### 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 +## Version 21.07 -Compilation +#### 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 @@ -274,14 +1187,16 @@ Compilation - Added experimental support for GitHub Codespaces - Switch test service from TravisCI to GitHub Actions -Commands: +#### 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: +#### Modules: + - ejabberd_listener: New `send_timeout` option - mod_mix: Improvements to update to 0.14.1 - mod_muc_room: Don't leak owner JIDs @@ -298,7 +1213,8 @@ Modules: - WebAdmin: New simple pages to view mnesia tables information and content - WebSocket: Fix typos -SQL: +#### 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 @@ -306,16 +1222,18 @@ SQL: - 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 +## Version 21.04 + +#### API Commands: -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: +#### 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 @@ -323,14 +1241,16 @@ Build and setup: - Update `sql_query` record to handle the Erlang/OTP 24 compiler reports - Updated dependencies to fix Dialyzer warnings -Miscellaneous: +#### 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: +#### 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 @@ -344,9 +1264,10 @@ Modules: - `mod_pubsub`: Fix `gen_pubsub_node:get_state` return value - `mod_vcard`: Obtain and provide photo type in vCard LDAP -# Version 21.01 +## Version 21.01 + +#### Miscellaneous changes: -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 @@ -361,7 +1282,8 @@ Miscellaneous changes: - Stun: Block loopback addresses by default - Several documentation fixes and clarifications -Commands: +#### 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 @@ -369,17 +1291,19 @@ Commands: - `send_message`: Don’t include empty in messages - `set_room_affiliation`: Validate affiliations -Running: +#### Running: + - Docker: New `Dockerfile` and `devcontainer.json` - New `ejabberdctl foreground-quiet` - Systemd: Allow for listening on privileged ports - Systemd: Integrate nicely with systemd -Translations: +#### 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 +## 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 @@ -388,13 +1312,13 @@ Translations: - start_room: new hook runs when a room process is started - check_decoded_jwt: new hook to check decoded JWT after success authentication -* Admin +#### 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: +#### 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 @@ -414,9 +1338,9 @@ Translations: - WebAdmin: Mark dangerous buttons with CSS - WebSocket: Make websocket send put back pressure on c2s process -# Version 20.07 +## Version 20.07 -* Changes in this version +#### 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 @@ -432,7 +1356,7 @@ Translations: 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 +- 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 @@ -441,13 +1365,13 @@ Translations: they were passed to handler modules - Make stun module work better with ipv6 addresses -# Version 20.03 +## Version 20.03 -* Changes in this version +#### 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 + 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 @@ -461,9 +1385,9 @@ Translations: - Fix reporting errors in `send_stanza` command when xml passed to it couldn't be passed correctly -# Version 20.02 +## Version 20.02 -* Changes in this version +#### 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 @@ -479,9 +1403,9 @@ Translations: override built-in values - Fix return value of reload_config and dump_config commands -# Version 20.01 +## Version 20.01 -* New features +#### New features - Implement OAUTH authentication in mqtt - Make logging infrastructure use new logger introduced in Erlang (requires OTP22) @@ -496,7 +1420,7 @@ Translations: - Generate man page automatically - Implement copy feature in mod_carboncopy -* Fixes +#### Fixes - Make webadmin work with configurable paths - Fix handling of result in xmlrpc module - Make webadmin work even when accessed through not declared domain @@ -512,20 +1436,20 @@ Translations: failed - Fix crash in stream management when timeout was not set -# Version 19.09 +## Version 19.09 -* Admin +#### 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 +#### 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 +#### ACME - Improve ACME implementation - Fix IDA support in ACME requests - Fix unicode formatting in ACME module @@ -536,10 +1460,10 @@ Translations: - Don't auto request certificate for localhost and IP-like domains - Add listener for ACME challenge in example config -* Authentication +#### Authentication - JWT-only authentication for some users (#3012) -* MUC +#### 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) @@ -547,11 +1471,11 @@ Translations: - Improve rooms_* commands to accept 'global' as MUC service argument (#2976) - Rename MUC command arguments from Host to Service (#2976) -* SQL +#### SQL - Fix transactions for Microsoft SQL Server (#2978) - Spawn SQL connections on demand only -* Misc +#### Misc - Add support for XEP-0328: JID Prep - Added gsfonts for captcha - Log Mnesia table type on creation @@ -565,14 +1489,14 @@ Translations: - Correctly handle unicode in log messages - Fix unicode processing in ejabberd.yml -# Version 19.08 +## Version 19.08 -* Administration +#### Administration - Improve ejabberd halting procedure - Process unexpected erlang messages uniformly: logging a warning - mod_configure: Remove modules management -* Configuration +#### Configuration - Use new configuration validator - ejabberd_http: Use correct virtual host when consulting trusted_proxies - Fix Elixir modules detection in the configuration file @@ -582,7 +1506,7 @@ Translations: - mod_stream_mgmt: Allow flexible timeout format - mod_mqtt: Allow flexible timeout format in session_expiry option -* Misc +#### Misc - Fix SQL connections leakage - New authentication method using JWT tokens - extauth: Add 'certauth' command @@ -597,22 +1521,22 @@ Translations: - mod_privacy: Don't attempt to query 'undefined' active list - mod_privacy: Fix race condition -* MUC +#### 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 +#### 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 +## Version 19.05 -* Admin +#### 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 @@ -623,19 +1547,19 @@ Translations: - Improve request_handlers validator - Fix syntax in example Elixir config file -* Auth +#### 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 +#### 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 +#### 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 @@ -644,7 +1568,7 @@ Translations: - Make get_subscribed_rooms work even for non-persistant rooms - Allow non-moderator subscribers to get list of room subscribers -* Offline +#### 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 @@ -656,27 +1580,27 @@ Translations: - 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: +#### 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 +#### 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 +#### 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 +#### 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 @@ -687,16 +1611,16 @@ Translations: - mod_mqtt: Support other socket modules - mod_push: Check for payload in encrypted messages -# Version 19.02 +## Version 19.02 -* Admin +#### 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 +#### Configuration - Allow specifying tag for listener for api_permission purposes - Change default ciphers to intermediate - Define default ciphers/protocol_option in example config @@ -706,29 +1630,29 @@ Translations: - 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 +#### 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 +#### 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 +#### 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 +#### 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 +#### 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 @@ -741,9 +1665,9 @@ Translations: - Translations: fixed "make translations" - WebAdmin: Fix support to restart module with new options -# Version 18.12 +## Version 18.12 -* MAM data store compression -* Proxy protocol support (http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) -* MUC Self-Ping optimization (XEP-0410) -* Bookmarks conversion (XEP-0411) +- 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 index d3b197910..e8855889e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -22,6 +22,21 @@ Examples of unacceptable behavior by participants include: * 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. @@ -34,13 +49,13 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@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. +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 [http://contributor-covenant.org/version/1/4][version] +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]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://www.contributor-covenant.org/ +[version]: https://www.contributor-covenant.org/version/1/4/ diff --git a/COMPILE.md b/COMPILE.md index 5bcfaa581..2eb8a1739 100644 --- a/COMPILE.md +++ b/COMPILE.md @@ -7,7 +7,7 @@ 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/installation/#source-code +[docs-source]: https://docs.ejabberd.im/admin/install/source/ Requirements @@ -15,20 +15,20 @@ Requirements To compile ejabberd you need: - - GNU Make - - GCC - - Libexpat ≥ 1.95 - - Libyaml ≥ 0.1.4 - - Erlang/OTP ≥ 19.3 - - OpenSSL ≥ 1.0.0 +- 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, to support Elixir, and alternative to rebar/rebar3 +- 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. @@ -43,7 +43,7 @@ There are several ways to obtain the ejabberd source code: - Source code package from [ejabberd GitHub Releases][ghr] - Latest development code from [ejabberd Git repository][gitrepo] -[p1dl]: https://www.process-one.net/en/ejabberd/downloads/ +[p1dl]: https://www.process-one.net/download/ejabberd/ [ghr]: https://github.com/processone/ejabberd/releases [gitrepo]: https://github.com/processone/ejabberd @@ -65,6 +65,11 @@ 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 --------------------- @@ -89,13 +94,8 @@ 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 --with-rebar=rebar3 - make rel - -Or, if you have Elixir available and plan to develop Elixir code: - - ./configure --with-rebar=mix - make dev + ./configure + make prod Check the full list of targets: diff --git a/CONTAINER.md b/CONTAINER.md index d42a0ae6e..e96081112 100644 --- a/CONTAINER.md +++ b/CONTAINER.md @@ -1,42 +1,44 @@ [![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) -[![GitHub Container](https://img.shields.io/github/v/tag/processone/ejabberd?label=container&sort=semver)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) -[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/ejabberd/ecs?label=docker)](https://hub.docker.com/r/ejabberd/ecs/) +[![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 -================== +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://ejabberd.im/ +[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 document explains how to use the -[ejabberd container images](https://github.com/processone/ejabberd/pkgs/container/ejabberd) -available in the GitHub Container Registry, -built using the files in `.github/container/`. +This page documents those container images ([images comparison](#images-comparison)): -Alternatively, there are also -[ejabberd-ecs Docker images](https://hub.docker.com/r/ejabberd/ecs/) -available in Docker Hub, -built using the -[docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) -repository. +- [![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. -If you are using a Windows operating system, check the tutorials mentioned in -[ejabberd Docs > Docker Image](https://docs.ejabberd.im/admin/installation/#docker-image). +- [![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 -------------- -### With default configuration +### daemon Start ejabberd in a new container: @@ -45,13 +47,7 @@ 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". - -Stop the running container: - -```bash -docker stop ejabberd -``` +using ejabberd default configuration file and XMPP domain `localhost`. Restart the stopped ejabberd container: @@ -59,60 +55,91 @@ Restart the stopped ejabberd container: docker restart ejabberd ``` +Stop the running container: -### Start with Erlang console attached +```bash +docker stop ejabberd +``` -Start ejabberd with an Erlang console attached using the `live` command: +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". +That uses the default configuration file and XMPP domain `localhost`. -### Start with your configuration and database +### with your data Pass a configuration file as a volume and share the local directory to store database: ```bash -mkdir database -chown ejabberd database +mkdir conf && cp ejabberd.yml.example conf/ejabberd.yml -cp ejabberd.yml.example ejabberd.yml +mkdir database && chown ejabberd database docker run --name ejabberd -it \ - -v $(pwd)/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml \ + -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`, +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 the administrator account +### Register admin account -The default ejabberd configuration does not grant admin privileges -to any account, -you may want to register a new account in ejabberd -and grant it admin rights. +#### [![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) -Register an account using the `ejabberdctl` script: +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 ``` -Then edit conf/ejabberd.yml and add the ACL as explained in -[ejabberd Docs: Administration Account](https://docs.ejabberd.im/admin/installation/#administration-account) - - -### Check ejabberd log files +### 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: @@ -122,7 +149,7 @@ docker exec -it ejabberd tail -f logs/ejabberd.log ``` -### Inspect the container files +### Inspect container files The container uses Alpine Linux. Start a shell inside the container: @@ -131,7 +158,7 @@ docker exec -it ejabberd sh ``` -### Open ejabberd debug console +### Open debug console Open an interactive debug Erlang console attached to a running ejabberd in a running container: @@ -154,10 +181,9 @@ Now update your ejabberd configuration file, for example: docker exec -it ejabberd vi conf/ejabberd.yml ``` -and add the required options: -``` -captcha_cmd: /opt/ejabberd-22.04/lib/ejabberd-22.04/priv/bin/captcha.sh -captcha_url: https://localhost:5443/captcha +and add this option: +```yaml +captcha_cmd: "$HOME/bin/captcha.sh" ``` Finally, reload the configuration file or restart the container: @@ -165,21 +191,35 @@ Finally, reload the configuration file or restart the container: 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 +``` -Advanced Container Configuration --------------------------------- +For more details about CAPTCHA options, please check the +[CAPTCHA](https://docs.ejabberd.im/admin/configuration/basic/#captcha) +documentation section. + + +Advanced +-------- ### Ports -This container image exposes the 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. +- `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 +- `5210`: Erlang connectivity when `ERL_DIST_PORT` is set, alternative to EPMD [:orange_circle:](#images-comparison) ### Volumes @@ -196,31 +236,269 @@ You should back up or export the content of the directory to persistent storage - `/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 `ejabberd` user inside the container. +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. -It's possible to install additional ejabberd modules using volumes, -[this comment](https://github.com/processone/docker-ejabberd/issues/81#issuecomment-1036115146) -explains how to install an additional module using docker-compose. +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 docker container is started, +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. -Example usage (see full example [docker-compose.yml](https://github.com/processone/docker-ejabberd/issues/64#issuecomment-887741332)): +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 @@ -231,23 +509,27 @@ 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`: -``` -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 + ```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 ``` @@ -258,45 +540,193 @@ Example using environment variables (see full example [docker-compose.yml](https - ERLANG_COOKIE=dummycookie123 ``` +--- -Generating a Container Image ----------------------------- +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. -This container image includes ejabberd as a standalone OTP release built using Elixir. +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 +``` -That OTP release is configured with: +### 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 -Build ejabberd Community Server base image from ejabberd master on GitHub: +#### Direct build + +Build ejabberd Community Server container image from ejabberd master git repository: ```bash -docker build \ +docker buildx build \ -t personal/ejabberd \ -f .github/container/Dockerfile \ . ``` -Build ejabberd Community Server base image for a given ejabberd version, -both for amd64 and arm64 architectures: +#### Podman build -```bash -VERSION=22.05 -git checkout $VERSION -docker buildx build \ - --platform=linux/amd64,linux/arm64 - -t personal/ejabberd:$VERSION \ - -f .github/container/Dockerfile \ - . -``` +To build the image using Podman, please notice: -It's also possible to use podman instead of docker, just 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 -- If you want to start with command `live`, add environment variable `EJABBERD_BYPASS_WARNINGS=true` +- to start with command `live`, you may want to add environment variable `EJABBERD_BYPASS_WARNINGS=true` + ```bash podman build \ -t ejabberd \ @@ -310,4 +740,356 @@ 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 index 136ef3077..819921ee5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,21 +3,21 @@ 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](#coc) -* [Questions and Problems](#question) -* [Issues and Bugs](#issue) -* [Feature Requests](#feature) -* [Issue Submission Guidelines](#submit) -* [Pull Request Submission Guidelines](#submit-pr) -* [Signing the CLA](#cla) +* [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 +## Code of Conduct Help us keep ejabberd community open-minded and inclusive. Please read and follow our [Code of Conduct][coc]. -## Questions, Bugs, Features +## Questions, Bugs, Features -### Got a Question or Problem? +### 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 @@ -25,25 +25,25 @@ 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 +* 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. +* 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 Mailing List][list] -- [ejabberd XMPP room][muc]: ejabberd@conference.process-one.net -- [ejabberd XMPP room logs][logs] -### Found an Issue or Bug? +* 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? +### Missing a Feature? You can request a new feature by submitting an issue to our [GitHub Repository][github-issues]. @@ -52,9 +52,9 @@ If you would like to implement a new feature then consider what kind of change i * **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](#submit-pr). + as a Pull Request. See the section about [Pull Request Submission Guidelines](#pull-request-submission-guidelines). -## Issue Submission Guidelines +## Issue Submission Guidelines Before you submit your issue search the archive, maybe your question was already answered. @@ -64,7 +64,7 @@ the effort we can spend fixing issues and adding new features, by not reporting 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 +## 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] @@ -80,6 +80,7 @@ Before you submit your pull request consider the following guidelines: ```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]. @@ -88,6 +89,7 @@ Before you submit your pull request consider the following guidelines: ```shell git commit -a ``` + Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. * Push your branch to GitHub: @@ -123,28 +125,25 @@ restarted. That's it! Thank you for your contribution! -## Signing the Contributor License Agreement (CLA) +## 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 -You can read [ProcessOne Contribution License Agreement][cla] in PDF. +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://lists.jabber.ru/mailman/listinfo/ejabberd +[list]: https://web.archive.org/web/20230319174915/http://lists.jabber.ru/mailman/listinfo/ejabberd [muc]: xmpp:ejabberd@conference.process-one.net -[logs]: https://process-one.net/logs/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://www.process-one.net/resources/ejabberd-cla.pdf -[license]: https://github.com/processone/ejabberd/blob/master/COPYING +[cla]: https://cla.process-one.net/ diff --git a/Makefile.in b/Makefile.in index 42b799c71..cf7480702 100644 --- a/Makefile.in +++ b/Makefile.in @@ -1,8 +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@ @@ -71,7 +86,10 @@ SPOOLDIR = @localstatedir@/lib/ejabberd # /var/log/ejabberd/ LOGDIR = @localstatedir@/log/ejabberd -INSTALLUSER=@INSTALLUSER@ +#. +#' install user +# + # if no user was enabled, don't set privileges or ownership ifeq ($(INSTALLUSER),) O_USER= @@ -86,18 +104,22 @@ else CHOWN_OUTPUT=&1 INIT_USER=$(INSTALLUSER) endif + # if no group was enabled, don't set privileges or ownership -INSTALLGROUP=@INSTALLGROUP@ ifneq ($(INSTALLGROUP),) G_USER=-g $(INSTALLGROUP) endif -ifeq "$(MIX)" "mix" +#. +#' rebar / rebar3 / mix +# + +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)}') +REBAR_VER:=$(shell $(REBAR) --version | $(AWK) -F '[ .]' '/rebar / {print $$2}') +REBAR_VER_318:=$(shell $(REBAR) --version | $(AWK) -F '[ .]' '/rebar / {print ($$2 == 3 && $$3 >= 18 ? 1 : 0)}') endif ifeq "$(REBAR_VER)" "6" @@ -112,11 +134,26 @@ ifeq "$(REBAR_VER)" "6" 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 + 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 @@ -134,8 +171,12 @@ endif XREFOPTIONS= CLEANARG=--all REBARREL=$(REBAR) as prod tar - REBARDEV=REBAR_PROFILE=dev $(REBAR) release - RELIVECMD=$(REBAR) relive + 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 @@ -151,10 +192,16 @@ else REBARREL=$(REBAR) generate REBARDEV= RELIVECMD=@echo "Rebar2 detected... relive not supported.\ - \nTry: ./configure --with-rebar=./rebar3 ; make relive" + \nTry: ./configure --with-rebar=rebar3 ; make relive" + REL_LIB_DIR = rel/ejabberd/lib + COPY_REL_TARGET = rel endif endif +#. +#' main targets +# + all: scripts deps src deps: $(DEPSDIR)/.got @@ -162,7 +209,7 @@ deps: $(DEPSDIR)/.got $(DEPSDIR)/.got: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built - mkdir -p $(DEPSDIR) + $(MKDIR_P) $(DEPSDIR) $(REBAR) $(GET_DEPS) && :> $(DEPSDIR)/.got $(CONFIGURE_DEPS) @@ -171,6 +218,7 @@ $(DEPSDIR)/.built: $(DEPSDIR)/.got src: $(DEPSDIR)/.built $(REBAR) $(SKIPDEPS) compile + $(EXPLICIT_ELIXIR_COMPILE) update: rm -rf $(DEPSDIR)/.got @@ -188,11 +236,45 @@ options: all tools/opt_types.sh ejabberd_option $(EBINDIR) translations: - tools/prepare-tr.sh $(DEPSDIR) + $(GET_DEPS_TRANSLATIONS) + tools/prepare-tr.sh $(DEPSDIR_TRANSLATIONS) -edoc: - $(ERL) -noinput +B -eval \ - 'case edoc:application(ejabberd, ".", []) of ok -> halt(0); error -> halt(1) end.' +doap: + tools/generate-doap.sh + +#. +#' edoc +# + +edoc: edoc_files edoc_compile + $(EDOCPRE) $(REBAR) $(EDOCTASK) + +edoc_compile: deps + $(EDOCPRE) $(REBAR) compile + +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)) @@ -279,26 +361,58 @@ copy-files: copy-files-sub: copy-files-sub2 +#. +#' copy-files-rel +# + +copy-files-rel: $(COPY_REL_TARGET) + # + # Libraries + (cd $(REL_LIB_DIR) && find . -follow -type f ! -executable -exec $(INSTALL) -vDm 640 $(G_USER) {} $(DESTDIR)$(LIBDIR)/{} \;) + # + # *.so: + (cd $(REL_LIB_DIR) && find . -follow -type f -executable -name *.so -exec $(INSTALL) -vDm 640 $(G_USER) {} $(DESTDIR)$(LIBDIR)/{} \;) + # + # 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 -iexpath=$(shell which iex) CONFIG_DIR = ${relivedir}/conf SPOOL_DIR = ${relivedir}/database LOGS_DIR = ${relivedir}/logs +#. +#' scripts +# + ejabberdctl.relive: - $(SED) -e "s*{{installuser}}*@INSTALLUSER@*g" \ + $(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}}/iex*$(iexpath)*g" \ - -e "s*{{bindir}}*@bindir@*g" \ - -e "s*{{libdir}}*${relivelibdir}*g" \ - -e "s*{{erl}}*@ERL@*g" \ - -e "s*{{epmd}}*@EPMD@*g" ejabberdctl.template \ + -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: @@ -314,19 +428,29 @@ ejabberd.service: chmod 644 ejabberd.service ejabberdctl.example: vars.config - $(SED) -e "s*{{installuser}}*@INSTALLUSER@*g" \ + $(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@*g" \ - -e "s*{{erl}}*@ERL@*g" \ - -e "s*{{epmd}}*@EPMD@*g" ejabberdctl.template \ + -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: copy-files +#. +#' install +# + +install: copy-files install-main + +install-rel: copy-files-rel install-main + +install-main: # # Configuration files $(INSTALL) -d -m 750 $(G_USER) $(DESTDIR)$(ETCDIR) @@ -349,12 +473,12 @@ install: copy-files # # Spool directory $(INSTALL) -d -m 750 $(O_USER) $(DESTDIR)$(SPOOLDIR) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(DESTDIR)$(SPOOLDIR) >$(CHOWN_OUTPUT) + $(CHOWN_COMMAND) -R $(INSTALLUSER) $(DESTDIR)$(SPOOLDIR) >$(CHOWN_OUTPUT) chmod -R 750 $(DESTDIR)$(SPOOLDIR) # # Log directory $(INSTALL) -d -m 750 $(O_USER) $(DESTDIR)$(LOGDIR) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(DESTDIR)$(LOGDIR) >$(CHOWN_OUTPUT) + $(CHOWN_COMMAND) -R $(INSTALLUSER) $(DESTDIR)$(LOGDIR) >$(CHOWN_OUTPUT) chmod -R 750 $(DESTDIR)$(LOGDIR) # # Documentation @@ -365,8 +489,14 @@ install: copy-files || 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 $(DESTDIR)$(SBINDIR)/ejabberdctl rm -f $(DESTDIR)$(BINDIR)/iex @@ -395,6 +525,7 @@ uninstall-binary: 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 $(DESTDIR)$(ETCDIR) @@ -402,6 +533,10 @@ uninstall-all: uninstall-binary rm -rf $(DESTDIR)$(SPOOLDIR) rm -rf $(DESTDIR)$(LOGDIR) +#. +#' clean +# + clean: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built @@ -424,27 +559,53 @@ distclean: clean clean-rel rm -f Makefile rm -f vars.config -rel: +#. +#' 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 -ifeq "$(REBAR_VER)" "3" +#. +#' dialyzer +# + +ifeq "$(REBAR_VER)" "6" # Mix +dialyzer: + MIX_ENV=test $(REBAR) dialyzer + +else +ifeq "$(REBAR_VER)" "3" # Rebar3 dialyzer: $(REBAR) dialyzer -else + +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 compiler erts \ @@ -452,13 +613,13 @@ dialyzer/erlang.plt: 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 @@ -480,6 +641,18 @@ dialyzer: erlang_plt deps_plt ejabberd_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 ***************************************" @@ -488,9 +661,32 @@ test: @cd priv && ln -sf ../sql $(REBAR) $(SKIPDEPS) ct -.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \ - install uninstall uninstall-binary uninstall-all translations deps test \ - quicktest erlang_plt deps_plt ejabberd_plt xref hooks options +.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 "" @@ -498,24 +694,37 @@ help: @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 " 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 " rel Build a production release" + @echo " prod Build a production release" @echo " dev Build a development release" @echo " relive Start a live ejabberd in _build/relive/" @echo "" - @echo " edoc Generate edoc documentation (unused)" + @echo " doap Generate DOAP file" + @echo " edoc Generate EDoc documentation [mix]" @echo " options Generate ejabberd_option.erl" - @echo " translations Extract translation files (requires --enable-tools)" - @echo " tags Generate tags file for text editors" + @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" - @echo " xref Run cross reference analysis" + @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/README.md b/README.md index 7d526ee55..646cc4a17 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,36 @@

- +

- - - - + + + +
- + - + + +

- [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 a list of [supported protocols and XEPs][xeps]. - +[ejabberd at ProcessOne][p1home], and the list of [supported protocols in ProcessOne][xeps] +and [XMPP.org][xmppej]. Installation ------------ @@ -38,12 +38,16 @@ Installation There are several ways to install ejabberd: - Source code: compile yourself, see [COMPILE](COMPILE.md) -- Installers from [ejabberd GitHub Releases][releases] (run/deb/rpm for x64 and arm64) -- Container image from [ejabberd Docker Hub][hubecs], see [ecs README][docker-ecs-readme] (for x64) -- Container image from [ejabberd Github Packages][packages], see [CONTAINER](CONTAINER.md) (for x64 and arm64) +- 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 ------------- @@ -60,7 +64,6 @@ Once ejabberd is installed, try: ejabberdctl help man ejabberd.yml - Development ----------- @@ -72,31 +75,37 @@ 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 +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][muc]: ejabberd@conference.process-one.net -- [Mailing list][list] +- 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.md)), -and [ejabberd translations](https://github.com/processone/ejabberd-po/) under MIT 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 @@ -105,22 +114,23 @@ and [ejabberd translations](https://github.com/processone/ejabberd-po/) under MI [erlang]: https://www.erlang.org/ [features]: https://docs.ejabberd.im/admin/introduction/ [fluux]: https://fluux.io/ -[github]: https://github.com/processone/ejabberd -[homebrew]: https://docs.ejabberd.im/admin/installation/#homebrew +[homebrew]: https://docs.ejabberd.im/admin/install/homebrew/ [hubecs]: https://hub.docker.com/r/ejabberd/ecs/ -[im]: https://ejabberd.im/ +[im]: https://www.ejabberd.im/ [issues]: https://github.com/processone/ejabberd/issues -[list]: https://lists.jabber.ru/mailman/listinfo/ejabberd [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/installation/#operating-system-packages -[p1contact]: https://www.process-one.net/en/company/contact/ -[p1home]: https://www.process-one.net/en/ejabberd/ +[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/en/ejabberd/protocols/ +[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/config/runtime.exs b/config/runtime.exs index b4e6dc5f1..adfc18c06 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -8,6 +8,8 @@ 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') + log_path: Path.join(rootpath, "logs/ejabberd.log") config :mnesia, - dir: Path.join(rootpath, 'database/') + dir: Path.join(rootpath, "database/") +config :exsync, + reload_callback: {:ejabberd_admin, :update, []} diff --git a/configure.ac b/configure.ac index 560be9155..b91595dc5 100644 --- a/configure.ac +++ b/configure.ac @@ -2,15 +2,26 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ(2.59) -AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 22.10` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) -REQUIRE_ERLANG_MIN="8.3 (Erlang/OTP 19.3)" +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 @@ -28,18 +39,42 @@ fi ]) AC_ARG_WITH(rebar, - AS_HELP_STRING([--with-rebar=bin],[use the rebar/rebar3/mix binary specified]), + 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="rebar" + rebar="rebar3" else rebar="$with_rebar" fi -], [rebar="rebar"]) +], [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 @@ -84,7 +119,7 @@ AC_ARG_ENABLE(debug, esac],[if test "x$debug" = "x"; then debug=true; fi]) AC_ARG_ENABLE(elixir, -[AS_HELP_STRING([--enable-elixir],[enable Elixir support (default: no)])], +[AS_HELP_STRING([--enable-elixir],[enable Elixir support in Rebar3 (default: no)])], [case "${enableval}" in yes) elixir=true ;; no) elixir=false ;; @@ -112,7 +147,7 @@ esac],[full_xml=false]) ENABLEGROUP="" AC_ARG_ENABLE(group, - [AS_HELP_STRING([--enable-group[[[[=GROUP]]]]], [allow this system group to start ejabberd (default: no)])], + [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="" ;; @@ -237,7 +272,7 @@ AC_ARG_ENABLE(system_deps, esac],[if test "x$system_deps" = "x"; then system_deps=false; fi]) AC_ARG_ENABLE(tools, -[AS_HELP_STRING([--enable-tools],[build development tools (default: no)])], +[AS_HELP_STRING([--enable-tools],[include debugging/development tools (default: no)])], [case "${enableval}" in yes) tools=true ;; no) tools=false ;; @@ -246,7 +281,7 @@ 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="" ;; @@ -280,6 +315,8 @@ case "`uname`" in ;; esac +AC_MSG_RESULT([build tool to use (change using --with-rebar): $rebar]) + AC_SUBST(roster_gateway_workaround) AC_SUBST(new_sql_schema) AC_SUBST(full_xml) @@ -305,3 +342,28 @@ 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/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.service.template b/ejabberd.service.template index 685a104d0..902a81cb2 100644 --- a/ejabberd.service.template +++ b/ejabberd.service.template @@ -9,7 +9,6 @@ Group=@installuser@ LimitNOFILE=65536 Restart=on-failure RestartSec=5 -WatchdogSec=30 ExecStart=@ctlscriptpath@/ejabberdctl foreground ExecStop=/bin/sh -c '@ctlscriptpath@/ejabberdctl stop && @ctlscriptpath@/ejabberdctl stopped' ExecReload=@ctlscriptpath@/ejabberdctl reload_config diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 8eb038dd0..b9df7799b 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -36,17 +36,17 @@ listen: - port: 5223 ip: "::" - tls: true module: ejabberd_c2s max_stanza_size: 262144 shaper: c2s_shaper access: c2s - starttls_required: true + tls: true - port: 5269 ip: "::" module: ejabberd_s2s_in max_stanza_size: 524288 + shaper: s2s_shaper - port: 5443 ip: "::" @@ -67,7 +67,7 @@ listen: /admin: ejabberd_web_admin /.well-known/acme-challenge: ejabberd_acme - - port: 3478 + port: 5478 ip: "::" transport: udp module: ejabberd_stun @@ -111,11 +111,19 @@ access_rules: api_permissions: "console commands": - from: - - ejabberd_ctl + from: ejabberd_ctl who: all what: "*" - "admin access": + "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: @@ -156,6 +164,7 @@ shaper_rules: modules: mod_adhoc: {} + mod_adhoc_api: {} mod_admin_extra: {} mod_announce: access: announce @@ -170,7 +179,7 @@ modules: mod_fail2ban: {} mod_http_api: {} mod_http_upload: - put_url: https://@HOST@:5443/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" @@ -196,6 +205,7 @@ modules: default_room_options: mam: true mod_muc_admin: {} + mod_muc_occupantid: {} mod_offline: access_max_user_messages: max_user_offline_messages mod_ping: {} @@ -224,6 +234,7 @@ modules: ip_access: trusted_network mod_roster: versioning: true + mod_s2s_bidi: {} mod_s2s_dialback: {} mod_shared_roster: {} mod_stream_mgmt: diff --git a/ejabberdctl.cfg.example b/ejabberdctl.cfg.example index 8b15933db..88f99cd78 100644 --- a/ejabberdctl.cfg.example +++ b/ejabberdctl.cfg.example @@ -108,10 +108,11 @@ #. #' 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. @@ -120,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 # @@ -183,6 +198,17 @@ # #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 758a85bca..0d124bead 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -15,8 +15,8 @@ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" # shellcheck disable=SC2034 ERTS_VSN="{{erts_vsn}}" ERL="{{erl}}" -IEX="{{bindir}}/iex" EPMD="{{epmd}}" +IEX="{{iexpath}}" INSTALLUSER="{{installuser}}" # check the proper system user is used @@ -66,18 +66,26 @@ done # 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 +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" -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) + 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 @@ -89,11 +97,12 @@ 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="$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="\ +$(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" @@ -121,7 +130,7 @@ set_dist_client() exec_cmd() { case $EXEC_CMD in - as_install_user) su -s /bin/sh -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; + as_install_user) su -s /bin/sh -c 'exec "$0" "$@"' "$INSTALLUSER" -- "$@" ;; as_current_user) "$@" ;; esac } @@ -149,9 +158,11 @@ 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" @@ -172,8 +183,10 @@ 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:" @@ -184,6 +197,39 @@ livewarning() 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 "" @@ -212,16 +258,34 @@ help() } # dynamic node name helper -uid() -{ - uuid=$(uuidgen 2>/dev/null) - random=$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null) - [ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid) - [ -z "$uuid" ] && uuid=$(printf "%X" "${random:-$$}$(date +%M%S)") - uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/') - [ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}" - [ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}" - [ $# -eq 2 ] && echo "${uuid}-${1}@${2}" +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 @@ -254,6 +318,13 @@ check_start() # 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" @@ -264,14 +335,71 @@ wait_status() if [ $timeout -eq 0 ] ; then status="$1" else - exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null + 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" || { @@ -279,6 +407,103 @@ cd "$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) @@ -305,25 +530,31 @@ case $1 in ;; etop) set_dist_client - exec_erl "$(uid top)" -hidden -node "$ERLANG_NODE" -s etop \ - -s erlang halt -output text + 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" --app ejabberd + 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 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ - -s erlang halt -output text + -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 @@ -333,15 +564,11 @@ case $1 in set_dist_client wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout ;; + mnesia_change) + mnesia_change $2 + ;; *) set_dist_client - exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" - result=$? - case $result in - 2|3) help;; - *) :;; - esac - exit $result + 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 31d01b6b8..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_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 b1023ee0e..000000000 --- a/examples/mtr/ejabberd.cfg +++ /dev/null @@ -1,65 +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_muc, []}, - {mod_pubsub, []}, - {mod_time, []}, - {mod_last, []}, - {mod_version, []} - ]}. - - - -% Local Variables: -% mode: erlang -% End: diff --git a/include/bosh.hrl b/include/bosh.hrl index e02675ed0..dd9f1b6a1 100644 --- a/include/bosh.hrl +++ b/include/bosh.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_auth.hrl b/include/ejabberd_auth.hrl index a29b2cc29..bf7660d3f 100644 --- a/include/ejabberd_auth.hrl +++ b/include/ejabberd_auth.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,5 +18,5 @@ %%% %%%---------------------------------------------------------------------- --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', +-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 9009f7bb2..14d19d2e1 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,13 +19,30 @@ %%%---------------------------------------------------------------------- -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: @@ -67,42 +84,24 @@ args_example = none :: none | [any()] | '_', result_example = none :: any()}). -%% TODO Fix me: Type is not up to date --type ejabberd_commands() :: #ejabberd_commands{name :: atom(), - tags :: [atom()], - desc :: string(), - longdesc :: string(), - version :: integer(), - module :: atom(), - function :: atom(), - args :: [aterm()], - policy :: open | restricted | admin | user, - access :: [{atom(),atom(),atom()}|atom()], - result :: rterm()}. +-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() + }. -%% @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. diff --git a/include/ejabberd_ctl.hrl b/include/ejabberd_ctl.hrl index 37935ebe2..cad82da89 100644 --- a/include/ejabberd_ctl.hrl +++ b/include/ejabberd_ctl.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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 92dfd870e..9e1373ce6 100644 --- a/include/ejabberd_http.hrl +++ b/include/ejabberd_http.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_oauth.hrl b/include/ejabberd_oauth.hrl index 7e7509454..4798d9070 100644 --- a/include/ejabberd_oauth.hrl +++ b/include/ejabberd_oauth.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_sm.hrl b/include/ejabberd_sm.hrl index d5286d793..54a828e1a 100644 --- a/include/ejabberd_sm.hrl +++ b/include/ejabberd_sm.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_sql.hrl b/include/ejabberd_sql.hrl index cfa238b48..d0ab55cba 100644 --- a/include/ejabberd_sql.hrl +++ b/include/ejabberd_sql.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,12 +34,14 @@ 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. @@ -48,3 +50,26 @@ 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 index fd61bdd03..f89f5c969 100644 --- a/include/ejabberd_sql_pt.hrl +++ b/include/ejabberd_sql_pt.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_web_admin.hrl b/include/ejabberd_web_admin.hrl index 99f27f8de..45e4beada 100644 --- a/include/ejabberd_web_admin.hrl +++ b/include/ejabberd_web_admin.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -62,6 +62,11 @@ [{<<"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, (translate:translate(Lang, Value)))). @@ -95,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((translate:translate(Lang, Text)))). -define(GL(Ref, Title), ?XAE(<<"div">>, [{<<"class">>, <<"guidelink">>}], [?XAE(<<"a">>, - [{<<"href">>, <<"https://docs.ejabberd.im/admin/configuration/", Ref/binary>>}, + [{<<"href">>, <<"https://docs.ejabberd.im/", Ref/binary>>}, {<<"target">>, <<"_blank">>}], [?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 0787d1f98..0b6dc97e5 100644 --- a/include/eldap.hrl +++ b/include/eldap.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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/http_bind.hrl b/include/http_bind.hrl index 8a5ceb8ab..ab1294e7d 100644 --- a/include/http_bind.hrl +++ b/include/http_bind.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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/logger.hrl b/include/logger.hrl index ed62f3607..e41ab73dd 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -39,20 +39,46 @@ -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), - begin ?LOG_DEBUG(Format, Args), ok end). + begin ?LOG_DEBUG(Format, Args, + #{clevel => ?CLEAD ++ ?CDEBUG, + ctext => ?CMID ++ ?CDEBUG}), + ok end). -define(INFO_MSG(Format, Args), - begin ?LOG_INFO(Format, Args), ok end). + begin ?LOG_INFO(Format, Args, + #{clevel => ?CLEAD ++ ?CINFO, + ctext => ?CCLEAN}), + ok end). -define(WARNING_MSG(Format, Args), - begin ?LOG_WARNING(Format, Args), ok end). + begin ?LOG_WARNING(Format, Args, + #{clevel => ?CLEAD ++ ?CWARNING, + ctext => ?CMID ++ ?CWARNING}), + ok end). -define(ERROR_MSG(Format, Args), - begin ?LOG_ERROR(Format, Args), ok end). + begin ?LOG_ERROR(Format, Args, + #{clevel => ?CLEAD ++ ?CERROR, + ctext => ?CMID ++ ?CERROR}), + ok end). -define(CRITICAL_MSG(Format, Args), - begin ?LOG_CRITICAL(Format, Args), ok end). + begin ?LOG_CRITICAL(Format, Args, + #{clevel => ?CLEAD++ ?CCRITICAL, + ctext => ?CMID ++ ?CCRITICAL}), + ok end). -endif. %% Use only when trying to troubleshoot test problem with ExUnit diff --git a/include/mod_announce.hrl b/include/mod_announce.hrl index 498dce8d0..77badf90e 100644 --- a/include/mod_announce.hrl +++ b/include/mod_announce.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_stacktrace.hrl b/include/mod_antispam.hrl similarity index 59% rename from include/ejabberd_stacktrace.hrl rename to include/mod_antispam.hrl index 2d8fc6e43..c30f24620 100644 --- a/include/ejabberd_stacktrace.hrl +++ b/include/mod_antispam.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,10 +18,19 @@ %%% %%%---------------------------------------------------------------------- --ifdef(DEPRECATED_GET_STACKTRACE). --define(EX_RULE(Class, Reason, Stack), Class:Reason:Stack). --define(EX_STACK(Stack), Stack). --else. --define(EX_RULE(Class, Reason, _), Class:Reason). --define(EX_STACK(_), erlang:get_stacktrace()). --endif. +-define(MODULE_ANTISPAM, mod_antispam). + +-type url() :: binary(). +-type filename() :: binary() | none | false. +-type jid_set() :: sets:set(ljid()). +-type url_set() :: sets:set(url()). + +-define(DEFAULT_RTBL_DOMAINS_NODE, <<"spam_source_domains">>). + +-record(rtbl_service, + {host = none :: binary() | none, + node = ?DEFAULT_RTBL_DOMAINS_NODE :: binary(), + subscribed = false :: boolean(), + retry_timer = undefined :: reference() | undefined}). + +-type rtbl_service() :: #rtbl_service{}. diff --git a/include/mod_caps.hrl b/include/mod_caps.hrl index 4bf404837..ee1bbe44e 100644 --- a/include/mod_caps.hrl +++ b/include/mod_caps.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_last.hrl b/include/mod_last.hrl index aed9bdc6f..b1c13621a 100644 --- a/include/mod_last.hrl +++ b/include/mod_last.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_mam.hrl b/include/mod_mam.hrl index 27c892abe..77ea54a5e 100644 --- a/include/mod_mam.hrl +++ b/include/mod_mam.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,8 @@ bare_peer = {<<"">>, <<"">>, <<"">>} :: ljid(), packet = #xmlel{} :: xmlel() | message(), nick = <<"">> :: binary(), - type = chat :: chat | groupchat}). + type = chat :: chat | groupchat, + origin_id = <<"">> :: binary()}). -record(archive_prefs, {us = {<<"">>, <<"">>} :: {binary(), binary()}, 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 index ea096c9fe..f801b29e1 100644 --- a/include/mod_muc.hrl +++ b/include/mod_muc.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_muc_room.hrl b/include/mod_muc_room.hrl index 48d72b111..5f81fe026 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,13 +38,13 @@ 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(), @@ -125,8 +125,9 @@ roles = #{} :: roles(), history = #lqueue{} :: lqueue(), subject = [] :: [text()], - subject_author = <<"">> :: binary(), - hats_users = #{} :: map(), % FIXME on OTP 21+: #{ljid() => #{binary() => binary()}}, + 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 :: ejabberd_shaper:shaper(), diff --git a/include/mod_offline.hrl b/include/mod_offline.hrl index ba04f9b87..e1bb236f6 100644 --- a/include/mod_offline.hrl +++ b/include/mod_offline.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_privacy.hrl b/include/mod_privacy.hrl index 99cd2310d..8118a6de6 100644 --- a/include/mod_privacy.hrl +++ b/include/mod_privacy.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_private.hrl b/include/mod_private.hrl index 14e3bc8e3..05adc7d8b 100644 --- a/include/mod_private.hrl +++ b/include/mod_private.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_proxy65.hrl b/include/mod_proxy65.hrl index 13f4d8c6a..4f017124a 100644 --- a/include/mod_proxy65.hrl +++ b/include/mod_proxy65.hrl @@ -2,7 +2,7 @@ %%% RFC 1928 constants. %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 index 161b68bdf..8a9de102b 100644 --- a/include/mod_push.hrl +++ b/include/mod_push.hrl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 diff --git a/include/mod_roster.hrl b/include/mod_roster.hrl index 6ec05b3da..a056dd22c 100644 --- a/include/mod_roster.hrl +++ b/include/mod_roster.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_shared_roster.hrl b/include/mod_shared_roster.hrl index a18d37414..4c35878e8 100644 --- a/include/mod_shared_roster.hrl +++ b/include/mod_shared_roster.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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_vcard.hrl b/include/mod_vcard.hrl index 14f8380b8..d97e5c900 100644 --- a/include/mod_vcard.hrl +++ b/include/mod_vcard.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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/mqtt.hrl b/include/mqtt.hrl index 6458a1234..bf910368f 100644 --- a/include/mqtt.hrl +++ b/include/mqtt.hrl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. diff --git a/include/pubsub.hrl b/include/pubsub.hrl index 1811aa926..316be342a 100644 --- a/include/pubsub.hrl +++ b/include/pubsub.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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/install-sh b/install-sh old mode 100644 new mode 100755 diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex index 9d17b157d..85d19191b 100644 --- a/lib/ejabberd/config/attr.ex +++ b/lib/ejabberd/config/attr.ex @@ -41,7 +41,7 @@ defmodule Ejabberd.Config.Attr do """ @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]) |> List.first + def validate(attr), do: validate([attr]) @doc """ Returns the type of an attribute, given its name. diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex index a1b91858a..a8805e612 100644 --- a/lib/ejabberd/config/config.ex +++ b/lib/ejabberd/config/config.ex @@ -36,8 +36,8 @@ defmodule Ejabberd.Config do case force do true -> - Ejabberd.Config.Store.stop - Ejabberd.Config.Store.start_link + Ejabberd.Config.Store.stop() + Ejabberd.Config.Store.start_link() do_init(file_path) false -> if not init_already_executed, do: do_init(file_path) @@ -105,11 +105,8 @@ defmodule Ejabberd.Config do Code.eval_file(file_path) |> extract_and_store_module_name() # Getting start/0 config - Ejabberd.Config.Store.get(:module_name) - |> case do - nil -> IO.puts "[ ERR ] Configuration module not found." - [module] -> call_start_func_and_store_data(module) - end + [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() diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex index 8b7858d23..5f9de4aa0 100644 --- a/lib/ejabberd/config/ejabberd_hook.ex +++ b/lib/ejabberd/config/ejabberd_hook.ex @@ -13,7 +13,6 @@ defmodule Ejabberd.Config.EjabberdHook do @doc """ Register a hook to ejabberd. """ - @spec start(EjabberdHook.t) :: none def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do host = Keyword.get(opts, :host, :global) priority = Keyword.get(opts, :priority, 50) diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex index 6a74fe460..6d5b1e467 100644 --- a/lib/ejabberd/config/ejabberd_module.ex +++ b/lib/ejabberd/config/ejabberd_module.ex @@ -7,12 +7,13 @@ defmodule Ejabberd.Config.EjabberdModule do the already existing Elixir.Module. """ - @type t :: %{module: atom, attrs: [Attr.t]} - - defstruct [:module, :attrs] - 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 @@ -29,7 +30,6 @@ defmodule Ejabberd.Config.EjabberdModule do a git attribute and tries to fetch the repo, then, it install them through :ext_mod.install/1 """ - @spec fetch_git_repos([EjabberdModule.t]) :: none() def fetch_git_repos(modules) do modules |> Enum.filter(&is_git_module?/1) @@ -60,7 +60,7 @@ defmodule Ejabberd.Config.EjabberdModule do defp fetch_and_store_repo_source_if_not_exists(path, repo) do unless File.exists?(path) do IO.puts "[info] Fetching: #{repo}" - :os.cmd('git clone #{repo} #{path}') + :os.cmd(~c"git clone #{repo} #{path}") end end diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex index 90970ba73..822571916 100644 --- a/lib/ejabberd/config/logger/ejabberd_logger.ex +++ b/lib/ejabberd/config/logger/ejabberd_logger.ex @@ -17,9 +17,9 @@ defmodule Ejabberd.Config.EjabberdLogger do 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 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." diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex index b7010ddfe..3d3db926f 100644 --- a/lib/ejabberd/config/opts_formatter.ex +++ b/lib/ejabberd/config/opts_formatter.ex @@ -14,15 +14,12 @@ defmodule Ejabberd.Config.OptsFormatter do Look at how Config.get_ejabberd_opts/0 is constructed for more informations. """ - @spec format_opts_for_ejabberd([{atom(), any()}]) :: list() + @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(opts) when is_list(opts), - do: Enum.map opts, &format_attrs_for_ejabberd/1 - defp format_attrs_for_ejabberd({:listeners, mods}), do: {:listen, format_listeners_for_ejabberd(mods)} @@ -32,6 +29,9 @@ defmodule Ejabberd.Config.OptsFormatter do 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]} diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex index af582676e..227a3545f 100644 --- a/lib/ejabberd/config/validator/validation.ex +++ b/lib/ejabberd/config/validator/validation.ex @@ -3,12 +3,12 @@ defmodule Ejabberd.Config.Validation do Module used to validate a list of modules. """ - @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} - @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map} - 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 diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex index 6a85c068d..e0e133b61 100644 --- a/lib/ejabberd/config/validator/validator_attrs.ex +++ b/lib/ejabberd/config/validator/validator_attrs.ex @@ -3,11 +3,12 @@ defmodule Ejabberd.Config.Validator.Attrs do Validator module used to validate attributes. """ - # TODO: Duplicated from validator.ex !!! - @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} - 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) @@ -17,9 +18,9 @@ defmodule Ejabberd.Config.Validator.Attrs do @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}) + case Attr.validate([attr]) do + [{:ok, _attr}] -> err + [{:error, attr, cause}] -> put_error(err, :attribute, {attr, cause}) end end diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex index d44c8a136..4eb466663 100644 --- a/lib/ejabberd/config/validator/validator_dependencies.ex +++ b/lib/ejabberd/config/validator/validator_dependencies.ex @@ -4,6 +4,8 @@ defmodule Ejabberd.Config.Validator.Dependencies do 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 diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex index 17805f748..6047618b6 100644 --- a/lib/ejabberd/config/validator/validator_utility.ex +++ b/lib/ejabberd/config/validator/validator_utility.ex @@ -4,8 +4,6 @@ defmodule Ejabberd.Config.ValidatorUtility do Imports utility functions for working with validation structures. """ - alias Ejabberd.Config.EjabberdModule - @doc """ Inserts an error inside the errors collection, for the given key. If the key doesn't exists then it creates an empty collection @@ -22,7 +20,6 @@ defmodule Ejabberd.Config.ValidatorUtility do Given a list of modules it extracts and returns a list of the module names (which are Elixir.Module). """ - @spec extract_module_names(EjabberdModule.t) :: [atom] def extract_module_names(modules) when is_list(modules) do modules |> Enum.map(&Map.get(&1, :module)) diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex index 6592104a2..71d854f15 100644 --- a/lib/ejabberd/config_util.ex +++ b/lib/ejabberd/config_util.ex @@ -7,7 +7,7 @@ defmodule Ejabberd.ConfigUtil do @doc """ Returns true when the config file is based on elixir. """ - @spec is_elixir_config(list) :: boolean + @spec is_elixir_config(binary) :: boolean def is_elixir_config(filename) when is_list(filename) do is_elixir_config(to_string(filename)) end diff --git a/lib/ejabberd/module.ex b/lib/ejabberd/module.ex deleted file mode 100644 index 9fb3f040c..000000000 --- a/lib/ejabberd/module.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Ejabberd.Module do - - defmacro __using__(opts) do - logger_enabled = Keyword.get(opts, :logger, true) - - quote do - @behaviour :gen_mod - import Ejabberd.Module - - unquote(if logger_enabled do - quote do: import Ejabberd.Logger - end) - end - end - - # gen_mod callbacks - def depends(_host, _opts), do: [] - def mod_opt_type(_), do: [] -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 index a3439c40b..e93b4aa48 100644 --- a/lib/mix/tasks/deps.tree.ex +++ b/lib/mix/tasks/deps.tree.ex @@ -14,15 +14,15 @@ defmodule Mix.Tasks.Ejabberd.Deps.Tree do 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.Store.start_link() Ejabberd.Config.init(:ejabberd_config.path()) - Mix.shell.info "ejabberd modules" + 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 + |> Mix.shell().info end defp format_mods(mods) when is_list(mods) do 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 f41a53a31..000000000 --- a/lib/mod_presence_demo.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule ModPresenceDemo do - use Ejabberd.Module - - def start(host, _opts) do - info('Starting ejabberd module Presence Demo') - Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50) - :ok - end - - def stop(host) do - info('Stopping ejabberd module Presence Demo') - 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/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 index a14a5c730..aa42e20b2 100644 --- a/man/ejabberd.yml.5 +++ b/man/ejabberd.yml.5 @@ -2,12 +2,12 @@ .\" Title: ejabberd.yml .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets vsnapshot -.\" Date: 10/12/2022 +.\" Date: 08/22/2025 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" -.TH "EJABBERD\&.YML" "5" "10/12/2022" "\ \&" "\ \&" +.TH "EJABBERD\&.YML" "5" "08/22/2025" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -82,17 +82,17 @@ All options can be changed in runtime by running \fIejabberdctl reload\-config\f .sp Some options can be specified for particular virtual host(s) only using \fIhost_config\fR or \fIappend_host_config\fR options\&. Such options are called \fIlocal\fR\&. Examples are \fImodules\fR, \fIauth_method\fR and \fIdefault_db\fR\&. The options that cannot be defined per virtual host are called \fIglobal\fR\&. Examples are \fIloglevel\fR, \fIcertfiles\fR and \fIlisten\fR\&. It is a configuration mistake to put \fIglobal\fR options under \fIhost_config\fR or \fIappend_host_config\fR section \- ejabberd will refuse to load such configuration\&. .sp -It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/22\&.10/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&. +It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/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\&. +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: ACLRules|ACLName}}\fR +\fBaccess_rules\fR: \fI{AccessName: {allow|deny: ACLName|ACLDefinition}}\fR .RS 4 This option defines -Access Rules\&. Each access rule is assigned a name that can be referenced from other parts of the configuration file (mostly from +\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 @@ -138,7 +138,8 @@ access_rules: .PP \fBacl\fR: \fI{ACLName: {ACLType: ACLValue}}\fR .RS 4 -The option defines 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 +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 @@ -247,7 +248,7 @@ is in the form of "regexp", the rule matches any JID with node part matching reg .PP \fBacme\fR: \fIOptions\fR .RS 4 -ACME +\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: @@ -303,7 +304,9 @@ acme: .PP \fBallow_contrib_modules\fR: \fItrue | false\fR .RS 4 -Whether to allow installation of third\-party modules or not\&. The default value is +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 @@ -317,7 +320,9 @@ means that the same username can be taken multiple times in anonymous login mode .PP \fBanonymous_protocol\fR: \fIlogin_anon | sasl_anon | both\fR .RS 4 -Define what anonymous protocol will be used: +Define what +\fIauthentication\&.md#anonymous\-login\-and\-sasl\-anonymous|anonymous\fR +protocol will be used: .sp .RS 4 .ie n \{\ @@ -361,16 +366,13 @@ The default value is \fIsasl_anon\fR\&. \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 → -API Permissions\&. +\fI\&.\&./\&.\&./developer/ejabberd\-api/permissions\&.md|API Permissions\fR\&. .RE .PP \fBappend_host_config\fR: \fI{Host: Options}\fR .RS 4 -To define specific ejabberd modules in a virtual host, you can define the global -\fImodules\fR -option with the common modules, and later add specific modules to certain virtual hosts\&. To accomplish that, -\fIappend_host_config\fR -option can be used\&. +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 @@ -397,9 +399,22 @@ Same as 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 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 +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 @@ -408,15 +423,16 @@ A list of authentication methods to use\&. If several methods are defined, authe This is used by the contributed module \fIejabberd_auth_http\fR that can be installed from the -ejabberd\-contrib +\fI\&.\&./\&.\&./admin/guide/modules\&.md#ejabberd\-contrib|ejabberd\-contrib\fR Git repository\&. Please refer to that module\(cqs README file for details\&. .RE -.sp -\fINote\fR about the next option: improved in 20\&.01: .PP \fBauth_password_format\fR: \fIplain | scram\fR .RS 4 -The option defines in what format the users passwords are stored: +\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 \{\ @@ -426,7 +442,7 @@ The option defines in what format the users passwords are stored: .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\&. +\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 @@ -437,17 +453,39 @@ The option defines in what format the users passwords are stored: .sp -1 .IP \(bu 2.3 .\} -\fIscram\fR: The password is not stored, only some information that allows to verify the hash provided by the client\&. It is impossible to obtain the original plain password from the stored information; for this reason, when this value is configured it cannot be changed to plain anymore\&. This format allows clients to authenticate using: SASL PLAIN and SASL SCRAM\-SHA\-1\&. The default value is -\fIplain\fR\&. +\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 SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. The default value is +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 @@ -463,9 +501,9 @@ Full path to a file containing one or more CA certificates in PEM format\&. All field\&. There is no default value\&. .RE .sp -You can use host_config to specify this option per\-vhost\&. +You can use \fIhost_config\fR to specify this option per\-vhost\&. .sp -To set a specific file per listener, use the listener\(cqs cafile option\&. Please notice that \fIc2s_cafile\fR overrides the listener\(cqs \fIcafile\fR option\&. +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 @@ -491,7 +529,8 @@ c2s_ciphers: .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 "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\&. +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 @@ -526,7 +565,7 @@ Whether to enable or disable TLS compression for c2s connections\&. The default 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 s2s_cafile option\&. +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 @@ -559,14 +598,21 @@ A maximum number of items (not memory!) in cache\&. The rule of thumb, for all t \fIrouter_cache_size\fR, and \fIsm_cache_size\fR\&. .RE -.sp -\fINote\fR about the next option: improved in 21\&.10: .PP -\fBcaptcha_cmd\fR: \fIPath\fR +\fBcaptcha_cmd\fR: \fIPath | ModuleName\fR .RS 4 -Full path to a script that generates -CAPTCHA -images\&. @VERSION@ is replaced with ejabberd version number in XX\&.YY format\&. @SEMVER@ is replaced with ejabberd version number in semver format when compiled with Elixir\(cqs mix, or XX\&.YY format otherwise\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. +\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 @@ -591,20 +637,28 @@ instead\&. \fBcaptcha_limit\fR: \fIpos_integer() | infinity\fR .RS 4 Maximum number of -CAPTCHA +\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\fR +\fBcaptcha_url\fR: \fIURL | auto | undefined\fR .RS 4 -An URL where -CAPTCHA +\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\&. There is no default value\&. +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 @@ -616,6 +670,8 @@ and so on\&. NOTE: if you modify the certificate files or change the value of th \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: @@ -646,23 +702,55 @@ A list of Erlang nodes to connect on ejabberd startup\&. This option is mostly i .PP \fBdefault_db\fR: \fImnesia | sql\fR .RS 4 -Default persistent storage for ejabberd\&. Modules and other components (e\&.g\&. authentication) may have its own value\&. The default value is +\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\&. Modules and other components (e\&.g\&. session management) may have its own value\&. The default value is +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_macro\fR: \fI{MacroName: MacroValue}\fR +\fBdefine_keyword\fR: \fI{NAME: Value}\fR .RS 4 -Defines a macro\&. The value can be any valid arbitrary YAML value\&. For convenience, it\(cqs recommended to define a -\fIMacroName\fR -in capital letters\&. Duplicated macros are not allowed\&. 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 is possible to use a -\fIMacroValue\fR -in the definition of another macro\&. +\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 @@ -696,9 +784,19 @@ or 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 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 +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: @@ -708,28 +806,43 @@ are: The number of components to balance\&. .RE .PP -\fBtype\fR: \fIrandom | source | destination | bare_source | bare_destination\fR +\fBtype\fR: \fIValue\fR .RS 4 -How to deliver stanzas to connected components: -\fIrandom\fR -\- an instance is chosen at random; -\fIdestination\fR -\- an instance is chosen by the full JID of the packet\(cqs +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; -\fIsource\fR -\- by the full JID of the packet\(cqs +attribute +.RE +.PP +\fB\- bare_source\fR +.RS 4 +by the bare JID (without resource) of the packet\(cqs \fIfrom\fR -attribute; -\fIbare_destination\fR -\- by the the bare JID (without resource) of the packet\(cqs +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; -\fIbare_source\fR -\- by the bare JID (without resource) of the packet\(cqs +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 is used\&. The default value is -\fIrandom\fR\&. +attribute .RE .sp \fBExample\fR: @@ -777,18 +890,22 @@ Define the base URI when performing ReST requests\&. The default value is: .PP \fBextauth_pool_name\fR: \fIName\fR .RS 4 -Define the pool name appendix, so the full pool name will be +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 external program to start for better load balancing\&. The default is the number of available CPU cores\&. +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 external authentication script\&. The script must be executable by ejabberd\&. +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 @@ -807,7 +924,8 @@ for backward compatibility\&. .RS 4 The option is used to redefine \fIOptions\fR -for virtual host +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 @@ -838,16 +956,46 @@ host_config: .PP \fBhosts\fR: \fI[Domain1, Domain2, \&.\&.\&.]\fR .RS 4 -The option defines a list containing one or more domains that -\fIejabberd\fR -will serve\&. This is a +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 additional configuration from +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 @@ -867,9 +1015,20 @@ Disallows the usage of those options in the included file .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 this 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 +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 @@ -877,18 +1036,24 @@ This ACL rule defines accounts that can use only this auth method, even if other .RS 4 By default, the JID is defined in the \fI"jid"\fR -JWT field\&. This option allows to specify other JWT field name where the JID is defined\&. +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 JWK Key\&. The default value is +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 -The option defines the default language of server strings that can be seen by XMPP clients\&. If an XMPP client does not possess +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\&. @@ -896,9 +1061,10 @@ attribute, the specified language is used\&. The default value is .PP \fBldap_backups\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 -A list of IP addresses or DNS names of LDAP backup servers\&. When no servers listed in +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 will try to connect to these backup servers\&. The default is an empty list, i\&.e\&. no backup servers specified\&. WARNING: ejabberd doesn\(cqt try to reconnect back to the main servers when they become operational again, so the only way to restore these connections is to restart ejabberd\&. This limitation might be fixed in future releases\&. +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 @@ -915,10 +1081,23 @@ Whether to dereference aliases or not\&. The default value is \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 "%u", "%d", "%s" and "%D" pattern variables in -\fIFilter\fR: "%u" is replaced by a user\(cqs 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 +\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 "%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 +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 @@ -945,7 +1124,10 @@ Whether to encrypt LDAP connection using TLS or not\&. The default value is \fBldap_filter\fR: \fIFilter\fR .RS 4 An LDAP filter as defined in -RFC4515\&. There is no default value\&. Example: "(&(objectClass=shadowAccount)(memberOf=XMPP Users))"\&. NOTE: don\(cqt forget to close brackets and don\(cqt use superfluous whitespaces\&. Also you must not use "uid" attribute in the filter because this attribute will be appended to the filter automatically\&. +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 @@ -969,7 +1151,8 @@ Bind Distinguished Name\&. The default value is an empty string, which means "an .PP \fBldap_servers\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 -A list of IP addresses or DNS names of your LDAP servers\&. The default value is +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 @@ -1007,34 +1190,45 @@ LDAP attributes which hold a list of attributes to use as alternatives for getti \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 "%u" which will be replaced by the user\(cqs part of the JID\&. For example, "%u@example\&.org"\&. If the value is in the form of +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 "%u"\&. +is assumed to be +\fI"%u"\fR\&. .RE .PP \fBlisten\fR: \fI[Options, \&.\&.\&.]\fR .RS 4 The option for listeners configuration\&. See the -Listen Modules +\fIlisten\&.md|Listen Modules\fR section for details\&. .RE -.sp -\fINote\fR about the next option: added in 22\&.10: .PP \fBlog_burst_limit_count\fR: \fINumber\fR .RS 4 -The number of messages to accept in +\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 +period before starting to drop them\&. Default +500 .RE -.sp -\fINote\fR about the next option: added in 22\&.10: .PP \fBlog_burst_limit_window_time\fR: \fINumber\fR .RS 4 -The time period to rate\-limit log messages by\&. Defaults to 1 second\&. +\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 @@ -1050,14 +1244,14 @@ crash\&.log\&.0\&. \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 -\fI10485760\fR -(that is, 10 Mb)\&. +\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 log files generated by ejabberd\&. The default value is +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 @@ -1069,15 +1263,15 @@ This option specifies the maximum number of elements in the queue of the FSM (Fi .PP \fBmodules\fR: \fI{Module: Options}\fR .RS 4 -The option for modules configuration\&. See -Modules -section for details\&. +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 -\fI30\fR +\fI120\fR seconds\&. .RE .PP @@ -1090,12 +1284,11 @@ This option can be used to tune tick time parameter of .PP \fBnew_sql_schema\fR: \fItrue | false\fR .RS 4 -Whether to use +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 -SQL schema\&. All schemas are located at -https://github\&.com/processone/ejabberd/tree/22\&.10/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The -\fInew\fR -schema allows to handle several XMPP domains in a single ejabberd database\&. Using this +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 @@ -1126,12 +1319,11 @@ Same as \fIcache_missed\fR will be used\&. .RE -.sp -\fINote\fR about the next option: added in 21\&.01: .PP \fBoauth_cache_rest_failure_life_time\fR: \fItimeout()\fR .RS 4 -The time that a failure in OAuth ReST is cached\&. The default value is +\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 @@ -1203,26 +1395,27 @@ option)\&. Later, when memory drops below this percents\&. .RE .PP -\fBoutgoing_s2s_families\fR: \fI[ipv4 | ipv6, \&.\&.\&.]\fR +\fBoutgoing_s2s_families\fR: \fI[ipv6 | ipv4, \&.\&.\&.]\fR .RS 4 -Specify which address families to try, in what order\&. The default is -\fI[ipv4, ipv6]\fR -which means it first tries connecting with IPv4, if that fails it tries using IPv6\&. +\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 -.sp -\fINote\fR about the next option: added in 20\&.12: .PP \fBoutgoing_s2s_ipv4_address\fR: \fIAddress\fR .RS 4 -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 +\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 -.sp -\fINote\fR about the next option: added in 20\&.12: .PP \fBoutgoing_s2s_ipv6_address\fR: \fIAddress\fR .RS 4 -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 +\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 @@ -1241,13 +1434,17 @@ seconds\&. .PP \fBpam_service\fR: \fIName\fR .RS 4 -This option defines the PAM service name\&. Refer to the PAM documentation of your operation system for more information\&. The default value is +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 PAM service: only the username, or the user\(cqs JID\&. Default is +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 @@ -1281,36 +1478,47 @@ option where file queues will be placed\&. The default value is .PP \fBredis_connect_timeout\fR: \fItimeout()\fR .RS 4 -A timeout to wait for the connection to be re\-established to the Redis server\&. The default is +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 -Redis database number\&. The default is +\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 Redis server\&. The default is an empty string, i\&.e\&. no password\&. +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 Redis server\&. The default value is +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 Redis server is accepting connections\&. The default is +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 Redis server\&. See description of +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 @@ -1319,9 +1527,13 @@ or if the latter is not set\&. .RE .PP -\fBredis_server\fR: \fIHostname\fR +\fBredis_server\fR: \fIHost | IP Address | Unix Socket Path\fR .RS 4 -A hostname or an IP address of the Redis server\&. The default is +\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 @@ -1341,6 +1553,30 @@ XMPP Core: section 7\&.7\&.2\&.2\&. The default value 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 @@ -1392,7 +1628,7 @@ seconds\&. \fBs2s_access\fR: \fIAccess\fR .RS 4 This -Access Rule +\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 @@ -1400,11 +1636,11 @@ defines to what remote servers can s2s connections be established\&. The default \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 -ca_file +\fIca_file\fR will be used\&. .RE .sp -You can use host_config to specify this option per\-vhost\&. +You can use \fIhost_config\fR to specify this option per\-vhost\&. .PP \fBs2s_ciphers\fR: \fI[Cipher, \&.\&.\&.]\fR .RS 4 @@ -1430,7 +1666,8 @@ s2s_ciphers: .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 "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\&. +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 @@ -1524,7 +1761,8 @@ XEP\-0138) or not\&. The default value is .PP \fBshaper\fR: \fI{ShaperName: Rate}\fR .RS 4 -The option defines a set of shapers\&. Every shaper is assigned a name +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 @@ -1554,9 +1792,11 @@ shaper: .\} .RE .PP -\fBshaper_rules\fR: \fI{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}\fR +\fBshaper_rules\fR: \fI{ShaperRuleName: {Number|ShaperName: ACLName|ACLDefinition}}\fR .RS 4 -An entry allowing to declaring shaper to use for matching user/hosts\&. Semantics is similar to +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 @@ -1642,19 +1882,29 @@ An SQL database name\&. For SQLite this must be a full path to a database file\& \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 -.sp -\fINote\fR about the next option: added in 20\&.12: .PP \fBsql_odbc_driver\fR: \fIPath\fR .RS 4 -Path to the ODBC driver to use to connect to a Microsoft SQL Server database\&. This option is only valid if the +\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\&. The default value is: +\fImssql\fR +and +\fIsql_server\fR +is not an ODBC connection string\&. The default value is: \fIlibtdsodbc\&.so\fR .RE .PP @@ -1665,7 +1915,8 @@ The password for SQL authentication\&. The default is empty string\&. .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 10\&. WARNING: for SQLite this value is +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 @@ -1680,14 +1931,13 @@ for PostgreSQL and \fI1433\fR for MS SQL\&. The option has no effect for SQLite\&. .RE -.sp -\fINote\fR about the next option: added in 20\&.01: .PP \fBsql_prepared_statements\fR: \fItrue | false\fR .RS 4 -This option is +\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\&. +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 @@ -1708,17 +1958,28 @@ or if the latter is not set\&. .RE .PP -\fBsql_server\fR: \fIHost\fR +\fBsql_server\fR: \fIHost | IP Address | ODBC Connection String | Unix Socket Path\fR .RS 4 -A hostname or an IP address of the SQL server\&. The default value is +\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 -.sp -\fINote\fR about the next option: improved in 20\&.03: .PP \fBsql_ssl\fR: \fItrue | false\fR .RS 4 -Whether to use SSL encrypted connections to the SQL server\&. The option is only available for MySQL and PostgreSQL\&. The default value is +\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 @@ -1729,7 +1990,7 @@ A path to a file with CA root certificates that will be used to verify SQL conne and \fIsql_ssl_verify\fR options are set to -\fItrue\fR\&. There is no default which means certificate verification is disabled\&. +\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 @@ -1737,7 +1998,7 @@ options are set to 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\&. +\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 @@ -1747,7 +2008,7 @@ Whether to verify SSL connection to the SQL server against CA root certificates option\&. Implies \fIsql_ssl\fR option is set to -\fItrue\fR\&. The default value is +\fItrue\fR\&. This option has no effect for MS SQL\&. The default value is \fIfalse\fR\&. .RE .PP @@ -1775,12 +2036,25 @@ A user name for SQL authentication\&. The default value is 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\&. This allows, if enabled, to be able to know the real IP of the request, for admin purpose, or security configuration (for example using +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 @@ -1812,7 +2086,8 @@ This option enables validation for 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 "https://test\&.example\&.org:8081"\&. +is +\fI"https://test\&.example\&.org:8081"\fR\&. .RE .PP \fBwebsocket_ping_interval\fR: \fItimeout()\fR @@ -1832,9 +2107,13 @@ seconds\&. .RE .SH "MODULES" .sp -This section describes options of all ejabberd modules\&. +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 @@ -1851,45 +2130,74 @@ 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 -.RS 4 -.ie n \{\ -\h'-04'\(bu\h'+03'\c -.\} -.el \{\ -.sp -1 -.IP \(bu 2.3 -.\} -\fIban\-acount\fR: 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\&. -.RE +\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 -.RS 4 -.ie n \{\ -\h'-04'\(bu\h'+03'\c -.\} -.el \{\ -.sp -1 -.IP \(bu 2.3 -.\} -\fIpushroster\fR: (and -\fIpushroster\-all\fR) 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\&. -.RE +\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 -.RS 4 -.ie n \{\ -\h'-04'\(bu\h'+03'\c -.\} -.el \{\ -.sp -1 -.IP \(bu 2.3 -.\} -\fIsrg\-create\fR: If you want to put a group Name with blankspaces, use the characters "\*(Aq and \*(Aq" to define when the Name starts and ends\&. See an example below\&. -.RE +\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 @@ -1922,7 +2230,7 @@ modules: .RE .\} .sp -Content of roster file for \fIpushroster\fR command: +Content of roster file for \fIpush_roster\fR API: .sp .if n \{\ .RS 4 @@ -1936,44 +2244,60 @@ Content of roster file for \fIpushroster\fR command: .RE .\} .sp -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 \fIBANNED_ACCOUNT\(em20080425T21:45:07\(em2176635\(emSpammed_rooms\fR +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" +ejabberdctl vhost example\&.org ban_account boby "Spammed rooms" .fi .if n \{\ .RE .\} .sp -Call to srg\-create using double\-quotes and single\-quotes: +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 +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 Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR API\&. +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\&. +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 -Note that 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\&. +.\} +.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 Ad\-hoc Commands are listed in the Server Discovery\&. For this feature to work, \fImod_adhoc\fR must be enabled\&. +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 -The specific JIDs where messages can be sent are listed below\&. The first JID in each entry will apply only to the specified virtual host example\&.org, while the JID between brackets will apply to all virtual hosts in ejabberd: +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 \{\ @@ -1983,7 +2307,7 @@ The specific JIDs where messages can be sent are listed below\&. The first JID i .sp -1 .IP \(bu 2.3 .\} -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 offline storage (see +\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 @@ -1995,7 +2319,7 @@ example\&.org/announce/all (example\&.org/announce/all\-hosts/all):: The message .sp -1 .IP \(bu 2.3 .\} -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\&. +\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 @@ -2006,7 +2330,8 @@ example\&.org/announce/online (example\&.org/announce/all\-hosts/online):: The m .sp -1 .IP \(bu 2.3 .\} -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 announce/online)\&. +\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 @@ -2017,7 +2342,7 @@ example\&.org/announce/motd (example\&.org/announce/all\-hosts/motd):: The messa .sp -1 .IP \(bu 2.3 .\} -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 not sent to any currently connected user\&. +\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 @@ -2028,7 +2353,64 @@ example\&.org/announce/motd/update (example\&.org/announce/all\-hosts/motd/updat .sp -1 .IP \(bu 2.3 .\} -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)\&. +\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 @@ -2081,6 +2463,186 @@ Same as top\-level 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\&. @@ -2130,7 +2692,7 @@ Limit any given JID by the number of avatars it is able to convert per minute\&. .RE .SS "mod_block_strangers" .sp -This module allows to block/log 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\&. +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 @@ -2171,7 +2733,7 @@ and some server\(cqs JID is in user\(cqs roster, then messages from any user of \fBcaptcha\fR: \fItrue | false\fR .RS 4 Whether to generate CAPTCHA or not in response to messages from strangers\&. See also section -CAPTCHA +\fIbasic\&.md#captcha|CAPTCHA\fR of the Configuration Guide\&. The default value is \fIfalse\fR\&. .RE @@ -2396,21 +2958,104 @@ While a client is inactive, queue presence stanzas that indicate (un)availabilit .RE .SS "mod_configure" .sp -The module provides server configuration functionality via XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR to be loaded\&. +The module provides server configuration functionalities using XEP\-0030: Service Discovery and XEP\-0050: Ad\-Hoc Commands: .sp -The module has no options\&. +.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 -This module is available since ejabberd 21\&.12\&. Several options were improved in ejabberd 22\&.05\&. +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 this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. -.sp -Make sure either \fImod_bosh\fR or \fIejabberd_http_ws\fR request_handlers are enabled\&. +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 @@ -2435,20 +3080,29 @@ Converse CSS URL\&. The keyword is replaced with the hostname\&. The default value is \fIauto\fR\&. .RE -.sp -\fINote\fR about the next option: added in 22\&.05: .PP \fBconversejs_options\fR: \fI{Name: Value}\fR .RS 4 -Specify additional options to be passed to Converse\&. See +\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 -.sp -\fINote\fR about the next option: added in 22\&.05: +.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 -Local path to the Converse files\&. If not set, the public Converse client will be used instead\&. +\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 @@ -2469,9 +3123,9 @@ is replaced with the hostname\&. The default value is .PP \fBwebsocket_url\fR: \fIauto | WebSocketURL\fR .RS 4 -A WebSocket URL to which Converse can connect to\&. The keyword +A WebSocket URL to which Converse can connect to\&. The \fI@HOST@\fR -is replaced with the real virtual host name\&. If set to +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 @@ -2503,6 +3157,7 @@ listen: modules: mod_bosh: {} mod_conversejs: + conversejs_plugins: ["libsignal"] websocket_url: "ws://@HOST@:5280/websocket" .fi .if n \{\ @@ -2526,7 +3181,9 @@ listen: modules: mod_conversejs: - conversejs_resources: "/home/ejabberd/conversejs\-9\&.0\&.0/package/dist" + 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 @@ -2641,7 +3298,6 @@ acl: server: sat\-pubsub\&.example\&.org modules: - \&.\&.\&. mod_delegation: namespaces: urn:xmpp:mam:1: @@ -2790,16 +3446,18 @@ hour\&. 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 -This module is available since ejabberd 22\&.05\&. +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 this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. -.sp -Notice it only works if ejabberd_http has tls enabled\&. +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 @@ -2863,15 +3521,31 @@ modules: .RE .SS "mod_http_api" .sp -This module provides a ReST interface to call ejabberd API commands using JSON data\&. +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 → request_handlers\&. +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 \fIvN\fR\&. For example: \fI/api/v2: mod_http_api\fR +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/\fR +To run a command, send a POST request to the corresponding URL: \fIhttp://localhost:5280/api/COMMAND\-NAME\fR .sp -The module has no options\&. +.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 @@ -2893,7 +3567,8 @@ listen: /api: mod_http_api modules: - mod_http_api: {} + mod_http_api: + default_version: 2 .fi .if n \{\ .RE @@ -2984,25 +3659,20 @@ List of accounts that are allowed to use this service\&. Default value: \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/archive/\fR\&. In this example a new content type \fIogg\fR is defined, \fIpng\fR is redefined, and \fIjpg\fR definition is deleted: +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/archive: mod_http_fileserver - \&.\&.\&. - \&.\&.\&. + /pub/content: mod_http_fileserver modules: - \&.\&.\&. mod_http_fileserver: docroot: /var/www accesslog: /var/log/ejabberd/access\&.log @@ -3016,7 +3686,6 @@ modules: \&.ogg: audio/ogg \&.png: image/png default_content_type: text/html - \&.\&.\&. .fi .if n \{\ .RE @@ -3026,7 +3695,7 @@ modules: .sp This module allows for requesting permissions to upload a file via HTTP as described in XEP\-0363: HTTP File Upload\&. If the request is accepted, the client receives a URL for uploading the file and another URL from which that file can later be downloaded\&. .sp -In order to use this module, it must be enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. +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 @@ -3051,26 +3720,34 @@ This option specifies additional header fields to be included in all HTTP respon .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 chmod(1) manual page) within double quotes\&. For example: "0755"\&. The default is undefined, which means no explicit permissions will be set\&. +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 @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"\&. +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 -in the -\fIImplementation\fR -section\&. There is no default value\&. +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 chmod(1) manual page) within double quotes\&. For example: "0644"\&. The default is undefined, which means no explicit permissions will be set\&. +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 @@ -3078,8 +3755,9 @@ This option defines the permission bits of uploaded files\&. The bits are specif This option specifies the initial part of the GET URLs used for downloading the files\&. The default value is \fIundefined\fR\&. When this option is \fIundefined\fR, this option is set to the same value as -\fIput_url\fR\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: if GET requests are handled by -\fImod_http_upload\fR, the +\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 @@ -3098,7 +3776,8 @@ instead\&. .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 "upload\&."\&. The keyword +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 @@ -3121,12 +3800,18 @@ must be specified\&. The default value is .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 "HTTP File Upload"\&. +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 @HOST@ is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is "https://@HOST@:5443/upload"\&. +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 @@ -3138,7 +3823,9 @@ This option specifies whether files uploaded by a user should be removed when th \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 8 characters, but it is recommended to choose a larger value\&. The default value is +\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 @@ -3159,30 +3846,22 @@ A custom vCard of the service that will be displayed by some XMPP clients in Ser \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp -For example, the following XML representation of vCard: -.sp -.if n \{\ -.RS 4 -.\} -.nf - - Conferences - - - Elm Street - - -.fi -.if n \{\ -.RE -.\} -.sp -will be translated to: +\fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf +# This XML representation of vCard: +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: vcard: fn: Conferences adr: @@ -3209,23 +3888,17 @@ vcard: .\} .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 @@ -3247,7 +3920,7 @@ This module depends on \fImod_http_upload\fR\&. .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 +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 @@ -3277,26 +3950,22 @@ directory, once per day\&. The default value is \fBExamples:\fR .RS 4 .sp -Please note that 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: +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 @@ -3390,7 +4059,23 @@ This type of authentication was obsoleted in 2008 and you unlikely need this mod The module has no options\&. .SS "mod_mam" .sp -This module implements XEP\-0313: Message Archive Management\&. Compatible XMPP clients can use it to store their chat history on the server\&. +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 @@ -3485,9 +4170,113 @@ option, but applied to this module only\&. .PP \fBuser_mucsub_from_muc_archive\fR: \fItrue | false\fR .RS 4 -When this option is disabled, for each individual subscriber a separa 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 +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 @@ -3616,9 +4405,11 @@ An internet port number at which the backend is listening for incoming connectio .RE .SS "mod_mix" .sp -This module is an experimental implementation of XEP\-0369: Mediated Information eXchange (MIX)\&. MIX support was added in ejabberd 16\&.03 as an experimental feature, updated in 19\&.02, and is not yet ready to use in production\&. It\(cqs asserted that the MIX protocol is going to replace the MUC protocol in the future (see \fImod_muc\fR)\&. +\fINote\fR about this option: added in 16\&.03 and improved in 19\&.02\&. .sp -To learn more about how to use that feature, you can refer to our tutorial: Getting started with XEP\-0369: Mediated Information eXchange (MIX) v0\&.1\&. +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 @@ -3654,7 +4445,8 @@ instead\&. .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 "mix\&."\&. The keyword +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 @@ -3730,7 +4522,7 @@ option, but applied to this module only\&. .RE .SS "mod_mqtt" .sp -This module adds support for the MQTT protocol version \fI3\&.1\&.1\fR and \fI5\&.0\fR\&. Remember to configure \fImod_mqtt\fR in \fImodules\fR and \fIlisten\fR sections\&. +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 @@ -3832,12 +4624,97 @@ Same as top\-level 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 @@ -3883,16 +4760,16 @@ room option\&. The default value is .PP \fBaccess_register\fR: \fIAccessName\fR .RS 4 -This option specifies who is allowed to register nickname within the Multi\-User Chat service\&. The default is +\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\&. +for backward compatibility, which means that any user is allowed to register any free nick in the MUC service and in the rooms\&. .RE -.sp -\fINote\fR about the next option: added in 22\&.05: .PP \fBcleanup_affiliations_on_start\fR: \fItrue | false\fR .RS 4 -Remove affiliations for non\-existing local users on startup\&. The default value is +\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 @@ -3902,12 +4779,10 @@ Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE -.sp -\fINote\fR about the next option: improved in 22\&.05: .PP \fBdefault_room_options\fR: \fIOptions\fR .RS 4 -This 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 +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 @@ -3917,12 +4792,6 @@ Allow occupants to change the subject\&. The default value is \fItrue\fR\&. .RE .PP -\fBallow_private_messages\fR: \fItrue | false\fR -.RS 4 -Occupants can send private messages to other occupants\&. 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 @@ -3939,7 +4808,7 @@ Occupants can send IQ queries to other occupants\&. The default value is \fBallow_subscription\fR: \fItrue | false\fR .RS 4 Allow users to subscribe to room events as described in -Multi\-User Chat Subscriptions\&. The default value is +\fI\&.\&./\&.\&./developer/xmpp\-clients\-bots/extensions/muc\-sub\&.md|Multi\-User Chat Subscriptions\fR\&. The default value is \fIfalse\fR\&. .RE .PP @@ -3967,6 +4836,12 @@ 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 @@ -3976,7 +4851,7 @@ The room is anonymous: occupants don\(cqt see the real JIDs of other occupants\& \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 -CAPTCHA +\fIbasic\&.md#captcha|CAPTCHA\fR in order to accept their join in the room\&. The default value is \fIfalse\fR\&. .RE @@ -3988,7 +4863,10 @@ Short description of the room\&. The default value is an empty string\&. .PP \fBenable_hats\fR: \fItrue | false\fR .RS 4 -Allow extended roles as defined in XEP\-0317 Hats\&. The default value is +\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 @@ -4054,7 +4932,7 @@ The room persists even if the last participant leaves\&. The default value is \fIfalse\fR\&. .RE .PP -\fBpresence_broadcast\fR: \fI[moderator | participant | visitor, \&.\&.\&.]\fR +\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, @@ -4104,6 +4982,12 @@ A human\-readable title of the room\&. There is no default value 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 @@ -4119,7 +5003,10 @@ Timeout before hibernating the room process, expressed in seconds\&. The default .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 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\(cqre only using modern clients and have +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 @@ -4139,20 +5026,18 @@ option is not specified, the only Jabber ID will be the hostname of the virtual \fI@HOST@\fR is replaced with the real virtual host name\&. .RE -.sp -\fINote\fR about the next option: added in 21\&.01: .PP \fBmax_captcha_whitelist\fR: \fINumber\fR .RS 4 -This option defines the maximum number of characters that Captcha Whitelist can have when configuring the room\&. The default value is +\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 -.sp -\fINote\fR about the next option: added in 21\&.01: .PP \fBmax_password\fR: \fINumber\fR .RS 4 -This option defines the maximum number of characters that Password can have when configuring the room\&. The default value is +\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 @@ -4201,18 +5086,22 @@ This option defines the number of service admins or room owners allowed to enter .PP \fBmax_users_presence\fR: \fINumber\fR .RS 4 -This option defines after how many users in the room, it is considered overcrowded\&. When a MUC room is considered overcrowed, presence broadcasts are limited to reduce load, traffic and excessive presence "storm" received by participants\&. The default value is +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 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\&. +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 4 seconds\&. +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 @@ -4271,30 +5160,22 @@ A custom vCard of the service that will be displayed by some XMPP clients in Ser \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp -For example, the following XML representation of vCard: -.sp -.if n \{\ -.RS 4 -.\} -.nf - - Conferences - - - Elm Street - - -.fi -.if n \{\ -.RE -.\} -.sp -will be translated to: +\fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf +# This XML representation of vCard: +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: vcard: fn: Conferences adr: @@ -4320,16 +5201,17 @@ This module depends on \fImod_muc\fR\&. .ps +1 \fBAvailable options:\fR .RS 4 -.sp -\fINote\fR about the next option: added in 22\&.05: .PP \fBsubscribe_room_many_max_users\fR: \fINumber\fR .RS 4 -How many users can be subscribed to a room at once using the +\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 -command\&. The default value is +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 @@ -4489,7 +5371,7 @@ to a remote file\&. By default a predefined CSS will be embedded into the HTML p .PP \fBdirname\fR: \fIroom_jid | room_name\fR .RS 4 -Allows to configure the name of the room directory\&. If set to +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 @@ -4583,6 +5465,43 @@ or a conference JID is appended to the 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\&. @@ -4748,22 +5667,8 @@ modules: .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\&. -.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 -\fIejabberdctl\fR has a command to delete expired messages (see chapter Managing an ejabberd server in online documentation\&. -.sp .5v -.RE +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 @@ -4775,15 +5680,17 @@ This module implements XEP\-0160: Best Practices for Handling Offline Messages a .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 error is returned to the sender\&. The default value is +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 optimisation 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 +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 optimisation is enabled\&. +\fIfalse\fR, meaning the optimization is enabled\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR @@ -4809,8 +5716,12 @@ option, but applied to this module only\&. .PP \fBstore_empty_body\fR: \fItrue | false | unless_chat_state\fR .RS 4 -Whether or not to store messages that lack a element\&. The default value is -\fIunless_chat_state\fR, which tells ejabberd to store messages even if they lack the element, unless they only contain a chat state notification (as defined in +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 @@ -4829,11 +5740,11 @@ option, but applied to this module only\&. .PP \fBuse_mam_for_storage\fR: \fItrue | false\fR .RS 4 -This is an experimental option\&. Enabling this option, -\fImod_offline\fR -uses the +This is an experimental option\&. By enabling the option, this module uses the +\fIarchive\fR +table from \fImod_mam\fR -archive table 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 +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 @@ -4878,6 +5789,8 @@ modules: .if n \{\ .RE .\} +.sp +\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#offline|offline\fR .RE .SS "mod_ping" .sp @@ -4893,7 +5806,11 @@ This module implements support for XEP\-0199: XMPP Ping and periodic keepalives\ .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\&. The default value is +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 @@ -4944,12 +5861,10 @@ is loaded and stream management is enabled by a client, killing the client conne .\} .nf modules: - \&.\&.\&. mod_ping: send_pings: true ping_interval: 4 min timeout_action: kill - \&.\&.\&. .fi .if n \{\ .RE @@ -4998,11 +5913,9 @@ minute\&. .\} .nf modules: - \&.\&.\&. mod_pres_counter: count: 5 interval: 30 secs - \&.\&.\&. .fi .if n \{\ .RE @@ -5024,7 +5937,7 @@ This module implements XEP\-0016: Privacy Lists\&. .ps -1 .br .sp -Nowadays modern XMPP clients rely on XEP\-0191: Blocking Command which is implemented by \fImod_blocking\fR module\&. However, you still need \fImod_privacy\fR loaded in order for \fImod_blocking\fR to work\&. +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 @@ -5077,6 +5990,8 @@ 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 @@ -5119,9 +6034,13 @@ 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\&. @@ -5168,6 +6087,36 @@ This module is complementary to \fImod_delegation\fR, but can also be used separ \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 @@ -5239,15 +6188,209 @@ Sets write access to a user\(cqs roster\&. The default value is .\} .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 @@ -5361,41 +6504,6 @@ bytes\&. 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 -For example, the following XML representation of vCard: -.sp -.if n \{\ -.RS 4 -.\} -.nf - - Conferences - - - Elm Street - - -.fi -.if n \{\ -.RE -.\} -.sp -will be translated to: -.sp -.if n \{\ -.RS 4 -.\} -.nf -vcard: - fn: Conferences - adr: - \- - work: true - street: Elm Street -.fi -.if n \{\ -.RE -.\} .RE .RE .sp @@ -5430,7 +6538,6 @@ shaper: proxyrate: 10240 modules: - \&.\&.\&. mod_proxy65: host: proxy1\&.example\&.org name: "File Transfer Proxy" @@ -5441,7 +6548,6 @@ modules: shaper: proxy65_shaper recbuf: 10240 sndbuf: 10240 - \&.\&.\&. .fi .if n \{\ .RE @@ -5530,14 +6636,13 @@ or 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 to raise user connection rate\&. The cost is memory usage, as every item is stored in memory\&. +\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 -.sp -\fINote\fR about the next option: added in 21\&.12: .PP \fBmax_item_expire_node\fR: \fItimeout() | infinity\fR .RS 4 -Specify the maximum item epiry time\&. Default value is: +\fINote\fR +about this option: added in 21\&.12\&. Specify the maximum item epiry time\&. Default value is: \fIinfinity\fR\&. .RE .PP @@ -5611,7 +6716,7 @@ nodetree before\&. .PP \fBpep_mapping\fR: \fIList of Key:Value\fR .RS 4 -This allows to define a list of key\-value to choose defined node plugins on given PEP namespace\&. The following example will use +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 @@ -5664,7 +6769,7 @@ plugin handles the default behaviour and follows standard XEP\-0060 implementati .IP \(bu 2.3 .\} \fIpep\fR -plugin adds extension to handle Personal Eventing Protocol (XEP\-0163) to the PubSub engine\&. Adding pep allows to handle PEP automatically\&. +plugin adds extension to handle Personal Eventing Protocol (XEP\-0163) to the PubSub engine\&. When enabled, PEP is handled automatically\&. .RE .RE .PP @@ -5674,32 +6779,24 @@ A custom vCard of the server that will be displayed by some XMPP clients in Serv \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp -The following XML representation of vCard: -.sp -.if n \{\ -.RS 4 -.\} -.nf - - PubSub Service - - - Elm Street - - -.fi -.if n \{\ -.RE -.\} -.sp -will be translated to: +\fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf +# This XML representation of vCard: +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: vcard: - fn: PubSub Service + fn: Conferences adr: \- work: true @@ -5726,7 +6823,6 @@ Example of configuration that uses flat nodes as default, and allows use of flat .\} .nf modules: - \&.\&.\&. mod_pubsub: access_createnode: pubsub_createnode max_subscriptions_node: 100 @@ -5737,7 +6833,6 @@ modules: plugins: \- flat \- pep - \&.\&.\&. .fi .if n \{\ .RE @@ -5750,7 +6845,6 @@ Using relational database requires using mod_pubsub with db_type \fIsql\fR\&. On .\} .nf modules: - \&.\&.\&. mod_pubsub: db_type: sql access_createnode: pubsub_createnode @@ -5759,7 +6853,61 @@ modules: 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 @@ -5767,7 +6915,7 @@ modules: .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\-dependant backend services such as FCM or APNS\&. +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 @@ -5819,12 +6967,23 @@ If this option is set to \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 @@ -5918,26 +7077,26 @@ This module reads also the top\-level \fIregistration_timeout\fR option defined .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\&. +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, -\fIejabberd\fR -doesn\(cqt 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\&. +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 -.sp -\fINote\fR about the next option: added in 21\&.12: .PP \fBallow_modules\fR: \fIall | [Module, \&.\&.\&.]\fR .RS 4 -List of modules that can register accounts, or +\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\&. @@ -5946,7 +7105,7 @@ List of modules that can register accounts, or \fBcaptcha_protected\fR: \fItrue | false\fR .RS 4 Protect registrations with -CAPTCHA\&. The default is +\fIbasic\&.md#captcha|CAPTCHA\fR\&. The default is \fIfalse\fR\&. .RE .PP @@ -5986,6 +7145,24 @@ Set a welcome message that is sent to each newly registered account\&. The messa \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" @@ -6025,11 +7202,11 @@ Change the password from an existing account on the server\&. Unregister an existing account on the server\&. .RE .sp -This module supports CAPTCHA to register a new account\&. To enable this feature, configure the top\-level \fIcaptcha_cmd\fR and top\-level \fIcaptcha_url\fR options\&. +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 → request_handlers, no need to enable in \fImodules\fR\&. The module depends on \fImod_register\fR where all the configuration is performed\&. +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 @@ -6149,11 +7326,38 @@ Enables/disables Roster Versioning\&. The default value is .\} .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 @@ -6207,14 +7411,54 @@ An access rule that can be used to restrict dialback for some servers\&. The def .\} .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 @@ -6251,12 +7495,10 @@ A list of servers or connected components to which stanzas will be forwarded\&. .\} .nf modules: - \&.\&.\&. mod_service_log: loggers: \- xmpp\-server\&.tld \- component\&.domain\&.tld - \&.\&.\&. .fi .if n \{\ .RE @@ -6268,7 +7510,7 @@ This module enables you to create shared roster groups: groups of accounts that .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\&. Each group has a unique name and those parameters: +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 \{\ @@ -6498,11 +7740,11 @@ Control parameters: .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 -LDAP Connection +\fIldap\&.md#ldap\-connection|LDAP Connection\fR section for more information about them\&. .RE .sp -Check also the Configuration examples section to get details about retrieving the roster, and configuration examples including Flat DIT and Deep DIT\&. +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 @@ -6572,7 +7814,7 @@ option, but applied to this module only\&. \fBldap_filter\fR .RS 4 Additional filter which is AND\-ed together with "User Filter" and "Group Filter"\&. For more information check the LDAP -Filters +\fIldap\&.md#filters|Filters\fR section\&. .RE .PP @@ -6624,7 +7866,7 @@ A globbing format for extracting user ID from the value of the attribute named b .RS 4 A regex for extracting user ID from the value of the attribute named by \fIldap_memberattr\fR\&. Check the LDAP -Control Parameters +\fIldap\&.md#control\-parameters|Control Parameters\fR section\&. .RE .PP @@ -6697,7 +7939,7 @@ option, but applied to this module only\&. \fIldap_userdesc\fR and \fIldap_useruid\fR\&. For more information check the LDAP -Filters +\fIldap\&.md#filters|Filters\fR section\&. .RE .PP @@ -6771,7 +8013,7 @@ This module adds SIP proxy/registrar support for the corresponding virtual host\ .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 ejabberd_sip listen module in the ejabberd Documentation\&. +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 @@ -6785,7 +8027,7 @@ It is not enough to just load this module\&. You should also configure listeners .PP \fBalways_record_route\fR: \fItrue | false\fR .RS 4 -Always insert "Record\-Route" header into SIP messages\&. This approach allows to bypass NATs/firewalls a bit more easily\&. The default value is +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 @@ -6851,7 +8093,6 @@ are mandatory (e\&.g\&. you cannot omit "port" or "scheme")\&. .\} .nf modules: - \&.\&.\&. mod_sip: always_record_route: false record_route: "sip:example\&.com;lr" @@ -6864,7 +8105,6 @@ modules: \- tls://sip\-tls\&.example\&.com:5061 \- tcp://sip\-tcp\&.example\&.com:5060 \- udp://sip\-udp\&.example\&.com:5060 - \&.\&.\&. .fi .if n \{\ .RE @@ -6937,7 +8177,7 @@ The protocol extension is deferred and seems like even a few clients that were s 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 acknowledgements and stream resumption\&. +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 @@ -6949,7 +8189,7 @@ This module adds support for XEP\-0198: Stream Management\&. This protocol allow .PP \fBack_timeout\fR: \fItimeout()\fR .RS 4 -A time to wait for stanza acknowledgements\&. Setting it to +A time to wait for stanza acknowledgments\&. Setting it to \fIinfinity\fR effectively disables the timeout\&. The default value is \fI1\fR @@ -7015,7 +8255,9 @@ minutes\&. .RE .SS "mod_stun_disco" .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\&. This module is included in ejabberd since version 20\&.04\&. +\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 @@ -7256,30 +8498,24 @@ A custom vCard of the server that will be displayed by some XMPP clients in Serv \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp -For example, the following XML representation of vCard: -.sp -.if n \{\ -.RS 4 -.\} -.nf - - Conferences - - - Elm Street - - -.fi -.if n \{\ -.RE -.\} -.sp -will be translated to: +\fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf +# This XML representation of vCard: +# +# +# Conferences +# +# +# Elm Street +# +# +# +# is translated to: +# vcard: fn: Conferences adr: @@ -7368,6 +8604,8 @@ for available words)\&. is the LDAP attribute or the pattern \fI%u\fR\&. .sp +\fBExamples\fR: +.sp The default is: .sp .if n \{\ @@ -7404,6 +8642,8 @@ is the vCard field name defined in the \fIldap_vcard_map\fR option\&. .sp +\fBExamples\fR: +.sp The default is: .sp .if n \{\ @@ -7491,6 +8731,8 @@ 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 \{\ @@ -7620,7 +8862,7 @@ Should the operating system be revealed or not\&. The default value is .RE .SH "LISTENERS" .sp -This section describes options of all ejabberd listeners\&. +This section describes listeners options of ejabberd 25\&.08\&. .sp TODO .SH "AUTHOR" @@ -7628,13 +8870,13 @@ TODO ProcessOne\&. .SH "VERSION" .sp -This document describes the configuration file of ejabberd 22\&.10\&. Configuration options of other ejabberd versions may differ significantly\&. +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/22\&.10/ejabberd\&.yml\&.example +Default configuration file: https://github\&.com/processone/ejabberd/blob/25\&.08/ejabberd\&.yml\&.example .sp Main site: https://ejabberd\&.im .sp @@ -7645,4 +8887,4 @@ Configuration Guide: https://docs\&.ejabberd\&.im/admin/configuration Source code: https://github\&.com/processone/ejabberd .SH "COPYING" .sp -Copyright (c) 2002\-2022 ProcessOne\&. +Copyright (c) 2002\-2025 ProcessOne\&. diff --git a/mix.exs b/mix.exs index 93e9a7d81..fcb3ac39e 100644 --- a/mix.exs +++ b/mix.exs @@ -3,31 +3,34 @@ defmodule Ejabberd.MixProject do 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] ++ Mix.compilers, + 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 - '0.0' -> "0.0.0" # the full git repository wasn't downloaded - 'latest.0' -> "0.0.0" # running 'docker-ejabberd/ecs/build.sh latest' + ~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 @@ -40,14 +43,24 @@ defmodule Ejabberd.MixProject do def application do [mod: {:ejabberd_app, []}, - extra_applications: [:mix], - applications: [:idna, :inets, :kernel, :sasl, :ssl, :stdlib, - :base64url, :fast_tls, :fast_xml, :fast_yaml, :jiffy, :jose, - :p1_utils, :stringprep, :syntax_tools, :yconf], + 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, :xmpp] - ++ cond_apps()] + :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 @@ -68,20 +81,24 @@ defmodule Ejabberd.MixProject do defp erlc_options do # Use our own includes + includes from all dependencies - includes = ["include"] ++ deps_include(["fast_xml", "xmpp", "p1_utils"]) + includes = ["include", deps_include()] result = [{:d, :ELIXIR_ENABLED}] ++ cond_options() ++ Enum.map(includes, fn (path) -> {:i, path} end) ++ - if_version_above('20', [{:d, :DEPRECATED_GET_STACKTRACE}]) ++ - if_version_above('20', [{:d, :HAVE_URI_STRING}]) ++ - if_version_above('20', [{:d, :HAVE_ERL_ERROR}]) ++ - 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_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 @@ -98,29 +115,27 @@ defmodule Ejabberd.MixProject do end defp deps do - [{:base64url, "~> 1.0"}, - {:cache_tab, "~> 1.0"}, + [{:cache_tab, "~> 1.0"}, + {:dialyxir, "~> 1.2", only: [:test], runtime: false}, {:eimp, "~> 1.0"}, - {:ex_doc, ">= 0.0.0", only: :dev}, - {:fast_tls, "~> 1.1"}, - {:fast_xml, "~> 1.1"}, + {: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"}, - {:jiffy, "~> 1.1.1"}, - {:jose, "~> 1.11.1"}, {:mqtree, "~> 1.0"}, - {:p1_acme, "~> 1.0"}, + {:p1_acme, ">= 1.0.28"}, {:p1_oauth2, "~> 0.6"}, {:p1_utils, "~> 1.0"}, {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, - {:xmpp, ">= 1.6.0"}, - {:yconf, "~> 1.0"}] + {: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(deps) do - base = if Mix.Project.umbrella?() do + defp deps_include() do + if Mix.Project.umbrella?() do "../../deps" else case Mix.Project.deps_paths()[:ejabberd] do @@ -128,28 +143,45 @@ defmodule Ejabberd.MixProject do _ -> ".." end end - Enum.map(deps, fn dep -> base<>"/#{dep}/include" end) end defp cond_deps do for {:true, dep} <- [{config(:pam), {:epam, "~> 1.0"}}, - {config(:redis), {:eredis, "~> 1.2.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_below('22', true), {:lager, "~> 3.9.1"}}, - {config(:lua), {:luerl, "~> 1.0"}}, - {config(:mysql), {:p1_mysql, "~> 1.0.20"}}, - {config(:pgsql), {:p1_pgsql, "~> 1.1"}}, + {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}, - {if_version_below('22', true), :lager}, + {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}, @@ -162,19 +194,30 @@ defmodule Ejabberd.MixProject 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: %{"Site" => "https://www.ejabberd.im", - "Documentation" => "http://docs.ejabberd.im", - "Source" => "https://github.com/processone/ejabberd", - "ProcessOne" => "http://www.process-one.net/"}] + 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 - case :file.consult("vars.config") 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 - _ -> [zlib: true] + _ -> [stun: true, zlib: true] + end + case Mix.env() do + :dev -> List.keystore(config2, :tools, 0, {:tools, true}) + _ -> config2 end end @@ -203,7 +246,7 @@ defmodule Ejabberd.MixProject do _ -> :ok end case Version.match?(System.version(), "< 1.11.4") - and :erlang.system_info(:otp_release) > '23' do + 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 @@ -223,6 +266,7 @@ defmodule Ejabberd.MixProject do ejabberd: [ include_executables_for: [:unix], # applications: [runtime_tools: :permanent] + strip_beams: Mix.env() != :dev, steps: [©_extra_files/1, :assemble | maybe_tar] ] ] @@ -239,6 +283,8 @@ defmodule Ejabberd.MixProject do 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()]), @@ -253,14 +299,14 @@ defmodule Ejabberd.MixProject do execute = fn(command) -> case function_exported?(System, :shell, 1) do true -> - System.shell(command) + 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 + case Version.match?(System.version(), "~> 1.11") do true -> :ok false -> @@ -271,8 +317,7 @@ defmodule Ejabberd.MixProject do 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.example3") - execute.("sed -e 's|^ERLANG_NODE=ejabberd@localhost|ERLANG_NODE=ejabberd|g' ejabberdctl.example3 > ejabberdctl.example4") + 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) @@ -320,13 +365,38 @@ defmodule Ejabberd.MixProject do end case Mix.env() do - :dev -> execute.("REL_DIR_TEMP=$PWD/rel/overlays/ rel/setup-dev.sh") + :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 @@ -339,7 +409,7 @@ defmodule Mix.Tasks.Compile.Asn1 do def run(args) do {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean]) - project = Mix.Project.config + project = Mix.Project.config() source_paths = project[:asn1_paths] || ["asn1"] dest_paths = project[:asn1_target] || ["src"] mappings = Enum.zip(source_paths, dest_paths) @@ -361,7 +431,7 @@ defmodule Mix.Tasks.Compile.Asn1 do end def manifests, do: [manifest()] - defp manifest, do: Path.join(Mix.Project.manifest_path, @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 index eb097dc7d..3eb53bce9 100644 --- a/mix.lock +++ b/mix.lock @@ -1,35 +1,39 @@ %{ "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"}, - "cache_tab": {:hex, :cache_tab, "1.0.30", "6d35eecfb65fbe5fc85988503a27338d32de01243f3fc8ea3ee7161af08725a4", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "6d8a5e00d8f84c42627706a6dbedb02e34d58495f3ed61935c8475ca0531cda0"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, - "eimp": {:hex, :eimp, "1.0.22", "fa9b376ef0b50e8455db15c7c11dea4522c6902e04412288aab436d26335f6eb", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "b3b9ffb1d9a5f4a2ba88ac418a819164932d9a9d3a2fc3d32ca338ce855c4392"}, - "epam": {:hex, :epam, "1.0.12", "2a5625d4133bca4b3943791a3f723ba764455a461ae9b6ba5debb262efcf4b40", [:rebar3], [], "hexpm", "54c166c4459cef72f2990a3d89a8f0be27180fe0ab0f24b28ddcc3b815f49f7f"}, - "eredis": {:hex, :eredis, "1.2.0", "0b8e9cfc2c00fa1374cd107ea63b49be08d933df2cf175e6a89b73dd9c380de4", [:rebar3], [], "hexpm", "d9b5abef2c2c8aba8f32aa018203e0b3dc8b1157773b254ab1d4c2002317f1e1"}, - "esip": {:hex, :esip, "1.0.48", "3b3b3afc798be9458517d4fd2730674322368e54c2c1211aa630327354946d1b", [:rebar3], [{:fast_tls, "1.1.16", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.2.6", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "02b9fc6e071415cbc62105f5115aeb68d11184bdad3960da7b62ea3e99e7fccf"}, - "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, - "ezlib": {:hex, :ezlib, "1.0.12", "ffe906ba10d03aaee7977e1e0e81d9ffc3bb8b47fb9cd8e2e453507a2e56221f", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "30e94355fb42260aab6e12582cb0c56bf233515e655c8aeaf48760e7561e4ebb"}, - "fast_tls": {:hex, :fast_tls, "1.1.16", "85fa7f3112ea4ff5ccb4f3abadc130a8c855ad74eb00869487399cb0c322d208", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "aa08cca89b4044e74f1f12e399817d8beaeae3ee006c98a893c0bfb1d81fba51"}, - "fast_xml": {:hex, :fast_xml, "1.1.49", "67d9bfcadd04efd930e0ee1412b5ea09d3e791f1fdbd4d3e9a8c8f29f8bfed8c", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "01da064d2f740818956961036637fee2475c17bf8aab9442217f90dc77883593"}, - "fast_yaml": {:hex, :fast_yaml, "1.0.34", "3be1ed8a37fe87a53f7f2ad1ee9586dcc257103d0b1d1f0ee6306cad9d54c29a", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "926dc1798399418d3983bd53356f3395b01c07a550f6b8d1dd5d6cc07c22c1c9"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, - "jiffy": {:hex, :jiffy, "1.1.1", "aca10f47aa91697bf24ab9582c74e00e8e95474c7ef9f76d4f1a338d0f5de21b", [:rebar3], [], "hexpm", "62e1f0581c3c19c33a725c781dfa88410d8bff1bbafc3885a2552286b4785c4c"}, - "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, - "luerl": {:hex, :luerl, "1.0.0", "1b68c30649323590d5339b967b419260500ffe520cd3abc1987482a82d3b5a6c", [:rebar3], [], "hexpm", "c17bc45cb4b0845ec975387f9a5d8c81ab60456698527a29c96f78992af86bd1"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "mqtree": {:hex, :mqtree, "1.0.15", "bc54d8b88698fdaebc1e27a9ac43688b927e3dbc05bd5cee4057e69a89a8cf17", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "294ac43c9b3d372e24eeea56c259e19c655522dcff64a55c401a639663b9d829"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "p1_acme": {:hex, :p1_acme, "1.0.20", "c976cbca2dd1bdcf71a6e17fb512e30451b5f258694157f7b63963767ee26560", [:rebar3], [{:base64url, "1.0.1", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.1.1", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.11.1", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.14", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "70e0ecf8c8729dfc01f6a15279ef9fa4003c3b5af47b6732d9312296a8ba4f5c"}, - "p1_mysql": {:hex, :p1_mysql, "1.0.20", "08aeade83a24902a5fca2dbf78fa674eef25ca4e66250b4be8bd3580f35880e7", [:rebar3], [], "hexpm", "12152e8feadcf8ce586334314ca27cb088f12e0a5c850c496a8df69859390877"}, - "p1_oauth2": {:hex, :p1_oauth2, "0.6.11", "96b4e85c08355720523c2f892011a81a07994d15c179ce4dd82d704fecad15b2", [:rebar3], [], "hexpm", "9c3c6ae59382b9525473bb02a32949889808f33f95f6db10594fd92acd1f63db"}, - "p1_pgsql": {:hex, :p1_pgsql, "1.1.19", "dc615844fd22a2e45182018d5bcc6b757ac19f576fab3fe6d69e1c0ff25cee2b", [:rebar3], [{:xmpp, "1.6.0", [hex: :xmpp, repo: "hexpm", optional: false]}], "hexpm", "4b0c6d30bbf881feb01171d13f444a6e05e1d19b6926e3f56f4028823d02730b"}, - "p1_utils": {:hex, :p1_utils, "1.0.25", "2d39b5015a567bbd2cc7033eeb93a7c60d8c84efe1ef69a3473faa07fa268187", [:rebar3], [], "hexpm", "9219214428f2c6e5d3187ff8eb9a8783695c2427420be9a259840e07ada32847"}, - "pkix": {:hex, :pkix, "1.0.9", "eb20b2715d71a23b4fe7e754dae9281a964b51113d0bba8adf9da72bf9d65ac2", [:rebar3], [], "hexpm", "daab2c09cdd4eda05c9b45a5c00e994a1a5f27634929e1377e2e59b707103e3a"}, - "sqlite3": {:hex, :sqlite3, "1.1.13", "94a6e0508936514e1493efeb9b939a9bbfa861f4b8dc93ef174ae88a1d9381d3", [:rebar3], [], "hexpm", "b77fad096d1ae9553ad8551ea75bd0d64a2f5b09923a7ca48b14215564dbfc48"}, - "stringprep": {:hex, :stringprep, "1.0.29", "02f23e8c3a219a3dfe40a22e908bece3a2f68af0ff599ea8a7b714ecb21e62ee", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "928eba304c3006eb1512110ebd7b87db163b00859a09375a1e4466152c6c462a"}, - "stun": {:hex, :stun, "1.2.6", "5d1978d340ea20efb28bc1e58779a3a1d64568c66168db4d20692e76ce813d5e", [:rebar3], [{:fast_tls, "1.1.16", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "21aed098457e5099e925129459590592e001c470cf7503e5614a7a6b688ff146"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, - "xmpp": {:hex, :xmpp, "1.6.0", "2ca2180eac1a97e929d1cfa1e4faabef4f32a719331c7f56e47d305c0ec8e438", [:rebar3], [{:ezlib, "1.0.12", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.16", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.49", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.29", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "5fd723c95bce79600a8f44ba79cf5d3b1dac80af65493a4a414e39791f7dd7e9"}, - "yconf": {:hex, :yconf, "1.0.14", "b216f385f729b338385b25176f6e4fe8cabfdf7ede9c40a35b2e77fc93e98fc8", [:rebar3], [{:fast_yaml, "1.0.34", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "a8a9262553c11ed4cd13cc8e656e53acb00f9385f0a50cd235af7d02e9204bce"}, + "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/priv/css/admin.css b/priv/css/admin.css index 276bff637..12fa97e22 100644 --- a/priv/css/admin.css +++ b/priv/css/admin.css @@ -131,16 +131,27 @@ ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a { 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: #3eaffa; - color: #fff; -} -thead tr td a { - color: #fff; + background: #cae7e4; } td.copy { text-align: center; @@ -227,24 +238,32 @@ h3 { 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: 0; + margin-top: 1em; margin-right: 1em; } div.guidelink a, p[dir=ltr] a { - display: inline-block; - border-radius: 3px; padding: 3px; - background: #3eaffa; - font-size: 0.75em; color: #fff; + border-radius: 2px; } table { margin-top: 1em; @@ -265,7 +284,7 @@ input, select { font-size: 1em; } -p.result { +.result { border: 1px; border-style: dashed; border-color: #FE8A02; @@ -284,3 +303,20 @@ p.result { 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/muc.css b/priv/css/muc.css index 48b7a19fa..b81ad5b52 100644 --- a/priv/css/muc.css +++ b/priv/css/muc.css @@ -16,12 +16,14 @@ a.roomjid {color: #336699; font-size: 24px; font-weight: bold; font-family: sans 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;} +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/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-fill.png b/priv/img/admin-logo-fill.png deleted file mode 100644 index 862163c50..000000000 Binary files a/priv/img/admin-logo-fill.png and /dev/null differ diff --git a/priv/img/admin-logo.png b/priv/img/admin-logo.png index 0088eddc8..041b37c69 100644 Binary files a/priv/img/admin-logo.png and b/priv/img/admin-logo.png differ diff --git a/priv/js/muc.js b/priv/js/muc.js index 9acd0dcfa..7f7de5c39 100644 --- a/priv/js/muc.js +++ b/priv/js/muc.js @@ -6,3 +6,14 @@ function sh(e) { document.getElementById(e).style.display='none'; } } +// Show/Hide join/leave elements +function jlf() { + var es = document.getElementsByClassName('jl'); + for (var i = 0; i < es.length; i++) { + if (es[i].style.display === 'block') { + es[i].style.display = 'none'; + } else { + es[i].style.display = 'block'; + } + } +} diff --git a/priv/js/sortable.min.js b/priv/js/sortable.min.js new file mode 100644 index 000000000..eb5443135 --- /dev/null +++ b/priv/js/sortable.min.js @@ -0,0 +1,3 @@ +document.addEventListener("click",function(c){try{function h(b,a){return b.nodeName===a?b:h(b.parentNode,a)}var v=c.shiftKey||c.altKey,d=h(c.target,"TH"),m=d.parentNode,n=m.parentNode,g=n.parentNode;function p(b){var a;return v?b.dataset.sortAlt:null!==(a=b.dataset.sort)&&void 0!==a?a:b.textContent}if("THEAD"===n.nodeName&&g.classList.contains("sortable")&&!d.classList.contains("no-sort")){var q,f=m.cells,r=+d.dataset.sortTbr;for(c=0;c 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 4e6c8d864..951430a9b 100644 --- a/priv/msgs/ca.msg +++ b/priv/msgs/ca.msg @@ -12,24 +12,17 @@ {"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 of authorize","Model d'Accés de autoritzar"}. -{"Access model of open","Model d'Accés de obert"}. -{"Access model of presence","Model d'Accés de presència"}. -{"Access model of roster","Model d'Accés de contactes"}. -{"Access model of whitelist","Model d'Accés de llista blanca"}. {"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 Jabber ID","Afegir Jabber ID"}. -{"Add New","Afegir nou"}. {"Add User","Afegir usuari"}. {"Administration of ","Administració de "}. {"Administration","Administració"}. {"Administrator privileges required","Es necessita tenir privilegis d'administrador"}. {"All activity","Tota l'activitat"}. {"All Users","Tots els usuaris"}. -{"Allow subscription","Permetre subscripcions"}. +{"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"}. @@ -53,7 +46,9 @@ {"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í"}. @@ -93,34 +88,25 @@ {"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 of room ~s","Configuració de la sala ~s"}. {"Configuration","Configuració"}. -{"Connected Resources:","Recursos connectats:"}. {"Contact Addresses (normally, room owner or owners)","Adreces de contacte (normalment, propietaris de la sala)"}. -{"Contrib Modules","Mòduls Contrib"}. {"Country","Pais"}. -{"CPU Time:","Temps de CPU:"}. {"Current Discussion Topic","Assumpte de discussió actual"}. {"Database failure","Error a la base de dades"}. -{"Database Tables at ~p","Taules de la base de dades en ~p"}. {"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 content","Eliminar contingut"}. {"Delete message of the day on all hosts","Elimina el missatge del dis de tots els hosts"}. {"Delete message of the day","Eliminar el missatge del dia"}. -{"Delete Selected","Eliminar els seleccionats"}. -{"Delete table","Eliminar taula"}. {"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' not added (they do not exist!): ","'Mostrats' no afegits (no existeixen!): "}. -{"Displayed:","Mostrats:"}. {"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"}. @@ -136,7 +122,6 @@ {"ejabberd vCard module","ejabberd mòdul vCard"}. {"ejabberd Web Admin","ejabberd Web d'administració"}. {"ejabberd","ejabberd"}. -{"Elements","Elements"}. {"Email Address","Adreça de correu"}. {"Email","Correu"}. {"Enable hats","Activar barrets"}. @@ -151,7 +136,6 @@ {"Enter path to text file","Introdueix ruta al fitxer de text"}. {"Enter the text you see","Introdueix el text que veus"}. {"Erlang XMPP Server","Servidor Erlang XMPP"}. -{"Error","Error"}. {"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):"}. @@ -170,7 +154,6 @@ {"Fill in the form to search for any matching XMPP User","Emplena camps per a buscar usuaris XMPP que concorden"}. {"Friday","Divendres"}. {"From ~ts","De ~ts"}. -{"From","De"}. {"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"}. @@ -180,23 +163,19 @@ {"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 that will be displayed to the members","Grups que seran mostrats als membres"}. -{"Groups","Grups"}. {"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"}. -{"Host","Host"}. {"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."}. @@ -210,7 +189,6 @@ {"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"}. -{"Incoming s2s Connections:","Connexions s2s d'entrada:"}. {"Incorrect CAPTCHA submit","El CAPTCHA proporcionat és incorrecte"}. {"Incorrect data form","El formulari de dades és incorrecte"}. {"Incorrect password","Contrasenya incorrecta"}. @@ -230,7 +208,6 @@ {"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"}. -{"It is not allowed to send private messages","No està permés enviar missatges privats"}. {"Jabber ID","ID Jabber"}. {"January","Gener"}. {"JID normalization denied by service policy","S'ha denegat la normalització del JID per política del servei"}. @@ -241,7 +218,6 @@ {"July","Juliol"}. {"June","Juny"}. {"Just created","Creació recent"}. -{"Label:","Etiqueta:"}. {"Last Activity","Última activitat"}. {"Last login","Últim login"}. {"Last message","Últim missatge"}. @@ -249,11 +225,10 @@ {"Last year","Últim any"}. {"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 rooms","Llista de sales"}. {"List of users with hats","Llista d'usuaris amb barrets"}. {"List users with hats","Llista d'usuaris amb barrets"}. +{"Logged Out","Desconectat"}. {"Logging","Registre"}. -{"Low level update script","Script d'actualització de baix nivell"}. {"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 només per a membres"}. @@ -271,11 +246,8 @@ {"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"}. -{"Members not added (inexistent vhost!): ","Membres no afegits (perquè el vhost no existeix): "}. {"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 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."}. -{"Memory","Memòria"}. {"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"}. @@ -287,15 +259,12 @@ {"Moderator privileges required","Es necessita tenir privilegis de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Només moderadors"}. -{"Modified modules","Mòduls modificats"}. {"Module failed to handle the query","El modul ha fallat al gestionar la petició"}. {"Monday","Dilluns"}. {"Multicast","Multicast"}. {"Multiple elements are not allowed by RFC6121","No estan permesos múltiples elements per RFC6121"}. {"Multi-User Chat","Multi-Usuari Converses"}. -{"Name in the rosters where this group will be displayed","Nom a les llistes de contactes on es mostrarà aquest grup"}. {"Name","Nom"}. -{"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'"}. @@ -360,14 +329,10 @@ {"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 Users","Usuaris conectats"}. -{"Online Users:","Usuaris en línia:"}. {"Online","Connectat"}. -{"Only admins can see this","Només els administradors poden veure esto"}. {"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 or tags are allowed","Només es permeten etiquetes o "}. @@ -375,6 +340,7 @@ {"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"}. @@ -385,11 +351,9 @@ {"Organization Name","Nom de la organizació"}. {"Organization Unit","Unitat de la organizació"}. {"Other Modules Available:","Altres mòduls disponibles:"}. -{"Outgoing s2s Connections:","Connexions d'eixida s2s:"}. {"Outgoing s2s Connections","Connexions s2s d'eixida"}. {"Owner privileges required","Es requerixen privilegis de propietari de la sala"}. {"Packet relay is denied by service policy","S'ha denegat el reenviament del paquet per política del servei"}. -{"Packet","Paquet"}. {"Participant ID","ID del Participant"}. {"Participant","Participant"}. {"Password Verification","Verificació de la Contrasenya"}. @@ -398,8 +362,7 @@ {"Password:","Contrasenya:"}. {"Path to Dir","Ruta al directori"}. {"Path to File","Ruta al fitxer"}. -{"Payload type","Tipus de payload"}. -{"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"}. @@ -433,26 +396,22 @@ {"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 an XMPP account","Registrar un compte XMPP"}. -{"Registered Users","Usuaris registrats"}. -{"Registered Users:","Usuaris registrats:"}. {"Register","Registrar"}. {"Remote copy","Còpia remota"}. {"Remove a hat from a user","Eliminar un barret d'un usuari"}. -{"Remove All Offline Messages","Eliminar tots els missatges offline"}. {"Remove User","Eliminar usuari"}. -{"Remove","Borrar"}. {"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 Service","Reiniciar el Servei"}. -{"Restart","Reiniciar"}. {"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 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"}. @@ -463,20 +422,15 @@ {"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 of ~ts","Llista de contactes de ~ts"}. {"Roster size","Mida de la llista"}. -{"Roster:","Llista de contactes:"}. -{"RPC Call Error","Error de cridada RPC"}. {"Running Nodes","Nodes funcionant"}. {"~s invites you to the room ~s","~s et convida a la sala ~s"}. {"Saturday","Dissabte"}. -{"Script check","Comprovar script"}. {"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 "}. -{"Select All","Seleccionar Tots"}. {"Send announcement to all online users on all hosts","Enviar anunci a tots els usuaris connectats a tots els hosts"}. {"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"}. @@ -489,6 +443,7 @@ {"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"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. @@ -497,25 +452,20 @@ {"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"}. +{"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"}. -{"Statistics of ~p","Estadístiques de ~p"}. -{"Statistics","Estadístiques"}. -{"Stop","Detindre"}. {"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:"}. {"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"}. -{"Submit","Enviar"}. {"Submitted","Enviat"}. {"Subscriber Address","Adreça del Subscriptor"}. {"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"}. -{"Subscription","Subscripció"}. {"Sunday","Diumenge"}. {"Text associated with a picture","Text associat amb una imatge"}. {"Text associated with a sound","Text associat amb un so"}. @@ -561,10 +511,10 @@ {"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 type of node data, usually specified by the namespace of the payload (if any)","El tipus de dades al node, usualment especificat pel namespace del payload (si n'hi ha)"}. {"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: "}. @@ -578,7 +528,6 @@ {"Thursday","Dijous"}. {"Time delay","Temps de retard"}. {"Timed out waiting for stream resumption","Massa temps esperant que es resumisca la connexió"}. -{"Time","Data"}. {"To register, visit ~s","Per a registrar-te, visita ~s"}. {"To ~ts","A ~ts"}. {"Token TTL","Token TTL"}. @@ -591,13 +540,8 @@ {"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"}. -{"To","Per a"}. -{"Total rooms","Sales totals"}. {"Traffic rate limit is exceeded","El límit de tràfic ha sigut sobrepassat"}. -{"Transactions Aborted:","Transaccions Avortades:"}. -{"Transactions Committed:","Transaccions Realitzades:"}. -{"Transactions Logged:","Transaccions registrades:"}. -{"Transactions Restarted:","Transaccions reiniciades:"}. +{"~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"}. @@ -608,24 +552,20 @@ {"Uninstall","Desinstal·lar"}. {"Unregister an XMPP account","Anul·lar el registre d'un compte XMPP"}. {"Unregister","Anul·lar el registre"}. -{"Unselect All","Deseleccionar tots"}. {"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 ~p","Actualitzar ~p"}. -{"Update plan","Pla d'actualització"}. -{"Update script","Script d'actualització"}. {"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"}. -{"Update","Actualitzar"}. +{"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"}. -{"Uptime:","Temps en marxa:"}. {"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"}. @@ -635,7 +575,6 @@ {"Users Last Activity","Última activitat d'usuari"}. {"Users","Usuaris"}. {"User","Usuari"}. -{"Validate","Validar"}. {"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"}. @@ -643,14 +582,13 @@ {"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"}. -{"View Queue","Vore Cua"}. -{"View Roster","Vore Llista de contactes"}. {"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ó"}. @@ -660,9 +598,10 @@ {"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","Permetre subscripcions"}. +{"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"}. @@ -674,6 +613,7 @@ {"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"}. diff --git a/priv/msgs/cs.msg b/priv/msgs/cs.msg index c6d3b2207..cd57ebefb 100644 --- a/priv/msgs/cs.msg +++ b/priv/msgs/cs.msg @@ -9,8 +9,6 @@ {"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 Jabber ID","Přidat Jabber ID"}. -{"Add New","Přidat nový"}. {"Add User","Přidat uživatele"}. {"Administration of ","Administrace "}. {"Administration","Administrace"}. @@ -60,22 +58,17 @@ {"Conference room does not exist","Místnost neexistuje"}. {"Configuration of room ~s","Konfigurace místnosti ~s"}. {"Configuration","Konfigurace"}. -{"Connected Resources:","Připojené zdroje:"}. {"Country","Země"}. -{"CPU Time:","Čas procesoru:"}. {"Database failure","Chyba databáze"}. -{"Database Tables at ~p","Databázové tabulky na ~p"}. {"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 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"}. {"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"}. @@ -87,7 +80,6 @@ {"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"}. {"Enable message archiving","Povolit ukládání historie zpráv"}. @@ -99,7 +91,6 @@ {"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"}. -{"Error","Chyba"}. {"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):"}. @@ -115,24 +106,20 @@ {"February",". února"}. {"File larger than ~w bytes","Soubor větší než ~w bytů"}. {"Friday","Pátek"}. -{"From","Od"}. +{"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 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"}. {"Host unknown","Neznámý hostitel"}. -{"Host","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."}. {"Import Directory","Import adresáře"}. {"Import File","Import souboru"}. @@ -144,24 +131,24 @@ {"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"}. -{"Incoming s2s Connections:","Příchozí s2s spojení:"}. {"Incorrect CAPTCHA submit","Nesprávné odeslání CAPTCHA"}. {"Incorrect data form","Nesprávný datový formulář"}. {"Incorrect password","Nesprávné heslo"}. {"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"}. {"is now known as","se přejmenoval(a) na"}. {"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é zprávy typu \"skupinová zpráva\" "}. +{"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"}. -{"It is not allowed to send private messages","Je zakázáno posílat soukromé zprávy"}. {"Jabber ID","Jabber ID"}. {"January",". ledna"}. +{"Joined MIX channels:","Připojené MIX kanály:"}. {"joins the room","vstoupil(a) do místnosti"}. {"July",". července"}. {"June",". června"}. @@ -170,8 +157,6 @@ {"Last month","Poslední měsíc"}. {"Last year","Poslední rok"}. {"leaves the room","opustil(a) místnost"}. -{"List of rooms","Seznam místností"}. -{"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"}. @@ -182,26 +167,22 @@ {"Malformed username","Chybně formátováné jméno uživatele"}. {"March",". března"}. {"Max payload size in bytes","Maximální náklad v bajtech"}. -{"Maximum Number of Occupants","Počet účastníků"}. +{"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"}. -{"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"}. {"Moderator","Moderátor"}. -{"Modified modules","Aktualizované moduly"}. {"Module failed to handle the query","Modul chyboval při zpracování dotazu"}. {"Monday","Pondělí"}. {"Multicast","Multicast"}. {"Multi-User Chat","Víceuživatelský chat"}. {"Name","Jméno"}. -{"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'"}. +{"Neither 'role' nor 'affiliation' attribute found","Nebyl nalezen atribut 'role' ani 'affiliation'"}. {"Never","Nikdy"}. {"New Password:","Nové heslo:"}. {"Nickname Registration at ","Registrace přezdívky na "}. @@ -248,12 +229,9 @@ {"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 Users:","Připojení uživatelé:"}. {"Online","Online"}. {"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"}. @@ -267,10 +245,9 @@ {"Only service administrators are allowed to send service messages","Pouze správci služby smí odesílat servisní zprávy"}. {"Organization Name","Název firmy"}. {"Organization Unit","Oddělení"}. +{"Other Modules Available:","Ostatní dostupné moduly:"}. {"Outgoing s2s Connections","Odchozí s2s spojení"}. -{"Outgoing s2s Connections:","Odchozí s2s spojení:"}. {"Owner privileges required","Jsou vyžadována práva vlastníka"}. -{"Packet","Paket"}. {"Participant","Účastník"}. {"Password Verification","Ověření hesla"}. {"Password Verification:","Ověření hesla:"}. @@ -278,7 +255,6 @@ {"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ý"}. @@ -297,17 +273,12 @@ {"RAM copy","Kopie RAM"}. {"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 místnosti"}. -{"Registered Users","Registrovaní uživatelé"}. -{"Registered Users:","Registrovaní uživatelé:"}. {"Register","Zaregistrovat se"}. {"Remote copy","Vzdálená kopie"}. -{"Remove All Offline Messages","Odstranit všechny offline zprávy"}. {"Remove User","Odstranit uživatele"}. -{"Remove","Odstranit"}. {"Replaced by new connection","Nahrazeno novým spojením"}. {"Resources","Zdroje"}. {"Restart Service","Restartovat službu"}. -{"Restart","Restart"}. {"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 immediately:","Okamžitě obnovit binární zálohu:"}. @@ -321,10 +292,8 @@ {"Room title","Název místnosti"}. {"Roster groups allowed to subscribe","Skupiny kontaktů, které mohou odebírat"}. {"Roster size","Velikost seznamu kontaktů"}. -{"RPC Call Error","Chyba RPC volání"}. {"Running Nodes","Běžící uzly"}. {"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 on all hosts","Odeslat oznámení všem online uživatelům na všech hostitelích"}. @@ -334,7 +303,7 @@ {"September",". září"}. {"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"}. @@ -342,38 +311,34 @@ {"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í"}. -{"Statistics of ~p","Statistiky ~p"}. -{"Statistics","Statistiky"}. {"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 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 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 creating the account: ","Při vytváření účtu došlo k chybě:"}. +{"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 deleting the account: ","Při mazání účtu došlo k chybě: "}. {"This room is not anonymous","Tato místnost není anonymní"}. {"Thursday","Čtvrtek"}. {"Time delay","Časový posun"}. -{"Time","Čas"}. {"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ů"}. @@ -383,13 +348,7 @@ {"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"}. -{"To","Pro"}. -{"Total rooms","Celkem místností"}. {"Traffic rate limit is exceeded","Byl překročen limit"}. -{"Transactions Aborted:","Transakcí zrušených:"}. -{"Transactions Committed:","Transakcí potvrzených:"}. -{"Transactions Logged:","Transakcí zaznamenaných:"}. -{"Transactions Restarted:","Transakcí restartovaných:"}. {"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"}. @@ -399,11 +358,6 @@ {"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 ~p","Aktualizovat ~p"}. -{"Update plan","Aktualizovat plán"}. -{"Update script","Aktualizované skripty"}. -{"Update","Aktualizovat"}. -{"Uptime:","Čas běhu:"}. {"User already exists","Uživatel již existuje"}. {"User JID","Jabber ID uživatele"}. {"User (jid)","Uživatel (JID)"}. @@ -415,10 +369,9 @@ {"Users Last Activity","Poslední aktivita uživatele"}. {"Users","Uživatelé"}. {"User","Uživatel"}. -{"Validate","Ověřit"}. {"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 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"}. diff --git a/priv/msgs/de.msg b/priv/msgs/de.msg index 017689273..7247d5f55 100644 --- a/priv/msgs/de.msg +++ b/priv/msgs/de.msg @@ -12,17 +12,10 @@ {"A Web Page","Eine Webseite"}. {"Accept","Akzeptieren"}. {"Access denied by service policy","Zugriff aufgrund der Dienstrichtlinien verweigert"}. -{"Access model of authorize","Zugriffsmodell von 'authorize'"}. -{"Access model of open","Zugriffsmodell von 'open'"}. -{"Access model of presence","Zugriffsmodell von 'presence'"}. -{"Access model of roster","Zugriffsmodell der Kontaktliste"}. -{"Access model of whitelist","Zugriffsmodell von 'whitelist'"}. {"Access model","Zugriffsmodell"}. {"Account doesn't exist","Konto existiert nicht"}. {"Action on user","Aktion auf Benutzer"}. {"Add a hat to a user","Funktion zu einem Benutzer hinzufügen"}. -{"Add Jabber ID","Jabber-ID hinzufügen"}. -{"Add New","Neue(n) hinzufügen"}. {"Add User","Benutzer hinzufügen"}. {"Administration of ","Administration von "}. {"Administration","Verwaltung"}. @@ -53,7 +46,9 @@ {"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"}. @@ -97,29 +92,20 @@ {"Conference room does not exist","Konferenzraum existiert nicht"}. {"Configuration of room ~s","Konfiguration des Raumes ~s"}. {"Configuration","Konfiguration"}. -{"Connected Resources:","Verbundene Ressourcen:"}. {"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 at ~p","Datenbanktabellen bei ~p"}. {"Database Tables Configuration at ","Datenbanktabellen-Konfiguration bei "}. {"Database","Datenbank"}. {"December","Dezember"}. {"Default users as participants","Benutzer werden standardmäßig Teilnehmer"}. -{"Delete content","Inhalt löschen"}. {"Delete message of the day on all hosts","Lösche Nachricht des Tages auf allen Hosts"}. {"Delete message of the day","Lösche Nachricht des Tages"}. -{"Delete Selected","Markierte löschen"}. -{"Delete table","Tabelle löschen"}. {"Delete User","Benutzer löschen"}. {"Deliver event notifications","Ereignisbenachrichtigungen zustellen"}. {"Deliver payloads with event notifications","Nutzdaten mit Ereignisbenachrichtigungen zustellen"}. -{"Description:","Beschreibung:"}. {"Disc only copy","Nur auf Festplatte"}. -{"'Displayed groups' not added (they do not exist!): ","'Angezeigte Gruppen' nicht hinzugefügt (sie existieren nicht!): "}. -{"Displayed:","Angezeigt:"}. {"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"}. @@ -135,7 +121,6 @@ {"ejabberd vCard module","ejabberd vCard-Modul"}. {"ejabberd Web Admin","ejabberd Web-Admin"}. {"ejabberd","ejabberd"}. -{"Elements","Elemente"}. {"Email Address","E-Mail-Adresse"}. {"Email","E-Mail"}. {"Enable hats","Funktion einschalten"}. @@ -150,7 +135,6 @@ {"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"}. -{"Error","Fehler"}. {"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:"}. @@ -169,7 +153,6 @@ {"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 ~ts","Von ~ts"}. -{"From","Von"}. {"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"}. @@ -179,23 +162,19 @@ {"Get Number of Registered Users","Anzahl der registrierten Benutzer abrufen"}. {"Get Pending","Ausstehende abrufen"}. {"Get User Last Login Time","letzte Anmeldezeit des Benutzers abrufen"}. -{"Get User Password","Benutzerpasswort abrufen"}. {"Get User Statistics","Benutzerstatistiken abrufen"}. {"Given Name","Vorname"}. {"Grant voice to this person?","Dieser Person Sprachrechte erteilen?"}. -{"Group","Gruppe"}. -{"Groups that will be displayed to the members","Gruppen, die den Mitgliedern angezeigt werden"}. -{"Groups","Gruppen"}. {"has been banned","wurde gebannt"}. {"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"}. -{"Host","Host"}. {"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."}. @@ -209,7 +188,6 @@ {"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"}. -{"Incoming s2s Connections:","Eingehende s2s-Verbindungen:"}. {"Incorrect CAPTCHA submit","Falsche CAPTCHA-Eingabe"}. {"Incorrect data form","Falsches Datenformular"}. {"Incorrect password","Falsches Passwort"}. @@ -229,16 +207,16 @@ {"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"}. -{"It is not allowed to send private messages","Es ist nicht erlaubt private Nachrichten zu senden"}. {"Jabber ID","Jabber-ID"}. {"January","Januar"}. {"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"}. -{"Label:","Label:"}. {"Last Activity","Letzte Aktivität"}. {"Last login","Letzte Anmeldung"}. {"Last message","Letzte Nachricht"}. @@ -246,11 +224,10 @@ {"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"}. -{"List of rooms","Liste von Räumen"}. {"List of users with hats","Liste der Benutzer mit Funktionen"}. {"List users with hats","Benutzer mit Funktionen auflisten"}. +{"Logged Out","Abgemeldet"}. {"Logging","Protokollierung"}. -{"Low level update script","Low-Level-Aktualisierungsscript"}. {"Make participants list public","Teilnehmerliste öffentlich machen"}. {"Make room CAPTCHA protected","Raum mittels CAPTCHA schützen"}. {"Make room members-only","Raum nur für Mitglieder zugänglich machen"}. @@ -268,11 +245,8 @@ {"Maximum number of items to persist","Maximale Anzahl persistenter Items"}. {"Maximum Number of Occupants","Maximale Anzahl der Teilnehmer"}. {"May","Mai"}. -{"Members not added (inexistent vhost!): ","Mitglieder nicht hinzugefügt (nicht existierender vhost!): "}. {"Membership is required to enter this room","Mitgliedschaft ist erforderlich um diesen Raum zu betreten"}. -{"Members:","Mitglieder:"}. {"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."}. -{"Memory","Speicher"}. {"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"}. @@ -284,14 +258,11 @@ {"Moderator privileges required","Moderatorrechte erforderlich"}. {"Moderator","Moderator"}. {"Moderators Only","nur Moderatoren"}. -{"Modified modules","Geänderte Module"}. {"Module failed to handle the query","Modul konnte die Anfrage nicht verarbeiten"}. {"Monday","Montag"}. {"Multicast","Multicast"}. {"Multiple elements are not allowed by RFC6121","Mehrere -Elemente sind laut RFC6121 nicht erlaubt"}. {"Multi-User Chat","Mehrbenutzer-Chat (MUC)"}. -{"Name in the rosters where this group will be displayed","Name in den Kontaktlisten wo diese Gruppe angezeigt werden wird"}. -{"Name:","Name:"}. {"Name","Vorname"}. {"Natural Language for Room Discussions","Natürliche Sprache für Raumdiskussionen"}. {"Natural-Language Room Name","Raumname in natürlicher Sprache"}. @@ -357,14 +328,10 @@ {"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:","Altes Passwort:"}. {"Online Users","Angemeldete Benutzer"}. -{"Online Users:","Angemeldete Benutzer:"}. {"Online","Angemeldet"}. -{"Only admins can see this","Nur Admins können dies sehen"}. {"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 or tags are allowed","Nur - oder -Tags sind erlaubt"}. @@ -372,6 +339,7 @@ {"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 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"}. @@ -383,10 +351,8 @@ {"Organization Unit","Abteilung"}. {"Other Modules Available:","Andere Module verfügbar:"}. {"Outgoing s2s Connections","Ausgehende s2s-Verbindungen"}. -{"Outgoing s2s Connections:","Ausgehende s2s-Verbindungen:"}. {"Owner privileges required","Besitzerrechte erforderlich"}. {"Packet relay is denied by service policy","Paket-Relay aufgrund der Dienstrichtlinien verweigert"}. -{"Packet","Paket"}. {"Participant ID","Teilnehmer-ID"}. {"Participant","Teilnehmer"}. {"Password Verification","Passwort bestätigen"}. @@ -395,8 +361,6 @@ {"Password:","Passwort:"}. {"Path to Dir","Pfad zum Verzeichnis"}. {"Path to File","Pfad zur Datei"}. -{"Payload type","Nutzdatentyp"}. -{"Pending","Ausstehend"}. {"Period: ","Zeitraum: "}. {"Persist items to storage","Items dauerhaft speichern"}. {"Persistent","Persistent"}. @@ -431,25 +395,21 @@ {"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:"}. {"Remote copy","Fernkopie"}. {"Remove a hat from a user","Eine Funktion bei einem Benutzer entfernen"}. -{"Remove All Offline Messages","Alle Offline-Nachrichten löschen"}. {"Remove User","Benutzer löschen"}. -{"Remove","Entfernen"}. {"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 Service","Dienst neustarten"}. -{"Restart","Neustart"}. {"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"}. +{"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"}. @@ -460,20 +420,15 @@ {"Room terminates","Raum wird beendet"}. {"Room title","Raumname"}. {"Roster groups allowed to subscribe","Kontaktlistengruppen die abonnieren dürfen"}. -{"Roster of ~ts","Kontaktliste von ~ts"}. {"Roster size","Kontaktlistengröße"}. -{"Roster:","Kontaktliste:"}. -{"RPC Call Error","Fehler bei RPC-Aufruf"}. {"Running Nodes","Laufende Knoten"}. {"~s invites you to the room ~s","~s lädt Sie in den Raum ~s ein"}. {"Saturday","Samstag"}. -{"Script check","Script-Überprüfung"}. {"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 "}. -{"Select All","Alles auswählen"}. {"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"}. @@ -494,24 +449,19 @@ {"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"}. -{"Statistics of ~p","Statistiken von ~p"}. -{"Statistics","Statistiken"}. -{"Stop","Anhalten"}. {"Stopped Nodes","Angehaltene Knoten"}. -{"Storage Type","Speichertyp"}. {"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"}. {"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"}. -{"Subscription","Abonnement"}. {"Subscriptions are not allowed","Abonnements sind nicht erlaubt"}. {"Sunday","Sonntag"}. {"Text associated with a picture","Text verbunden mit einem Bild"}. @@ -561,7 +511,6 @@ {"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 type of node data, usually specified by the namespace of the payload (if any)","Die Art der Knotendaten, üblicherweise vom Namensraum der Nutzdaten angegeben (gegebenenfalls)"}. {"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: "}. @@ -575,10 +524,8 @@ {"Thursday","Donnerstag"}. {"Time delay","Zeitverzögerung"}. {"Timed out waiting for stream resumption","Zeitüberschreitung beim Warten auf Streamfortsetzung"}. -{"Time","Zeit"}. {"To register, visit ~s","Um sich zu registrieren, besuchen Sie ~s"}. {"To ~ts","An ~ts"}. -{"To","An"}. {"Token TTL","Token-TTL"}. {"Too many active bytestreams","Zu viele aktive Bytestreams"}. {"Too many CAPTCHA requests","Zu viele CAPTCHA-Anforderungen"}. @@ -589,12 +536,8 @@ {"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"}. -{"Total rooms","Gesamte Räume"}. {"Traffic rate limit is exceeded","Datenratenlimit wurde überschritten"}. -{"Transactions Aborted:","Abgebrochene Transaktionen:"}. -{"Transactions Committed:","Übergebene 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 kein CAPTCHA erstellen"}. @@ -605,19 +548,14 @@ {"Uninstall","Deinstallieren"}. {"Unregister an XMPP account","Ein XMPP-Konto entfernen"}. {"Unregister","Deregistrieren"}. -{"Unselect All","Alle abwählen"}. {"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 ~p","~p aktualisieren"}. -{"Update script","Aktualisierungsscript"}. {"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"}. -{"Update","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"}. -{"Uptime:","Betriebszeit:"}. {"URL for Archived Discussion Logs","URL für archivierte Diskussionsprotokolle"}. {"User already exists","Benutzer existiert bereits"}. {"User (jid)","Benutzer (JID)"}. @@ -632,21 +570,20 @@ {"Users are not allowed to register accounts so quickly","Benutzer dürfen Konten nicht so schnell registrieren"}. {"Users Last Activity","Letzte Benutzeraktivität"}. {"Users","Benutzer"}. -{"Validate","Validieren"}. {"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 Queue","Warteschlange ansehen"}. -{"View Roster","Kontaktliste ansehen"}. +{"View joined MIX channels","Beitretene MIX-Channel ansehen"}. {"Virtual Hosts","Virtuelle Hosts"}. {"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 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"}. @@ -659,6 +596,7 @@ {"Whether to allow subscriptions","Ob Abonnements erlaubt sind"}. {"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"}. @@ -670,6 +608,7 @@ {"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"}. diff --git a/priv/msgs/el.msg b/priv/msgs/el.msg index ec07d58c1..0d18b30c4 100644 --- a/priv/msgs/el.msg +++ b/priv/msgs/el.msg @@ -12,16 +12,10 @@ {"A Web Page","Μία ιστοσελίδα"}. {"Accept","Αποδοχή"}. {"Access denied by service policy","Άρνηση πρόσβασης, λόγω τακτικής παροχής υπηρεσιών"}. -{"Access model of authorize","Μοντέλο πρόσβασης της πιστοποίησης"}. -{"Access model of open","Μοντέλο πρόσβασης του ανοικτού"}. -{"Access model of presence","Μοντέλο πρόσβασης της παρουσίας"}. -{"Access model of roster","Μοντέλο πρόσβασης της Λίστας Επαφών"}. -{"Access model of whitelist","Μοντέλο πρόσβασης της Λευκής Λίστας"}. -{"Access model","Καθορίστε το μοντέλο πρόσβασης"}. +{"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","Διαχείριση"}. @@ -52,7 +46,9 @@ {"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' δεν επιτρέπεται εδώ"}. @@ -78,6 +74,7 @@ {"Changing role/affiliation is not allowed","Η αλλαγή ρόλου/ομάδας δεν επιτρέπεται"}. {"Channel already exists","Το κανάλι υπάρχει ήδη"}. {"Channel does not exist","Το κανάλι δεν υπάρχει"}. +{"Channel JID","JID καναλιού"}. {"Channels","Κανάλια"}. {"Characters not allowed:","Χαρακτήρες που δεν επιτρέπονται:"}. {"Chatroom configuration modified","Η ρύθμιση παραμέτρων της αίθουσας σύνεδριασης τροποποιηθηκε"}. @@ -91,33 +88,25 @@ {"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:","Συνδεδεμένοι Πόροι:"}. {"Contact Addresses (normally, room owner or owners)","Διευθύνσεις της Επαφής (κανονικά, ιδιοκτήτης (-ες) αίθουσας)"}. {"Country","Χώρα"}. -{"CPU Time:","Ώρα CPU:"}. {"Current Discussion Topic","Τρέχων θέμα συζήτησης"}. {"Database failure","Αποτυχία βάσης δεδομένων"}. -{"Database Tables at ~p","Πίνακες βάσης δεδομένων στο ~p"}. {"Database Tables Configuration at ","Διαμόρφωση Πίνακων βάσης δεδομένων στο "}. {"Database","Βάση δεδομένων"}. {"December","Δεκέμβριος"}. {"Default users as participants","Προρυθμισμένοι χρήστες ως συμμετέχοντες"}. -{"Delete content","Διαγραφή περιεχομένων"}. {"Delete message of the day on all hosts","Διαγράψτε το μήνυμα της ημέρας σε όλους τους κεντρικούς υπολογιστές"}. {"Delete message of the day","Διαγράψτε το μήνυμα της ημέρας"}. -{"Delete Selected","Διαγραφή επιλεγμένων"}. -{"Delete table","Διαγραφή Πίνακα"}. {"Delete User","Διαγραφή Χρήστη"}. {"Deliver event notifications","Παράδοση ειδοποιήσεων συμβάντων"}. {"Deliver payloads with event notifications","Κοινοποίηση φόρτου εργασιών με τις ειδοποιήσεις συμβάντων"}. -{"Description:","Περιγραφή:"}. {"Disc only copy","Αντίγραφο μόνο σε δίσκο"}. -{"'Displayed groups' not added (they do not exist!): ","'Οι εμφανιζόμενες ομάδες' δεν προστέθηκαν (δεν υπάρχουν!): "}. -{"Displayed:","Απεικονίζεται:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Μην πείτε τον κωδικό πρόσβασής σας σε κανέναν, ούτε στους διαχειριστές του διακομιστή XMPP."}. {"Dump Backup to Text File at ","Αποθήκευση Αντιγράφου Ασφαλείας σε αρχείο κειμένου στο "}. {"Dump to Text File","Αποθήκευση σε αρχείο κειμένου"}. @@ -133,9 +122,9 @@ {"ejabberd vCard module","ejabberd vCard module"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. -{"Elements","Στοιχεία"}. {"Email Address","Ηλεκτρονική Διεύθυνση"}. {"Email","Ηλεκτρονικό ταχυδρομείο"}. +{"Enable hats","Ενεργοποίηση καπέλων"}. {"Enable logging","Ενεργοποίηση καταγραφής"}. {"Enable message archiving","Ενεργοποιήστε την αρχειοθέτηση μηνυμάτων"}. {"Enabling push without 'node' attribute is not supported","Η ενεργοποίηση της ώθησης χωρίς το χαρακτηριστικό 'κόμβος' δεν υποστηρίζεται"}. @@ -147,7 +136,6 @@ {"Enter path to text file","Εισάγετε Τοποθεσία Αρχείου Κειμένου"}. {"Enter the text you see","Πληκτρολογήστε το κείμενο που βλέπετε"}. {"Erlang XMPP Server","Διακομιστής Erlang XMPP"}. -{"Error","Σφάλμα"}. {"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):"}. @@ -166,28 +154,28 @@ {"Fill in the form to search for any matching XMPP User","Συμπληρώστε την φόρμα για αναζήτηση χρηστών XMPP"}. {"Friday","Παρασκευή"}. {"From ~ts","Από ~ts"}. -{"From","Από"}. {"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 that will be displayed to the members","Ομάδες που θα εμφανίζονται στα μέλη"}. -{"Groups","Ομάδες"}. -{"Group","Ομάδα"}. {"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","Hash του vCard-temp avatar αυτού του δωματίου"}. +{"Hat title","Τίτλος καπέλου"}. +{"Hat URI","Καπέλο URI"}. +{"Hats limit exceeded","Υπέρβαση του ορίου καπέλων"}. {"Host unknown","Άγνωστος εξυπηρετητής"}. -{"Host","Εξυπηρετητής"}. {"HTTP File Upload","Ανέβασμα αρχείου"}. {"Idle connection","Αδρανής σύνδεση"}. {"If you don't see the CAPTCHA image here, visit the web page.","Εάν δεν βλέπετε την εικόνα CAPTCHA εδώ, επισκεφθείτε την ιστοσελίδα."}. @@ -201,13 +189,14 @@ {"Import Users From jabberd14 Spool Files","Εισαγωγή Χρηστών από αρχεία σειράς jabberd14"}. {"Improper domain part of 'from' attribute","Ανάρμοστο τμήμα τομέα του χαρακτηριστικού 'from'"}. {"Improper message type","Ακατάλληλο είδος μηνύματος"}. -{"Incoming s2s Connections:","Εισερχόμενες συνδέσεις s2s:"}. {"Incorrect CAPTCHA submit","Λάθος υποβολή CAPTCHA"}. {"Incorrect data form","Εσφαλμένη φόρμα δεδομένων"}. {"Incorrect password","Εσφαλμένος κωδικός πρόσβασης"}. {"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","Μη έγκυρο χαρακτηριστικό 'από' στο προωθούμενο μήνυμα"}. @@ -219,16 +208,16 @@ {"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 ID","Ταυτότητα Jabber"}. {"January","Ιανουάριος"}. {"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","Μόλις δημιουργήθηκε"}. -{"Label:","Ετικέτα:"}. {"Last Activity","Τελευταία Δραστηριότητα"}. {"Last login","Τελευταία σύνδεση"}. {"Last message","Τελευταίο μήνυμα"}. @@ -236,9 +225,10 @@ {"Last year","Πέρυσι"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Τα ψηφία μικρότερης αξίας του αθροίσματος SHA-256 του κειμένου θα έπρεπε να ισούνται με την δεκαεξαδική ετικέτα"}. {"leaves the room","εγκαταλείπει την αίθουσα"}. -{"List of rooms","Κατάλογος αιθουσών"}. +{"List of users with hats","Λίστα των χρηστών με καπέλα"}. +{"List users with hats","Λίστα χρηστών με καπέλα"}. +{"Logged Out","Αποσυνδεδεμένος"}. {"Logging","Καταγραφή"}. -{"Low level update script","Προγράμα ενημέρωσης χαμηλού επίπεδου"}. {"Make participants list public","Κάντε δημόσιο τον κατάλογο συμμετεχόντων"}. {"Make room CAPTCHA protected","Κάντε την αίθουσα προστατεύομενη με CAPTCHA"}. {"Make room members-only","Κάντε την αίθουσα μόνο για μέλη"}. @@ -249,17 +239,15 @@ {"Malformed username","Λανθασμένη μορφή ονόματος χρήστη"}. {"MAM preference modification denied by service policy","Άρνηση αλλαγής προτιμήσεων MAM, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"March","Μάρτιος"}. +{"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","Μάιος"}. -{"Members not added (inexistent vhost!): ","Τα μέλη δεν προστέθηκαν (ανύπαρκτος vhost!): "}. {"Membership is required to enter this room","Απαιτείται αίτηση συμετοχής για είσοδο σε αυτή την αίθουσα"}. -{"Members:","Μέλη:"}. {"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 δεν υπάρχει αυτοματοποιημένος τρόπος ανάκτησης του κωδικού πρόσβασής σας εάν τον ξεχάσετε."}. -{"Memory","Μνήμη"}. {"Mere Availability in XMPP (No Show Value)","Διαθεσιμότητα στο XMPP (Χωρίς ένδειξη)"}. {"Message body","Περιεχόμενο μηνύματος"}. {"Message not found in forwarded payload","Δεν βρέθηκε μήνυμα στον προωθημένο φόρτο εργασίας"}. @@ -271,15 +259,12 @@ {"Moderator privileges required","Aπαιτούνται προνόμια επόπτου"}. {"Moderators Only","Επόπτες μόμον"}. {"Moderator","Επόπτης"}. -{"Modified modules","Τροποποιημένα modules"}. {"Module failed to handle the query","Το module απέτυχε να χειριστεί το ερώτημα"}. {"Monday","Δευτέρα"}. {"Multicast","Πολλαπλή διανομή (Multicast)"}. {"Multiple elements are not allowed by RFC6121","Πολλαπλά στοιχεία δεν επιτρέπονται από το RFC6121"}. {"Multi-User Chat","Συνομιλία με πολλούς χρήστες"}. -{"Name in the rosters where this group will be displayed","Όνομα στις λίστες όπου αυτή η ομάδα θα εμφανίζεται"}. {"Name","Όνομα"}. -{"Name:","Όνομα:"}. {"Natural Language for Room Discussions","Μητρική Γλώσσα για τις Συζητήσεις Αιθουσών"}. {"Natural-Language Room Name","Αίθουσα Μητρικής Γλώσσας"}. {"Neither 'jid' nor 'nick' attribute found","Δεν βρέθηκε κανένα χαρακτηριστικό 'jid' ούτε 'nick'"}. @@ -325,6 +310,7 @@ {"Node ~p","Κόμβος ~p"}. {"Nodeprep has failed","Το Nodeprep απέτυχε"}. {"Nodes","Κόμβοι"}. +{"Node","Κόμβος"}. {"None","Κανένα"}. {"Not allowed","Δεν επιτρέπεται"}. {"Not Found","Δε βρέθηκε"}. @@ -338,17 +324,15 @@ {"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 Χρήστες:"}. {"Online Users","Συνδεμένοι χρήστες"}. {"Online","Συνδεδεμένο"}. -{"Only admins can see this","Μόνον οι διαχειριστές μπορούν να το δουν αυτό"}. {"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 ή "}. @@ -356,6 +340,7 @@ {"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","Μόνο οι συμμετέχοντες επιτρέπεται να στείλουν ερωτήματα στη διάσκεψη"}. @@ -365,11 +350,11 @@ {"Only those on a whitelist may subscribe and retrieve items","Μόνο όσοι βρίσκονται στη λίστα επιτρεπόμενων μπορούν να εγγραφούν και να ανακτήσουν αντικείμενα"}. {"Organization Name","Όνομα Οργανισμού"}. {"Organization Unit","Μονάδα Οργανισμού"}. +{"Other Modules Available:","Διαθέσιμες άλλες ενότητες:"}. {"Outgoing s2s Connections","Εξερχόμενες S2S Συνδέσεις"}. -{"Outgoing s2s Connections:","Εξερχόμενες S2S Συνδέσεις:"}. {"Owner privileges required","Aπαιτούνται προνόμια ιδιοκτήτη"}. {"Packet relay is denied by service policy","Απαγορεύεται η αναμετάδοση πακέτων, λόγω της τακτικής Παροχής Υπηρεσιών"}. -{"Packet","Πακέτο"}. +{"Participant ID","ID συμμετέχοντος"}. {"Participant","Συμμετέχων"}. {"Password Verification","Επαλήθευση κωδικού πρόσβασης"}. {"Password Verification:","Επαλήθευση κωδικού πρόσβασης:"}. @@ -377,8 +362,7 @@ {"Password:","Κωδικός πρόσβασης:"}. {"Path to Dir","Τοποθεσία κατάλογου αρχείων"}. {"Path to File","Τοποθεσία Αρχείου"}. -{"Payload type","Τύπος φόρτου εργασιών"}. -{"Pending","Εκκρεμεί"}. +{"Payload semantic type information","Πληροφορίες σημασιολογικού τύπου ωφέλιμου φορτίου"}. {"Period: ","Περίοδος: "}. {"Persist items to storage","Μόνιμη αποθήκευση στοιχείων"}. {"Persistent","Μόνιμη"}. @@ -412,25 +396,22 @@ {"Receive notification of new nodes only","Λάβετε ειδοποίηση μόνο από νέους κόμβους"}. {"Recipient is not in the conference room","Ο παραλήπτης δεν είναι στην αίθουσα συνεδριάσεων"}. {"Register an XMPP account","Καταχωρείστε έναν XMPP λογαριασμό χρήστη"}. -{"Registered Users","Εγγεγραμμένοι Χρήστες"}. -{"Registered Users:","Εγγεγραμμένοι Χρήστες:"}. {"Register","Καταχωρήστε"}. {"Remote copy","Εξ αποστάσεως αντίγραφο"}. -{"Remove All Offline Messages","Αφαίρεση όλων των μηνυμάτων χωρίς σύνδεση"}. +{"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):","Επαναφορά δυαδικού αντιγράφου ασφαλείας μετά την επόμενη επανεκκίνηση του 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","Ρόλοι που επιτρέπεται να αποστέλλουν ιδιωτικά μηνύματα"}. @@ -441,20 +422,15 @@ {"Room terminates","Τερματισμός Αίθουσας"}. {"Room title","Τίτλος Αίθουσας"}. {"Roster groups allowed to subscribe","Ομάδες Καταλόγου Επαφών μπορούν να εγγραφούν"}. -{"Roster of ~ts","Καταλόγου Επαφών του ~ts"}. {"Roster size","Μέγεθος Καταλόγου Επαφών"}. -{"Roster:","Καταλόγος Επαφών:"}. -{"RPC Call Error","Σφάλμα RPC Κλήσης"}. {"Running Nodes","Ενεργοί Κόμβοι"}. {"~s invites you to the room ~s","~s Σας καλεί στο δωμάτιο ~s"}. {"Saturday","Σάββατο"}. -{"Script check","Script ελέγχου"}. {"Search from the date","Αναζήτηση από της"}. {"Search Results for ","Αποτελέσματα αναζήτησης για "}. {"Search the text","Αναζήτηση του κειμένου"}. {"Search until the date","Αναζήτηση μέχρι της"}. {"Search users in ","Αναζήτηση χρηστών στο "}. -{"Select All","Επιλογή όλων"}. {"Send announcement to all online users on all hosts","Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Send announcement to all online users","Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες"}. {"Send announcement to all users on all hosts","Αποστολή ανακοίνωσης σε όλους τους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. @@ -467,32 +443,29 @@ {"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","Τερματισμός Υπηρεσίας"}. {"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","Καθορίστε το μοντέλο εκδότη"}. +{"Stanza id is not valid","Το Stanza id δεν είναι έγκυρο"}. {"Stanza ID","Ταυτότητα Δωματίου"}. {"Statically specify a replyto of the node owner(s)","Προσδιορίστε (στατικά) το Απάντηση Προς του ιδιοκτήτη-ων του κόμβου"}. -{"Statistics of ~p","Στατιστικές του ~p"}. -{"Statistics","Στατιστικές"}. {"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","Διεύθυνση Συνδρομητή"}. {"Subscribers may publish","Οι συνδρομητές μπορούν να δημοσιεύσουν"}. {"Subscription requests must be approved and only subscribers may retrieve items","Τα αιτήματα για συνδρομή πρέπει να εγκριθούν και μόνο οι συνδρομητές μπορούν να λάβουν αντικείμενα"}. {"Subscriptions are not allowed","Οι συνδρομές δεν επιτρέπονται"}. -{"Subscription","Συνδρομή"}. {"Sunday","Κυριακή"}. {"Text associated with a picture","Το κείμενο σχετίστηκε με μία εικόνα"}. {"Text associated with a sound","Το κείμενο σχετίστηκε με έναν ήχο"}. @@ -516,7 +489,10 @@ {"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","Ο κόμβος είναι κόμβος Συλλογής"}. @@ -535,10 +511,10 @@ {"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 type of node data, usually specified by the namespace of the payload (if any)","Ο τύπος των δεδομένων του κόμβου συνήθως προσδιορίζεται από το namespace του φόρτου εργασιών (αν υπάρχουν)"}. {"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: ","Παρουσιάστηκε σφάλμα κατά την αλλαγή του κωδικού πρόσβασης: "}. @@ -552,7 +528,6 @@ {"Thursday","Πέμπτη"}. {"Time delay","Χρόνος καθυστέρησης"}. {"Timed out waiting for stream resumption","Υπερέβην το όριο αναμονής για επανασύνδεση της Ροής"}. -{"Time","Χρόνος"}. {"To register, visit ~s","Για να εγγραφείτε, επισκεφθείτε το ~s"}. {"To ~ts","Προς ~ts"}. {"Token TTL","Διακριτικό TTL"}. @@ -565,13 +540,8 @@ {"Too many receiver fields were specified","Πάρα πολλά πεδία δεκτών προσδιορίστηκαν"}. {"Too many unacked stanzas","Πάρα πολλές μη αναγνωρισμένες stanzas"}. {"Too many users in this conference","Πάρα πολλοί χρήστες σε αυτή τη διάσκεψη"}. -{"Total rooms","Συνολικές Αίθουσες σύνεδριασης"}. -{"To","Προς"}. {"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"}. @@ -579,23 +549,23 @@ {"Unauthorized","Χωρίς Εξουσιοδότηση"}. {"Unexpected action","Απροσδόκητη ενέργεια"}. {"Unexpected error condition: ~p","Απροσδόκητες συνθήκες σφάλματος: ~p"}. +{"Uninstall","Απεγκατάσταση"}. {"Unregister an XMPP account","Καταργήση λογαριασμού XMPP"}. {"Unregister","Καταργήση εγγραφής"}. -{"Unselect All","Αποεπιλογή όλων"}. {"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 ~p","Ενημέρωση ~p"}. -{"Update script","Προγράμα ενημέρωσης"}. -{"Update","Ενημέρωση"}. -{"Uptime:","Χρόνος σε λειτουργία:"}. +{"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","Διαχείριση χρηστών"}. +{"User not allowed to perform an IQ set on another user's vCard.","Ο χρήστης δεν επιτρέπεται να εκτελέσει ένα σετ IQ στην vCard ενός άλλου χρήστη."}. {"User removed","Ο Χρήστης αφαιρέθηκε"}. {"User session not found","Η περίοδος σύνδεσης χρήστη δεν βρέθηκε"}. {"User session terminated","Η περίοδος σύνδεσης χρήστη τερματίστηκε"}. @@ -605,21 +575,20 @@ {"Users Last Activity","Τελευταία Δραστηριότητα Χρήστη"}. {"Users","Χρήστες"}. {"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 Αναζήτηση χρηστών"}. -{"View Queue","Εμφάνιση λίστας αναμονής"}. -{"View Roster","Εμφάνιση λίστας Επαφών"}. +{"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","Οι επισκέπτες δεν επιτρέπεται να στείλουν μηνύματα σε όλους τους συμμετέχοντες"}. {"Visitor","Επισκέπτης"}. {"Voice requests are disabled in this conference","Τα αιτήματα φωνής είναι απενεργοποιημένα, σε αυτό το συνέδριο"}. {"Voice request","Αίτημα φωνής"}. +{"Web client which allows to join the room anonymously","Web client που επιτρέπει την ανώνυμη είσοδο στην αίθουσα"}. {"Wednesday","Τετάρτη"}. {"When a new subscription is processed and whenever a subscriber comes online","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία και όποτε ένας συνδρομητής συνδεθεί"}. {"When a new subscription is processed","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία"}. @@ -632,6 +601,7 @@ {"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"}. @@ -643,6 +613,7 @@ {"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","Σας έχει απαγορευθεί η είσοδος σε αυτή την αίθουσα"}. diff --git a/priv/msgs/eo.msg b/priv/msgs/eo.msg index a7d9ad214..553980422 100644 --- a/priv/msgs/eo.msg +++ b/priv/msgs/eo.msg @@ -12,14 +12,9 @@ {"A Web Page","Retpaĝo"}. {"Accept","Akcepti"}. {"Access denied by service policy","Atingo rifuzita de serv-politiko"}. -{"Access model of open","Atingomodelo de malfermo"}. -{"Access model of presence","Atingomodelo de ĉeesto"}. -{"Access model of whitelist","Atingomodelo de permesolisto"}. {"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 of ","Mastrumado de "}. {"Administration","Administro"}. @@ -79,22 +74,17 @@ {"Conference room does not exist","Babilejo ne ekzistas"}. {"Configuration of room ~s","Agordo de babilejo ~s"}. {"Configuration","Agordo"}. -{"Connected Resources:","Konektataj risurcoj:"}. {"Country","Lando"}. -{"CPU Time:","CPU-tempo"}. {"Current Discussion Topic","Aktuala Diskuta Temo"}. -{"Database Tables at ~p","Datumbaz-tabeloj je ~p"}. {"Database Tables Configuration at ","Agordo de datumbaz-tabeloj je "}. {"Database","Datumbazo"}. {"December","Decembro"}. {"Default users as participants","Kutime farigu uzantojn kiel partpoprenantoj"}. {"Delete message of the day on all hosts","Forigu mesaĝo de la tago je ĉiu gastigo"}. {"Delete message of the day","Forigu mesaĝo de la tago"}. -{"Delete Selected","Forigu elektata(j)n"}. {"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"}. {"Dump Backup to Text File at ","Skribu sekurkopion en plata teksto al "}. {"Dump to Text File","Skribu en plata tekst-dosiero"}. @@ -108,7 +98,6 @@ {"ejabberd vCard module","ejabberd vCard-modulo"}. {"ejabberd Web Admin","ejabberd Teksaĵa Administro"}. {"ejabberd","ejabberd"}. -{"Elements","Eroj"}. {"Email Address","Retpoŝta Adreso"}. {"Email","Retpoŝto"}. {"Enable logging","Ŝaltu protokoladon"}. @@ -120,7 +109,6 @@ {"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"}. -{"Error","Eraro"}. {"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):"}. @@ -130,23 +118,18 @@ {"February","Februaro"}. {"File larger than ~w bytes","Dosiero pli granda ol ~w bajtoj"}. {"Friday","Vendredo"}. -{"From","De"}. {"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 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"}. -{"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."}. {"Import Directory","Importu dosierujo"}. {"Import File","Importu dosieron"}. @@ -163,21 +146,17 @@ {"is now known as","nun nomiĝas"}. {"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"}. -{"It is not allowed to send private messages","Ne estas permesata sendi privatajn mesaĝojn"}. {"Jabber ID","Jabber ID"}. {"January","Januaro"}. {"joins the room","eniras la babilejo"}. {"July","Julio"}. {"June","Junio"}. {"Just created","Ĵus kreita"}. -{"Label:","Etikedo:"}. {"Last Activity","Lasta aktiveco"}. {"Last login","Lasta ensaluto"}. {"Last month","Lasta monato"}. {"Last year","Lasta jaro"}. {"leaves the room","eliras la babilejo"}. -{"List of rooms","Listo de babilejoj"}. -{"Low level update script","Bazanivela ĝisdatigo-skripto"}. {"Make participants list public","Farigu partoprento-liston publika"}. {"Make room CAPTCHA protected","Protektu babilejon per CAPTCHA"}. {"Make room members-only","Farigu babilejon sole por membroj"}. @@ -191,20 +170,16 @@ {"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:"}. -{"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"}. -{"Modified modules","Ĝisdatigitaj moduloj"}. {"Module failed to handle the query","Modulo malsukcesis trakti la informpeton"}. {"Monday","Lundo"}. {"Multicast","Multicast"}. {"Multiple elements are not allowed by RFC6121","RFC 6121 ne permesas plurajn -elementojn"}. {"Multi-User Chat","Grupbabilado"}. {"Name","Nomo"}. -{"Name:","Nomo:"}. {"Natural Language for Room Discussions","Homa Lingvo por Diskutoj en Babilejo"}. {"Never","Neniam"}. {"New Password:","Nova Pasvorto:"}. @@ -236,11 +211,8 @@ {"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 Users:","Konektataj uzantoj:"}. {"Online Users","Konektataj Uzantoj"}. {"Online","Konektata"}. {"Only deliver notifications to available users","Nur liveru sciigojn al konektataj uzantoj"}. @@ -257,16 +229,13 @@ {"Organization Name","Organiz-nomo"}. {"Organization Unit","Organiz-parto"}. {"Outgoing s2s Connections","Elirantaj s-al-s-konektoj"}. -{"Outgoing s2s Connections:","Elirantaj s-al-s-konektoj:"}. {"Owner privileges required","Mastraj rajtoj bezonata"}. -{"Packet","Pakaĵo"}. {"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"}. @@ -285,17 +254,12 @@ {"RAM copy","RAM-kopio"}. {"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"}. -{"Registered Users","Registritaj uzantoj"}. -{"Registered Users:","Registritaj uzantoj:"}. {"Register","Registru"}. {"Remote copy","Fora kopio"}. -{"Remove All Offline Messages","Forigu ĉiujn liverontajn mesaĝojn"}. {"Remove User","Forigu uzanton"}. -{"Remove","Forigu"}. {"Replaced by new connection","Anstataŭigita je nova konekto"}. {"Resources","Risurcoj"}. {"Restart Service","Restartu Servon"}. -{"Restart","Restartu"}. {"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"}. {"Restore binary backup immediately:","Restaŭrigu duuman sekurkopion tuj:"}. @@ -308,10 +272,8 @@ {"Room title","Babilejo-nomo"}. {"Roster groups allowed to subscribe","Kontaktlist-grupoj kiuj rajtas aboni"}. {"Roster size","Kontaktlist-grando"}. -{"RPC Call Error","Eraro de RPC-alvoko"}. {"Running Nodes","Funkciantaj Nodoj"}. {"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"}. @@ -329,19 +291,13 @@ {"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"}. -{"Statistics of ~p","Statistikoj de ~p"}. -{"Statistics","Statistikoj"}. -{"Stop","Haltigu"}. {"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"}. {"Subscribers may publish","Abonantoj rajtas publici"}. -{"Subscription","Abono"}. {"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"}. @@ -358,28 +314,16 @@ {"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"}. {"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"}. -{"Total rooms","Babilejoj"}. {"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","Malregistru"}. {"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 ~p","Ĝisdatigu ~p-n"}. -{"Update plan","Ĝisdatigo-plano"}. -{"Update script","Ĝisdatigo-skripto"}. -{"Update","Ĝisdatigu"}. -{"Uptime:","Daŭro de funkciado"}. {"URL for Archived Discussion Logs","Retpaĝa adreso de Enarkivigitaj Diskutprotokoloj"}. {"User JID","Uzant-JID"}. {"User Management","Uzanto-administrado"}. @@ -388,7 +332,6 @@ {"Users Last Activity","Lasta aktiveco de uzanto"}. {"Users","Uzantoj"}. {"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"}. diff --git a/priv/msgs/es.msg b/priv/msgs/es.msg index f3cebdb44..a914a43a1 100644 --- a/priv/msgs/es.msg +++ b/priv/msgs/es.msg @@ -12,17 +12,10 @@ {"A Web Page","Una página web"}. {"Accept","Aceptar"}. {"Access denied by service policy","Acceso denegado por la política del servicio"}. -{"Access model of authorize","Modelo de acceso de Autorizar"}. -{"Access model of open","Modelo de acceso de Abierto"}. -{"Access model of presence","Modelo de acceso de Presencia"}. -{"Access model of roster","Modelo de acceso de Roster"}. -{"Access model of whitelist","Modelo de acceso de Lista Blanca"}. {"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 Jabber ID","Añadir Jabber ID"}. -{"Add New","Añadir nuevo"}. {"Add User","Añadir usuario"}. {"Administration of ","Administración de "}. {"Administration","Administración"}. @@ -53,7 +46,9 @@ {"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"}. @@ -93,34 +88,25 @@ {"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 of room ~s","Configuración para la sala ~s"}. {"Configuration","Configuración"}. -{"Connected Resources:","Recursos conectados:"}. {"Contact Addresses (normally, room owner or owners)","Direcciones de contacto (normalmente la del dueño o dueños de la sala)"}. -{"Contrib Modules","Módulos Contrib"}. {"Country","País"}. -{"CPU Time:","Tiempo consumido de CPU:"}. {"Current Discussion Topic","Tema de discusión actual"}. {"Database failure","Error en la base de datos"}. -{"Database Tables at ~p","Tablas de la base de datos en ~p"}. {"Database Tables Configuration at ","Configuración de tablas de la base de datos en "}. {"Database","Base de datos"}. {"December","Diciembre"}. {"Default users as participants","Los usuarios son participantes por defecto"}. -{"Delete content","Borrar contenido"}. {"Delete message of the day on all hosts","Borrar el mensaje del día en todos los dominios"}. {"Delete message of the day","Borrar mensaje del dia"}. -{"Delete Selected","Borrar los seleccionados"}. -{"Delete table","Borrar tabla"}. {"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' not added (they do not exist!): ","'Mostrados' que no han sido añadidos (¡no existen!): "}. -{"Displayed:","Mostrados:"}. {"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"}. @@ -136,7 +122,6 @@ {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. -{"Elements","Elementos"}. {"Email Address","Dirección de correo electrónico"}. {"Email","Correo electrónico"}. {"Enable hats","Activar sombreros"}. @@ -151,7 +136,6 @@ {"Enter path to text file","Introduce ruta al fichero de texto"}. {"Enter the text you see","Teclea el texto que ves"}. {"Erlang XMPP Server","Servidor XMPP en Erlang"}. -{"Error","Error"}. {"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):"}. @@ -170,7 +154,6 @@ {"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"}. -{"From","De"}. {"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"}. @@ -180,23 +163,19 @@ {"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"}. +{"Given Name","Nombre de pila"}. {"Grant voice to this person?","¿Conceder voz a esta persona?"}. -{"Group","Grupo"}. -{"Groups that will be displayed to the members","Grupos que se mostrarán a los miembros"}. -{"Groups","Grupos"}. {"has been banned","ha sido bloqueado"}. {"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"}. +{"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"}. -{"Host","Dominio"}. {"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."}. @@ -210,7 +189,6 @@ {"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"}. -{"Incoming s2s Connections:","Conexiones S2S entrantes:"}. {"Incorrect CAPTCHA submit","El CAPTCHA proporcionado es incorrecto"}. {"Incorrect data form","Formulario de datos incorrecto"}. {"Incorrect password","Contraseña incorrecta"}. @@ -230,7 +208,6 @@ {"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"}. -{"It is not allowed to send private messages","No está permitido enviar mensajes privados"}. {"Jabber ID","Jabber ID"}. {"January","Enero"}. {"JID normalization denied by service policy","Se ha denegado la normalización del JID por política del servicio"}. @@ -241,7 +218,6 @@ {"July","Julio"}. {"June","Junio"}. {"Just created","Recién creada"}. -{"Label:","Etiqueta:"}. {"Last Activity","Última actividad"}. {"Last login","Última conexión"}. {"Last message","Último mensaje"}. @@ -249,11 +225,10 @@ {"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"}. -{"List of rooms","Lista de salas"}. {"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"}. -{"Low level update script","Script de actualización a bajo nivel"}. {"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"}. @@ -271,11 +246,8 @@ {"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"}. -{"Members not added (inexistent vhost!): ","Miembros no añadidos (el vhost no existe): "}. {"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 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."}. -{"Memory","Memoria"}. {"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"}. @@ -287,15 +259,12 @@ {"Moderator privileges required","Se necesita privilegios de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Solo moderadores"}. -{"Modified modules","Módulos modificados"}. {"Module failed to handle the query","El módulo falló al gestionar la petición"}. {"Monday","Lunes"}. -{"Multicast","Multicast"}. +{"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 in the rosters where this group will be displayed","Nombre del grupo con que aparecerá en las listas de contactos"}. {"Name","Nombre"}. -{"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'"}. @@ -360,14 +329,10 @@ {"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"}. -{"Offline Messages","Mensajes diferidos"}. -{"Offline Messages:","Mensajes diferidos:"}. {"OK","Aceptar"}. {"Old Password:","Contraseña antigua:"}. {"Online Users","Usuarios conectados"}. -{"Online Users:","Usuarios conectados:"}. {"Online","Conectado"}. -{"Only admins can see this","Solo los administradores pueden ver esto"}. {"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 "}. @@ -375,6 +340,7 @@ {"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"}. @@ -386,10 +352,8 @@ {"Organization Unit","Unidad de la organización"}. {"Other Modules Available:","Otros módulos disponibles:"}. {"Outgoing s2s Connections","Conexiones S2S salientes"}. -{"Outgoing s2s Connections:","Conexiones S2S salientes:"}. {"Owner privileges required","Se requieren privilegios de propietario de la sala"}. {"Packet relay is denied by service policy","Se ha denegado el reenvío del paquete por política del servicio"}. -{"Packet","Paquete"}. {"Participant ID","ID del Participante"}. {"Participant","Participante"}. {"Password Verification","Verificación de la contraseña"}. @@ -398,8 +362,7 @@ {"Password:","Contraseña:"}. {"Path to Dir","Ruta al directorio"}. {"Path to File","Ruta al fichero"}. -{"Payload type","Tipo de payload"}. -{"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"}. @@ -433,26 +396,22 @@ {"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 an XMPP account","Registrar una cuenta XMPP"}. -{"Registered Users","Usuarios registrados"}. -{"Registered Users:","Usuarios registrados:"}. {"Register","Registrar"}. {"Remote copy","Copia remota"}. {"Remove a hat from a user","Quitarle un sombrero a un usuario"}. -{"Remove All Offline Messages","Borrar todos los mensajes diferidos"}. {"Remove User","Eliminar usuario"}. -{"Remove","Borrar"}. {"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 Service","Reiniciar el servicio"}. -{"Restart","Reiniciar"}. {"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"}. @@ -463,20 +422,15 @@ {"Room terminates","Cerrando la sala"}. {"Room title","Título de la sala"}. {"Roster groups allowed to subscribe","Grupos de contactos que pueden suscribirse"}. -{"Roster of ~ts","Lista de contactos de ~ts"}. {"Roster size","Tamaño de la lista de contactos"}. -{"Roster:","Lista de contactos:"}. -{"RPC Call Error","Error en la llamada RPC"}. {"Running Nodes","Nodos funcionando"}. {"~s invites you to the room ~s","~s te invita a la sala ~s"}. {"Saturday","Sábado"}. -{"Script check","Comprobación de script"}. {"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 "}. -{"Select All","Seleccionar todo"}. {"Send announcement to all online users on all hosts","Enviar anuncio a todos los usuarios conectados en todos los dominios"}. {"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"}. @@ -489,6 +443,7 @@ {"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"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. @@ -497,25 +452,20 @@ {"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"}. +{"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"}. -{"Statistics of ~p","Estadísticas de ~p"}. -{"Statistics","Estadísticas"}. -{"Stop","Detener"}. {"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"}. {"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"}. -{"Subscription","Subscripción"}. {"Sunday","Domingo"}. {"Text associated with a picture","Texto asociado con una imagen"}. {"Text associated with a sound","Texto asociado con un sonido"}. @@ -561,10 +511,10 @@ {"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 type of node data, usually specified by the namespace of the payload (if any)","El tipo de datos del nodo, usualmente especificado por el namespace del payload (en caso de haberlo)"}. {"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: "}. @@ -578,7 +528,6 @@ {"Thursday","Jueves"}. {"Time delay","Retraso temporal"}. {"Timed out waiting for stream resumption","Ha pasado demasiado tiempo esperando que la conexión se restablezca"}. -{"Time","Fecha"}. {"To register, visit ~s","Para registrarte, visita ~s"}. {"To ~ts","A ~ts"}. {"Token TTL","Token TTL"}. @@ -591,13 +540,8 @@ {"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"}. -{"To","Para"}. -{"Total rooms","Salas totales"}. {"Traffic rate limit is exceeded","Se ha excedido el límite de tráfico"}. -{"Transactions Aborted:","Transacciones abortadas:"}. -{"Transactions Committed:","Transacciones finalizadas:"}. -{"Transactions Logged:","Transacciones registradas:"}. -{"Transactions Restarted:","Transacciones reiniciadas:"}. +{"~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"}. @@ -608,24 +552,20 @@ {"Uninstall","Desinstalar"}. {"Unregister an XMPP account","Borrar una cuenta XMPP"}. {"Unregister","Borrar"}. -{"Unselect All","Deseleccionar todo"}. {"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 ~p","Actualizar ~p"}. -{"Update plan","Plan de actualización"}. -{"Update script","Script de actualización"}. {"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"}. -{"Update","Actualizar"}. +{"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"}. -{"Uptime:","Tiempo desde el inicio:"}. {"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"}. @@ -635,7 +575,6 @@ {"Users Last Activity","Última actividad de los usuarios"}. {"Users","Usuarios"}. {"User","Usuario"}. -{"Validate","Validar"}. {"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"}. @@ -643,14 +582,13 @@ {"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"}. -{"View Queue","Ver Cola"}. -{"View Roster","Ver Lista de contactos"}. {"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"}. +{"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"}. @@ -663,6 +601,7 @@ {"Whether to allow subscriptions","Permitir subscripciones"}. {"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"}. @@ -674,6 +613,7 @@ {"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"}. diff --git a/priv/msgs/fr.msg b/priv/msgs/fr.msg index aa8499c1b..0f8e99eeb 100644 --- a/priv/msgs/fr.msg +++ b/priv/msgs/fr.msg @@ -12,16 +12,9 @@ {"A Web Page","Une page Web"}. {"Accept","Accepter"}. {"Access denied by service policy","L'accès au service est refusé"}. -{"Access model of authorize","Modèle d’accès de « autoriser »"}. -{"Access model of open","Modèle d’accès de « ouvrir »"}. -{"Access model of presence","Modèle d’accès de « présence »"}. -{"Access model of roster","Modèle d’accès de « liste »"}. -{"Access model of whitelist","Modèle d’accès de « liste blanche »"}. {"Access model","Modèle d’accès"}. {"Account doesn't exist","Le compte n’existe pas"}. {"Action on user","Action sur l'utilisateur"}. -{"Add Jabber ID","Ajouter un Jabber ID"}. -{"Add New","Ajouter"}. {"Add User","Ajouter un utilisateur"}. {"Administration of ","Administration de "}. {"Administration","Administration"}. @@ -95,29 +88,20 @@ {"Conference room does not exist","Le salon de discussion n'existe pas"}. {"Configuration of room ~s","Configuration pour le salon ~s"}. {"Configuration","Configuration"}. -{"Connected Resources:","Ressources connectées :"}. {"Contact Addresses (normally, room owner or owners)","Adresses de contact (normalement les administrateurs du salon)"}. {"Country","Pays"}. -{"CPU Time:","Temps CPU :"}. {"Current Discussion Topic","Sujet de discussion courant"}. {"Database failure","Échec sur la base de données"}. -{"Database Tables at ~p","Tables de base de données sur ~p"}. {"Database Tables Configuration at ","Configuration des tables de base de données sur "}. {"Database","Base de données"}. {"December","Décembre"}. {"Default users as participants","Les utilisateurs sont participant par défaut"}. -{"Delete content","Supprimer le contenu"}. {"Delete message of the day on all hosts","Supprimer le message du jour sur tous les domaines"}. {"Delete message of the day","Supprimer le message du jour"}. -{"Delete Selected","Suppression des éléments sélectionnés"}. -{"Delete table","Supprimer la table"}. {"Delete User","Supprimer l'utilisateur"}. {"Deliver event notifications","Envoyer les notifications d'événement"}. {"Deliver payloads with event notifications","Inclure le contenu du message avec la notification"}. -{"Description:","Description :"}. {"Disc only copy","Copie sur disque uniquement"}. -{"'Displayed groups' not added (they do not exist!): ","« Groupes affichés » non ajoutés (ils n’existent pas !) : "}. -{"Displayed:","Affichés :"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Ne révélez votre mot de passe à personne, pas même aux administrateurs du serveur XMPP."}. {"Dump Backup to Text File at ","Enregistrer la sauvegarde dans un fichier texte sur "}. {"Dump to Text File","Sauvegarder dans un fichier texte"}. @@ -132,7 +116,6 @@ {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Console Web d'administration de ejabberd"}. {"ejabberd","ejabberd"}. -{"Elements","Éléments"}. {"Email Address","Adresse courriel"}. {"Email","Courriel"}. {"Enable logging","Activer l'archivage"}. @@ -146,7 +129,6 @@ {"Enter path to text file","Entrez le chemin vers le fichier texte"}. {"Enter the text you see","Tapez le texte que vous voyez"}. {"Erlang XMPP Server","Serveur XMPP Erlang"}. -{"Error","Erreur"}. {"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) :"}. @@ -165,7 +147,6 @@ {"Fill in the form to search for any matching XMPP User","Complétez le formulaire pour rechercher un utilisateur XMPP correspondant"}. {"Friday","Vendredi"}. {"From ~ts","De ~ts"}. -{"From","De"}. {"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"}. @@ -174,13 +155,9 @@ {"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"}. {"Given Name","Nom"}. {"Grant voice to this person?","Accorder le droit de parole à cet utilisateur ?"}. -{"Group","Groupe"}. -{"Groups that will be displayed to the members","Groupes qui seront affichés aux membres"}. -{"Groups","Groupes"}. {"has been banned","a été banni"}. {"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"}. @@ -188,7 +165,6 @@ {"has been kicked","a été expulsé"}. {"Hats limit exceeded","La limite a été dépassée"}. {"Host unknown","Serveur inconnu"}. -{"Host","Serveur"}. {"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."}. @@ -202,13 +178,13 @@ {"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"}. -{"Incoming s2s Connections:","Connexions s2s entrantes :"}. {"Incorrect CAPTCHA submit","Entrée CAPTCHA incorrecte"}. {"Incorrect data form","Formulaire incorrect"}. {"Incorrect password","Mot de passe incorrect"}. {"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"}. @@ -220,22 +196,18 @@ {"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 privés à la conférence"}. -{"It is not allowed to send private messages","L'envoi de messages privés n'est pas autorisé"}. {"Jabber ID","Jabber ID"}. {"January","Janvier"}. {"joins the room","rejoint le salon"}. {"July","Juillet"}. {"June","Juin"}. {"Just created","Vient d'être créé"}. -{"Label:","Étiquette :"}. {"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"}. -{"List of rooms","Liste des salons"}. -{"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"}. @@ -252,9 +224,7 @@ {"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 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é."}. -{"Memory","Mémoire"}. {"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"}. @@ -265,14 +235,12 @@ {"Moderator privileges required","Les droits de modérateur sont nécessaires"}. {"Moderator","Modérateur"}. {"Moderators Only","Modérateurs uniquement"}. -{"Modified modules","Modules mis à jour"}. {"Module failed to handle the query","Échec de traitement de la demande"}. {"Monday","Lundi"}. {"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"}. -{"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"}. @@ -335,14 +303,10 @@ {"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 hors ligne :"}. {"OK","OK"}. {"Old Password:","Ancien mot de passe :"}. -{"Online Users:","Utilisateurs connectés :"}. {"Online Users","Utilisateurs en ligne"}. {"Online","En ligne"}. -{"Only admins can see this","Seuls les administrateurs peuvent voir cela"}. {"Only deliver notifications to available users","Envoyer les notifications uniquement aux utilisateurs disponibles"}. {"Only 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"}. @@ -357,9 +321,7 @@ {"Organization Name","Nom de l'organisation"}. {"Organization Unit","Unité de l'organisation"}. {"Outgoing s2s Connections","Connexions s2s sortantes"}. -{"Outgoing s2s Connections:","Connexions s2s sortantes :"}. {"Owner privileges required","Les droits de propriétaire sont nécessaires"}. -{"Packet","Paquet"}. {"Participant","Participant"}. {"Password Verification","Vérification du mot de passe"}. {"Password Verification:","Vérification du mot de passe :"}. @@ -367,7 +329,6 @@ {"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 : "}. {"Persist items to storage","Stockage persistant des éléments"}. {"Persistent","Persistant"}. @@ -399,20 +360,15 @@ {"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 an XMPP account","Inscrire un compte XMPP"}. -{"Registered Users","Utilisateurs enregistrés"}. -{"Registered Users:","Utilisateurs enregistrés :"}. {"Register","Enregistrer"}. {"Remote copy","Copie distante"}. -{"Remove All Offline Messages","Effacer tous les messages hors ligne"}. {"Remove User","Supprimer l'utilisateur"}. -{"Remove","Supprimer"}. {"Replaced by new connection","Remplacé par une nouvelle connexion"}. {"Request has timed out","La demande a expiré"}. {"Request is ignored","La demande est ignorée"}. {"Requested role","Rôle demandé"}. {"Resources","Ressources"}. {"Restart Service","Redémarrer le service"}. -{"Restart","Redémarrer"}. {"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 :"}. @@ -424,19 +380,14 @@ {"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 of ~ts","Liste de contacts de ~ts"}. {"Roster size","Taille de la liste de contacts"}. -{"Roster:","Liste de contacts :"}. -{"RPC Call Error","Erreur d'appel RPC"}. {"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 "}. -{"Select All","Tout sélectionner"}. {"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 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"}. @@ -456,20 +407,14 @@ {"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"}. {"Stanza ID","Identifiant Stanza"}. -{"Statistics of ~p","Statistiques de ~p"}. -{"Statistics","Statistiques"}. -{"Stop","Arrêter"}. {"Stopped Nodes","Nœuds arrêtés"}. -{"Storage Type","Type de stockage"}. {"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é"}. {"Subscribers may publish","Les souscripteurs peuvent publier"}. -{"Subscription","Abonnement"}. {"Subscriptions are not allowed","Les abonnement ne sont pas autorisés"}. {"Sunday","Dimanche"}. {"Text associated with a picture","Texte associé à une image"}. @@ -518,10 +463,8 @@ {"Thursday","Jeudi"}. {"Time delay","Délais"}. {"Timed out waiting for stream resumption","Expiration du délai d’attente pour la reprise du flux"}. -{"Time","Heure"}. {"To register, visit ~s","Pour vous enregistrer, visitez ~s"}. {"To ~ts","À ~ts"}. -{"To","A"}. {"Token TTL","Jeton TTL"}. {"Too many active bytestreams","Trop de flux SOCKS5 actifs"}. {"Too many CAPTCHA requests","Trop de requêtes CAPTCHA"}. @@ -531,30 +474,21 @@ {"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"}. -{"Total rooms","Nombre de salons"}. {"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é"}. {"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"}. -{"Unselect All","Tout désélectionner"}. {"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 plan","Plan de mise à jour"}. -{"Update ~p","Mise à jour de ~p"}. -{"Update script","Script de mise à jour"}. -{"Update","Mettre à jour"}. -{"Uptime:","Temps depuis le démarrage :"}. +{"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"}. @@ -569,14 +503,12 @@ {"Users Last Activity","Dernière activité des utilisateurs"}. {"Users","Utilisateurs"}. {"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"}. -{"View Queue","Afficher la file d’attente"}. {"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"}. diff --git a/priv/msgs/gl.msg b/priv/msgs/gl.msg index 07b35e994..a72e07360 100644 --- a/priv/msgs/gl.msg +++ b/priv/msgs/gl.msg @@ -9,8 +9,6 @@ {"Accept","Aceptar"}. {"Access denied by service policy","Acceso denegado pola política do servizo"}. {"Action on user","Acción no usuario"}. -{"Add Jabber ID","Engadir ID Jabber"}. -{"Add New","Engadir novo"}. {"Add User","Engadir usuario"}. {"Administration of ","Administración de "}. {"Administration","Administración"}. @@ -60,22 +58,17 @@ {"Conference room does not exist","A sala de conferencias non existe"}. {"Configuration of room ~s","Configuración para a sala ~s"}. {"Configuration","Configuración"}. -{"Connected Resources:","Recursos conectados:"}. {"Country","País"}. -{"CPU Time:","Tempo da CPU:"}. {"Database failure","Erro na base de datos"}. -{"Database Tables at ~p","Táboas da base de datos en ~p"}. {"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 message of the day on all hosts","Borrar a mensaxe do día en todos os dominios"}. {"Delete message of the day","Borrar mensaxe do dia"}. -{"Delete Selected","Eliminar os seleccionados"}. {"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"}. {"Dump Backup to Text File at ","Exporta copia de seguridade a ficheiro de texto en "}. {"Dump to Text File","Exportar a ficheiro de texto"}. @@ -87,7 +80,6 @@ {"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"}. {"Email","Email"}. {"Enable logging","Gardar históricos"}. {"Enable message archiving","Activar o almacenamento de mensaxes"}. @@ -99,7 +91,6 @@ {"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"}. -{"Error","Erro"}. {"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):"}. @@ -115,24 +106,19 @@ {"February","Febreiro"}. {"File larger than ~w bytes","O ficheiro é maior que ~w bytes"}. {"Friday","Venres"}. -{"From","De"}. {"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"}. {"Given Name","Nome"}. {"Grant voice to this person?","¿Conceder voz a esta persoa?"}. -{"Group","Grupo"}. -{"Groups","Grupos"}. {"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 the room has been changed to members-only","foi expulsado, porque a sala cambiouse a só-membros"}. {"has been kicked","foi expulsado"}. {"Host unknown","Dominio descoñecido"}. -{"Host","Host"}. {"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"}. @@ -144,7 +130,6 @@ {"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"}. -{"Incoming s2s Connections:","Conexións S2S saíntes:"}. {"Incorrect CAPTCHA submit","O CAPTCHA proporcionado é incorrecto"}. {"Incorrect data form","Formulario de datos incorrecto"}. {"Incorrect password","Contrasinal incorrecta"}. @@ -159,7 +144,6 @@ {"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"}. -{"It is not allowed to send private messages","Non está permitido enviar mensaxes privadas"}. {"Jabber ID","Jabber ID"}. {"January","Xaneiro"}. {"joins the room","entra na sala"}. @@ -170,8 +154,6 @@ {"Last month","Último mes"}. {"Last year","Último ano"}. {"leaves the room","sae da sala"}. -{"List of rooms","Lista de salas"}. -{"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"}. @@ -185,21 +167,17 @@ {"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"}. {"Moderator","Moderator"}. -{"Modified modules","Módulos Modificados"}. {"Module failed to handle the query","O módulo non puido xestionar a consulta"}. {"Monday","Luns"}. {"Multicast","Multicast"}. {"Multi-User Chat","Salas de Charla"}. {"Name","Nome"}. -{"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"}. @@ -248,12 +226,9 @@ {"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"}. {"Old Password:","Contrasinal anterior:"}. {"Online Users","Usuarios conectados"}. -{"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 "}. @@ -268,9 +243,7 @@ {"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:"}. {"Owner privileges required","Requírense privilexios de propietario da sala"}. -{"Packet","Paquete"}. {"Participant","Participante"}. {"Password Verification","Verificación da contrasinal"}. {"Password Verification:","Verificación da Contrasinal:"}. @@ -278,7 +251,6 @@ {"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"}. @@ -297,17 +269,12 @@ {"RAM copy","Copia en RAM"}. {"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:"}. {"Register","Rexistrar"}. {"Remote copy","Copia remota"}. -{"Remove All Offline Messages","Borrar Todas as Mensaxes Sen conexión"}. {"Remove User","Eliminar usuario"}. -{"Remove","Borrar"}. {"Replaced by new connection","Substituído por unha nova conexión"}. {"Resources","Recursos"}. {"Restart Service","Reiniciar o servizo"}. -{"Restart","Reiniciar"}. {"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):"}. {"Restore binary backup immediately:","Restaurar inmediatamente copia de seguridade binaria:"}. @@ -321,10 +288,8 @@ {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Lista de grupos autorizados a subscribir"}. {"Roster size","Tamaño da lista de contactos"}. -{"RPC Call Error","Erro na chamada RPC"}. {"Running Nodes","Nodos funcionando"}. {"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 on all hosts","Enviar anuncio a todos os usuarios conectados en todos os dominios"}. @@ -342,19 +307,13 @@ {"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"}. -{"Statistics of ~p","Estatísticas de ~p"}. -{"Statistics","Estatísticas"}. -{"Stop","Deter"}. {"Stopped Nodes","Nodos detidos"}. -{"Storage Type","Tipo de almacenamento"}. {"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"}. {"Subscriptions are not allowed","Non se permiten subscricións"}. -{"Subscription","Subscripción"}. {"Sunday","Domingo"}. {"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"}. @@ -373,7 +332,6 @@ {"This room is not anonymous","Sala non anónima"}. {"Thursday","Xoves"}. {"Time delay","Atraso temporal"}. -{"Time","Data"}. {"To register, visit ~s","Para rexistrarse, visita ~s"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","Demasiados bytestreams activos"}. @@ -383,13 +341,7 @@ {"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"}. -{"To","Para"}. -{"Total rooms","Salas totais"}. {"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"}. @@ -399,11 +351,6 @@ {"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 ~p","Actualizar ~p"}. -{"Update plan","Plan de actualización"}. -{"Update script","Script de actualización"}. -{"Update","Actualizar"}. -{"Uptime:","Tempo desde o inicio:"}. {"User already exists","O usuario xa existe"}. {"User JID","Jabber ID do usuario"}. {"User (jid)","Usuario (jid)"}. @@ -415,7 +362,6 @@ {"Users Last Activity","Última actividade dos usuarios"}. {"Users","Usuarios"}. {"User","Usuario"}. -{"Validate","Validar"}. {"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"}. diff --git a/priv/msgs/he.msg b/priv/msgs/he.msg index 5a4926a88..1dabbd028 100644 --- a/priv/msgs/he.msg +++ b/priv/msgs/he.msg @@ -9,8 +9,6 @@ {"Accept","קבל"}. {"Access denied by service policy","גישה נדחתה על ידי פוליסת שירות"}. {"Action on user","פעולה על משתמש"}. -{"Add Jabber ID","הוסף מזהה Jabber"}. -{"Add New","הוסף חדש"}. {"Add User","הוסף משתמש"}. {"Administration of ","ניהול של "}. {"Administration","הנהלה"}. @@ -58,22 +56,17 @@ {"Conference room does not exist","חדר ועידה לא קיים"}. {"Configuration of room ~s","תצורת חדר ~s"}. {"Configuration","תצורה"}. -{"Connected Resources:","משאבים מחוברים:"}. {"Country","ארץ"}. -{"CPU Time:","זמן מחשב (CPU):"}. {"Database failure","כשל מסד נתונים"}. -{"Database Tables at ~p","טבלאות מסד נתונים אצל ~p"}. {"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","מחק משתמש"}. {"Deliver event notifications","מסור התראות אירוע"}. {"Deliver payloads with event notifications","מסור מטעני ייעוד (מטע״ד) יחד עם התראות אירוע"}. -{"Description:","תיאור:"}. {"Disc only copy","העתק של תקליטור בלבד"}. {"Dump Backup to Text File at ","השלך גיבוי לקובץ טקסט אצל "}. {"Dump to Text File","השלך לקובץ טקסט"}. @@ -85,7 +78,6 @@ {"ejabberd SOCKS5 Bytestreams module","מודול SOCKS5 Bytestreams של ejabberd"}. {"ejabberd vCard module","מודול vCard של ejabberd"}. {"ejabberd Web Admin","מנהל רשת ejabberd"}. -{"Elements","אלמנטים"}. {"Email","דוא״ל"}. {"Enable logging","אפשר רישום פעילות"}. {"Enable message archiving","אפשר אחסון הודעות"}. @@ -96,7 +88,6 @@ {"Enter path to jabberd14 spool file","הזן נתיב לקובץ סליל (spool file) של jabberd14"}. {"Enter path to text file","הזן נתיב לקובץ טקסט"}. {"Enter the text you see","הזן את הכיתוב שאתה רואה"}. -{"Error","שגיאה"}. {"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):"}. @@ -109,24 +100,19 @@ {"February","פברואר"}. {"File larger than ~w bytes","קובץ גדול יותר משיעור של ~w בייטים"}. {"Friday","יום שישי"}. -{"From","מאת"}. {"Full Name","שם מלא"}. {"Get Number of Online Users","השג מספר של משתמשים מקוונים"}. {"Get Number of Registered Users","השג מספר של משתמשים רשומים"}. {"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 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","נבעט/ה"}. {"Host unknown","מארח לא ידוע"}. -{"Host","מארח"}. {"If you don't see the CAPTCHA image here, visit the web page.","אם אינך רואה תמונת CAPTCHA כאן, בקר בעמוד רשת."}. {"Import Directory","ייבוא מדור"}. {"Import File","ייבוא קובץ"}. @@ -137,7 +123,6 @@ {"Import Users from Dir at ","ייבוא משתמשים מתוך מדור אצל "}. {"Import Users From jabberd14 Spool Files","יבא משתמשים מתוך קבצי סליל (Spool Files) של jabberd14"}. {"Improper message type","טיפוס הודעה לא מתאים"}. -{"Incoming s2s Connections:","חיבורי s2s נכנסים:"}. {"Incorrect CAPTCHA submit","נשלחה CAPTCHA שגויה"}. {"Incorrect data form","טופס מידע לא תקין"}. {"Incorrect password","מילת מעבר שגויה"}. @@ -148,7 +133,6 @@ {"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 ID","מזהה Jabber"}. {"January","ינואר"}. {"joins the room","נכנס/ת אל החדר"}. @@ -159,8 +143,6 @@ {"Last month","חודש אחרון"}. {"Last year","שנה אחרונה"}. {"leaves the room","עוזב/ת את החדר"}. -{"List of rooms","רשימה של חדרים"}. -{"Low level update script","תסריט עדכון Low level"}. {"Make participants list public","הפוך רשימת משתתפים לפומבית"}. {"Make room CAPTCHA protected","הפוך חדר לחדר מוגן CAPTCHA"}. {"Make room members-only","הפוך חדר לחדר עבור חברים-בלבד"}. @@ -174,20 +156,16 @@ {"Maximum Number of Occupants","מספר מרבי של נוכחים"}. {"May","מאי"}. {"Membership is required to enter this room","נדרשת חברות כדי להיכנס אל חדר זה"}. -{"Members:","חברים:"}. -{"Memory","זיכרון"}. {"Message body","גוף הודעה"}. {"Middle Name","שם אמצעי"}. {"Minimum interval between voice requests (in seconds)","תדירות מינימלית בין בקשות ביטוי (בשניות)"}. {"Moderator privileges required","נדרשות הרשאות אחראי"}. {"Moderator","אחראי"}. -{"Modified modules","מודולים שהותאמו"}. {"Module failed to handle the query","מודול נכשל לטפל בשאילתא"}. {"Monday","יום שני"}. {"Multicast","שידור מרובב"}. {"Multi-User Chat","שיחה מרובת משתמשים"}. {"Name","שם"}. -{"Name:","שם:"}. {"Never","אף פעם"}. {"New Password:","סיסמה חדשה:"}. {"Nickname Registration at ","רישום שם כינוי אצל "}. @@ -224,12 +202,9 @@ {"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","רק תגיות או הינן מורשות"}. @@ -243,9 +218,7 @@ {"Organization Name","שם ארגון"}. {"Organization Unit","יחידת איגוד"}. {"Outgoing s2s Connections","חיבורי s2s יוצאים"}. -{"Outgoing s2s Connections:","חיבורי s2s יוצאים:"}. {"Owner privileges required","נדרשות הרשאות בעלים"}. -{"Packet","חבילת מידע"}. {"Participant","משתתף"}. {"Password Verification","אימות סיסמה"}. {"Password Verification:","אימות סיסמה:"}. @@ -253,7 +226,6 @@ {"Password:","סיסמה:"}. {"Path to Dir","נתיב למדור"}. {"Path to File","נתיב לקובץ"}. -{"Pending","ממתינות"}. {"Period: ","משך זמן: "}. {"Persist items to storage","פריטים קבועים לאחסון"}. {"Ping query is incorrect","שאילתת פינג הינה שגויה"}. @@ -271,17 +243,12 @@ {"RAM copy","העתק RAM"}. {"Really delete message of the day?","באמת למחוק את בשורת היום?"}. {"Recipient is not in the conference room","מקבל אינו מצוי בחדר הועידה"}. -{"Registered Users","משתמשים רשומים"}. -{"Registered Users:","משתמשים רשומים:"}. {"Register","הרשם"}. {"Remote copy","העתק מרוחק"}. -{"Remove All Offline Messages","הסר את כל ההודעות הלא מקוונות"}. {"Remove User","הסר משתמש"}. -{"Remove","הסר"}. {"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:","שחזר גיבוי בינארי לאלתר:"}. @@ -295,10 +262,8 @@ {"Room title","כותרת חדר"}. {"Roster groups allowed to subscribe","קבוצות רשימה מורשות להירשם"}. {"Roster size","גודל רשימה"}. -{"RPC Call Error","שגיאת קריאת RPC"}. {"Running Nodes","צמתים מורצים"}. {"Saturday","יום שבת"}. -{"Script check","בדיקת תסריט"}. {"Search Results for ","תוצאות חיפוש עבור "}. {"Search users in ","חיפוש משתמשים אצל "}. {"Send announcement to all online users on all hosts","שלח בשורה לכל המשתמשים המקוונים בכל המארחים"}. @@ -316,19 +281,13 @@ {"Specify the access model","ציין מודל גישה"}. {"Specify the event message type","ציין טיפוס הודעת אירוע"}. {"Specify the publisher model","ציין מודל פרסום"}. -{"Statistics of ~p","סטטיסטיקות של ~p"}. -{"Statistics","סטטיסטיקה"}. {"Stopped Nodes","צמתים שנפסקו"}. -{"Stop","הפסק"}. -{"Storage Type","טיפוס אחסון"}. {"Store binary backup:","אחסן גיבוי בינארי:"}. {"Store plain text backup:","אחסן גיבוי טקסט גלוי (plain text):"}. {"Subject","נושא"}. {"Submitted","נשלח"}. -{"Submit","שלח"}. {"Subscriber Address","כתובת מנוי"}. {"Subscriptions are not allowed","הרשמות אינן מורשות"}. -{"Subscription","הרשמה"}. {"Sunday","יום ראשון"}. {"That nickname is already in use by another occupant","שם כינוי זה כבר מצוי בשימוש על ידי נוכח אחר"}. {"That nickname is registered by another person","שם כינוי זה הינו רשום על ידי מישהו אחר"}. @@ -337,13 +296,11 @@ {"The collections with which a node is affiliated","האוספים עמם צומת מסונף"}. {"The password is too weak","הסיסמה חלשה מדי"}. {"the password is","הסיסמה היא"}. -{"The type of node data, usually specified by the namespace of the payload (if any)","סוג מידע ממסר, לרוב מצוין לפי מרחב־שמות של מטען הייעוד (אם בכלל)"}. {"There was an error creating the account: ","אירעה שגיאה ביצירת החשבון: "}. {"There was an error deleting the account: ","אירעה שגיאה במחיקת החשבון: "}. {"This room is not anonymous","חדר זה אינו אנונימי"}. {"Thursday","יום חמישי"}. {"Time delay","זמן שיהוי"}. -{"Time","זמן"}. {"To register, visit ~s","כדי להירשם, בקרו ~s"}. {"Token TTL","סימן TTL"}. {"Too many active bytestreams","יותר מדי יחידות bytestream פעילות"}. @@ -351,13 +308,7 @@ {"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","יותר מדי משתמשים בועידה זו"}. -{"Total rooms","חדרים סה״כ"}. -{"To","לכבוד"}. {"Traffic rate limit is exceeded","מגבלת שיעור תעבורה נחצתה"}. -{"Transactions Aborted:","טרנזקציות שבוטלו:"}. -{"Transactions Committed:","טרנזקציות שבוצעו:"}. -{"Transactions Logged:","טרנזקציות שנרשמו:"}. -{"Transactions Restarted:","טרנזקציות שהותחלו מחדש:"}. {"Tuesday","יום שלישי"}. {"Unable to generate a CAPTCHA","אין אפשרות להפיק CAPTCHA"}. {"Unauthorized","לא מורשה"}. @@ -365,11 +316,6 @@ {"Unregister","בטל רישום"}. {"Update message of the day (don't send)","עדכן את בשורת היום (אל תשלח)"}. {"Update message of the day on all hosts (don't send)","עדכן את בשורת היום בכל המארחים (אל תשלח)"}. -{"Update plan","תכנית עדכון"}. -{"Update ~p","עדכון ~p"}. -{"Update script","תסריט עדכון"}. -{"Update","עדכן"}. -{"Uptime:","זמן פעילות:"}. {"User already exists","משתמש כבר קיים"}. {"User JID","‏JID משתמש"}. {"User (jid)","משתמש (jid)"}. @@ -381,7 +327,6 @@ {"Users Last Activity","פעילות משתמשים אחרונה"}. {"Users","משתמשים"}. {"User","משתמש"}. -{"Validate","הענק תוקף"}. {"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"}. diff --git a/priv/msgs/hu.msg b/priv/msgs/hu.msg index bf7782e49..32e3174d0 100644 --- a/priv/msgs/hu.msg +++ b/priv/msgs/hu.msg @@ -10,8 +10,6 @@ {"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 Jabber ID","Jabber-azonosító hozzáadása"}. -{"Add New","Új hozzáadása"}. {"Add User","Felhasználó hozzáadása"}. {"Administration of ","Adminisztrációja ennek: "}. {"Administration","Adminisztráció"}. @@ -66,22 +64,15 @@ {"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"}. -{"Connected Resources:","Kapcsolódott erőforrások:"}. {"Country","Ország"}. -{"CPU Time:","Processzoridő:"}. {"Database failure","Adatbázishiba"}. -{"Database Tables at ~p","Adatbázistáblák itt: ~p"}. {"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 content","Tartalom törlése"}. {"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 Selected","Kijelöltek törlése"}. -{"Delete table","Tábla törlése"}. {"Delete User","Felhasználó törlése"}. -{"Description:","Leírás:"}. {"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"}. @@ -95,7 +86,6 @@ {"ejabberd vCard module","ejabberd vCard modul"}. {"ejabberd Web Admin","ejabberd webes adminisztráció"}. {"ejabberd","ejabberd"}. -{"Elements","Elemek"}. {"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"}. @@ -106,7 +96,6 @@ {"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"}. -{"Error","Hiba"}. {"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):"}. @@ -121,24 +110,19 @@ {"File larger than ~w bytes","A fájl nagyobb ~w bájtnál"}. {"Friday","péntek"}. {"From ~ts","Feladó: ~ts"}. -{"From","Feladó"}. {"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 Password","Felhasználó jelszavának lekérése"}. {"Get User Statistics","Felhasználói statisztikák lekérése"}. {"Given Name","Keresztnév"}. -{"Group","Csoport"}. -{"Groups","Csoportok"}. {"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"}. -{"Host","Gép"}. {"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."}. @@ -152,7 +136,6 @@ {"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"}. -{"Incoming s2s Connections:","Bejövő s2s kapcsolatok:"}. {"Incorrect CAPTCHA submit","Hibás CAPTCHA beküldés"}. {"Incorrect data form","Hibás adatűrlap"}. {"Incorrect password","Hibás jelszó"}. @@ -170,7 +153,6 @@ {"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"}. -{"It is not allowed to send private messages","Nem engedélyezett személyes üzeneteket küldeni"}. {"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"}. @@ -183,8 +165,6 @@ {"Last month","Múlt hónap"}. {"Last year","Múlt év"}. {"leaves the room","elhagyta a szobát"}. -{"List of rooms","Szobák listája"}. -{"Low level update script","Alacsony szintű frissítő parancsfájl"}. {"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"}. @@ -198,20 +178,16 @@ {"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"}. -{"Members:","Tagok:"}. -{"Memory","Memória"}. {"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"}. -{"Modified modules","Módosított modulok"}. {"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"}. -{"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"}. @@ -260,12 +236,9 @@ {"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"}. -{"Offline Messages","Kapcsolat nélküli üzenetek"}. -{"Offline Messages:","Kapcsolat nélküli üzenetek:"}. {"OK","Rendben"}. {"Old Password:","Régi jelszó:"}. {"Online Users","Elérhető felhasználók"}. -{"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"}. @@ -279,17 +252,14 @@ {"Organization Name","Szervezet neve"}. {"Organization Unit","Szervezeti egység"}. {"Outgoing s2s Connections","Kimenő s2s kapcsolatok"}. -{"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"}. -{"Packet","Csomag"}. {"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"}. -{"Pending","Függőben"}. {"Period: ","Időszak: "}. {"Ping query is incorrect","A lekérdezés pingelése hibás"}. {"Ping","Ping"}. @@ -311,19 +281,14 @@ {"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"}. -{"Registered Users","Regisztrált felhasználók"}. -{"Registered Users:","Regisztrált felhasználók:"}. {"Register","Regisztráció"}. {"Remote copy","Távoli másolás"}. -{"Remove All Offline Messages","Összes kapcsolat nélküli üzenet eltávolítása"}. {"Remove User","Felhasználó eltávolítása"}. -{"Remove","Eltávolítás"}. {"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"}. -{"Restart","Újraindítás"}. {"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:"}. @@ -335,15 +300,11 @@ {"Room Occupants","Szoba résztvevői"}. {"Room terminates","Szoba megszűnik"}. {"Room title","Szoba címe"}. -{"Roster of ~ts","~ts névsora"}. {"Roster size","Névsor mérete"}. -{"RPC Call Error","RPC hívási hiba"}. {"Running Nodes","Futó csomópontok"}. {"Saturday","szombat"}. -{"Script check","Parancsfájl-ellenőrzés"}. {"Search Results for ","Keresési eredménye ennek: "}. {"Search users in ","Felhasználók keresése ebben: "}. -{"Select All","Összes kijelölése"}. {"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"}. @@ -358,19 +319,13 @@ {"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"}. -{"Statistics of ~p","~p statisztikái"}. -{"Statistics","Statisztikák"}. -{"Stop","Leállítás"}. {"Stopped Nodes","Leállított csomópontok"}. -{"Storage Type","Tárolótípus"}. {"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"}. -{"Submit","Elküldés"}. {"Submitted","Elküldve"}. -{"Subscription","Feliratkozás"}. {"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ő"}. @@ -395,26 +350,19 @@ {"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"}. -{"Time","Idő"}. {"To register, visit ~s","Regisztráláshoz látogassa meg ezt az oldalt: ~s"}. {"To ~ts","Címzett: ~ts"}. -{"To","Címzett"}. {"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 (~ts) A cím ~ts-kor lesz feloldva UTC szerint"}. +{"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"}. -{"Total rooms","Szobák összesen"}. {"Traffic rate limit is exceeded","Forgalom sebességkorlátja elérve"}. -{"Transactions Aborted:","Megszakított tranzakciók:"}. -{"Transactions Committed:","Véglegesített tranzakciók:"}. -{"Transactions Logged:","Naplózott tranzakciók:"}. -{"Transactions Restarted:","Újraindított tranzakciók:"}. {"~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"}. @@ -423,16 +371,10 @@ {"Unexpected action","Váratlan művelet"}. {"Unexpected error condition: ~p","Váratlan hibafeltétel: ~p"}. {"Unregister","Regisztráció törlése"}. -{"Unselect All","Összes kijelölésének megszünteté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)"}. -{"Update plan","Frissítési terv"}. -{"Update ~p","~p frissítése"}. -{"Update script","Frissítő parancsfájl"}. -{"Update","Frissítés"}. -{"Uptime:","Működési idő:"}. {"User already exists","A felhasználó már létezik"}. {"User (jid)","Felhasználó (JID)"}. {"User Management","Felhasználó-kezelés"}. @@ -445,7 +387,6 @@ {"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"}. -{"Validate","Ellenőrzés"}. {"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"}. diff --git a/priv/msgs/id.msg b/priv/msgs/id.msg index 08a3df2b4..ac6db281c 100644 --- a/priv/msgs/id.msg +++ b/priv/msgs/id.msg @@ -12,16 +12,9 @@ {"A Web Page","Halaman web"}. {"Accept","Diterima"}. {"Access denied by service policy","Akses ditolak oleh kebijakan layanan"}. -{"Access model of authorize","Model akses otorisasi"}. -{"Access model of open","Model akses terbuka"}. -{"Access model of presence","Model akses kehadiran"}. -{"Access model of roster","model akses daftar kontak"}. -{"Access model of whitelist","Model akses daftar putih"}. {"Access model","Model akses"}. {"Account doesn't exist","Akun tidak ada"}. {"Action on user","Tindakan pada pengguna"}. -{"Add Jabber ID","Tambah Jabber ID"}. -{"Add New","Tambah Baru"}. {"Add User","Tambah Pengguna"}. {"Administration of ","Administrasi "}. {"Administration","Administrasi"}. @@ -92,28 +85,20 @@ {"Conference room does not exist","Ruang Konferensi tidak ada"}. {"Configuration of room ~s","Pengaturan ruangan ~s"}. {"Configuration","Pengaturan"}. -{"Connected Resources:","Sumber Daya Terhubung:"}. {"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 at ~p","Tabel Database pada ~p"}. {"Database Tables Configuration at ","Konfigurasi Tabel Database pada "}. {"Database","Database"}. {"December","Desember"}. {"Default users as participants","pengguna pertama kali masuk sebagai participant"}. -{"Delete content","Hapus isi"}. {"Delete message of the day on all hosts","Hapus pesan harian pada semua host"}. {"Delete message of the day","Hapus pesan harian"}. -{"Delete Selected","Hapus Yang Terpilih"}. -{"Delete table","Hapus tabel"}. {"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:","Tampilkan:"}. {"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"}. @@ -128,7 +113,6 @@ {"ejabberd vCard module","Modul ejabberd vCard"}. {"ejabberd Web Admin","Admin Web ejabberd"}. {"ejabberd","ejabberd"}. -{"Elements","Elemen-elemen"}. {"Email Address","Alamat email"}. {"Email","Email"}. {"Enable logging","Aktifkan log"}. @@ -142,7 +126,6 @@ {"Enter path to text file","Masukkan path ke file teks"}. {"Enter the text you see","Masukkan teks yang Anda lihat"}. {"Erlang XMPP Server","Server Erlang XMPP"}. -{"Error","Kesalahan"}. {"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):"}. @@ -160,7 +143,6 @@ {"Fill in the form to search for any matching XMPP User","Isi kolom untuk mencari pengguna XMPP"}. {"Friday","Jumat"}. {"From ~ts","Dari ~ts"}. -{"From","Dari"}. {"Full List of Room Admins","Daftar Lengkap Admin Kamar"}. {"Full List of Room Owners","Daftar Lengkap Pemilik Kamar"}. {"Full Name","Nama Lengkap"}. @@ -168,20 +150,15 @@ {"Get Number of Registered Users","Dapatkan Jumlah Pengguna Yang Terdaftar"}. {"Get Pending","Lihat yang tertunda"}. {"Get User Last Login Time","Lihat Waktu Login Terakhir Pengguna"}. -{"Get User Password","Dapatkan User Password"}. {"Get User Statistics","Dapatkan Statistik Pengguna"}. {"Given Name","Nama"}. {"Grant voice to this person?","Ijinkan akses suara kepadanya?"}. -{"Group","Grup"}. -{"Groups that will be displayed to the members","Grup yang akan ditampilkan kepada anggota"}. -{"Groups","Grup"}. {"has been banned","telah dibanned"}. {"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"}. {"Host unknown","Host tidak dikenal"}. -{"Host","Host"}. {"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."}. @@ -194,7 +171,6 @@ {"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"}. -{"Incoming s2s Connections:","Koneksi s2s masuk:"}. {"Incorrect CAPTCHA submit","Isian CAPTCHA salah"}. {"Incorrect data form","Formulir data salah"}. {"Incorrect password","Kata sandi salah"}. @@ -207,7 +183,6 @@ {"is now known as","sekarang dikenal sebagai"}. {"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"}. -{"It is not allowed to send private messages","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi"}. {"Jabber ID","Jabber ID"}. {"January","Januari"}. {"joins the room","bergabung ke ruangan"}. @@ -218,7 +193,6 @@ {"Last month","Akhir bulan"}. {"Last year","Akhir tahun"}. {"leaves the room","meninggalkan ruangan"}. -{"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"}. @@ -230,17 +204,13 @@ {"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"}. -{"Memory","Memori"}. {"Message body","Isi Pesan"}. {"Middle Name","Nama Tengah"}. {"Moderator privileges required","Hak istimewa moderator dibutuhkan"}. -{"Modified modules","Modifikasi modul-modul"}. {"Monday","Senin"}. {"Multiple elements are not allowed by RFC6121","Beberapa elemen tidak diizinkan oleh RFC6121"}. {"Name","Nama"}. -{"Name:","Nama:"}. {"Never","Tidak Pernah"}. {"New Password:","Password Baru:"}. {"Nickname Registration at ","Pendaftaran Julukan pada "}. @@ -263,11 +233,8 @@ {"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 Users:","Pengguna Online:"}. {"Online Users","Pengguna Yang Online"}. {"Online","Online"}. {"Only deliver notifications to available users","Hanya mengirimkan pemberitahuan kepada pengguna yang tersedia"}. @@ -279,9 +246,7 @@ {"Organization Name","Nama Organisasi"}. {"Organization Unit","Unit Organisasi"}. {"Outgoing s2s Connections","Koneksi Keluar s2s"}. -{"Outgoing s2s Connections:","Koneksi s2s yang keluar:"}. {"Owner privileges required","Hak istimewa owner dibutuhkan"}. -{"Packet","Paket"}. {"Participant","Partisipan"}. {"Password Verification:","Verifikasi Kata Sandi:"}. {"Password Verification","Verifikasi Sandi"}. @@ -289,8 +254,6 @@ {"Password","Sandi"}. {"Path to Dir","Jalur ke Dir"}. {"Path to File","Jalur ke File"}. -{"Payload type","Tipe payload"}. -{"Pending","Tertunda"}. {"Period: ","Periode: "}. {"Persist items to storage","Pertahankan item ke penyimpanan"}. {"Persistent","Persisten"}. @@ -321,20 +284,15 @@ {"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 an XMPP account","Daftarkan sebuah akun XMPP"}. -{"Registered Users","Pengguna Terdaftar"}. -{"Registered Users:","Pengguna Terdaftar:"}. {"Register","Mendaftar"}. {"Remote copy","Salinan Remote"}. -{"Remove All Offline Messages","Hapus Semua Pesan Offline"}. {"Remove User","Hapus Pengguna"}. -{"Remove","Menghapus"}. {"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 Service","Restart Layanan"}. -{"Restart","Jalankan Ulang"}. {"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:"}. @@ -348,20 +306,15 @@ {"Room terminates","Ruang dihentikan"}. {"Room title","Nama Ruangan"}. {"Roster groups allowed to subscribe","Kelompok kontak yang diizinkan untuk berlangganan"}. -{"Roster of ~ts","Daftar ~ts"}. {"Roster size","Ukuran Daftar Kontak"}. -{"Roster:","Daftar:"}. -{"RPC Call Error","Panggilan Kesalahan RPC"}. {"Running Nodes","Menjalankan Node"}. {"~s invites you to the room ~s","~s mengundang anda masuk kamar ~s"}. {"Saturday","Sabtu"}. -{"Script check","Periksa naskah"}. {"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 "}. -{"Select All","Pilih Semua"}. {"Send announcement to all online users on all hosts","Kirim pengumuman untuk semua pengguna yang online pada semua host"}. {"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"}. @@ -379,21 +332,15 @@ {"Specify the event message type","Tentukan jenis acara pesan"}. {"Specify the publisher model","Tentukan model penerbitan"}. {"Stanza ID","ID Stanza"}. -{"Statistics of ~p","statistik dari ~p"}. -{"Statistics","Statistik"}. -{"Stop","Hentikan"}. {"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"}. {"Subscribers may publish","Pelanggan dapat mempublikasikan"}. -{"Subscription","Berlangganan"}. {"Subscriptions are not allowed","Langganan tidak diperbolehkan"}. {"Sunday","Minggu"}. {"Text associated with a picture","Teks yang terkait dengan gambar"}. @@ -429,11 +376,9 @@ {"This service can not process the address: ~s","Layanan ini tidak dapat memproses alamat: ~s"}. {"Thursday","Kamis"}. {"Time delay","Waktu tunda"}. -{"Time","Waktu"}. {"To register, visit ~s","Untuk mendaftar, kunjungi ~s"}. {"To ~ts","Kepada ~ts"}. {"Token TTL","TTL Token"}. -{"To","Kepada"}. {"Too many active bytestreams","Terlalu banyak bytestream aktif"}. {"Too many CAPTCHA requests","Terlalu banyak permintaan CAPTCHA"}. {"Too many child elements","Terlalu banyak elemen turunan"}. @@ -442,12 +387,7 @@ {"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"}. -{"Total rooms","Total kamar"}. {"Traffic rate limit is exceeded","Batas tingkat lalu lintas terlampaui"}. -{"Transactions Aborted:","Transaksi dibatalkan:"}. -{"Transactions Committed:","Transaksi yang dilakukan:"}. -{"Transactions Logged:","Transaksi yang ditempuh:"}. -{"Transactions Restarted:","Transaksi yang dijalankan ulang:"}. {"~ts's Offline Messages Queue","~ts's antrian Pesan Offline"}. {"Tuesday","Selasa"}. {"Unable to generate a CAPTCHA","Tidak dapat menghasilkan CAPTCHA"}. @@ -457,16 +397,10 @@ {"Unexpected error condition: ~p","Kondisi kerusakan yang tidak diduga: ~p"}. {"Unregister an XMPP account","Nonaktifkan akun XMPP"}. {"Unregister","Nonaktifkan"}. -{"Unselect All","Batalkan semua"}. {"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 ~p","Memperbaharui ~p"}. -{"Update script","Perbarui naskah"}. -{"Update","Memperbarui"}. -{"Uptime:","Sampai saat:"}. {"User already exists","Pengguna sudah ada"}. {"User (jid)","Pengguna (jid)"}. {"User JID","Pengguna JID"}. @@ -480,15 +414,12 @@ {"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"}. -{"View Queue","Lihat antrian"}. -{"View Roster","Lihat daftar kontak"}. {"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"}. diff --git a/priv/msgs/it.msg b/priv/msgs/it.msg index 8f117c9f2..926d16687 100644 --- a/priv/msgs/it.msg +++ b/priv/msgs/it.msg @@ -3,40 +3,77 @@ %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ -{" (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"}. +{" (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 è prevista una password"}. -{"Access denied by service policy","Accesso impedito dalle politiche del servizio"}. +{"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 Jabber ID","Aggiungere un Jabber ID (Jabber ID)"}. -{"Add New","Aggiungere nuovo"}. +{"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","Necessari i privilegi di amministratore"}. +{"Administrator privileges required","Sono richiesti privilegi di amministratore"}. {"All activity","Tutta l'attività"}. {"All Users","Tutti gli utenti"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Consentire a questo Jabber ID l'iscrizione a questo nodo pubsub?"}. +{"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"}. +{"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"}. +{"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 to File at ","Salvataggio sul file "}. -{"Backup","Salvare"}. +{"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,39 +83,48 @@ {"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 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 of room ~s","Configurazione per la stanza ~s"}. {"Configuration","Configurazione"}. -{"Connected Resources:","Risorse connesse:"}. +{"Contact Addresses (normally, room owner or owners)","Indirizzi di contatto (normalmente, proprietario o proprietari della stanza)"}. {"Country","Paese"}. -{"CPU Time:","Tempo CPU:"}. +{"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 message of the day on all hosts","Eliminare il messaggio del giorno (MOTD) su tutti gli host"}. {"Delete message of the day","Eliminare il messaggio del giorno (MOTD)"}. -{"Delete Selected","Eliminare gli elementi selezionati"}. {"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"}. +{"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 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"}. +{"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 nickname you want to register","Immettere il nickname che si vuole registrare"}. {"Enter path to backup file","Immettere il percorso del file di salvataggio"}. @@ -86,30 +132,49 @@ {"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"}. -{"Error","Errore"}. +{"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"}. +{"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 ~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 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"}. -{"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."}. {"Import Directory","Importare una directory"}. {"Import File","Importare un file"}. @@ -119,24 +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"}. +{"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"}. {"is now known as","è ora conosciuta/o come"}. +{"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"}. -{"It is not allowed to send private messages","Non è consentito l'invio di messaggi privati"}. {"Jabber ID","Jabber ID (Jabber ID)"}. {"January","Gennaio"}. +{"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"}. {"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"}. -{"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"}. @@ -144,113 +233,199 @@ {"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"}. +{"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:"}. -{"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"}. -{"Modified modules","Moduli modificati"}. +{"Moderator","Moderatore/Moderatrice"}. +{"Moderators Only","Solo i Moderatori"}. +{"Module failed to handle the query","Il modulo non è riuscito a gestire la query"}. {"Monday","Lunedì"}. +{"Multicast","Multicast"}. +{"Multiple elements are not allowed by RFC6121","Più elementi non sono consentiti da RFC6121"}. +{"Multi-User Chat","Chat Multiutente"}. {"Name","Nome"}. -{"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 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"}. -{"Nickname","Nickname"}. +{"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"}. +{"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"}. -{"Node ID","ID del nodo"}. +{"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","Nessuno"}. +{"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 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"}. +{"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"}. +{"Other Modules Available:","Altri Moduli Disponibili:"}. {"Outgoing s2s Connections","Connessioni s2s in uscita"}. -{"Outgoing s2s Connections:","Connessioni s2s in uscita:"}. {"Owner privileges required","Necessari i privilegi di proprietario"}. -{"Packet","Pacchetto"}. +{"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"}. +{"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, "}. +{"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)"}. {"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"}. -{"Registered Users","Utenti registrati"}. -{"Registered Users:","Utenti registrati:"}. +{"Register an XMPP account","Registra un account XMPP"}. {"Register","Registra"}. {"Remote copy","Copia remota"}. -{"Remove All Offline Messages","Eliminare tutti i messaggi offline"}. -{"Remove User","Eliminare l'utente"}. -{"Remove","Eliminare"}. +{"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 Service","Riavviare il servizio"}. -{"Restart","Riavviare"}. {"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 size","Dimensione della lista dei contatti"}. -{"RPC Call Error","Errore di chiamata RPC"}. {"Running Nodes","Nodi attivi"}. +{"~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 on all hosts","Inviare l'annuncio a tutti gli utenti online su tutti gli host"}. {"Send announcement to all online users","Inviare l'annuncio a tutti gli utenti online"}. @@ -258,80 +433,193 @@ {"Send announcement to all users","Inviare l'annuncio a tutti gli utenti"}. {"September","Settembre"}. {"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"}. +{"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"}. -{"Statistics of ~p","Statistiche di ~p"}. -{"Statistics","Statistiche"}. -{"Stop","Arrestare"}. +{"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 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 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.","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"}. +{"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"}. +{"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"}. +{"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"}. -{"Update","Aggiornare"}. -{"Uptime:","Tempo dall'avvio:"}. +{"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"}. -{"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?"}. +{"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 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 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/ja.msg b/priv/msgs/ja.msg index dbdb1ed8a..bf1401f54 100644 --- a/priv/msgs/ja.msg +++ b/priv/msgs/ja.msg @@ -4,7 +4,7 @@ %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (* を最後に付けると部分文字列にマッチします)"}. -{" has set the subject to: "," は件名を設定しました: "}. +{" has set the subject to: "," は題を設定しました: "}. {"A description of the node","ノードの説明"}. {"A friendly name for the node","ノードのフレンドリネーム"}. {"A password is required to enter this room","このチャットルームに入るにはパスワードが必要です"}. @@ -14,8 +14,6 @@ {"Access model","アクセスモデル"}. {"Account doesn't exist","アカウントは存在しません"}. {"Action on user","ユーザー操作"}. -{"Add Jabber ID","Jabber ID を追加"}. -{"Add New","新規追加"}. {"Add User","ユーザーを追加"}. {"Administration of ","管理: "}. {"Administration","管理"}. @@ -36,6 +34,7 @@ {"Anyone","誰にでも"}. {"April","4月"}. {"August","8月"}. +{"Automatic node creation is not enabled","ノードの自動作成は有効になっていません"}. {"Backup Management","バックアップ管理"}. {"Backup of ~p","バックアップ: ~p"}. {"Backup to File at ","ファイルにバックアップ: "}. @@ -44,11 +43,13 @@ {"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","チャットルームの設定が変更されました"}. @@ -65,25 +66,19 @@ {"Conference room does not exist","会議室は存在しません"}. {"Configuration of room ~s","チャットルーム ~s の設定"}. {"Configuration","設定"}. -{"Connected Resources:","接続リソース:"}. {"Contact Addresses (normally, room owner or owners)","連絡先 (通常は会議室の主宰者またはその複数)"}. {"Country","国"}. -{"CPU Time:","CPU時間:"}. {"Current Discussion Topic","現在の話題"}. -{"Database Tables at ~p","データーベーステーブル: ~p"}. +{"Database failure","データーベース障害"}. {"Database Tables Configuration at ","データーベーステーブル設定 "}. {"Database","データーベース"}. {"December","12月"}. {"Default users as participants","デフォルトのユーザーは参加者"}. -{"Delete content","内容を削除"}. {"Delete message of the day on all hosts","全ホストのお知らせメッセージを削除"}. {"Delete message of the day","お知らせメッセージを削除"}. -{"Delete Selected","選択した項目を削除"}. -{"Delete table","テーブルを削除"}. {"Delete User","ユーザーを削除"}. {"Deliver event notifications","イベント通知を配送する"}. {"Deliver payloads with event notifications","イベント通知と同時にペイロードを配送する"}. -{"Description:","説明:"}. {"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 ","テキストファイルにバックアップ: "}. @@ -99,7 +94,6 @@ {"ejabberd vCard module","ejabberd vCard モジュール"}. {"ejabberd Web Admin","ejabberd ウェブ管理"}. {"ejabberd","ejabberd"}. -{"Elements","要素"}. {"Email Address","メールアドレス"}. {"Email","メール"}. {"Enable logging","ロギングを有効"}. @@ -112,38 +106,38 @@ {"Enter path to text file","テキストファイルのパスを入力してください"}. {"Enter the text you see","見えているテキストを入力してください"}. {"Erlang XMPP Server","Erlang XMPP サーバー"}. -{"Error","エラー"}. {"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 the form to search for any matching XMPP User","XMPP ユーザーを検索するには欄に入力してください"}. {"Friday","金曜日"}. {"From ~ts","From ~ts"}. -{"From","差出人"}. {"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?","この人に発言権を与えますか ?"}. -{"Groups","グループ"}. -{"Group","グループ"}. {"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","はキックされました"}. {"Host unknown","不明なホスト"}. -{"Host","ホスト"}. {"HTTP File Upload","HTTP ファイルアップロード"}. {"If you don't see the CAPTCHA image here, visit the web page.","ここに CAPTCHA 画像が表示されない場合、ウェブページを参照してください。"}. {"Import Directory","ディレクトリインポート"}. @@ -155,33 +149,33 @@ {"Import Users from Dir at ","ディレクトリからユーザーをインポート: "}. {"Import Users From jabberd14 Spool Files","jabberd14 Spool ファイルからユーザーをインポート"}. {"Improper message type","誤ったメッセージタイプです"}. -{"Incoming s2s Connections:","内向き s2s コネクション:"}. {"Incorrect data form","データ形式が違います"}. {"Incorrect password","パスワードが違います"}. +{"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 アドレス"}. {"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 ID","Jabber ID"}. {"January","1月"}. +{"JID normalization failed","JID の正規化に失敗しました"}. {"joins the room","がチャットルームに参加しました"}. {"July","7月"}. {"June","6月"}. {"Just created","作成しました"}. -{"Label:","ラベル:"}. {"Last Activity","活動履歴"}. {"Last login","最終ログイン"}. {"Last message","最終メッセージ"}. {"Last month","先月"}. {"Last year","去年"}. {"leaves the room","がチャットルームから退出しました"}. -{"List of rooms","チャットルームの一覧"}. -{"Low level update script","低レベル更新スクリプト"}. {"Make participants list public","参加者一覧を公開"}. {"Make room CAPTCHA protected","チャットルームを CAPTCHA で保護"}. {"Make room members-only","チャットルームをメンバーのみに制限"}. @@ -196,20 +190,16 @@ {"Maximum Number of Occupants","最大在室者数"}. {"May","5月"}. {"Membership is required to enter this room","このチャットルームに入るにはメンバーでなければなりません"}. -{"Members:","メンバー:"}. {"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 ではパスワードのリカバリを自動的に行うことはできません。"}. -{"Memory","メモリ"}. {"Message body","本文"}. {"Middle Name","ミドルネーム"}. {"Minimum interval between voice requests (in seconds)","発言権の要求の最小時間間隔 (秒)"}. {"Moderator privileges required","モデレーター権限が必要です"}. {"Moderator","モデレーター"}. -{"Modified modules","更新されたモジュール"}. {"Monday","月曜日"}. {"Multicast","マルチキャスト"}. {"Multi-User Chat","マルチユーザーチャット"}. {"Name","名"}. -{"Name:","名前:"}. {"Natural-Language Room Name","自然言語での会議室名"}. {"Never","なし"}. {"New Password:","新しいパスワード:"}. @@ -217,14 +207,22 @@ {"Nickname Registration at ","ニックネーム登録: "}. {"Nickname ~s does not exist in the room","ニックネーム ~s はこのチャットルームにいません"}. {"Nickname","ニックネーム"}. +{"No address elements found","アドレス要素が見つかりません"}. +{"No addresses element found","アドレス要素が見つかりません"}. {"No body provided for announce message","アナウンスメッセージはありませんでした"}. +{"No child elements found","子要素が見つかりません"}. {"No Data","データなし"}. +{"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","ノード"}. +{"Node","ノード"}. {"None","なし"}. {"Not Found","見つかりません"}. {"Notify subscribers when items are removed from the node","アイテムがノードから消された時に購読者へ通知する"}. @@ -238,14 +236,13 @@ {"Occupants are allowed to invite others","在室者は誰かを招待することができます"}. {"Occupants May Change the Subject","ユーザーによる件名の変更を許可"}. {"October","10月"}. -{"Offline Messages","オフラインメッセージ"}. -{"Offline Messages:","オフラインメッセージ:"}. {"OK","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","モデレーターのみがチャットルームの件名を変更できます"}. @@ -256,9 +253,8 @@ {"Organization Name","会社名"}. {"Organization Unit","部署名"}. {"Outgoing s2s Connections","外向き s2s コネクション"}. -{"Outgoing s2s Connections:","外向き s2s コネクション:"}. {"Owner privileges required","主宰者の権限が必要です"}. -{"Packet","パケット"}. +{"Participant ID","参加者 ID"}. {"Participant","参加者"}. {"Password Verification","パスワード (確認)"}. {"Password Verification:","パスワード (確認):"}. @@ -266,7 +262,6 @@ {"Password:","パスワード:"}. {"Path to Dir","ディレクトリのパス"}. {"Path to File","ファイルのパス"}. -{"Pending","保留"}. {"Period: ","期間: "}. {"Persist items to storage","アイテムをストレージに保存する"}. {"Persistent","チャットルームを永続化"}. @@ -275,27 +270,26 @@ {"Please, wait for a while before sending new voice request","新しい発言権の要求を送るまで少し間をおいてください"}. {"Pong","Pong"}. {"Present real Jabber IDs to","本当の Jabber ID を公開"}. +{"Previous session not found","前のセッションが見つかりません"}. {"private, ","プライベート、"}. +{"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","このチャットルームでは、会議のメンバーへのクエリーは禁止されています"}. +{"Query to another users is forbidden","他のユーザーへのクエリは禁止されています"}. {"RAM and disc copy","RAM, ディスクコピー"}. {"RAM copy","RAM コピー"}. {"Really delete message of the day?","本当にお知らせメッセージを削除しますか ?"}. {"Recipient is not in the conference room","受信者はこの会議室にいません"}. {"Register an XMPP account","XMPP アカウントを登録"}. -{"Registered Users","登録ユーザー"}. -{"Registered Users:","登録ユーザー:"}. {"Register","登録"}. {"Remote copy","リモートコピー"}. -{"Remove All Offline Messages","すべてのオフラインメッセージを削除"}. {"Remove User","ユーザーを削除"}. -{"Remove","削除"}. {"Replaced by new connection","新しいコネクションによって置き換えられました"}. +{"Request is ignored","リクエストは無視されます"}. {"Resources","リソース"}. {"Restart Service","サービスを再起動"}. -{"Restart","再起動"}. {"Restore Backup from File at ","ファイルからバックアップをリストア: "}. {"Restore binary backup after next ejabberd restart (requires less memory):","ejabberd の再起動時にバイナリバックアップからリストア (メモリ少):"}. {"Restore binary backup immediately:","直ちにバイナリバックアップからリストア:"}. @@ -308,17 +302,12 @@ {"Room Occupants","在室者"}. {"Room title","チャットルームのタイトル"}. {"Roster groups allowed to subscribe","名簿グループは購読を許可しました"}. -{"Roster of ~ts","~ts の名簿"}. {"Roster size","名簿サイズ"}. -{"Roster:","名簿:"}. -{"RPC Call Error","RPC 呼び出しエラー"}. {"Running Nodes","起動ノード"}. {"~s invites you to the room ~s","~s はあなたをチャットルーム ~s に招待しています"}. {"Saturday","土曜日"}. -{"Script check","スクリプトチェック"}. {"Search Results for ","検索結果: "}. {"Search users in ","ユーザーの検索: "}. -{"Select All","すべて選択"}. {"Send announcement to all online users on all hosts","全ホストのオンラインユーザーにアナウンスを送信"}. {"Send announcement to all online users","すべてのオンラインユーザーにアナウンスを送信"}. {"Send announcement to all users on all hosts","全ホストのユーザーにアナウンスを送信"}. @@ -331,23 +320,18 @@ {"Show Integral Table","累積の表を表示"}. {"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.","XMPP クライアントはコンピューターにパスワードを記憶できます。コンピューターが安全であると信頼できる場合にのみ、この機能を使用してください。"}. {"Specify the access model","アクセスモデルを設定する"}. {"Specify the event message type","イベントメッセージ種別を設定"}. {"Specify the publisher model","公開モデルを指定する"}. {"Stanza ID","スタンザ ID"}. -{"Statistics of ~p","~p の統計"}. -{"Statistics","統計"}. {"Stopped Nodes","停止ノード"}. -{"Stop","停止"}. -{"Storage Type","ストレージタイプ"}. {"Store binary backup:","バイナリバックアップを保存:"}. {"Store plain text backup:","プレーンテキストバックアップを保存:"}. {"Subject","件名"}. {"Submitted","送信完了"}. -{"Submit","送信"}. {"Subscriber Address","購読者のアドレス"}. -{"Subscription","認可"}. {"Sunday","日曜日"}. {"That nickname is already in use by another occupant","そのニックネームは既にほかの在室者によって使用されています"}. {"That nickname is registered by another person","ニックネームはほかの人によって登録されています"}. @@ -374,46 +358,40 @@ {"This room is not anonymous","このチャットルームは非匿名です"}. {"Thursday","木曜日"}. {"Time delay","遅延時間"}. -{"Time","時間"}. {"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","多くのスタンザが応答していません"}. -{"Total rooms","チャットルーム数"}. -{"To","To"}. +{"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","認証されていません"}. +{"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 ~p","更新 ~p"}. -{"Update script","スクリプトの更新"}. -{"Update","更新"}. -{"Uptime:","起動時間:"}. +{"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 are not allowed to register accounts so quickly","それほど速くアカウントを登録することはできません"}. {"Users Last Activity","ユーザーの活動履歴"}. {"Users","ユーザー"}. {"User","ユーザー"}. -{"Validate","検証"}. +{"Value of '~s' should be boolean","'~s' の値はブール値である必要があります"}. +{"Value of '~s' should be integer","'~s' の値は整数である必要があります"}. {"vCard User Search","vCard検索"}. -{"View Queue","キューを表示"}. -{"View Roster","名簿を表示"}. {"Virtual Hosts","バーチャルホスト"}. {"Visitors are not allowed to change their nicknames in this room","傍聴者はこのチャットルームでニックネームを変更することはできません"}. {"Visitors are not allowed to send messages to all occupants","傍聴者はすべての在室者にメッセージを送信することはできません"}. @@ -421,11 +399,15 @@ {"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","購読を許可するかどうか"}. +{"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","フォームの\"ニックネーム\"欄を入力する必要があります"}. diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg index 5a5010b89..d7799f9c5 100644 --- a/priv/msgs/nl.msg +++ b/priv/msgs/nl.msg @@ -3,13 +3,12 @@ %% 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 Jabber ID","Jabber ID toevoegen"}. -{"Add New","Toevoegen"}. {"Add User","Gebruiker toevoegen"}. {"Administration of ","Beheer van "}. {"Administration","Beheer"}. @@ -52,21 +51,16 @@ {"Conference room does not exist","De chatruimte bestaat niet"}. {"Configuration of room ~s","Instellingen van chatruimte ~s"}. {"Configuration","Instellingen"}. -{"Connected Resources:","Verbonden bronnen:"}. {"Country","Land"}. -{"CPU Time:","Processortijd:"}. -{"Database Tables at ~p","Databasetabellen van ~p"}. {"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 on all hosts","Verwijder bericht-van-de-dag op alle hosts"}. {"Delete message of the day","Bericht van de dag verwijderen"}. -{"Delete Selected","Geselecteerde verwijderen"}. {"Delete User","Verwijder Gebruiker"}. {"Deliver event notifications","Gebeurtenisbevestigingen Sturen"}. {"Deliver payloads with event notifications","Berichten bezorgen samen met gebeurtenisnotificaties"}. -{"Description:","Beschrijving:"}. {"Disc only copy","Harde schijf"}. {"Dump Backup to Text File at ","Backup naar een tekstbestand schrijven op "}. {"Dump to Text File","Backup naar een tekstbestand schrijven"}. @@ -78,7 +72,6 @@ {"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"}. {"Enable message archiving","Zet bericht-archivering aan"}. @@ -89,7 +82,6 @@ {"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"}. -{"Error","Fout"}. {"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):"}. @@ -98,22 +90,17 @@ {"Family Name","Achternaam"}. {"February","Februari"}. {"Friday","Vrijdag"}. -{"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","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","is weggestuurd"}. -{"Host","Host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Als U het CAPTCHA-plaatje niet ziet, bezoek dan de webpagina."}. {"Import Directory","Directory importeren"}. {"Import File","Bestand importeren"}. @@ -129,7 +116,6 @@ {"is now known as","heet nu"}. {"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"}. -{"It is not allowed to send private messages","Het is niet toegestaan priveberichten te sturen"}. {"Jabber ID","Jabber ID"}. {"January","Januari"}. {"joins the room","betrad de chatruimte"}. @@ -140,8 +126,6 @@ {"Last month","Afgelopen maand"}. {"Last year","Afgelopen jaar"}. {"leaves the room","verliet de chatruimte"}. -{"List of rooms","Lijst van groepsgesprekken"}. -{"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"}. @@ -153,19 +137,15 @@ {"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"}. -{"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"}. -{"Modified modules","Gewijzigde modules"}. {"Monday","Maandag"}. {"Multicast","Multicast"}. {"Multi-User Chat","Groepschat"}. {"Name","Naam"}. -{"Name:","Naam:"}. {"Never","Nooit"}. {"New Password:","Nieuw Wachtwoord:"}. {"Nickname Registration at ","Registratie van een bijnaam op "}. @@ -188,12 +168,9 @@ {"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 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"}. @@ -205,16 +182,13 @@ {"Organization Name","Organisatie"}. {"Organization Unit","Afdeling"}. {"Outgoing s2s Connections","Uitgaande s2s-verbindingen"}. -{"Outgoing s2s Connections:","Uitgaande s2s-verbindingen:"}. {"Owner privileges required","U hebt eigenaarsprivileges nodig"}. -{"Packet","Pakket"}. {"Password Verification","Wachtwoord Bevestiging"}. {"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"}. @@ -231,17 +205,12 @@ {"RAM copy","RAM"}. {"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"}. -{"Registered Users","Geregistreerde gebruikers"}. -{"Registered Users:","Geregistreerde gebruikers:"}. {"Register","Registreer"}. {"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 Service","Herstart Service"}. -{"Restart","Herstarten"}. {"Restore Backup from File at ","Binaire backup direct herstellen op "}. {"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:"}. @@ -254,10 +223,8 @@ {"Room title","Naam van de chatruimte"}. {"Roster groups allowed to subscribe","Contactlijst-groepen die mogen abonneren"}. {"Roster size","Contactlijst Groote"}. -{"RPC Call Error","RPC-oproepfout"}. {"Running Nodes","Draaiende nodes"}. {"Saturday","Zaterdag"}. -{"Script check","Controle van script"}. {"Search Results for ","Zoekresultaten voor "}. {"Search users in ","Gebruikers zoeken in "}. {"Send announcement to all online users on all hosts","Mededeling verzenden naar alle online gebruikers op alle virtuele hosts"}. @@ -275,18 +242,12 @@ {"Specify the access model","Geef toegangsmodel"}. {"Specify the event message type","Geef type van eventbericht"}. {"Specify the publisher model","Publicatietype opgeven"}. -{"Statistics of ~p","Statistieken van ~p"}. -{"Statistics","Statistieken"}. {"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"}. @@ -300,28 +261,16 @@ {"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"}. {"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"}. -{"Total rooms","Aantal groepsgesprekken"}. {"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","Opheffen"}. {"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 plan","Plan voor de opwaardering"}. -{"Update ~p","Opwaarderen van ~p"}. -{"Update script","Script voor de opwaardering"}. -{"Update","Bijwerken"}. -{"Uptime:","Uptime:"}. {"User JID","JID Gebruiker"}. {"User Management","Gebruikersbeheer"}. {"User","Gebruiker"}. @@ -329,7 +278,6 @@ {"Users are not allowed to register accounts so quickly","Het is gebruikers niet toegestaan zo snel achter elkaar te registreren"}. {"Users Last Activity","Laatste activiteit van gebruikers"}. {"Users","Gebruikers"}. -{"Validate","Bevestigen"}. {"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"}. diff --git a/priv/msgs/no.msg b/priv/msgs/no.msg index e883518a4..b24b9eca9 100644 --- a/priv/msgs/no.msg +++ b/priv/msgs/no.msg @@ -8,11 +8,7 @@ {"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"}. -{"Access model of presence","Tilgangsmodell for tilstedeværelse"}. -{"Access model of roster","Tilgangsmodell for kontaktliste"}. {"Action on user","Handling på bruker"}. -{"Add Jabber ID","Legg til Jabber-ID"}. -{"Add New","Legg til ny"}. {"Add User","Legg til bruker"}. {"Administration of ","Administrasjon av "}. {"Administration","Administrasjon"}. @@ -61,23 +57,17 @@ {"Conference room does not exist","Konferanserommet finnes ikke"}. {"Configuration of room ~s","Oppsett for rom ~s"}. {"Configuration","Oppsett"}. -{"Connected Resources:","Tilkoblede ressurser:"}. {"Country","Land"}. -{"CPU Time:","Prosessortid:"}. {"Current Discussion Topic","Nåværende diskusjonstema"}. -{"Database Tables at ~p","Databasetabeller på ~p"}. {"Database Tables Configuration at ","Database-tabelloppsett på "}. {"Database","Database"}. {"December","desember"}. {"Default users as participants","Standard brukere som deltakere"}. {"Delete message of the day","Slett melding for dagen"}. -{"Delete Selected","Slett valgte"}. {"Delete User","Slett bruker"}. {"Deliver event notifications","Lever begivenhetskunngjøringer"}. {"Deliver payloads with event notifications","Send innhold sammen med hendelsesmerknader"}. -{"Description:","Beskrivelse:"}. {"Disc only copy","Kun diskkopi"}. -{"'Displayed groups' not added (they do not exist!): ","«Viste grupper» ikke lagt til (de finnes ikke!): "}. {"Dump Backup to Text File at ","Dump sikkerhetskopi til tekstfil på "}. {"Dump to Text File","Dump til tekstfil"}. {"Edit Properties","Rediger egenskaper"}. @@ -86,7 +76,6 @@ {"ejabberd MUC module","ejabberd-MUC-modul"}. {"ejabberd Multicast service","ejabberd-multikastingstjeneste"}. {"ejabberd Publish-Subscribe module","ejabberd-Publish-Subscribe-modul"}. -{"Elements","Elementer"}. {"Email","E-post"}. {"Enable logging","Skru på loggføring"}. {"Enable message archiving","Skru på meldingsarkivering"}. @@ -97,7 +86,6 @@ {"Enter path to jabberd14 spool dir","Skriv inn sti til jabberd14 spoolkatalog"}. {"Enter path to text file","Skriv inn sti til tekstfil"}. {"Enter the text you see","Skriv inn teksten du ser"}. -{"Error","Feil"}. {"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):"}. @@ -109,23 +97,18 @@ {"February","februar"}. {"File larger than ~w bytes","Fil større enn ~w byte"}. {"Friday","fredag"}. -{"From","Fra"}. {"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 Password","Hent brukers passord"}. {"Get User Statistics","Vis brukerstatistikk"}. -{"Group","Gruppe"}. -{"Groups","Grupper"}. {"has been banned","har blitt bannlyst"}. {"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"}. {"Host unknown","Ukjent vert"}. -{"Host","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"}. @@ -141,7 +124,6 @@ {"IP addresses","IP-adresser"}. {"is now known as","er nå kjent som"}. {"It is not allowed to send private messages to the conference","Det er ikke tillatt å sende private meldinger til konferansen"}. -{"It is not allowed to send private messages","Det er ikke tillatt å sende private meldinger"}. {"Jabber ID","Jabber-ID"}. {"January","januar"}. {"JID normalization failed","JID-normalisering mislyktes"}. @@ -149,16 +131,13 @@ {"July","juli"}. {"June","juni"}. {"Just created","Akkurat opprettet"}. -{"Label:","Etikett:"}. {"Last Activity","Siste aktivitet"}. {"Last login","Siste innlogging"}. {"Last month","Siste måned"}. {"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"}. -{"List of rooms","Romliste"}. {"Logging","Loggføring"}. -{"Low level update script","Lavnivå-oppdateringsskript"}. {"Make participants list public","Gjør deltakerlisten offentlig"}. {"Make room members-only","Gjør rommet tilgjengelig kun for medlemmer"}. {"Make room password protected","Passordbeskytt rommet"}. @@ -169,8 +148,6 @@ {"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 dette rommet"}. -{"Members:","Medlemmer:"}. -{"Memory","Minne"}. {"Message body","Meldingskropp"}. {"Message not found in forwarded payload","Fant ikke melding i videresendt nyttelast"}. {"Messages from strangers are rejected","Meldinger fra ukjente avvises"}. @@ -178,13 +155,10 @@ {"Messages of type normal","Meldinger av normal type"}. {"Middle Name","Mellomnavn"}. {"Minimum interval between voice requests (in seconds)","Minimumsintervall mellom lydforespørsler (i sekunder)"}. -{"Modified modules","Endrede moduler"}. {"Monday","mandag"}. {"Multicast","Multikasting"}. {"Multi-User Chat","Multibrukersludring"}. -{"Name in the rosters where this group will be displayed","Navn i kontaktlistene der denne gruppen vises"}. {"Name","Navn"}. -{"Name:","Navn:"}. {"Never","Aldri"}. {"New Password:","Nytt passord:"}. {"Nickname can't be empty","Kallenavn kan ikke stå tomt"}. @@ -209,14 +183,10 @@ {"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:"}. {"Online Users","Tilkoblede brukere"}. -{"Online Users:","Tilkoblede brukere:"}. {"Online","Tilkoblet"}. -{"Only admins can see this","Kun administratorer kan se dette"}. {"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","Kun deltakere tillates å sende forespørsler til konferansen"}. @@ -225,9 +195,7 @@ {"Organization Name","Organisasjonsnavn"}. {"Organization Unit","Organisasjonsenhet"}. {"Outgoing s2s Connections","Utgående s2s-koblinger"}. -{"Outgoing s2s Connections:","Utgående s2s-koblinger"}. {"Owner privileges required","Eierprivilegier kreves"}. -{"Packet","Pakke"}. {"Participant","Deltager"}. {"Password Verification","Passordbekreftelse"}. {"Password Verification:","Passordbekreftelse:"}. @@ -235,7 +203,6 @@ {"Password:","Passord:"}. {"Path to Dir","Sti til mappe"}. {"Path to File","Sti til fil"}. -{"Pending","Ventende"}. {"Period: ","Periode: "}. {"Persist items to storage","Vedvarende elementer til lagring"}. {"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."}. @@ -252,19 +219,14 @@ {"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"}. -{"Registered Users","Registrerte brukere"}. -{"Registered Users:","Registrerte brukere:"}. {"Register","Registrer"}. -{"Remove All Offline Messages","Fjern Alle frakoblede meldinger"}. {"Remove User","Fjern bruker"}. -{"Remove","Fjern"}. {"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","Omstart av tjeneste"}. -{"Restart","Start på ny"}. {"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:"}. @@ -279,10 +241,8 @@ {"Roster size","Kontaktlistestørrelse"}. {"Running Nodes","Kjørende noder"}. {"Saturday","lørdag"}. -{"Script check","Skript-sjekk"}. {"Search Results for ","Søkeresultater for "}. {"Search users in ","Søk etter brukere i "}. -{"Select All","Velg alt"}. {"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 verter"}. @@ -296,19 +256,13 @@ {"Specify the access model","Spesifiser tilgangsmodellen"}. {"Specify the event message type","Spesifiser hendelsesbeskjedtypen"}. {"Specify the publisher model","Angi publiseringsmodell"}. -{"Statistics of ~p","Statistikk for ~p"}. -{"Statistics","Statistikk"}. {"Stopped Nodes","Stoppede noder"}. -{"Stop","Stopp"}. -{"Storage Type","Lagringstype"}. {"Store binary backup:","Lagre binær sikkerhetskopi:"}. {"Store plain text backup:","Lagre klartekst-sikkerhetskopi:"}. {"Subject","Emne"}. -{"Submit","Send"}. {"Submitted","Innsendt"}. {"Subscriber Address","Abonnementsadresse"}. {"Subscribers may publish","Abonnenter kan publisere"}. -{"Subscription","Abonnement"}. {"Subscriptions are not allowed","Abonnementer tillates ikke"}. {"Sunday","søndag"}. {"Text associated with a picture","Tekst tilknyttet et bilde"}. @@ -337,17 +291,11 @@ {"This room is not anonymous","Dette rommet er ikke anonymt"}. {"Thursday","torsdag"}. {"Time delay","Tidsforsinkelse"}. -{"Time","Tid"}. {"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"}. -{"To","Til"}. {"Traffic rate limit is exceeded","Grense for tillatt trafikkmengde overskredet"}. -{"Transactions Aborted:","Avbrutte transaksjoner:"}. -{"Transactions Committed:","Sendte transaksjoner:"}. -{"Transactions Logged:","Loggede transaksjoner:"}. -{"Transactions Restarted:","Omstartede transaksjoner:"}. {"Tuesday","tirsdag"}. {"Unable to generate a CAPTCHA","Kunne ikke generere CAPTCHA"}. {"Unauthorized","Uautorisert"}. @@ -356,11 +304,6 @@ {"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 verter (ikke send)"}. -{"Update plan","Oppdateringplan"}. -{"Update ~p","Oppdater ~p"}. -{"Update script","Oppdateringsskript"}. -{"Update","Oppdater"}. -{"Uptime:","Oppetid:"}. {"User already exists","Brukeren finnes allerede"}. {"User Management","Brukerhåndtering"}. {"User removed","Fjernet bruker"}. @@ -370,9 +313,7 @@ {"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"}. -{"Validate","Bekrefte gyldighet"}. {"vCard User Search","vCard-brukersøk"}. -{"View Queue","Vis 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"}. diff --git a/priv/msgs/pl.msg b/priv/msgs/pl.msg index 52cc374a1..e4015091c 100644 --- a/priv/msgs/pl.msg +++ b/priv/msgs/pl.msg @@ -6,11 +6,11 @@ {" 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 Jabber ID","Dodaj Jabber ID"}. -{"Add New","Dodaj nowe"}. +{"Add a hat to a user","Dodaj kapelusz do użytkownika"}. {"Add User","Dodaj użytkownika"}. {"Administration of ","Zarządzanie "}. {"Administration","Administracja"}. @@ -18,6 +18,7 @@ {"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"}. @@ -60,22 +61,17 @@ {"Conference room does not exist","Pokój konferencyjny nie istnieje"}. {"Configuration of room ~s","Konfiguracja pokoju ~s"}. {"Configuration","Konfiguracja"}. -{"Connected Resources:","Zasoby zalogowane:"}. {"Country","Państwo"}. -{"CPU Time:","Czas CPU:"}. {"Database failure","Błąd bazy danych"}. -{"Database Tables at ~p","Tabele bazy na ~p"}. {"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"}. {"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"}. {"Dump Backup to Text File at ","Zapisz kopię zapasową w pliku tekstowym na "}. {"Dump to Text File","Wykonaj kopie do pliku tekstowego"}. @@ -87,7 +83,6 @@ {"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"}. {"Enable message archiving","Włącz archiwizowanie rozmów"}. @@ -99,7 +94,6 @@ {"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"}. -{"Error","Błąd"}. {"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):"}. @@ -115,24 +109,19 @@ {"February","Luty"}. {"File larger than ~w bytes","Plik jest większy niż ~w bajtów"}. {"Friday","Piątek"}. -{"From","Od"}. {"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 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"}. {"Host unknown","Nieznany host"}. -{"Host","Host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Jeśli nie widzisz obrazka CAPTCHA, odwiedź stronę internetową."}. {"Import Directory","Importuj katalog"}. {"Import File","Importuj plik"}. @@ -144,7 +133,6 @@ {"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"}. -{"Incoming s2s Connections:","Przychodzące połączenia s2s:"}. {"Incorrect CAPTCHA submit","Nieprawidłowa odpowiedz dla CAPTCHA"}. {"Incorrect data form","Nieprawidłowe dane w formatce"}. {"Incorrect password","Nieprawidłowe hasło"}. @@ -159,7 +147,6 @@ {"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 ID","Jabber ID"}. {"January","Styczeń"}. {"joins the room","dołącza do pokoju"}. @@ -170,8 +157,6 @@ {"Last month","Miniony miesiąc"}. {"Last year","Miniony rok"}. {"leaves the room","opuszcza pokój"}. -{"List of rooms","Lista pokoi"}. -{"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"}. @@ -184,22 +169,18 @@ {"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ść"}. -{"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"}. {"Moderator","Moderatorzy"}. -{"Modified modules","Zmodyfikowane moduły"}. {"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"}. @@ -247,12 +228,9 @@ {"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 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 "}. @@ -267,9 +245,7 @@ {"Organization Name","Nazwa organizacji"}. {"Organization Unit","Dział"}. {"Outgoing s2s Connections","Wychodzące połączenia s2s"}. -{"Outgoing s2s Connections:","Wychodzące połączenia s2s:"}. {"Owner privileges required","Wymagane uprawnienia właściciela"}. -{"Packet","Pakiet"}. {"Participant","Uczestnicy"}. {"Password Verification","Weryfikacja hasła"}. {"Password Verification:","Weryfikacja hasła:"}. @@ -277,7 +253,6 @@ {"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"}. @@ -296,17 +271,12 @@ {"RAM copy","Kopia w pamięci RAM"}. {"Really delete message of the day?","Na pewno usunąć wiadomość dnia?"}. {"Recipient is not in the conference room","Odbiorcy nie ma w pokoju"}. -{"Registered Users","Użytkownicy zarejestrowani"}. -{"Registered Users:","Użytkownicy zarejestrowani:"}. {"Register","Zarejestruj"}. {"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ą:"}. @@ -320,10 +290,8 @@ {"Room title","Tytuł pokoju"}. {"Roster groups allowed to subscribe","Grupy kontaktów uprawnione do subskrypcji"}. {"Roster size","Rozmiar listy kontaktów"}. -{"RPC Call Error","Błąd żądania RPC"}. {"Running Nodes","Uruchomione węzły"}. {"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"}. @@ -341,19 +309,13 @@ {"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"}. -{"Statistics of ~p","Statystyki ~p"}. -{"Statistics","Statystyki"}. {"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"}. {"Subscriptions are not allowed","Subskrypcje nie są dozwolone"}. -{"Subscription","Subskrypcja"}. {"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ę"}. @@ -372,9 +334,7 @@ {"This room is not anonymous","Ten pokój nie jest anonimowy"}. {"Thursday","Czwartek"}. {"Time delay","Opóźnienie"}. -{"Time","Czas"}. {"To register, visit ~s","Żeby się zarejestrować odwiedź ~s"}. -{"To","Do"}. {"Token TTL","Limit czasu tokenu"}. {"Too many active bytestreams","Zbyt wiele strumieni danych"}. {"Too many CAPTCHA requests","Za dużo żądań CAPTCHA"}. @@ -383,12 +343,7 @@ {"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"}. -{"Total rooms","Wszystkich pokoi"}. {"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"}. @@ -398,11 +353,6 @@ {"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 ~p","Uaktualnij ~p"}. -{"Update script","Skrypt aktualizacji"}. -{"Update","Aktualizuj"}. -{"Uptime:","Czas pracy:"}. {"User already exists","Użytkownik już istnieje"}. {"User JID","Użytkownik "}. {"User (jid)","Użytkownik (jid)"}. @@ -414,7 +364,6 @@ {"Users Last Activity","Ostatnia aktywność użytkowników"}. {"Users","Użytkownicy"}. {"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"}. diff --git a/priv/msgs/pt-br.msg b/priv/msgs/pt-br.msg index bd295de2e..9fbbb0096 100644 --- a/priv/msgs/pt-br.msg +++ b/priv/msgs/pt-br.msg @@ -12,17 +12,10 @@ {"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 of authorize","Modelo de acesso da autorização"}. -{"Access model of open","Modelo para acesso aberto"}. -{"Access model of presence","Modelo para acesso presença"}. -{"Access model of roster","Modelo para acesso lista"}. -{"Access model of whitelist","Modelo de acesso da lista branca"}. {"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 Jabber ID","Adicionar ID jabber"}. -{"Add New","Adicionar novo"}. {"Add User","Adicionar usuário"}. {"Administration of ","Administração de "}. {"Administration","Administração"}. @@ -53,7 +46,9 @@ {"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"}. @@ -93,34 +88,25 @@ {"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 de conferência 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)"}. -{"Contrib Modules","Módulos contrib"}. {"Country","País"}. -{"CPU Time:","Tempo da CPU:"}. {"Current Discussion Topic","Assunto em discussão"}. {"Database failure","Falha no banco de dados"}. -{"Database Tables at ~p","Tabelas da Base de dados em ~p"}. {"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 content","Excluir o conteúdo"}. {"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 Selected","Remover os selecionados"}. -{"Delete table","Excluir a tabela"}. {"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 cópia em disco"}. -{"'Displayed groups' not added (they do not exist!): ","Os 'Grupos exibidos' não foi adicionado (eles não existem!): "}. -{"Displayed:","Exibido:"}. {"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 texto"}. @@ -136,7 +122,6 @@ {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. -{"Elements","Elementos"}. {"Email Address","Endereço de e-mail"}. {"Email","Email"}. {"Enable hats","Ativa chapéus"}. @@ -151,7 +136,6 @@ {"Enter path to text file","Introduza caminho para o arquivo texto"}. {"Enter the text you see","Insira o texto que você vê"}. {"Erlang XMPP Server","Servidor XMPP Erlang"}. -{"Error","Erro"}. {"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):"}. @@ -170,7 +154,6 @@ {"Fill in the form to search for any matching XMPP User","Preencha campos para procurar por quaisquer usuários XMPP"}. {"Friday","Sexta"}. {"From ~ts","De ~s"}. -{"From","De"}. {"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"}. @@ -180,23 +163,19 @@ {"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"}. -{"Given Name","Sobrenome"}. +{"Given Name","Prenome"}. {"Grant voice to this person?","Dar voz a esta pessoa?"}. -{"Group","Grupo"}. -{"Groups that will be displayed to the members","Os grupos que serão exibidos para os membros"}. -{"Groups","Grupos"}. {"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"}. -{"Host","Máquina"}. {"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."}. @@ -210,7 +189,6 @@ {"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"}. -{"Incoming s2s Connections:","Conexões s2s de Entrada:"}. {"Incorrect CAPTCHA submit","CAPTCHA submetido incorretamente"}. {"Incorrect data form","Formulário dos dados incorreto"}. {"Incorrect password","Senha incorreta"}. @@ -230,7 +208,6 @@ {"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"}. -{"It is not allowed to send private messages","Não é permitido enviar mensagens privadas"}. {"Jabber ID","ID Jabber"}. {"January","Janeiro"}. {"JID normalization denied by service policy","Normalização JID negada por causa da política de serviços"}. @@ -241,7 +218,6 @@ {"July","Julho"}. {"June","Junho"}. {"Just created","Acabou de ser criado"}. -{"Label:","Rótulo:"}. {"Last Activity","Última atividade"}. {"Last login","Último login"}. {"Last message","Última mensagem"}. @@ -249,11 +225,10 @@ {"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 rooms","Lista de salas"}. -{"List of users with hats","Lista os usuários com chapéus"}. +{"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"}. -{"Low level update script","Script de atualização low level"}. {"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"}. @@ -271,11 +246,8 @@ {"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"}. -{"Members not added (inexistent vhost!): ","Membros que não foram adicionados (o vhost não existe!): "}. {"Membership is required to enter this room","É necessário ser membro desta sala para poder entrar"}. -{"Members:","Membros:"}. {"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."}. -{"Memory","Memória"}. {"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"}. @@ -287,15 +259,12 @@ {"Moderator privileges required","Se necessita privilégios de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Somente moderadores"}. -{"Modified modules","Módulos atualizados"}. {"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-usuário"}. -{"Name in the rosters where this group will be displayed","O nome nas listas onde este grupo será exibido"}. {"Name","Nome"}. -{"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"}. @@ -351,7 +320,7 @@ {"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","Número de participantes"}. +{"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"}. @@ -360,14 +329,10 @@ {"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"}. {"Old Password:","Senha Antiga:"}. {"Online Users","Usuários conectados"}. -{"Online Users:","Usuários online:"}. {"Online","Conectado"}. -{"Only admins can see this","Apenas administradores podem ver isso"}. {"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"}. @@ -375,6 +340,7 @@ {"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","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"}. @@ -386,10 +352,8 @@ {"Organization Unit","Departamento/Unidade"}. {"Other Modules Available:","Outros módulos disponíveis:"}. {"Outgoing s2s Connections","Conexões s2s de Saída"}. -{"Outgoing s2s Connections:","Saída das conexões s2s:"}. {"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"}. -{"Packet","Pacote"}. {"Participant ID","ID do participante"}. {"Participant","Participante"}. {"Password Verification:","Verificação da Senha:"}. @@ -398,8 +362,7 @@ {"Password:","Senha:"}. {"Path to Dir","Caminho para o diretório"}. {"Path to File","Caminho do arquivo"}. -{"Payload type","Tipo da carga útil"}. -{"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"}. @@ -433,50 +396,41 @@ {"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"}. -{"Registered Users:","Usuários registrados:"}. -{"Registered Users","Usuários Registrados"}. {"Register","Registrar"}. {"Remote copy","Cópia remota"}. {"Remove a hat from a user","Remove um chapéu de um usuário"}. -{"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. {"Remove User","Remover usuário"}. -{"Remove","Remover"}. {"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"}. -{"Restart","Reiniciar"}. {"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 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","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 of ~ts","Lista de ~ts"}. {"Roster size","Tamanho da Lista"}. -{"Roster:","Lista:"}. -{"RPC Call Error","Erro de chamada RPC"}. {"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 "}. -{"Select All","Selecione tudo"}. {"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 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"}. @@ -489,6 +443,7 @@ {"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"}. {"SOCKS5 Bytestreams","Bytestreams SOCKS5"}. @@ -497,25 +452,20 @@ {"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 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ó"}. -{"Statistics of ~p","Estatísticas de ~p"}. -{"Statistics","Estatísticas"}. -{"Stop","Parar"}. {"Stopped Nodes","Nós parados"}. -{"Storage Type","Tipo de armazenamento"}. {"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"}. {"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"}. -{"Subscription","Subscrição"}. {"Sunday","Domingo"}. {"Text associated with a picture","Um texto associado a uma imagem"}. {"Text associated with a sound","Um texto associado a um som"}. @@ -561,10 +511,10 @@ {"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 type of node data, usually specified by the namespace of the payload (if any)","O tipo dos dados do nó, normalmente definido pelo espaço dos nomes da carga útil (caso haja)"}. {"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: "}. @@ -578,7 +528,6 @@ {"Thursday","Quinta"}. {"Time delay","Intervalo (Tempo)"}. {"Timed out waiting for stream resumption","Tempo limite expirou durante à espera da retomada da transmissão"}. -{"Time","Tempo"}. {"To register, visit ~s","Para registrar, visite ~s"}. {"To ~ts","Para ~s"}. {"Token TTL","Token TTL"}. @@ -591,13 +540,8 @@ {"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"}. -{"To","Para"}. -{"Total rooms","Salas no total"}. {"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"}. @@ -608,24 +552,20 @@ {"Uninstall","Desinstalar"}. {"Unregister an XMPP account","Excluir uma conta XMPP"}. {"Unregister","Deletar registro"}. -{"Unselect All","Desmarcar todos"}. {"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 ~p","Atualizar ~p"}. -{"Update plan","Plano de Atualização"}. -{"Update script","Script de atualização"}. {"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"}. -{"Update","Atualizar"}. +{"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"}. -{"Uptime:","Tempo de atividade:"}. {"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"}. @@ -635,7 +575,6 @@ {"Users Last Activity","Últimas atividades dos usuários"}. {"Users","Usuários"}. {"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"}. @@ -643,14 +582,13 @@ {"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"}. -{"View Queue","Exibir a fila"}. -{"View Roster","Ver a lista"}. {"Virtual Hosts","Hosts virtuais"}. {"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 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"}. @@ -663,6 +601,7 @@ {"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 em uma coleção"}. {"Wrong parameters in the web formulary","O formulário web está com os parâmetros errados"}. {"Wrong xmlns","Xmlns errado"}. @@ -674,6 +613,7 @@ {"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"}. diff --git a/priv/msgs/pt.msg b/priv/msgs/pt.msg index 99a8de3a3..358eb4858 100644 --- a/priv/msgs/pt.msg +++ b/priv/msgs/pt.msg @@ -3,7 +3,7 @@ %% 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)"}. +{" (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ó"}. @@ -12,17 +12,10 @@ {"A Web Page","Uma página da web"}. {"Accept","Aceito"}. {"Access denied by service policy","Acesso negado pela política de serviço"}. -{"Access model of authorize","Modelo de acesso da autorização"}. -{"Access model of open","Modelo para acesso aberto"}. -{"Access model of presence","Modelo para acesso presença"}. -{"Access model of roster","Modelo para acesso lista"}. -{"Access model of whitelist","Modelo de acesso da lista branca"}. {"Access model","Modelo de acesso"}. {"Account doesn't exist","A conta não existe"}. {"Action on user","Acção no utilizador"}. {"Add a hat to a user","Adiciona um chapéu num utilizador"}. -{"Add Jabber ID","Adicionar ID jabber"}. -{"Add New","Adicionar novo"}. {"Add User","Adicionar utilizador"}. {"Administration of ","Administração de "}. {"Administration","Administração"}. @@ -53,7 +46,9 @@ {"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"}. @@ -79,6 +74,7 @@ {"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"}. @@ -92,33 +88,25 @@ {"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"}. -{"CPU Time:","Tempo da CPU:"}. {"Current Discussion Topic","Assunto em discussão"}. {"Database failure","Falha no banco de dados"}. -{"Database Tables at ~p","Tabelas da Base de dados em ~p"}. {"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 content","Apagar o conteúdo"}. {"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 Selected","Eliminar os seleccionados"}. -{"Delete table","Apagar a tabela"}. {"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"}. -{"Description:","Descrição:"}. {"Disc only copy","Cópia apenas em disco"}. -{"'Displayed groups' not added (they do not exist!): ","Os 'Grupos exibidos' não foi adicionado (eles não existem!): "}. -{"Displayed:","Exibido:"}. {"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"}. @@ -134,7 +122,6 @@ {"ejabberd vCard module","Módulo vCard de ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. -{"Elements","Elementos"}. {"Email Address","Endereço de e-mail"}. {"Email","Email"}. {"Enable hats","Ativa chapéus"}. @@ -149,7 +136,6 @@ {"Enter path to text file","Introduza caminho para o ficheiro de texto"}. {"Enter the text you see","Insira o texto que vê"}. {"Erlang XMPP Server","Servidor XMPP Erlang"}. -{"Error","Erro"}. {"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):"}. @@ -168,31 +154,28 @@ {"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"}. -{"From","De"}. {"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 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 Password","Obter palavra-passe do utilizador"}. {"Get User Statistics","Obter estatísticas do utilizador"}. {"Given Name","Sobrenome"}. {"Grant voice to this person?","Dar voz a esta pessoa?"}. -{"Group","Grupo"}. -{"Groups that will be displayed to the members","Os grupos que serão exibidos para os membros"}. -{"Groups","Grupos"}. {"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"}. -{"Host","Máquina"}. {"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."}. @@ -206,13 +189,14 @@ {"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"}. -{"Incoming s2s Connections:","Conexões s2s de Entrada:"}. {"Incorrect CAPTCHA submit","CAPTCHA submetido incorretamente"}. {"Incorrect data form","Formulário dos dados incorreto"}. {"Incorrect password","Palavra-chave incorrecta"}. {"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"}. @@ -224,16 +208,16 @@ {"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"}. -{"It is not allowed to send private messages","Não é permitido enviar mensagens privadas"}. {"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"}. -{"Label:","Rótulo:"}. {"Last Activity","Última actividade"}. {"Last login","Último login"}. {"Last message","Última mensagem"}. @@ -241,11 +225,10 @@ {"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 rooms","Lista de salas"}. {"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"}. -{"Low level update script","Script de atualização low level"}. {"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"}. @@ -263,11 +246,8 @@ {"Maximum number of items to persist","Quantidade máxima dos itens para manter"}. {"Maximum Number of Occupants","Quantidate máxima de participantes"}. {"May","Maio"}. -{"Members not added (inexistent vhost!): ","Membros que não foram adicionados (o vhost não existe!): "}. {"Membership is required to enter this room","É necessário ser membro desta sala para poder entrar"}. -{"Members:","Membros:"}. {"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."}. -{"Memory","Memória"}. {"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"}. @@ -279,15 +259,12 @@ {"Moderator privileges required","São necessários privilégios de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Somente moderadores"}. -{"Modified modules","Módulos atualizados"}. {"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 in the rosters where this group will be displayed","O nome nas listas onde este grupo será exibido"}. {"Name","Nome"}. -{"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"}. @@ -349,16 +326,13 @@ {"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"}. -{"Offline Messages","Mensagens offline"}. -{"Offline Messages:","Mensagens offline:"}. {"OK","OK"}. {"Old Password:","Palavra-passe Antiga:"}. {"Online Users","Utilizadores ligados"}. -{"Online Users:","Utilizadores online:"}. {"Online","Ligado"}. -{"Only admins can see this","Apenas administradores podem ver isso"}. {"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"}. @@ -366,6 +340,7 @@ {"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"}. @@ -375,11 +350,11 @@ {"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"}. -{"Outgoing s2s Connections:","Saída das conexões s2s:"}. {"Owner privileges required","São necessários privilégios de dono"}. {"Packet relay is denied by service policy","A retransmissão de pacote é negada por causa da política de serviço"}. -{"Packet","Pacote"}. +{"Participant ID","ID do participante"}. {"Participant","Participante"}. {"Password Verification:","Verificação da Palavra-passe:"}. {"Password Verification","Verificação de Palavra-passe"}. @@ -387,8 +362,7 @@ {"Password:","Palavra-chave:"}. {"Path to Dir","Caminho para o directório"}. {"Path to File","Caminho do ficheiro"}. -{"Payload type","Tipo da carga útil"}. -{"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"}. @@ -422,26 +396,22 @@ {"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"}. {"Register an XMPP account","Registe uma conta XMPP"}. -{"Registered Users","Utilizadores registados"}. -{"Registered Users:","Utilizadores registados:"}. {"Register","Registar"}. {"Remote copy","Cópia remota"}. {"Remove a hat from a user","Remove um chapéu de um utilizador"}. -{"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. {"Remove User","Eliminar utilizador"}. -{"Remove","Remover"}. {"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"}. -{"Restart","Reiniciar"}. {"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"}. @@ -452,20 +422,15 @@ {"Room terminates","Terminação da sala"}. {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Listar grupos autorizados"}. -{"Roster of ~ts","Lista de ~ts"}. {"Roster size","Tamanho da Lista"}. -{"Roster:","Lista:"}. -{"RPC Call Error","Erro de chamada RPC"}. {"Running Nodes","Nodos a correr"}. {"~s invites you to the room ~s","~s convidaram-o à 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 utilizadores em "}. -{"Select All","Selecione tudo"}. {"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"}. @@ -478,32 +443,29 @@ {"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ó"}. -{"Statistics of ~p","Estatísticas de ~p"}. -{"Statistics","Estatísticas"}. -{"Stop","Parar"}. {"Stopped Nodes","Nodos parados"}. -{"Storage Type","Tipo de armazenagem"}. {"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"}. -{"Submit","Enviar"}. {"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"}. -{"Subscription","Subscrição"}. {"Sunday","Domingo"}. {"Text associated with a picture","Um texto associado a uma imagem"}. {"Text associated with a sound","Um texto associado a um som"}. @@ -527,6 +489,8 @@ {"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"}. @@ -547,10 +511,10 @@ {"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 type of node data, usually specified by the namespace of the payload (if any)","O tipo dos dados do nó, normalmente definido pelo espaço dos nomes da carga útil (caso haja)"}. {"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: "}. @@ -564,7 +528,6 @@ {"Thursday","Quinta"}. {"Time delay","Intervalo (Tempo)"}. {"Timed out waiting for stream resumption","Tempo limite expirou durante à espera da retomada da transmissão"}. -{"Time","Data"}. {"To register, visit ~s","Para registar, visite ~s"}. {"To ~ts","Para ~s"}. {"Token TTL","Token TTL"}. @@ -577,13 +540,8 @@ {"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"}. -{"To","Para"}. -{"Total rooms","Salas no total"}. {"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"}. @@ -591,23 +549,23 @@ {"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"}. -{"Unselect All","Desmarcar todos"}. {"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 ~p","Atualizar ~p"}. -{"Update plan","Plano de atualização"}. -{"Update script","Script de atualização"}. -{"Update","Actualizar"}. -{"Uptime:","Tempo de atividade:"}. +{"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"}. @@ -617,21 +575,20 @@ {"Users Last Activity","Últimas atividades dos utilizadores"}. {"Users","Utilizadores"}. {"User","Utilizador"}. -{"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 Utilizador vCard"}. -{"View Queue","Exibir a fila"}. -{"View Roster","Ver a lista"}. +{"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"}. @@ -644,6 +601,7 @@ {"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"}. @@ -655,6 +613,7 @@ {"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"}. diff --git a/priv/msgs/ru.msg b/priv/msgs/ru.msg index 962c83ae8..ca23d2167 100644 --- a/priv/msgs/ru.msg +++ b/priv/msgs/ru.msg @@ -12,8 +12,6 @@ {"Access denied by service policy","Доступ запрещён политикой службы"}. {"Account doesn't exist","Учётная запись не существует"}. {"Action on user","Действие над пользователем"}. -{"Add Jabber ID","Добавить Jabber ID"}. -{"Add New","Добавить"}. {"Add User","Добавить пользователя"}. {"Administration of ","Администрирование "}. {"Administration","Администрирование"}. @@ -72,24 +70,17 @@ {"Conference room does not exist","Конференция не существует"}. {"Configuration of room ~s","Конфигурация комнаты ~s"}. {"Configuration","Конфигурация"}. -{"Connected Resources:","Подключённые ресурсы:"}. {"Country","Страна"}. -{"CPU Time:","Процессорное время:"}. {"Database failure","Ошибка базы данных"}. -{"Database Tables at ~p","Таблицы базы данных на ~p"}. {"Database Tables Configuration at ","Конфигурация таблиц базы данных на "}. {"Database","База данных"}. {"December","декабря"}. {"Default users as participants","Сделать пользователей участниками по умолчанию"}. -{"Delete content","Удалить содержимое"}. {"Delete message of the day on all hosts","Удалить сообщение дня со всех виртуальных серверов"}. {"Delete message of the day","Удалить сообщение дня"}. -{"Delete Selected","Удалить выделенные"}. -{"Delete table","Удалить таблицу"}. {"Delete User","Удалить пользователя"}. {"Deliver event notifications","Доставлять уведомления о событиях"}. {"Deliver payloads with event notifications","Доставлять вместе с уведомлениями o публикациях сами публикации"}. -{"Description:","Описание:"}. {"Disc only copy","только диск"}. {"Dump Backup to Text File at ","Копирование в текстовый файл на "}. {"Dump to Text File","Копирование в текстовый файл"}. @@ -102,7 +93,6 @@ {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Web-интерфейс администрирования ejabberd"}. {"ejabberd","ejabberd"}. -{"Elements","Элементы"}. {"Email","Электронная почта"}. {"Enable logging","Включить журналирование"}. {"Enable message archiving","Включить хранение сообщений"}. @@ -114,7 +104,6 @@ {"Enter path to jabberd14 spool file","Введите путь к файлу из спула jabberd14"}. {"Enter path to text file","Введите путь к текстовому файлу"}. {"Enter the text you see","Введите увиденный текст"}. -{"Error","Ошибка"}. {"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):"}. @@ -130,25 +119,20 @@ {"February","февраля"}. {"File larger than ~w bytes","Файл больше ~w байт"}. {"Friday","Пятница"}. -{"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 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","выгнали из комнаты"}. {"Host unknown","Неизвестное имя сервера"}. -{"Host","Хост"}. {"HTTP File Upload","Передача файлов по HTTP"}. {"Idle connection","Неиспользуемое соединение"}. {"If you don't see the CAPTCHA image here, visit the web page.","Если вы не видите изображение капчи, перейдите по ссылке."}. @@ -162,7 +146,6 @@ {"Import Users From jabberd14 Spool Files","Импорт пользователей из спула jabberd14"}. {"Improper domain part of 'from' attribute","Неправильная доменная часть атрибута 'from'"}. {"Improper message type","Неправильный тип сообщения"}. -{"Incoming s2s Connections:","Входящие s2s соединения:"}. {"Incorrect CAPTCHA submit","Неверный ввод капчи"}. {"Incorrect data form","Некорректная форма данных"}. {"Incorrect password","Неправильный пароль"}. @@ -180,7 +163,6 @@ {"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 ID","Jabber ID"}. {"January","января"}. {"joins the room","вошёл(а) в комнату"}. @@ -191,8 +173,6 @@ {"Last month","За последний месяц"}. {"Last year","За последний год"}. {"leaves the room","вышел(а) из комнаты"}. -{"List of rooms","Список комнат"}. -{"Low level update script","Низкоуровневый сценарий обновления"}. {"Make participants list public","Сделать список участников видимым всем"}. {"Make room CAPTCHA protected","Сделать комнату защищённой капчей"}. {"Make room members-only","Комната только для зарегистрированных участников"}. @@ -207,21 +187,17 @@ {"Maximum Number of Occupants","Максимальное количество участников"}. {"May","мая"}. {"Membership is required to enter this room","В эту конференцию могут входить только её члены"}. -{"Members:","Члены:"}. -{"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","Требуются права модератора"}. -{"Modified modules","Изменённые модули"}. {"Module failed to handle the query","Ошибка модуля при обработке запроса"}. {"Monday","Понедельник"}. {"Multicast","Мультикаст"}. {"Multi-User Chat","Конференция"}. {"Name","Название"}. -{"Name:","Название:"}. {"Neither 'jid' nor 'nick' attribute found","Не найден атрибут 'jid' или 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","Не найден атрибут 'role' или 'affiliation'"}. {"Never","Никогда"}. @@ -276,12 +252,9 @@ {"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","Допустимы только тэги или "}. @@ -295,18 +268,15 @@ {"Only service administrators are allowed to send service messages","Только администратор службы может посылать служебные сообщения"}. {"Organization Name","Название организации"}. {"Organization Unit","Отдел организации"}. -{"Outgoing s2s Connections:","Исходящие s2s-серверы:"}. {"Outgoing s2s Connections","Исходящие s2s-соединения"}. {"Owner privileges required","Требуются права владельца"}. {"Packet relay is denied by service policy","Пересылка пакетов запрещена политикой службы"}. -{"Packet","Пакет"}. {"Password Verification","Проверка пароля"}. {"Password Verification:","Проверка пароля:"}. {"Password","Пароль"}. {"Password:","Пароль:"}. {"Path to Dir","Путь к директории"}. {"Path to File","Путь к файлу"}. -{"Pending","Ожидание"}. {"Period: ","Период"}. {"Persist items to storage","Сохранять публикации в хранилище"}. {"Ping query is incorrect","Некорректный пинг-запрос"}. @@ -331,18 +301,13 @@ {"RAM copy","ОЗУ"}. {"Really delete message of the day?","Действительно удалить сообщение дня?"}. {"Recipient is not in the conference room","Адресата нет в конференции"}. -{"Registered Users","Зарегистрированные пользователи"}. -{"Registered Users:","Зарегистрированные пользователи:"}. {"Register","Зарегистрировать"}. {"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:","Восстановить из бинарной резервной копии немедленно:"}. @@ -356,13 +321,10 @@ {"Room title","Название комнаты"}. {"Roster groups allowed to subscribe","Группы списка контактов, которым разрешена подписка"}. {"Roster size","Размер списка контактов"}. -{"RPC Call Error","Ошибка вызова RPC"}. {"Running Nodes","Работающие узлы"}. {"Saturday","Суббота"}. -{"Script check","Проверка сценария"}. {"Search Results for ","Результаты поиска в "}. {"Search users in ","Поиск пользователей в "}. -{"Select All","Выбрать всё"}. {"Send announcement to all online users on all hosts","Разослать объявление всем подключённым пользователям на всех виртуальных серверах"}. {"Send announcement to all online users","Разослать объявление всем подключённым пользователям"}. {"Send announcement to all users on all hosts","Разослать объявление всем пользователям на всех виртуальных серверах"}. @@ -380,21 +342,15 @@ {"Specify the access model","Укажите механизм управления доступом"}. {"Specify the event message type","Укажите тип сообщения о событии"}. {"Specify the publisher model","Условия публикации"}. -{"Statistics of ~p","статистика узла ~p"}. -{"Statistics","Статистика"}. {"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","Адрес подписчика"}. {"Subscriptions are not allowed","Подписки недопустимы"}. -{"Subscription","Подписка"}. {"Sunday","Воскресенье"}. {"That nickname is already in use by another occupant","Этот псевдоним уже занят другим участником"}. {"That nickname is registered by another person","Этот псевдоним зарегистрирован кем-то другим"}. @@ -419,7 +375,6 @@ {"Thursday","Четверг"}. {"Time delay","По истечение"}. {"Timed out waiting for stream resumption","Истекло время ожидания возобновления потока"}. -{"Time","Время"}. {"To register, visit ~s","Для регистрации посетите ~s"}. {"Token TTL","Токен TTL"}. {"Too many active bytestreams","Слишком много активных потоков данных"}. @@ -431,13 +386,7 @@ {"Too many receiver fields were specified","Указано слишком много получателей"}. {"Too many unacked stanzas","Слишком много неподтверждённых пакетов"}. {"Too many users in this conference","Слишком много пользователей в этой конференции"}. -{"Total rooms","Все комнаты"}. -{"To","Кому"}. {"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","Нельзя регистрировать маршруты на существующие локальные домены"}. @@ -445,16 +394,10 @@ {"Unexpected action","Неожиданное действие"}. {"Unexpected error condition: ~p","Неожиданная ошибка: ~p"}. {"Unregister","Удалить"}. -{"Unselect All","Снять всё выделение"}. {"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 ~p","Обновление ~p"}. -{"Update script","Сценарий обновления"}. -{"Update","Обновить"}. -{"Uptime:","Время работы:"}. {"User already exists","Пользователь уже существует"}. {"User JID","JID пользователя"}. {"User (jid)","Пользователь (XMPP адрес)"}. @@ -467,7 +410,6 @@ {"Users Last Activity","Статистика последнего подключения пользователей"}. {"Users","Пользователи"}. {"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' должно быть датой"}. diff --git a/priv/msgs/sk.msg b/priv/msgs/sk.msg index aaee00ef1..54f711610 100644 --- a/priv/msgs/sk.msg +++ b/priv/msgs/sk.msg @@ -8,8 +8,6 @@ {"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 Jabber ID","Pridať Jabber ID"}. -{"Add New","Pridať nový"}. {"Add User","Pridať používateľa"}. {"Administration of ","Administrácia "}. {"Administration","Administrácia"}. @@ -51,20 +49,16 @@ {"Conference room does not exist","Diskusná miestnosť neexistuje"}. {"Configuration of room ~s","Konfigurácia miestnosti ~s"}. {"Configuration","Konfigurácia"}. -{"Connected Resources:","Pripojené zdroje:"}. {"Country","Krajina"}. -{"CPU Time:","Čas procesoru"}. {"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"}. {"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"}. {"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"}. @@ -75,7 +69,6 @@ {"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"}. {"End User Session","Ukončiť reláciu užívateľa"}. @@ -85,7 +78,6 @@ {"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"}. -{"Error","Chyba"}. {"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):"}. @@ -93,22 +85,17 @@ {"Family Name","Priezvisko"}. {"February","Február"}. {"Friday","Piatok"}. -{"From","Od"}. {"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"}. {"Grant voice to this person?","Prideltiť Voice tejto osobe?"}. -{"Group","Skupina"}. -{"Groups","Skupiny"}. {"has been banned","bol(a) zablokovaný(á)"}. {"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"}. -{"Host","Server"}. {"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"}. @@ -124,7 +111,6 @@ {"is now known as","sa premenoval(a) na"}. {"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"}. -{"It is not allowed to send private messages","Nieje povolené posielať súkromné správy"}. {"Jabber ID","Jabber ID"}. {"January","Január"}. {"joins the room","vstúpil(a) do miestnosti"}. @@ -135,7 +121,6 @@ {"Last month","Posledný mesiac"}. {"Last year","Posledný rok"}. {"leaves the room","odišiel(a) z miestnosti"}. -{"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"}. @@ -147,17 +132,13 @@ {"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"}. -{"Modified modules","Modifikované moduly"}. {"Monday","Pondelok"}. {"Name","Meno"}. -{"Name:","Meno:"}. {"Never","Nikdy"}. {"New Password:","Nové heslo:"}. {"Nickname Registration at ","Registrácia prezývky na "}. @@ -179,11 +160,8 @@ {"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"}. {"Old Password:","Staré heslo:"}. -{"Online Users:","Online používatelia:"}. {"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"}. @@ -196,16 +174,13 @@ {"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:"}. {"Owner privileges required","Sú vyžadované práva vlastníka"}. -{"Packet","Paket"}. {"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"}. @@ -222,17 +197,12 @@ {"RAM copy","Kópia RAM"}. {"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:"}. {"Register","Zoznam kontaktov"}. {"Remote copy","Vzdialená kópia"}. -{"Remove All Offline Messages","Odstrániť všetky offline správy"}. {"Remove User","Odstrániť užívateľa"}. -{"Remove","Odstrániť"}. {"Replaced by new connection","Nahradené novým spojením"}. {"Resources","Zdroje"}. {"Restart Service","Reštartovať službu"}. -{"Restart","Reštart"}. {"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:"}. @@ -245,10 +215,8 @@ {"Room title","Názov miestnosti"}. {"Roster groups allowed to subscribe","Skupiny kontaktov, ktoré môžu odoberať"}. {"Roster size","Počet kontaktov v zozname"}. -{"RPC Call Error","Chyba RPC volania"}. {"Running Nodes","Bežiace uzly"}. {"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 on all hosts","Odoslať oznam všetkým online používateľom na všetkých serveroch"}. @@ -265,18 +233,12 @@ {"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"}. -{"Statistics of ~p","Štatistiky ~p"}. -{"Statistics","Štatistiky"}. {"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"}. @@ -290,24 +252,14 @@ {"This room is not anonymous","Táto miestnosť nie je anonymná"}. {"Thursday","Štvrtok"}. {"Time delay","Časový posun"}. -{"Time","Čas"}. {"Too many CAPTCHA requests","Príliš veľa žiadostí o CAPTCHA"}. -{"To","Pre"}. {"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ý"}. {"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"}. -{"Update","Aktualizovať"}. -{"Uptime:","Uptime:"}. {"User JID","Používateľ "}. {"User Management","Správa užívateľov"}. {"Username:","IRC prezývka"}. @@ -315,7 +267,6 @@ {"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"}. diff --git a/priv/msgs/sq.msg b/priv/msgs/sq.msg index e6305d935..de51a9614 100644 --- a/priv/msgs/sq.msg +++ b/priv/msgs/sq.msg @@ -11,8 +11,6 @@ {"A Web Page","Faqe Web"}. {"Accept","Pranoje"}. {"Account doesn't exist","Llogaria s’ekziston"}. -{"Add Jabber ID","Shtoni ID Jabber"}. -{"Add New","Shtoni të Ri"}. {"Add User","Shtoni Përdorues"}. {"Administration of ","Administrim i "}. {"Administration","Administrim"}. @@ -30,7 +28,9 @@ {"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"}. @@ -52,6 +52,7 @@ {"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"}. @@ -68,25 +69,18 @@ {"Configuration of room ~s","Formësim i dhomë ~s"}. {"Configuration","Formësim"}. {"Country","Vend"}. -{"CPU Time:","Kohë CPU-je:"}. {"Current Discussion Topic","Tema e Tanishme e Diskutimit"}. {"Database failure","Dështim baze të dhënash"}. -{"Database Tables at ~p","Tabela Baze të Dhënash te ~p"}. {"Database Tables Configuration at ","Formësim Tabelash Baze të Dhënash te "}. {"Database","Bazë të dhënash"}. {"December","Dhjetor"}. -{"Delete content","Fshini lëndë"}. {"Delete message of the day","Fshini mesazhin e ditës"}. -{"Delete Selected","Fshi të Përzgjedhurin"}. -{"Delete table","Fshini tabelën"}. {"Delete User","Fshi Përdorues"}. {"Deliver event notifications","Dërgo njoftime aktesh"}. -{"Description:","Përshkrim:"}. {"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"}. -{"Elements","Elementë"}. {"Email Address","Adresë Email"}. {"Email","Email"}. {"Enable logging","Aktivizo regjistrim"}. @@ -94,7 +88,6 @@ {"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"}. -{"Error","Gabim"}. {"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"}. @@ -106,23 +99,20 @@ {"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"}. -{"From","Nga"}. {"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 Password","Merr Fjalëkalim Përdoruesi"}. {"Get User Statistics","Merr Statistika Përdoruesi"}. {"Given Name","Emër"}. {"Grant voice to this person?","T’i akordohet zë këtij personi?"}. -{"Group","Grup"}. -{"Groups that will be displayed to the members","Grupe që do t’u shfaqen anëtarëve"}. -{"Groups","Grupe"}. {"has been banned","është dëbuar"}. {"has been kicked","është përzënë"}. +{"Hat title","Titull kapeleje"}. {"Host unknown","Strehë e panjohur"}. -{"Host","Strehë"}. {"HTTP File Upload","Ngarkim Kartelash HTTP"}. {"Idle connection","Lidhje e plogësht"}. {"Import Directory","Importoni Drejtori"}. @@ -130,7 +120,6 @@ {"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"}. -{"Incoming s2s Connections:","Lidhje s2s Ardhëse:"}. {"Incorrect CAPTCHA submit","Parashtim Captcha-je të pasaktë"}. {"Incorrect password","Fjalëkalim i pasaktë"}. {"Incorrect value of 'action' attribute","Vlerë e pavlefshme atributi 'action'"}. @@ -142,7 +131,6 @@ {"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"}. -{"It is not allowed to send private messages","Nuk lejohet të dërgohen mesazhe private"}. {"Jabber ID","ID Jabber"}. {"January","Janar"}. {"JID normalization failed","Normalizimi JID dështoi"}. @@ -150,14 +138,12 @@ {"July","Korrik"}. {"June","Qershor"}. {"Just created","Të sapokrijuara"}. -{"Label:","Etiketë:"}. {"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"}. -{"List of rooms","Listë dhomash"}. {"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"}. @@ -172,10 +158,7 @@ {"Maximum file size","Madhësi maksimum kartelash"}. {"Maximum Number of Occupants","Numër Maksimum të Pranishmish"}. {"May","Maj"}. -{"Members not added (inexistent vhost!): ","S’u shtuan anëtarë (vhost joekzistuese!): "}. -{"Members:","Anëtarë:"}. {"Membership is required to enter this room","Lypset anëtarësim për të hyrë në këtë dhomë"}. -{"Memory","Kujtesë"}. {"Message body","Lëndë mesazhi"}. {"Messages from strangers are rejected","Mesazhet prej të panjohurish hidhen tej"}. {"Messages of type headline","Mesazhe të llojit titull"}. @@ -189,7 +172,6 @@ {"Multicast","Multikast"}. {"Multi-User Chat","Fjalosje Me Shumë Përdorues Njëherësh"}. {"Name","Emër"}. -{"Name:","Emër:"}. {"Natural-Language Room Name","Emër Dhome Në Gjuhë Natyrale"}. {"Never","Kurrë"}. {"New Password:","Fjalëkalim i Ri:"}. @@ -215,6 +197,7 @@ {"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"}. @@ -227,25 +210,22 @@ {"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"}. -{"Offline Messages","Mesazhe Jo Në Linjë"}. -{"Offline Messages:","Mesazhe Jo Në Linjë:"}. {"OK","OK"}. {"Old Password:","Fjalëkalimi i Vjetër:"}. {"Online Users","Përdorues Në Linjë"}. -{"Online Users:","Përdorues Në Linjë:"}. {"Online","Në linjë"}. -{"Only admins can see this","Këtë mund ta shohin vetëm përgjegjësit"}. {"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"}. -{"Outgoing s2s Connections:","Lidhje s2s Ikëse:"}. {"Owner privileges required","Lypset privilegje të zoti"}. -{"Packet","Paketë"}. +{"Participant ID","ID Pjesëmarrësi"}. {"Participant","Pjesëmarrës"}. {"Password Verification","Verifikim Fjalëkalimi"}. {"Password Verification:","Verifikim Fjalëkalimi:"}. @@ -253,8 +233,6 @@ {"Password:","Fjalëkalim:"}. {"Path to Dir","Shteg për te Drejtori"}. {"Path to File","Shteg për te Kartelë"}. -{"Payload type","Lloj ngarkese"}. -{"Pending","Pezull"}. {"Period: ","Periudhë: "}. {"Ping","Ping"}. {"Pong","Pong"}. @@ -267,27 +245,21 @@ {"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"}. -{"Registered Users","Përdorues të Regjistruar"}. -{"Registered Users:","Përdorues të Regjistruar:"}. {"Register","Regjistrohuni"}. {"Remote copy","Kopje e largët"}. -{"Remove All Offline Messages","Hiq Krejt Mesazhet Jo Në Linjë"}. {"Remove User","Hiqeni Përdoruesin"}. -{"Remove","Hiqe"}. {"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"}. -{"Restart","Rinise"}. {"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"}. -{"RPC Call Error","Gabim Thirrjeje RPC"}. {"Running Nodes","Nyje Në Punë"}. {"Saturday","E shtunë"}. {"Search from the date","Kërko nga data"}. @@ -295,7 +267,6 @@ {"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 "}. -{"Select All","Përzgjidheni Krejt"}. {"Send announcement to all users","Dërgo njoftim krejt përdoruesve"}. {"September","Shtator"}. {"Server:","Shërbyes:"}. @@ -305,16 +276,11 @@ {"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"}. -{"Statistics of ~p","Statistika për ~p"}. -{"Statistics","Statistika"}. -{"Stop","Ndale"}. +{"Stanza id is not valid","ID Stanza s’është i vlefshëm"}. {"Stopped Nodes","Nyja të Ndalura"}. -{"Storage Type","Lloj Depozitimi"}. {"Subject","Subjekti"}. -{"Submit","Parashtrojeni"}. {"Submitted","Parashtruar"}. {"Subscriber Address","Adresë e Pajtimtarit"}. -{"Subscription","Pajtim"}. {"Sunday","E diel"}. {"The account already exists","Ka tashmë një llogari të tillë"}. {"The account was not unregistered","Llogaria s’qe çregjistruar"}. @@ -322,6 +288,7 @@ {"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"}. @@ -336,23 +303,19 @@ {"This room is not anonymous","Kjo dhomë s’është anonime"}. {"Thursday","E enjte"}. {"Time delay","Vonesë kohore"}. -{"Time","Kohë"}. {"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ë"}. -{"Total rooms","Dhoma gjithsej"}. {"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"}. -{"Unselect All","Shpërzgjidhi Krejt"}. {"Unsupported version","Version i pambuluar"}. -{"Update","Përditësoje"}. -{"Uptime:","Kohëpunim:"}. +{"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)"}. @@ -364,16 +327,16 @@ {"User","Përdorues"}. {"Users Last Activity","Veprimtaria e Fundit Nga Përdorues"}. {"Users","Përdorues"}. -{"Validate","Vleftësoje"}. -{"View Queue","Shihni Radhën"}. {"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"}. diff --git a/priv/msgs/sv.msg b/priv/msgs/sv.msg index afd38e2f6..8e454464e 100644 --- a/priv/msgs/sv.msg +++ b/priv/msgs/sv.msg @@ -7,8 +7,6 @@ {"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 Jabber ID","Lägg till Jabber ID"}. -{"Add New","Lägg till ny"}. {"Add User","Lägg till användare"}. {"Administration of ","Administration av "}. {"Administration","Administration"}. @@ -36,26 +34,21 @@ {"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 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 of room ~s","Konfiguration för ~s"}. {"Configuration","Konfiguration"}. -{"Connected Resources:","Anslutna resurser:"}. {"Country","Land"}. -{"CPU Time:","CPU tid"}. {"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 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"}. {"Dump Backup to Text File at ","Dumpa säkerhetskopia till textfil på "}. {"Dump to Text File","Dumpa till textfil"}. @@ -65,7 +58,6 @@ {"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"}. {"End User Session","Avsluta användarsession"}. @@ -75,27 +67,21 @@ {"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"}. -{"Error","Fel"}. {"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"}. {"Friday","Fredag"}. -{"From","Från"}. {"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 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"}. -{"Host","Server"}. {"Import Directory","Importera katalog"}. {"Import File","Importera fil"}. {"Import user data from jabberd14 spool file:","Importera användare från jabberd14 Spool filer"}. @@ -110,7 +96,6 @@ {"is now known as","är känd som"}. {"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"}. -{"It is not allowed to send private messages","Det ar inte tillåtet att skicka privata meddelanden"}. {"Jabber ID","Jabber ID"}. {"January","Januari"}. {"joins the room","joinar rummet"}. @@ -121,7 +106,6 @@ {"Last month","Senaste månaden"}. {"Last year","Senaste året"}. {"leaves the room","lämnar rummet"}. -{"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"}. @@ -133,15 +117,11 @@ {"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"}. -{"Modified modules","Uppdaterade moduler"}. {"Monday","Måndag"}. {"Name","Namn"}. -{"Name:","Namn:"}. {"Never","Aldrig"}. {"Nickname Registration at ","Registrera smeknamn på "}. {"Nickname ~s does not exist in the room","Smeknamnet ~s existerar inte i det här rummet"}. @@ -162,11 +142,8 @@ {"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 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"}. @@ -176,15 +153,12 @@ {"Organization Name","Organisationsnamn"}. {"Organization Unit","Organisationsenhet"}. {"Outgoing s2s Connections","Utgaende s2s anslutning"}. -{"Outgoing s2s Connections:","Utgående s2s anslutning"}. {"Owner privileges required","Ägarprivilegier krävs"}. -{"Packet","Paket"}. {"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"}. @@ -199,15 +173,11 @@ {"RAM copy","RAM-kopia"}. {"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"}. {"Remote copy","Sparas inte lokalt"}. {"Remove User","Ta bort användare"}. -{"Remove","Ta bort"}. {"Replaced by new connection","Ersatt av ny anslutning"}. {"Resources","Resurser"}. {"Restart Service","Starta om servicen"}. -{"Restart","Omstart"}. {"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"}. @@ -219,15 +189,12 @@ {"Room title","Rumstitel"}. {"Roster groups allowed to subscribe","Rostergrupper tillåts att prenumerera"}. {"Roster size","Roster storlek"}. -{"RPC Call Error","RPC Uppringningserror"}. {"Running Nodes","Körande noder"}. {"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"}. {"Set message of the day and send to online users","Sätt dagens status meddelande och skicka till alla användare"}. @@ -238,18 +205,12 @@ {"Shut Down Service","Stäng ner servicen"}. {"Specify the access model","Specificera accessmodellen"}. {"Specify the publisher model","Ange publiceringsmodell"}. -{"Statistics of ~p","Statistik på ~p"}. -{"Statistics","Statistik"}. {"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."}. @@ -257,27 +218,16 @@ {"This room is not anonymous","Detta rum är inte anonymt"}. {"Thursday","Torsdag"}. {"Time delay","Tidsförsening"}. -{"Time","Tid"}. -{"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"}. -{"Uptime:","Tid upp"}. {"User Management","Användarmanagement"}. {"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"}. {"Users","Användare"}. -{"Validate","Validera"}. {"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"}. 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 9ec823668..0def6490d 100644 --- a/priv/msgs/th.msg +++ b/priv/msgs/th.msg @@ -6,8 +6,6 @@ {" has set the subject to: "," ตั้งหัวข้อว่า: "}. {"Access denied by service policy","การเข้าถึงถูกปฏิเสธโดยนโยบายการบริการ"}. {"Action on user","การดำเนินการกับผู้ใช้"}. -{"Add Jabber ID","เพิ่ม Jabber ID"}. -{"Add New","เพิ่มผู้ใช้ใหม่"}. {"Add User","เพิ่มผู้ใช้"}. {"Administration of ","การดูแล "}. {"Administration","การดูแล"}. @@ -37,20 +35,16 @@ {"Commands","คำสั่ง"}. {"Conference room does not exist","ไม่มีห้องประชุม"}. {"Configuration","การกำหนดค่า"}. -{"Connected Resources:","ทรัพยากรที่เชื่อมต่อ:"}. {"Country","ประเทศ"}. -{"CPU Time:","เวลาการทำงานของ CPU:"}. {"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","ลบผู้ใช้"}. {"Deliver event notifications","ส่งการแจ้งเตือนเหตุการณ์"}. {"Deliver payloads with event notifications","ส่งส่วนของข้อมูล (payload) พร้อมกับการแจ้งเตือนเหตุการณ์"}. -{"Description:","รายละเอียด:"}. {"Disc only copy","คัดลอกเฉพาะดิสก์"}. {"Dump Backup to Text File at ","ถ่ายโอนการสำรองข้อมูลไปยังไฟล์ข้อความที่"}. {"Dump to Text File","ถ่ายโอนข้อมูลไปยังไฟล์ข้อความ"}. @@ -70,18 +64,13 @@ {"Family Name","นามสกุล"}. {"February","กุมภาพันธ์"}. {"Friday","วันศุกร์"}. -{"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","กลุ่"}. {"has been banned","ถูกสั่งห้าม"}. {"has been kicked","ถูกไล่ออก"}. -{"Host","โฮสต์"}. {"Import Directory","อิมพอร์ตไดเร็กทอรี"}. {"Import File","อิมพอร์ตไฟล์"}. {"Import User from File at ","อิมพอร์ตผู้ใช้จากไฟล์ที่"}. @@ -103,7 +92,6 @@ {"Last month","เดือนที่แล้ว"}. {"Last year","ปีที่แล้ว"}. {"leaves the room","ออกจากห้อง"}. -{"Low level update script","อัพเดตสคริปต์ระดับต่ำ"}. {"Make participants list public","สร้างรายการผู้เข้าร่วมสำหรับใช้งานโดยบุคคลทั่วไป"}. {"Make room members-only","สร้างห้องสำหรับสมาชิกเท่านั้น"}. {"Make room password protected","สร้างห้องที่มีการป้องกันด้วยรหัสผ่าน"}. @@ -113,14 +101,11 @@ {"Max payload size in bytes","ขนาดสูงสุดของส่วนของข้อมูล (payload) มีหน่วยเป็นไบต์"}. {"Maximum Number of Occupants","จำนวนผู้ครอบครองห้องสูงสุด"}. {"May","พฤษภาคม"}. -{"Members:","สมาชิก:"}. -{"Memory","หน่วยความจำ"}. {"Message body","เนื้อหาของข้อความ"}. {"Middle Name","ชื่อกลาง"}. {"Moderator privileges required","ต้องมีสิทธิพิเศษของผู้ดูแลการสนทนา"}. {"Monday","วันจันทร์"}. {"Name","ชื่อ"}. -{"Name:","ชื่อ:"}. {"Never","ไม่เคย"}. {"Nickname Registration at ","การลงทะเบียนชื่อเล่นที่ "}. {"Nickname","ชื่อเล่น"}. @@ -139,11 +124,8 @@ {"Number of online users","จำนวนผู้ใช้ออนไลน์"}. {"Number of registered users","จำนวนผู้ใช้ที่ลงทะเบียน"}. {"October","ตุลาคม"}. -{"Offline Messages","ข้อความออฟไลน์"}. -{"Offline Messages:","ข้อความออฟไลน์:"}. {"OK","ตกลง"}. {"Online Users","ผู้ใช้ออนไลน์"}. -{"Online Users:","ผู้ใช้ออนไลน์:"}. {"Online","ออนไลน์"}. {"Only deliver notifications to available users","ส่งการแจ้งเตือนถึงผู้ใช้ที่สามารถติดต่อได้เท่านั้น"}. {"Only occupants are allowed to send messages to the conference","ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความไปยังห้องประชุม"}. @@ -152,15 +134,12 @@ {"Organization Name","ชื่อองค์กร"}. {"Organization Unit","หน่วยขององค์กร"}. {"Outgoing s2s Connections","การเชื่อมต่อ s2s ขาออก"}. -{"Outgoing s2s Connections:","การเชื่อมต่อ s2s ขาออก:"}. {"Owner privileges required","ต้องมีสิทธิพิเศษของเจ้าของ"}. -{"Packet","แพ็กเก็ต"}. {"Password Verification","การตรวจสอบรหัสผ่าน"}. {"Password","รหัสผ่าน"}. {"Password:","รหัสผ่าน:"}. {"Path to Dir","พาธไปยัง Dir"}. {"Path to File","พาธของไฟล์ข้อมูล"}. -{"Pending","ค้างอยู่"}. {"Period: ","ระยะเวลา:"}. {"Persist items to storage","ยืนยันรายการที่จะจัดเก็บ"}. {"Ping","Ping"}. @@ -174,15 +153,11 @@ {"RAM copy","คัดลอก RAM"}. {"Really delete message of the day?","แน่ใจว่าต้องการลบข้อความของวันหรือไม่"}. {"Recipient is not in the conference room","ผู้รับไม่ได้อยู่ในห้องประชุม"}. -{"Registered Users","ผู้ใช้ที่ลงทะเบียน"}. -{"Registered Users:","ผู้ใช้ที่ลงทะเบียน:"}. {"Remote copy","คัดลอกระยะไกล"}. {"Remove User","ลบผู้ใช้"}. -{"Remove","ลบ"}. {"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:","คืนค่าข้อมูลสำรองแบบไบนารีโดยทันที:"}. @@ -192,10 +167,8 @@ {"Room creation is denied by service policy","การสร้างห้องสนทนาถูกปฏิเสธโดยนโยบายการบริการ"}. {"Room title","ชื่อห้อง"}. {"Roster size","ขนาดของบัญชีรายชื่อ"}. -{"RPC Call Error","ข้อผิดพลาดจากการเรียกใช้ RPC"}. {"Running Nodes","โหนดที่ทำงาน"}. {"Saturday","วันเสาร์"}. -{"Script check","ตรวจสอบคริปต์"}. {"Search Results for ","ผลการค้นหาสำหรับ "}. {"Search users in ","ค้นหาผู้ใช้ใน "}. {"Send announcement to all online users on all hosts","ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมดบนโฮสต์ทั้งหมด"}. @@ -211,42 +184,25 @@ {"Shut Down Service","ปิดการบริการ"}. {"Specify the access model","ระบุโมเดลการเข้าถึง"}. {"Specify the publisher model","ระบุโมเดลผู้เผยแพร่"}. -{"Statistics of ~p","สถิติของ ~p"}. -{"Statistics","สถิติ"}. {"Stopped Nodes","โหนดที่หยุด"}. -{"Stop","หยุด"}. -{"Storage Type","ชนิดที่เก็บข้อมูล"}. {"Store binary backup:","จัดเก็บข้อมูลสำรองแบบไบนารี:"}. {"Store plain text backup:","จัดเก็บข้อมูลสำรองที่เป็นข้อความธรรมดา:"}. {"Subject","หัวเรื่อง"}. {"Submitted","ส่งแล้ว"}. -{"Submit","ส่ง"}. {"Subscriber Address","ที่อยู่ของผู้สมัคร"}. -{"Subscription","การสมัครสมาชิก"}. {"Sunday","วันอาทิตย์"}. {"the password is","รหัสผ่านคือ"}. {"This room is not anonymous","ห้องนี้ไม่ปิดบังชื่อ"}. {"Thursday","วันพฤหัสบดี"}. {"Time delay","การหน่วงเวลา"}. -{"Time","เวลา"}. -{"To","ถึง"}. {"Traffic rate limit is exceeded","อัตราของปริมาณการเข้าใช้เกินขีดจำกัด"}. -{"Transactions Aborted:","ทรานแซกชันที่ถูกยกเลิก:"}. -{"Transactions Committed:","ทรานแซกชันที่ได้รับมอบหมาย:"}. -{"Transactions Logged:","ทรานแซกชันที่บันทึก:"}. -{"Transactions Restarted:","ทรานแซกชันที่เริ่มทำงานใหม่อีกครั้ง:"}. {"Tuesday","วันอังคาร"}. {"Update message of the day (don't send)","อัพเดตข้อความของวัน (ไม่ต้องส่ง)"}. {"Update message of the day on all hosts (don't send)","อัพเดตข้อความของวันบนโฮสต์ทั้งหมด (ไม่ต้องส่ง) "}. -{"Update plan","แผนการอัพเดต"}. -{"Update script","อัพเดตสคริปต์"}. -{"Update","อัพเดต"}. -{"Uptime:","เวลาการทำงานต่อเนื่อง:"}. {"User Management","การจัดการผู้ใช้"}. {"Users Last Activity","กิจกรรมล่าสุดของผู้ใช้"}. {"Users","ผู้ใช้"}. {"User","ผู้ใช้"}. -{"Validate","ตรวจสอบ"}. {"vCard User Search","ค้นหาผู้ใช้ vCard "}. {"Virtual Hosts","โฮสต์เสมือน"}. {"Visitors are not allowed to send messages to all occupants","ผู้เยี่ยมเยือนไม่ได้รับอนุญาตให้ส่งข้อความถึงผู้ครอบครองห้องทั้งหมด"}. diff --git a/priv/msgs/tr.msg b/priv/msgs/tr.msg index 8acdf3491..af108d514 100644 --- a/priv/msgs/tr.msg +++ b/priv/msgs/tr.msg @@ -8,8 +8,6 @@ {"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"}. {"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"}. @@ -51,20 +49,16 @@ {"Conference room does not exist","Konferans odası bulunamadı"}. {"Configuration of room ~s","~s odasının ayarları"}. {"Configuration","Ayarlar"}. -{"Connected Resources:","Bağlı Kaynaklar:"}. {"Country","Ülke"}. -{"CPU Time:","İşlemci Zamanı:"}. {"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 on all hosts","Tüm sunuculardaki günün mesajını sil"}. {"Delete message of the day","Günün mesajını sil"}. -{"Delete Selected","Seçilenleri 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"}. {"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"}. @@ -75,7 +69,6 @@ {"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ç"}. {"End User Session","Kullanıcı Oturumunu Kapat"}. @@ -85,7 +78,6 @@ {"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"}. -{"Error","Hata"}. {"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:"}. @@ -93,21 +85,17 @@ {"Family Name","Soyisim"}. {"February","Şubat"}. {"Friday","Cuma"}. -{"From","Kimden"}. {"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?"}. -{"Groups","Gruplar"}. {"has been banned","odaya girmesi yasaklandı"}. {"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ı"}. -{"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."}. {"Import Directory","Dizini İçe Aktar"}. {"Import File","Dosyayı İçe Aktar"}. @@ -123,7 +111,6 @@ {"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 to the conference","Konferansa özel mesajlar gönderilmesine izin verilmiyor"}. -{"It is not allowed to send private messages","Özel mesaj gönderilmesine izin verilmiyor"}. {"Jabber ID","Jabber ID"}. {"January","Ocak"}. {"joins the room","odaya katıldı"}. @@ -134,7 +121,6 @@ {"Last month","Geçen ay"}. {"Last year","Geçen yıl"}. {"leaves the room","odadan ayrıldı"}. -{"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"}. @@ -147,16 +133,12 @@ {"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:"}. -{"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"}. -{"Modified modules","Değişen modüller"}. {"Monday","Pazartesi"}. {"Name","İsim"}. -{"Name:","İsim:"}. {"Never","Asla"}. {"New Password:","Yeni Parola:"}. {"Nickname Registration at ","Takma İsim Kaydı : "}. @@ -178,12 +160,9 @@ {"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 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"}. @@ -195,16 +174,13 @@ {"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ı:"}. {"Owner privileges required","Sahip yetkileri gerekli"}. -{"Packet","Paket"}. {"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"}. @@ -221,17 +197,12 @@ {"RAM copy","RAM kopyala"}. {"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"}. -{"Registered Users","Kayıtlı Kullanıcılar"}. -{"Registered Users:","Kayıtlı Kullanıcılar:"}. {"Register","Kayıt Ol"}. {"Remote copy","Uzak kopyala"}. -{"Remove All Offline Messages","Tüm Çevirim-dışı Mesajları Kaldır"}. {"Remove User","Kullanıcıyı Kaldır"}. -{"Remove","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:"}. @@ -244,10 +215,8 @@ {"Room title","Oda başlığı"}. {"Roster groups allowed to subscribe","Üye olunmasına izin verilen kontak listesi grupları"}. {"Roster size","İsim listesi boyutu"}. -{"RPC Call Error","RPC Çağrı Hatası"}. {"Running Nodes","Çalışan Düğümler"}. {"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 on all hosts","Duyuruyu tüm sunuculardaki tüm bağlı kullanıcılara yolla"}. @@ -265,18 +234,12 @@ {"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"}. -{"Statistics of ~p","~p istatistikleri"}. -{"Statistics","İstatistikler"}. -{"Stop","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ş"}. @@ -290,24 +253,14 @@ {"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"}. {"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","Kaydı Sil"}. {"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"}. -{"Update","GÜncelle"}. -{"Uptime:","Hizmet Süresi:"}. {"User JID","Kullanıcı JID"}. {"User Management","Kullanıcı Yönetimi"}. {"User","Kullanıcı"}. @@ -315,7 +268,6 @@ {"Users are not allowed to register accounts so quickly","Kullanıcıların bu kadar hızlı hesap açmalarına izin verilmiyor"}. {"Users Last Activity","Kullanıcıların Son Aktiviteleri"}. {"Users","Kullanıcılar"}. -{"Validate","Geçerli"}. {"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"}. diff --git a/priv/msgs/uk.msg b/priv/msgs/uk.msg index e2b99949f..cf950ac73 100644 --- a/priv/msgs/uk.msg +++ b/priv/msgs/uk.msg @@ -12,11 +12,10 @@ {"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 Jabber ID","Додати Jabber ID"}. -{"Add New","Додати"}. {"Add User","Додати користувача"}. {"Administration of ","Адміністрування "}. {"Administration","Адміністрування"}. @@ -34,85 +33,85 @@ {"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 Voice","Усі, хто має голос"}. -{"April","квітня"}. -{"Attribute 'channel' is required for this request","Для цього запиту потрібен атрибут \"канал\""}. -{"Attribute 'id' is mandatory for MIX messages","Для MIX повідомлень потрібен атрибут \"id\""}. -{"Attribute 'jid' is not allowed here","Атрибут 'jid' тут заборонений"}. -{"Attribute 'node' is not allowed here","Атрибут \"вузол\" тут заборонений"}. -{"August","серпня"}. +{"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 ~p","Резервне копіювання ~p"}. {"Backup to File at ","Резервне копіювання в файл на "}. {"Backup","Резервне копіювання"}. -{"Bad format","Неправильний формат"}. +{"Bad format","Невірний формат"}. {"Birthday","День народження"}. -{"Both the username and the resource are required","Потрібне ім'я користувача та ресурс"}. +{"Both the username and the resource are required","Обов'язково потрібне ім'я користувача та джерело"}. {"Bytestream already activated","Потік байтів вже активовано"}. {"Cannot remove active list","Неможливо видалити активний список"}. -{"Cannot remove default list","Неможливо видалити список за промовчанням"}. -{"CAPTCHA web page","Адреса капчі"}. +{"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 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 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:","Підключені ресурси:"}. {"Contact Addresses (normally, room owner or owners)","Контактні адреси (зазвичай, власника або власників кімнати)"}. {"Country","Країна"}. -{"CPU Time:","Процесорний час:"}. {"Current Discussion Topic","Поточна тема обговорення"}. -{"Database failure","Помилка база даних"}. -{"Database Tables at ~p","Таблиці бази даних на ~p"}. +{"Database failure","Збій бази даних"}. {"Database Tables Configuration at ","Конфігурація таблиць бази даних на "}. {"Database","База даних"}. -{"December","грудня"}. -{"Default users as participants","Зробити користувачів учасниками за замовчуванням"}. -{"Delete content","Видалити вміст"}. +{"December","Грудень"}. +{"Default users as participants","Користувачі за замовчуванням як учасники"}. {"Delete message of the day on all hosts","Видалити повідомлення дня на усіх хостах"}. {"Delete message of the day","Видалити повідомлення дня"}. -{"Delete Selected","Видалити виділені"}. -{"Delete table","Видалити таблицю"}. -{"Delete User","Видалити Користувача"}. +{"Delete User","Видалити користувача"}. {"Deliver event notifications","Доставляти сповіщення про події"}. {"Deliver payloads with event notifications","Доставляти разом з повідомленнями про публікації самі публікації"}. -{"Description:","Опис:"}. {"Disc only copy","Тільки диск"}. -{"'Displayed groups' not added (they do not exist!): ","\"Відображені групи\" не додано (вони не існують!): "}. -{"Displayed:","Відображено:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Нікому не кажіть свій пароль, навіть адміністраторам XMPP-сервера."}. {"Dump Backup to Text File at ","Копіювання в текстовий файл на "}. {"Dump 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","Служба відвантаження по HTTP для ejabberd"}. @@ -123,12 +122,12 @@ {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Веб-інтерфейс Адміністрування ejabberd"}. {"ejabberd","ejabberd"}. -{"Elements","Елементи"}. {"Email Address","Адреса ел. пошти"}. {"Email","Електронна пошта"}. {"Enable hats","Увімкнути капелюхи"}. {"Enable logging","Увімкнути журнал роботи"}. {"Enable message archiving","Ввімкнути архівацію повідомлень"}. +{"Enabling push without 'node' attribute is not supported","Увімкнення push без атрибута node не підтримується"}. {"End User Session","Закінчити Сеанс Користувача"}. {"Enter nickname you want to register","Введіть псевдонім, який ви хочете зареєструвати"}. {"Enter path to backup file","Введіть шлях до резервного файла"}. @@ -137,7 +136,6 @@ {"Enter path to text file","Введіть шлях до текстового файла"}. {"Enter the text you see","Введіть текст, що ви бачите"}. {"Erlang XMPP Server","Ерланґ XMPP Сервер"}. -{"Error","Помилка"}. {"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):"}. @@ -151,31 +149,33 @@ {"Failed to process option '~s'","Не вдалося обробити параметр \"~s\""}. {"Family Name","Прізвище"}. {"FAQ Entry","Запис в ЧаПи"}. -{"February","лютого"}. +{"February","Лютого"}. {"File larger than ~w bytes","Файл більший, ніж ~w байт"}. {"Fill in the form to search for any matching XMPP User","Заповніть форму для пошуку будь-якого відповідного користувача XMPP"}. {"Friday","П'ятниця"}. {"From ~ts","Від ~ts"}. -{"From","Від кого"}. {"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?","Надати голос персоні?"}. -{"Groups that will be displayed to the members","Групи, які показуватимуться учасникам"}. -{"Groups","Групи"}. -{"Group","Група"}. {"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 цієї кімнати"}. +{"Hat title","Назва кімнати"}. +{"Hat URI","Назва URI"}. +{"Hats limit exceeded","Перевищено швидкість передачі інформації"}. {"Host unknown","Невідоме ім'я сервера"}. -{"Host","Хост"}. {"HTTP File Upload","Відвантаження файлів по HTTP"}. {"Idle connection","Неактивне підключення"}. {"If you don't see the CAPTCHA image here, visit the web page.","Якщо ви не бачите зображення CAPTCHA, перейдіть за адресою."}. @@ -189,13 +189,14 @@ {"Import Users From jabberd14 Spool Files","Імпорт користувачів з jabberd14 файлів \"Spool\""}. {"Improper domain part of 'from' attribute","Неправильна доменна частина атрибута \"from\""}. {"Improper message type","Неправильний тип повідомлення"}. -{"Incoming s2s Connections:","Вхідні s2s-з'єднання:"}. {"Incorrect CAPTCHA submit","Неправильний ввід CAPTCHA"}. {"Incorrect data form","Неправильна форма даних"}. {"Incorrect password","Неправильний пароль"}. {"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\" у пересланому повідомленні"}. @@ -207,45 +208,47 @@ {"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 ID","Jabber ID"}. -{"January","січня"}. +{"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","увійшов(ла) в кімнату"}. -{"July","липня"}. -{"June","червня"}. +{"July","Липня"}. +{"June","Червня"}. {"Just created","Щойно створено"}. -{"Label:","Мітка:"}. {"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 rooms","Перелік кімнат"}. {"List of users with hats","Список користувачів із капелюхами"}. {"List users with hats","Список користувачів із капелюхами"}. +{"Logged Out","Вийшов із системи"}. {"Logging","Журналювання"}. -{"Low level update script","Низькорівневий сценарій поновлення"}. {"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","березня"}. +{"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","травня"}. -{"Members not added (inexistent vhost!): ","Учасників не додано (вірт. сервер не існує!): "}. {"Membership is required to enter this room","В цю конференцію можуть входити тільки її члени"}. -{"Members:","Члени:"}. -{"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","Повідомлення від незнайомців відхиляються"}. @@ -254,15 +257,16 @@ {"Middle Name","По-батькові"}. {"Minimum interval between voice requests (in seconds)","Мінімальний інтервал між голосовими запитами (в секундах)"}. {"Moderator privileges required","Необхідні права модератора"}. +{"Moderators Only","Тільки модераторам"}. {"Moderator","Модератор"}. -{"Modified modules","Змінені модулі"}. {"Module failed to handle the query","Модулю не вдалося обробити запит"}. {"Monday","Понеділок"}. {"Multicast","Мультікаст"}. {"Multiple elements are not allowed by RFC6121","Кілька елементів не дозволені RFC6121"}. {"Multi-User Chat","Багато-користувальницький чат"}. {"Name","Назва"}. -{"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","Ніколи"}. @@ -298,6 +302,7 @@ {"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","Індекс вузла не знайдено"}. @@ -308,26 +313,26 @@ {"Node","Вузол"}. {"None","Немає"}. {"Not allowed","Не дозволяється"}. -{"Not Found","не знайдено"}. +{"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 admins can see this","Тільки адміністратори можуть це бачити"}. {"Only collection node owners may associate leaf nodes with the collection","Лише власники вузлів колекції можуть асоціювати листові вузли з колекцією"}. {"Only deliver notifications to available users","Доставляти повідомленнями тільки доступним користувачам"}. {"Only or tags are allowed","Дозволені лише теги або "}. @@ -335,19 +340,21 @@ {"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-з'єднання"}. -{"Outgoing s2s Connections:","Вихідні s2s-з'єднання:"}. {"Owner privileges required","Необхідні права власника"}. {"Packet relay is denied by service policy","Пересилання пакетів заборонене політикою сервісу"}. -{"Packet","Пакет"}. +{"Participant ID","ID учасника"}. {"Participant","Учасник"}. {"Password Verification","Перевірка Пароля"}. {"Password Verification:","Перевірка Пароля:"}. @@ -355,42 +362,57 @@ {"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. Якщо Ви також використовуєте інше сховище для даних (наприклад за допомогою модуля ODBC), то його резервне копіювання потрібно робити окремо."}. -{"Please, wait for a while before sending new voice request","Будь ласка, почекайте деякий час перед тим, як знову відправляти голосовий запит"}. -{"Pong","Понг"}. -{"Present real Jabber IDs to","Зробити реальні Jabber ID учасників видимими"}. -{"Previous session not found","Попередній сеанс не знайдено"}. +{"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, ","приватна, "}. +{"Public","Публічний"}. +{"Publish model","Модель публікації"}. {"Publish-Subscribe","Публікація-Підписка"}. -{"PubSub subscriber request","Запит на підписку PubSub"}. -{"Purge all items when the relevant publisher goes offline","Видалити всі елементи, коли особа, що їх опублікувала, вимикається від мережі"}. +{"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","Запити до користувачів в цій конференції заборонені"}. -{"RAM and disc copy","ОЗП та диск"}. -{"RAM copy","ОЗП"}. -{"Really delete message of the day?","Насправді, видалити повідомлення дня?"}. +{"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","Зареєструвати XMPP-запис"}. -{"Registered Users","Зареєстровані користувачі"}. -{"Registered Users:","Зареєстровані користувачі:"}. {"Register","Реєстрація"}. -{"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","Конфігурація кімнати"}. @@ -400,68 +422,112 @@ {"Room terminates","Кімната припиняється"}. {"Room title","Назва кімнати"}. {"Roster groups allowed to subscribe","Дозволені для підписки групи ростера"}. -{"Roster of ~ts","Список контактів ~ts"}. {"Roster size","Кількість контактів"}. -{"Roster:","Список контактів:"}. -{"RPC Call Error","Помилка виклику RPC"}. {"Running Nodes","Працюючі вузли"}. {"~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","вересня"}. +{"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","Умови публікації"}. -{"Statistics of ~p","Статистика вузла ~p"}. -{"Statistics","Статистика"}. +{"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 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 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 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-сервера."}. {"This room is not anonymous","Ця кімната не анонімна"}. +{"This service can not process the address: ~s","Ця служба не може обробити адресу: ~s"}. {"Thursday","Четвер"}. {"Time delay","Час затримки"}. {"Timed out waiting for stream resumption","Час очікування на відновлення потоку закінчився"}. -{"Time","Час"}. {"To register, visit ~s","Щоб зареєструватися, відвідайте ~s"}. {"To ~ts","До ~ts"}. {"Token TTL","Токен TTL"}. @@ -474,13 +540,8 @@ {"Too many receiver fields were specified","Вказано забагато одержувачів"}. {"Too many unacked stanzas","Занадто багато пакетів без відповідей"}. {"Too many users in this conference","Надто багато користувачів у цій конференції"}. -{"Total rooms","Всього кімнат"}. -{"To","Кому"}. {"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","Нема можливості згенерувати капчу"}. @@ -488,23 +549,23 @@ {"Unauthorized","Не авторизовано"}. {"Unexpected action","Несподівана дія"}. {"Unexpected error condition: ~p","Умова несподіваної помилки: ~p"}. +{"Uninstall","Видалити"}. {"Unregister an XMPP account","Видалити обліковий запис XMPP"}. -{"Unregister","Видалити"}. -{"Unselect All","Скасувати виділення з усіх"}. +{"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 ~p","Оновлення ~p"}. -{"Update script","Сценарій поновлення"}. -{"Update","Обновити"}. -{"Uptime:","Час роботи:"}. +{"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","Сеанс користувача припинено"}. @@ -514,33 +575,45 @@ {"Users Last Activity","Статистика останнього підключення користувачів"}. {"Users","Користувачі"}. {"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"}. -{"View Queue","Переглянути чергу"}. -{"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","Дозволяти підписку"}. {"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","Вам заборонено входити в цю конференцію"}. diff --git a/priv/msgs/vi.msg b/priv/msgs/vi.msg index 600ef6422..186eb20e7 100644 --- a/priv/msgs/vi.msg +++ b/priv/msgs/vi.msg @@ -6,8 +6,6 @@ {" 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ụ"}. {"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ị"}. @@ -37,20 +35,16 @@ {"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 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"}. {"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"}. {"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"}. @@ -70,18 +64,13 @@ {"Family Name","Họ"}. {"February","Tháng Hai"}. {"Friday","Thứ Sáu"}. -{"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"}. -{"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"}. @@ -103,7 +92,6 @@ {"Last month","Tháng trước"}. {"Last year","Năm trước"}. {"leaves the room","rời khỏi phòng này"}. -{"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"}. @@ -113,14 +101,11 @@ {"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"}. {"Monday","Thứ Hai"}. {"Name","Tên"}. -{"Name:","Tên:"}. {"Never","Không bao giờ"}. {"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"}. @@ -140,11 +125,8 @@ {"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 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"}. @@ -153,15 +135,12 @@ {"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:"}. {"Owner privileges required","Yêu cầu đặc quyền của người sở hữu"}. -{"Packet","Gói thông tin"}. {"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"}. @@ -175,15 +154,11 @@ {"RAM copy","Sao chép vào RAM"}. {"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ý:"}. {"Remote copy","Sao chép từ xa"}. {"Remove User","Gỡ Bỏ Người Sử Dụng"}. -{"Remove","Gỡ bỏ"}. {"Replaced by new connection","Được thay thế bởi kết nối mới"}. {"Resources","Nguồn tài nguyên"}. {"Restart Service","Khởi Động Lại Dịch Vụ"}. -{"Restart","Khởi động lại"}. {"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:"}. @@ -193,10 +168,8 @@ {"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 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"}. {"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 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ủ"}. @@ -212,42 +185,25 @@ {"Shut Down Service","Tắt Dịch Vụ"}. {"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"}. -{"Statistics of ~p","Thống kê về ~p"}. -{"Statistics","Số liệu thống kê"}. -{"Stop","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"}. {"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 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"}. -{"Update","Cập Nhật"}. -{"Uptime:","Thời gian tải lên:"}. {"User Management","Quản Lý 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ự"}. diff --git a/priv/msgs/wa.msg b/priv/msgs/wa.msg index f6b618465..23fcf49f5 100644 --- a/priv/msgs/wa.msg +++ b/priv/msgs/wa.msg @@ -9,8 +9,6 @@ {"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 Jabber ID","Radjouter èn ID Jabber"}. -{"Add New","Radjouter"}. {"Add User","Radjouter èn uzeu"}. {"Administration of ","Manaedjaedje di "}. {"Administration","Manaedjaedje"}. @@ -53,21 +51,16 @@ {"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 Tables at ~p","Tåves del båze di dnêyes so ~p"}. {"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 message of the day on all hosts","Disfacer l' messaedje do djoû so tos les lodjoes"}. {"Delete message of the day","Disfacer l' messaedje do djoû"}. -{"Delete Selected","Disfacer les elemints tchoezis"}. {"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"}. {"Dump Backup to Text File at ","Copeye di såvritè viè on fitchî tecse so "}. {"Dump to Text File","Schaper en on fitchî tecse"}. @@ -79,7 +72,6 @@ {"ejabberd SOCKS5 Bytestreams module","Module SOCKS5 Bytestreams po ejabberd"}. {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Manaedjeu waibe ejabberd"}. -{"Elements","Elemints"}. {"Email","Emile"}. {"Enable logging","Mete en alaedje li djournå"}. {"Enable message archiving","Mete en alaedje l' årtchivaedje des messaedjes"}. @@ -90,7 +82,6 @@ {"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"}. {"Enter the text you see","Tapez l' tecse ki vos voeyoz"}. -{"Error","Aroke"}. {"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):"}. @@ -99,22 +90,17 @@ {"Family Name","No d' famile"}. {"February","fevrî"}. {"Friday","vénrdi"}. -{"From","Di"}. {"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"}. {"Grant voice to this person?","Permete li vwès po cisse djin ci?"}. -{"Group","Groupe"}. -{"Groups","Groupes"}. {"has been banned","a stî bani"}. {"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 been kicked","a stî pité evoye"}. -{"Host","Sierveu"}. {"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î"}. @@ -125,14 +111,12 @@ {"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"}. -{"Incoming s2s Connections:","Raloyaedjes s2s en intrêye:"}. {"Incorrect password","Sicret nén corek"}. {"IP addresses","Adresses IP"}. {"is now known as","est asteure kinoxhou come"}. {"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"}. -{"It is not allowed to send private messages","Ci n' est nén permetou d' evoyî des messaedjes privés"}. {"Jabber ID","ID Jabber"}. {"January","djanvî"}. {"joins the room","arive sol såle"}. @@ -143,8 +127,6 @@ {"Last month","Dierin moes"}. {"Last year","Dierinne anêye"}. {"leaves the room","cwite li såle"}. -{"List of rooms","Djivêye des såles"}. -{"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"}. @@ -157,19 +139,15 @@ {"Maximum Number of Occupants","Nombe macsimom di prezints"}. {"May","may"}. {"Membership is required to enter this room","I fåt esse mimbe po poleur intrer dins cisse såle ci"}. -{"Members:","Mimbes:"}. -{"Memory","Memwere"}. {"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"}. {"Moderator","Moderateu"}. -{"Modified modules","Modules di candjîs"}. {"Monday","londi"}. {"Multicast","Multicast"}. {"Multi-User Chat","Berdelaedje a sacwants"}. {"Name","No"}. -{"Name:","Pitit no:"}. {"Never","Måy"}. {"New Password:","Novea scret:"}. {"Nickname Registration at ","Edjîstraedje di metou no amon "}. @@ -192,12 +170,9 @@ {"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"}. {"Old Password:","Vî scret:"}. {"Online Users","Uzeus raloyîs"}. -{"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"}. @@ -210,9 +185,7 @@ {"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:"}. {"Owner privileges required","I fåt des priviledjes di prôpietaire"}. -{"Packet","Paket"}. {"Participant","Pårticipant"}. {"Password Verification","Acertinaedje do scret"}. {"Password Verification:","Acertinaedje do scret:"}. @@ -220,7 +193,6 @@ {"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"}. @@ -237,17 +209,12 @@ {"RAM copy","Copeye e memwere (RAM)"}. {"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:"}. {"Register","Edjîstrer"}. {"Remote copy","Copeye å lon"}. -{"Remove All Offline Messages","Oister tos les messaedjes ki ratindèt"}. {"Remove User","Disfacer l' uzeu"}. -{"Remove","Oister"}. {"Replaced by new connection","Replaecî pa on novea raloyaedje"}. {"Resources","Rissoûces"}. {"Restart Service","Renonder siervice"}. -{"Restart","Renonder"}. {"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:"}. @@ -261,10 +228,8 @@ {"Room title","Tite del såle"}. {"Roster groups allowed to subscribe","Pårtaedjîs groupes di soçons k' on s' î pout abouner"}. {"Roster size","Grandeu del djivêye des soçons"}. -{"RPC Call Error","Aroke di houcaedje RPC"}. {"Running Nodes","Nuks en alaedje"}. {"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 on all hosts","Evoyî l' anonce a tos les uzeus raloyîs so tos les lodjoes"}. @@ -282,18 +247,12 @@ {"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"}. -{"Statistics of ~p","Sitatistikes di ~p"}. -{"Statistics","Sitatistikes"}. -{"Stop","Arester"}. {"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î"}. @@ -307,28 +266,16 @@ {"This room is not anonymous","Cisse såle ci n' est nén anonime"}. {"Thursday","djudi"}. {"Time delay","Tårdjaedje"}. -{"Time","Date"}. {"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"}. -{"To","Po"}. -{"Total rooms","Totå di såles"}. {"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 plan","Plan d' metaedje a djoû"}. -{"Update ~p","Metaedje a djoû di ~p"}. -{"Update script","Sicripe di metaedje a djoû"}. -{"Update","Mete a djoû"}. -{"Uptime:","Tins dispoy l' enondaedje:"}. {"User JID","JID d' l' uzeu"}. {"User Management","Manaedjaedje des uzeus"}. {"Username:","No d' uzeu:"}. @@ -336,7 +283,6 @@ {"Users Last Activity","Dierinne activité des uzeus"}. {"Users","Uzeus"}. {"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"}. diff --git a/priv/msgs/zh.msg b/priv/msgs/zh.msg index 8eb4f244b..4f5688244 100644 --- a/priv/msgs/zh.msg +++ b/priv/msgs/zh.msg @@ -3,26 +3,19 @@ %% 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","进入此房间需要密码"}. +{" (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 of authorize","授权的访问模型"}. -{"Access model of open","开启的访问模型"}. -{"Access model of presence","状态的访问模型"}. -{"Access model of roster","花名册的访问模型"}. -{"Access model of whitelist","白名单的访问模型"}. +{"Access denied by service policy","服务策略拒绝访问"}. {"Access model","访问模型"}. {"Account doesn't exist","账号不存在"}. -{"Action on user","对用户的动作"}. +{"Action on user","对用户的操作"}. {"Add a hat to a user","给用户添加头衔"}. -{"Add Jabber ID","添加Jabber ID"}. -{"Add New","添加新用户"}. {"Add User","添加用户"}. {"Administration of ","管理 "}. {"Administration","管理"}. @@ -30,661 +23,608 @@ {"All activity","所有活动"}. {"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 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 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","任何人都可以将叶子节点与集合关联"}. +{"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","对全部或来源进行了状态订阅的任何人均可订阅并检索内容项"}. -{"Anyone with Voice","任何带声音的人"}. +{"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","四月"}. -{"Attribute 'channel' is required for this request","此请求要求'频道'属性"}. -{"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' 属性"}. +{"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 ~p","~p的备份"}. +{"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","无法移除缺省列表"}. +{"Bytestream already activated","字节流已激活"}. +{"Cannot remove active list","无法移除活动列表"}. +{"Cannot remove default list","无法移除默认列表"}. {"CAPTCHA web page","验证码网页"}. -{"Challenge ID","挑战ID"}. +{"Challenge ID","挑战 ID"}. {"Change Password","更改密码"}. {"Change User Password","更改用户密码"}. -{"Changing password is not allowed","不允许修改密码"}. -{"Changing role/affiliation is 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 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:","已连接资源:"}. -{"Contact Addresses (normally, room owner or owners)","联系人地址 (通常为房间持有人)"}. -{"Contrib Modules","Contrib 模块"}. -{"Country","国家"}. -{"CPU Time:","CPU时间:"}. +{"Contact Addresses (normally, room owner or owners)","联系地址(通常为房间所有者)"}. +{"Country","国家/地区"}. {"Current Discussion Topic","当前讨论话题"}. {"Database failure","数据库失败"}. -{"Database Tables at ~p","位于~p的数据库表"}. -{"Database Tables Configuration at ","数据库表格配置位于 "}. +{"Database Tables Configuration at ","数据库表配置在 "}. {"Database","数据库"}. {"December","十二月"}. -{"Default users as participants","用户默认被视为参与人"}. -{"Delete content","删除内容"}. +{"Default users as participants","默认用户为参与者"}. {"Delete message of the day on all hosts","删除所有主机上的每日消息"}. {"Delete message of the day","删除每日消息"}. -{"Delete Selected","删除已选内容"}. -{"Delete table","删除表格"}. {"Delete User","删除用户"}. {"Deliver event notifications","传递事件通知"}. -{"Deliver payloads with event notifications","用事件通告传输有效负载"}. -{"Description:","描述:"}. -{"Disc only copy","仅磁盘复制"}. -{"'Displayed groups' not added (they do not exist!): ","'显示的群组' 未被添加 (它们不存在!): "}. -{"Displayed:","已显示:"}. -{"Don't tell your password to anybody, not even the administrators of the XMPP server.","不要将密码告诉任何人, 就算是XMPP服务器的管理员也不可以."}. +{"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"}. +{"Duplicated groups are not allowed by RFC6121","按照 RFC6121 的规则,不允许重复的组"}. +{"Dynamically specify a replyto of the item publisher","动态指定项目发布者的 replyto"}. {"Edit Properties","编辑属性"}. -{"Either approve or decline the voice request.","接受或拒绝声音请求."}. +{"Either approve or decline the voice request.","批准或拒绝发言权请求。"}. {"ejabberd HTTP Upload service","ejabberd HTTP 上传服务"}. {"ejabberd MUC module","ejabberd MUC 模块"}. -{"ejabberd Multicast service","ejabberd多重映射服务"}. -{"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网页管理"}. +{"ejabberd vCard module","ejabberd vCard 模块"}. +{"ejabberd Web Admin","ejabberd Web 管理"}. {"ejabberd","ejabberd"}. -{"Elements","元素"}. -{"Email Address","电邮地址"}. +{"Email Address","电子邮件地址"}. {"Email","电子邮件"}. {"Enable hats","启用头衔"}. -{"Enable logging","启用服务器端聊天记录"}. +{"Enable logging","启用日志记录"}. {"Enable message archiving","启用消息归档"}. -{"Enabling push without 'node' attribute is not supported","不支持未使用'node'属性就开启推送"}. +{"Enabling push without 'node' attribute is not supported","不支持没有“node”属性就启用推送"}. {"End User Session","结束用户会话"}. -{"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 dir","请输入 jabberd14 spool 目录的路径"}. {"Enter path to jabberd14 spool file","请输入 jabberd14 spool 文件的路径"}. {"Enter path to text file","请输入文本文件的路径"}. -{"Enter the text you see","请输入您所看到的文本"}. +{"Enter the text you see","请输入您看到的文本"}. {"Erlang XMPP Server","Erlang XMPP 服务器"}. -{"Error","错误"}. -{"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","外部组件失败"}. +{"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'处理失败"}. +{"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","常见问题入口"}. +{"FAQ Entry","常见问题条目"}. {"February","二月"}. {"File larger than ~w bytes","文件大于 ~w 字节"}. -{"Fill in the form to search for any matching XMPP User","填充表单来搜索任何匹配的XMPP用户"}. -{"Friday","星期五"}. +{"Fill in the form to search for any matching XMPP User","填写表单以搜索任何匹配的 XMPP 用户"}. +{"Friday","周五"}. {"From ~ts","来自 ~ts"}. -{"From","从"}. -{"Full List of Room Admins","房间管理员完整列表"}. -{"Full List of Room Owners","房间持有人完整列表"}. +{"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 Pending","获取待处理"}. {"Get User Last Login Time","获取用户上次登录时间"}. -{"Get User Password","获取用户密码"}. -{"Get User Statistics","获取用户统计"}. -{"Given Name","中间名"}. -{"Grant voice to this person?","为此人授权声音?"}. -{"Groups that will be displayed to the members","将显示给会员的群组"}. -{"Groups","组"}. -{"Group","组"}. -{"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","因该房间改为只对会员开放而被踢出"}. +{"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","头衔 URI"}. {"Hats limit exceeded","已超过头衔限制"}. -{"Host unknown","主人未知"}. -{"Host","主机"}. -{"HTTP File Upload","HTTP文件上传"}. -{"Idle connection","空闲的连接"}. -{"If you don't see the CAPTCHA image here, visit the web page.","如果您在这里没有看到验证码图片, 请访问网页."}. +{"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 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 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 domain part of 'from' attribute","不恰当的'from'属性域名部分"}. -{"Improper message type","不恰当的消息类型"}. -{"Incoming s2s Connections:","入站 s2s 连接:"}. +{"Improper domain part of 'from' attribute","“from”属性域名部分不正确"}. +{"Improper message type","消息类型不正确"}. {"Incorrect CAPTCHA submit","提交的验证码不正确"}. -{"Incorrect data form","数据形式不正确"}. +{"Incorrect data form","数据表单不正确"}. {"Incorrect password","密码不正确"}. -{"Incorrect value of 'action' attribute","'action' 属性的值不正确"}. -{"Incorrect value of 'action' in data form","数据表单中 'action' 的值不正确"}. -{"Incorrect value of 'path' in data form","数据表单中 'path' 的值不正确"}. +{"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' 值"}. +{"Invalid 'from' attribute in forwarded message","转发消息中的“from”属性无效"}. +{"Invalid node name","节点名称无效"}. +{"Invalid 'previd' value","“previd”值无效"}. {"Invitations are not allowed in this conference","此会议不允许邀请"}. -{"IP addresses","IP地址"}. -{"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","不允许向会议发送私聊消息"}. -{"It is not allowed to send private messages","不可以发送私聊消息"}. +{"IP addresses","IP 地址"}. +{"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"}. {"January","一月"}. -{"JID normalization denied by service policy","JID规范化被服务策略拒绝"}. -{"JID normalization failed","JID规范化失败"}. +{"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","加入房间"}. {"July","七月"}. {"June","六月"}. {"Just created","刚刚创建"}. -{"Label:","标签:"}. {"Last Activity","上次活动"}. {"Last login","上次登录"}. -{"Last message","最近消息"}. +{"Last message","最后一条消息"}. {"Last month","上个月"}. -{"Last year","上一年"}. -{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","文本的SHA-256哈希的最低有效位应等于十六进制标签"}. +{"Last year","去年"}. +{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","文本的 SHA-256 散列的最低有效位应等于十六进制标签"}. {"leaves the room","离开房间"}. -{"List of rooms","房间列表"}. -{"List of users with hats","有头衔用户的列表"}. -{"List users with hats","有头衔用户列表"}. -{"Logging","正在记录"}. -{"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","使房间可被公开搜索"}. -{"Malformed username","用户名无效"}. -{"MAM preference modification denied by service policy","MAM偏好被服务策略拒绝"}. +{"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","三月"}. -{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","要保留的最大项目数 #,`max`表示除了服务器强加的最大值之外没有特定限制"}. -{"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","允许的与会人最大数"}. +{"Maximum Number of History Messages Returned by Room","房间返回的最大历史消息数"}. +{"Maximum number of items to persist","要保留的最大项目数"}. +{"Maximum Number of Occupants","最大使用者数"}. {"May","五月"}. -{"Members not added (inexistent vhost!): ","成员未添加 (不存在的vhost!): "}. -{"Membership is required to enter this room","进入此房间需要会员身份"}. -{"Members:","会员:"}. -{"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也没有自动恢复密码的方式."}. -{"Memory","内存"}. -{"Mere Availability in XMPP (No Show Value)","仅XMPP中的可用性 (不显示值)"}. -{"Message body","消息主体"}. -{"Message not found in forwarded payload","转发的有效载荷中找不到消息"}. -{"Messages from strangers are rejected","陌生人的消息会被拒绝"}. +{"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","仅限主持人"}. +{"Moderators Only","仅主持人"}. {"Moderator","主持人"}. -{"Modified modules","被修改模块"}. -{"Module failed to handle the query","模块未能处理查询"}. -{"Monday","星期一"}. -{"Multicast","多重映射"}. +{"Module failed to handle the query","模块无法处理查询"}. +{"Monday","周一"}. +{"Multicast","多播"}. {"Multiple elements are not allowed by RFC6121","按照 RFC6121,多个 元素是不允许的"}. {"Multi-User Chat","多用户聊天"}. -{"Name in the rosters where this group will be displayed","花名册中将显示的该分组的名称"}. -{"Name","姓名"}. -{"Name:","姓名:"}. +{"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:","新密码:"}. +{"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 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 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 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' 标签"}. +{"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 已失效"}. +{"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 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","当节点被删除时通知订阅人"}. +{"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 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","成员可以修改主题"}. +{"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:","旧密码:"}. +{"Old Password:","旧密码:"}. {"Online Users","在线用户"}. -{"Online Users:","在线用户:"}. {"Online","在线"}. -{"Only admins can see this","仅管理员可以看见此内容"}. -{"Only collection node owners may associate leaf nodes with the collection","只有集合节点所有者可以将叶子节点与集合关联"}. -{"Only deliver notifications to available users","仅将通知发送给可发送的用户"}. +{"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 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","仅白名单用户可以订阅和检索内容项"}. +{"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 连接"}. -{"Outgoing s2s Connections:","出站 s2s 连接:"}. -{"Owner privileges required","需要持有人权限"}. -{"Packet relay is denied by service policy","包中继被服务策略拒绝"}. -{"Packet","数据包"}. +{"Outgoing s2s Connections","传出 s2s 连接"}. +{"Owner privileges required","需要所有者权限"}. +{"Packet relay is denied by service policy","服务策略拒绝数据包中继"}. {"Participant ID","参与者 ID"}. -{"Participant","参与人"}. -{"Password Verification:","密码确认:"}. -{"Password Verification","确认密码"}. +{"Participant","参与者"}. +{"Password Verification","密码验证"}. +{"Password Verification:","密码验证:"}. {"Password","密码"}. -{"Password:","密码:"}. -{"Path to Dir","目录的路径"}. +{"Password:","密码:"}. +{"Path to Dir","目录路径"}. {"Path to File","文件路径"}. -{"Payload type","有效载荷类型"}. -{"Pending","挂起"}. -{"Period: ","持续时间: "}. -{"Persist items to storage","持久化内容条目"}. -{"Persistent","永久"}. +{"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"}. -{"Possessing 'ask' attribute is not allowed by RFC6121","按照 RFC6121, 不允许处理 'ask' 属性"}. -{"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, ","保密, "}. +{"Possessing 'ask' attribute is not allowed by RFC6121","按照 RFC6121, 不允许有“ask”属性"}. +{"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","没有找到推送记录"}. -{"Queries to the conference members are not allowed in this room","本房间不可以查询会议成员信息"}. +{"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 copy","内存(RAM)复制"}. -{"Really delete message of the day?","确实要删除每日消息吗?"}. +{"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 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帐户"}. -{"Registered Users","注册用户"}. -{"Registered Users:","注册用户:"}. +{"Recipient is not in the conference room","接收者不在会议室"}. +{"Register an XMPP account","注册 XMPP 账号"}. {"Register","注册"}. -{"Remote copy","远程复制"}. +{"Remote copy","远程副本"}. {"Remove a hat from a user","移除用户头衔"}. -{"Remove All Offline Messages","移除所有离线消息"}. -{"Remove User","删除用户"}. -{"Remove","移除"}. -{"Replaced by new connection","被新的连接替换"}. +{"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 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","可以发送私聊消息的角色"}. +{"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 ~ts","~ts的花名册"}. -{"Roster size","花名册大小"}. -{"Roster:","花名册:"}. -{"RPC Call Error","RPC 调用错误"}. -{"Running Nodes","运行中的节点"}. -{"~s invites you to the room ~s","~s邀请你到房间~s"}. -{"Saturday","星期六"}. -{"Script check","脚本检查"}. +{"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 Results for ","搜索结果 "}. {"Search the text","搜索文本"}. {"Search until the date","搜索截至日期"}. {"Search users in ","在以下位置搜索用户 "}. -{"Select All","全选"}. -{"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","九月"}. -{"Server:","服务器:"}. +{"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 Ordinary Table","显示普通列表"}. +{"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","指定访问范例"}. +{"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","节ID"}. -{"Statically specify a replyto of the node owner(s)","静态指定节点所有者的回复"}. -{"Statistics of ~p","~p的统计"}. -{"Statistics","统计"}. -{"Stopped Nodes","已经停止的节点"}. -{"Stop","停止"}. -{"Storage Type","存储类型"}. -{"Store binary backup:","存储为二进制备份:"}. -{"Store plain text backup:","存储为普通文本备份:"}. -{"Stream management is already enabled","流管理已启用"}. -{"Stream management is not enabled","流管理未启用"}. -{"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","订阅人地址"}. -{"Subscribers may publish","订阅人可以发布"}. -{"Subscription requests must be approved and only subscribers may retrieve items","订阅请求必须得到批准, 只有订阅人才能检索项目"}. +{"Subscriber Address","订阅者地址"}. +{"Subscribers may publish","订阅者可以发布"}. +{"Subscription requests must be approved and only subscribers may retrieve items","订阅请求必须得到批准,只有订阅者才能检索项目"}. {"Subscriptions are not allowed","不允许订阅"}. -{"Subscription","订阅"}. -{"Sunday","星期天"}. +{"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","帐户未注册"}. +{"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 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 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 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 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 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 stanza MUST contain only one element, one element, or one element","节必须仅包含一个 元素、一个 元素或一个 元素"}. {"The subscription identifier associated with the subscription request","与订阅请求关联的订阅标识符"}. -{"The type of node data, usually specified by the namespace of the payload (if any)","节点数据的类型, 如果有, 通常由有效负载的名称空间指定"}. -{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","XSL转换的URL,可以将其应用于有效负载以生成适当的消息正文元素。"}. -{"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","XSL转换的URL, 可以将其应用于有效负载格式, 以生成有效的数据表单结果, 客户端可以使用通用数据表单呈现引擎来显示该结果"}. -{"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","星期四"}. +{"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","时间延迟"}. {"Timed out waiting for stream resumption","等待流恢复超时"}. -{"Time","时间"}. {"To register, visit ~s","要注册,请访问 ~s"}. -{"To ~ts","发送到~ts"}. -{"Token TTL","TTL令牌"}. -{"Too many active bytestreams","活跃的字节流太多"}. +{"To ~ts","到 ~ts"}. +{"Token TTL","令牌 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","来自IP地址(~p)的(~s)失败认证太多。将在UTC时间 ~s 解除对该地址的封锁"}. +{"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","该会议的用户太多"}. -{"Total rooms","所有房间"}. -{"To","到"}. -{"Traffic rate limit is exceeded","已经超过传输率限制"}. -{"Transactions Aborted:","取消的事务:"}. -{"Transactions Committed:","提交的事务:"}. -{"Transactions Logged:","记入日志的事务:"}. -{"Transactions Restarted:","重启的事务:"}. -{"~ts's Offline Messages Queue","~ts的离线消息队列"}. -{"Tuesday","星期二"}. +{"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","无法生成验证码"}. -{"Unable to register route on existing local domain","在已存在的本地域上无法注册路由"}. -{"Unauthorized","未认证的"}. -{"Unexpected action","意外行为"}. -{"Unexpected error condition: ~p","意外错误条件: ~p"}. +{"Unable to register route on existing local domain","无法在现有本地域上注册路由"}. +{"Unauthorized","未经授权"}. +{"Unexpected action","意外操作"}. +{"Unexpected error condition: ~p","意外错误条件:~p"}. {"Uninstall","卸载"}. -{"Unregister an XMPP account","注销XMPP帐户"}. -{"Unregister","取消注册"}. -{"Unselect All","取消全选"}. +{"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 ~p","更新~p"}. -{"Update script","更新脚本"}. -{"Update specs to get modules source, then install desired ones.","更新参数获取模块源,然后安装所需的模块。"}. -{"Update Specs","更新参数"}. -{"Update","更新"}. +{"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","升级"}. -{"Uptime:","正常运行时间:"}. -{"URL for Archived Discussion Logs","已归档对话日志的URL"}. +{"URL for Archived Discussion Logs","已归档的讨论日志 URL"}. {"User already exists","用户已存在"}. -{"User (jid)","用户 (jid)"}. -{"User JID","用户JID"}. +{"User JID","用户 JID"}. +{"User (jid)","用户(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 not found","未找到用户会话"}. {"User session terminated","用户会话已终止"}. -{"User ~ts","用户~ts"}. -{"Username:","用户名:"}. -{"Users are not allowed to register accounts so quickly","不允许用户太频繁地注册帐户"}. +{"User ~ts","用户 ~ts"}. +{"Username:","用户名:"}. +{"Users are not allowed to register accounts so quickly","不允许用户太频繁地注册账号"}. {"Users Last Activity","用户上次活动"}. {"Users","用户"}. {"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用户搜索"}. +{"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 频道"}. -{"View Queue","查看队列"}. -{"View Roster","查看花名册"}. {"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","实体是否要接收通知的摘要(汇总)或单独接收所有通知"}. +{"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 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 may associate leaf nodes with a collection","谁可以将叶子节点与集合关联"}. -{"Wrong parameters in the web formulary","网络配方中的参数错误"}. +{"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","因系统关机, 你正在被从房间移除"}. +{"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.","你的活跃私聊列表拒绝了在此房间进行路由分发."}. +{"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","您不可以创建节点"}. +{"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/rebar b/rebar index b6f011846..3f6203bcf 100755 Binary files a/rebar and b/rebar differ diff --git a/rebar.config b/rebar.config index 14c8c81e5..3d85402e3 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,63 +18,70 @@ %%% %%%---------------------------------------------------------------------- -{deps, [{base64url, ".*", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}, - {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.30"}}}, - {eimp, ".*", {git, "https://github.com/processone/eimp", {tag, "1.0.22"}}}, - {if_var_true, tools, - {ejabberd_po, ".*", {git, "https://github.com/processone/ejabberd-po", {branch, "main"}}}}, - {if_var_true, elixir, - {elixir, ".*", {git, "https://github.com/elixir-lang/elixir", {tag, "v1.4.4"}}}}, +%%% +%%% 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, ".*", {git, "https://github.com/processone/epam", {tag, "1.0.12"}}}}, + {epam, "~> 1.0.14", {git, "https://github.com/processone/epam", {tag, "1.0.14"}}}}, {if_var_true, redis, - {eredis, ".*", {git, "https://github.com/wooga/eredis", {tag, "v1.2.0"}}}}, - {if_var_true, sip, - {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.48"}}}}, - {if_var_true, zlib, - {ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.12"}}}}, - {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.16"}}}, - {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.49"}}}, - {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.34"}}}, - {idna, ".*", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}}, - {if_version_above, "19", - {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}}, - {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.1.0"}}} % for R19 and below - }, - {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}, - {if_version_below, "22", - {lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}} - }, - {if_var_true, lua, {if_not_rebar3, - {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}} - }}, - {if_var_true, lua, + {eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}} + }}, + {if_var_true, redis, {if_rebar3, - {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0.0"}}} - }}, - {mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.15"}}}, - {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.20"}}}, + {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, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.20"}}}}, - {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.11"}}}, + {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, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.19"}}}}, - {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.25"}}}, - {pkix, ".*", {git, "https://github.com/processone/pkix", {tag, "1.0.9"}}}, - {if_not_rebar3, %% Needed because modules are not fully migrated to new structure and mix - {if_var_true, elixir, - {rebar_elixir_plugin, ".*", {git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}}, + {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, ".*", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.13"}}}}, - {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.29"}}}, + {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, ".*", {git, "https://github.com/processone/stun", {tag, "1.2.6"}}}}, - {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.6.0"}}}, - {yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.14"}}} + {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, [elixir]}. +{gitonly_deps, [ejabberd_po]}. {if_var_true, latest_deps, {floating_deps, [cache_tab, @@ -98,13 +105,27 @@ 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, 'DEPRECATED_GET_STACKTRACE'}}, {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'}}, @@ -114,7 +135,11 @@ {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'}}, @@ -124,31 +149,55 @@ {if_var_true, stun, {d, 'STUN'}}, {src_dirs, [src, {if_rebar3, sql}, - {if_var_true, tools, tools}, - {if_var_true, elixir, include}]}]}. + {if_var_true, tools, tools}]}]}. -{if_rebar3, {plugins, [rebar3_hex, {provider_asn1, "0.2.0"}]}}. -{if_rebar3, {project_plugins, [configure_deps]}}. +{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_var_true, elixir, rebar_elixir_compiler}, - {if_var_true, elixir, rebar_exunit} + deps_erl_opts, override_deps_versions2, override_opts, configure_deps ]}}. {if_rebar3, {if_var_true, elixir, - {project_app_dirs, [".", "elixir/lib"]}}}. -{if_not_rebar3, {if_var_true, elixir, - {lib_dirs, ["deps/elixir/lib"]}}}. -{if_var_true, elixir, - {src_dirs, ["include"]}}. + {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}. -{xref_checks, [deprecated_function_calls]}. +{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\":_/_)", @@ -163,26 +212,55 @@ {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"}. -{recursive_cmds, ['configure-deps']}. -{overrides, [ - {del, [{erl_opts, [warnings_as_errors]}]}]}. - -{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", []}]}. +%%% +%%% OTP Release +%%% {relx, [{release, {ejabberd, {cmd, "grep {vsn, vars.config | sed 's|{vsn, \"||;s|\"}.||' | tr -d '\012'"}}, [ejabberd]}, @@ -194,6 +272,12 @@ {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"}]} @@ -207,7 +291,8 @@ {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"}]}, + {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}, @@ -218,10 +303,12 @@ {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"}]} ]}. diff --git a/rebar.config.script b/rebar.config.script index 33aeb8130..e476df448 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -151,6 +151,30 @@ ProcessVars = fun F([], 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) -> @@ -195,20 +219,35 @@ AppendList2 = fun(Append) -> end end, -Rebar3DepsFilter = +% Convert our rich deps syntax to rebar2 format: +% https://github.com/rebar/rebar/wiki/Dependency-management +Rebar2DepsFilter = fun(DepsList, GitOnlyDeps) -> - lists:map(fun({DepName, _, {git, _, {tag, Version}}} = Dep) -> - case lists:member(DepName, GitOnlyDeps) of - true -> - Dep; - _ -> - {DepName, Version} - end; - (Dep) -> - Dep + lists:map(fun({DepName, _HexVersion, Source}) -> + {DepName, ".*", Source} end, DepsList) end, +% 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, + + DepAlts = fun("esip") -> ["esip", "p1_sip"]; ("xmpp") -> ["xmpp", "p1_xmpp"]; ("fast_xml") -> ["fast_xml", "p1_xml"]; @@ -232,7 +271,8 @@ LibDir = fun(Name, Suffix) -> GlobalDepsFilter = fun(Deps) -> DepNames = lists:map(fun({DepName, _, _}) -> DepName; - ({DepName, _}) -> DepName + ({DepName, _}) -> DepName; + (DepName) -> DepName end, Deps), lists:filtermap(fun(Dep) -> case LibDir(atom_to_list(Dep), "") of @@ -352,14 +392,11 @@ VarsApps = case file:consult(filename:join([filename:dirname(SCRIPT),"vars.confi ProcessRelx = fun(Relx, Deps) -> {value, {release, NameVersion, DefaultApps}, RelxTail} = lists:keytake(release, 1, Relx), - ProfileApps = case os:getenv("REBAR_PROFILE") of - "dev" -> [observer, runtime_tools, wx, debugger]; - _ -> [] - end, DepApps = lists:map(fun({DepName, _, _}) -> DepName; - ({DepName, _}) -> DepName + ({DepName, _}) -> DepName; + (DepName) -> DepName end, Deps), - [{release, NameVersion, DefaultApps ++ VarsApps ++ ProfileApps ++ DepApps} | RelxTail] + [{release, NameVersion, DefaultApps ++ VarsApps ++ DepApps} | RelxTail] end, GithubConfig = case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of @@ -380,6 +417,8 @@ GithubConfig = case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of end, Rules = [ + {[plugins], IsRebar3, + AppendList([{pc, "~> 1.15.0"}]), []}, {[provider_hooks], IsRebar3, AppendList([{pre, [ {compile, {asn, compile}}, @@ -401,6 +440,8 @@ Rules = [ ProcessRelx, [], []}, {[deps], [floating_deps], true, ProcessFloatingDeps, [], []}, + {[deps], [gitonly_deps], (not IsRebar3), + Rebar2DepsFilter, [], []}, {[deps], [gitonly_deps], IsRebar3, Rebar3DepsFilter, [], []}, {[deps], SystemDeps /= false, 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 index bf1935ce6..deaea1f90 100755 Binary files a/rebar3 and b/rebar3 differ diff --git a/rel/relive.config b/rel/relive.config index 7e3901fd4..49da88b79 100644 --- a/rel/relive.config +++ b/rel/relive.config @@ -1,3 +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/reltool.config.script b/rel/reltool.config.script index ac3f5c7cb..4f142efe2 100644 --- a/rel/reltool.config.script +++ b/rel/reltool.config.script @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013-2022, Evgeniy Khramtsov +%%% @copyright (C) 2013-2025, Evgeniy Khramtsov %%% @doc %%% %%% @end @@ -38,11 +38,12 @@ Vars = case file:consult(filename:join([TopDir, "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]; (_) -> @@ -53,6 +54,8 @@ OTPApps = RequiredOTPApps ++ ConfiguredOTPApps, DepApps = lists:usort(lists:flatten(GetDeps(filename:join(TopDir, "rebar.config"), GetDeps))), +SysVer = erlang:system_info(otp_release), + Sys = [{lib_dirs, []}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, @@ -70,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}, diff --git a/rel/setup-dev.sh b/rel/setup-dev.sh index 79171ffe0..af3875cf0 100755 --- a/rel/setup-dev.sh +++ b/rel/setup-dev.sh @@ -1,28 +1,36 @@ -echo -n "===> Preparing dev configuration files: " +printf "===> Preparing dev configuration files: " -PWD_DIR=`pwd` +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/ -BIN_DIR_TEMP=$REL_DIR_TEMP/bin/ -cd $CON_DIR_TEMP +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" ] \ - && echo -n "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" ] \ - && echo -n "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/ejabberd console" 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 index 4e726be88..a4c88f6c5 100755 --- a/rel/setup-relive.sh +++ b/rel/setup-relive.sh @@ -1,4 +1,4 @@ -PWD_DIR=`pwd` +PWD_DIR=$(pwd) REL_DIR=$PWD_DIR/_build/relive/ CON_DIR=$REL_DIR/conf/ @@ -15,16 +15,17 @@ 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 +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" ] \ - && echo -n "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" ] \ - && echo -n "ejabberdctl.cfg " \ - && mv ejabberdctl.cfg.example ejabberdctl.cfg + && printf "ejabberdctl.cfg " \ + && mv ejabberdctl.cfg.example ejabberdctl.cfg \ + || printf diff --git a/sql/lite.new.sql b/sql/lite.new.sql index 9eb34c974..42f289fb3 100644 --- a/sql/lite.new.sql +++ b/sql/lite.new.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,14 @@ 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) + PRIMARY KEY (server_host, username, type) ); @@ -52,7 +53,6 @@ CREATE TABLE rosterusers ( ); CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers (server_host, username, jid); -CREATE INDEX i_rosteru_sh_username ON rosterusers (server_host, username); CREATE INDEX i_rosteru_sh_jid ON rosterusers (server_host, jid); @@ -84,7 +84,6 @@ CREATE TABLE sr_user ( ); CREATE UNIQUE INDEX i_sr_user_sh_jid_grp ON sr_user (server_host, jid, grp); -CREATE INDEX i_sr_user_sh_jid ON sr_user (server_host, jid); CREATE INDEX i_sr_user_sh_grp ON sr_user (server_host, grp); CREATE TABLE spool ( @@ -108,6 +107,7 @@ CREATE TABLE archive ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind text, nick text, + origin_id text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -115,6 +115,11 @@ CREATE INDEX i_archive_sh_username_timestamp ON archive (server_host, username, 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, @@ -190,7 +195,6 @@ CREATE TABLE privacy_list ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX i_privacy_list_sh_username ON privacy_list (server_host, username); CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list (server_host, username, name); CREATE TABLE privacy_list_data ( @@ -215,9 +219,6 @@ CREATE TABLE private_storage ( PRIMARY KEY (server_host, username, namespace) ); -CREATE INDEX i_private_storage_sh_username ON private_storage (server_host, username); - - CREATE TABLE roster_version ( username text NOT NULL, server_host text NOT NULL, @@ -319,7 +320,6 @@ CREATE TABLE muc_online_users ( ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users (username, server, resource, name, host); -CREATE INDEX i_muc_online_users_us ON muc_online_users (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, @@ -389,7 +389,6 @@ CREATE TABLE route ( ); CREATE UNIQUE INDEX i_route ON route(domain, server_host, node, pid); -CREATE INDEX i_route_domain ON route(domain); CREATE TABLE bosh ( sid text NOT NULL, @@ -422,6 +421,7 @@ CREATE TABLE push_session ( ); 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, @@ -449,7 +449,6 @@ CREATE TABLE mix_participant ( ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); -CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, @@ -461,9 +460,7 @@ CREATE TABLE mix_subscription ( ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); -CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); -CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, @@ -475,7 +472,6 @@ CREATE TABLE mix_pam ( ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); -CREATE INDEX i_mix_pam_us ON mix_pam (username, server_host); CREATE TABLE mqtt_pub ( username text NOT NULL, diff --git a/sql/lite.sql b/sql/lite.sql index 0580fcbaa..b31e02b79 100644 --- a/sql/lite.sql +++ b/sql/lite.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,12 +17,14 @@ -- CREATE TABLE users ( - username text PRIMARY KEY, + 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 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + primary key (username, type) ); @@ -47,7 +49,6 @@ CREATE TABLE rosterusers ( ); CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers (username, jid); -CREATE INDEX i_rosteru_username ON rosterusers (username); CREATE INDEX i_rosteru_jid ON rosterusers (jid); @@ -74,7 +75,6 @@ CREATE TABLE sr_user ( ); CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user (jid, grp); -CREATE INDEX i_sr_user_jid ON sr_user (jid); CREATE INDEX i_sr_user_grp ON sr_user (grp); CREATE TABLE spool ( @@ -96,6 +96,7 @@ CREATE TABLE archive ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind text, nick text, + origin_id text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -103,6 +104,11 @@ 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, @@ -169,7 +175,6 @@ CREATE TABLE privacy_list ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX i_privacy_list_username ON privacy_list (username); CREATE UNIQUE INDEX i_privacy_list_username_name ON privacy_list (username, name); CREATE TABLE privacy_list_data ( @@ -192,7 +197,6 @@ CREATE TABLE private_storage ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX i_private_storage_username ON private_storage (username); CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage (username, namespace); @@ -291,7 +295,6 @@ CREATE TABLE muc_online_users ( ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users (username, server, resource, name, host); -CREATE INDEX i_muc_online_users_us ON muc_online_users (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, @@ -358,7 +361,6 @@ CREATE TABLE route ( ); CREATE UNIQUE INDEX i_route ON route(domain, server_host, node, pid); -CREATE INDEX i_route_domain ON route(domain); CREATE TABLE bosh ( sid text NOT NULL, @@ -417,7 +419,6 @@ CREATE TABLE mix_participant ( ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); -CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, @@ -429,9 +430,7 @@ CREATE TABLE mix_subscription ( ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); -CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); -CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, @@ -442,7 +441,6 @@ CREATE TABLE mix_pam ( ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); -CREATE INDEX i_mix_pam_us ON mix_pam (username); CREATE TABLE mqtt_pub ( username text NOT NULL, 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 index e05a7b51d..ab5596d48 100644 --- a/sql/mssql.sql +++ b/sql/mssql.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,6 +31,7 @@ CREATE TABLE [dbo].[archive] ( [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 ( @@ -50,6 +51,9 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW 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, @@ -118,10 +122,10 @@ 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] [text] NOT NULL, - [pid] [text] NOT NULL + [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) @@ -133,13 +137,11 @@ CREATE TABLE [dbo].[muc_online_users] ( [resource] [varchar] (250) NOT NULL, [name] [varchar] (250) NOT NULL, [host] [varchar] (250) NOT NULL, - node text 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 UNIQUE CLUSTERED INDEX [muc_online_users_us] ON [muc_online_users] (username, server) -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, @@ -174,9 +176,6 @@ CREATE TABLE [dbo].[privacy_list] ( )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ); -CREATE INDEX [privacy_list_username] ON [privacy_list] (username) -WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = 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); @@ -203,9 +202,6 @@ CREATE TABLE [dbo].[private_storage] ( [created_at] [datetime] NOT NULL DEFAULT GETDATE() ) TEXTIMAGE_ON [PRIMARY]; -CREATE INDEX [private_storage_username] ON [private_storage] (username) -WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); - 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); @@ -226,9 +222,9 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW CREATE TABLE [dbo].[pubsub_node_option] ( [nodeid] [bigint] NULL, - [name] [text] NOT NULL, - [val] [text] NOT NULL -) TEXTIMAGE_ON [PRIMARY]; + [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); @@ -272,13 +268,13 @@ CREATE TABLE [dbo].[pubsub_node] ( [host] [varchar] (255) NOT NULL, [node] [varchar] (255) NOT NULL, [parent] [varchar] (255) NOT NULL DEFAULT '', - [plugin] [text] NOT NULL, + [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) -) TEXTIMAGE_ON [PRIMARY]; +); CREATE INDEX [pubsub_node_parent] ON [pubsub_node] (parent) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); @@ -320,9 +316,6 @@ CREATE TABLE [dbo].[rosterusers] ( 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_username] ON [rosterusers] ([username]) -WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = 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); @@ -366,13 +359,12 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW CREATE TABLE [dbo].[sr_group] ( [name] [varchar] (250) NOT NULL, [opts] [text] NOT NULL, - [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [sr_group_PRIMARY] PRIMARY KEY CLUSTERED -( - [name] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) + [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, @@ -382,14 +374,12 @@ CREATE TABLE [dbo].[sr_user] ( 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_jid] ON [sr_user] ([jid]) -WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = 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 '', @@ -397,7 +387,8 @@ CREATE TABLE [dbo].[users] ( [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED ( - [username] 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]; @@ -521,9 +512,6 @@ CREATE TABLE [dbo].[route] ( 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 INDEX [route_domain] ON [route] (domain) -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, @@ -545,25 +533,88 @@ CREATE TABLE [dbo].[push_session] ( 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 UNIQUE INDEX [i_push_ut] ON [push_session] (username, timestamp) +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](191) NOT NULL, - [server_host] [varchar](191) NOT NULL, - [resource] [varchar](191) NOT NULL, - [topic] [varchar](191) 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, - CONSTRAINT [i_mqtt_topic_server] PRIMARY KEY CLUSTERED -( - [topic] ASC, - [server_host] ASC -)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] + [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/mysql.new.sql b/sql/mysql.new.sql index dc514becf..cf818ad3d 100644 --- a/sql/mysql.new.sql +++ b/sql/mysql.new.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,14 @@ 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) + 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: @@ -56,7 +57,6 @@ CREATE TABLE rosterusers ( ) 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_username ON rosterusers(server_host(191), username); CREATE INDEX i_rosteru_sh_jid ON rosterusers(server_host(191), jid); CREATE TABLE rostergroups ( @@ -86,8 +86,7 @@ CREATE TABLE sr_user ( PRIMARY KEY (server_host(191), jid, grp) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE UNIQUE INDEX i_sr_user_sh_jid_group ON sr_user(server_host(191), jid, grp); -CREATE INDEX i_sr_user_sh_jid ON sr_user(server_host(191), jid); +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 ( @@ -112,6 +111,7 @@ CREATE TABLE archive ( 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; @@ -120,6 +120,12 @@ CREATE INDEX i_archive_sh_username_timestamp USING BTREE ON archive(server_host( 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, @@ -195,7 +201,6 @@ CREATE TABLE privacy_list ( created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE INDEX i_privacy_list_sh_username USING BTREE ON privacy_list(server_host(191), username); 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 ( @@ -218,11 +223,10 @@ CREATE TABLE private_storage ( server_host varchar(191) NOT NULL, namespace varchar(191) NOT NULL, data text NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (server_host(191), username, namespace) + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE INDEX i_private_storage_sh_username USING BTREE ON private_storage(server_host(191), username); +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 ( @@ -335,7 +339,6 @@ CREATE TABLE muc_online_users ( ) 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 INDEX i_muc_online_users_us USING BTREE ON muc_online_users(username(75), server(75)); CREATE TABLE muc_room_subscribers ( room varchar(191) NOT NULL, @@ -405,7 +408,6 @@ CREATE TABLE route ( ) 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 INDEX i_route_domain ON route(domain(75)); CREATE TABLE bosh ( sid text NOT NULL, @@ -438,6 +440,7 @@ CREATE TABLE push_session ( ) 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, @@ -465,7 +468,6 @@ CREATE TABLE mix_participant ( ) 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 INDEX i_mix_participant_chan_serv ON mix_participant (channel(191), service(191)); CREATE TABLE mix_subscription ( channel text NOT NULL, @@ -477,9 +479,7 @@ CREATE TABLE mix_subscription ( ) 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_ud ON mix_subscription (channel(191), service(191), username(191), domain(191)); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); -CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel(191), service(191)); CREATE TABLE mix_pam ( username text NOT NULL, @@ -491,7 +491,6 @@ CREATE TABLE mix_pam ( ) 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 INDEX i_mix_pam_us ON mix_pam (username(191), server_host(191)); CREATE TABLE mqtt_pub ( username varchar(191) NOT NULL, diff --git a/sql/mysql.old-to-new.sql b/sql/mysql.old-to-new.sql index 9614d55a8..a58a90a46 100644 --- a/sql/mysql.old-to-new.sql +++ b/sql/mysql.old-to-new.sql @@ -17,6 +17,7 @@ BEGIN 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; @@ -33,14 +34,12 @@ BEGIN 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_username` (`server_host`, `username`); 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 `private_storage` ADD INDEX `i_private_storage_sh_username` USING BTREE (`server_host`, `username`); 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; @@ -75,10 +74,10 @@ BEGIN 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 `sr_group` ADD PRIMARY KEY (`server_host`, `name`); ALTER TABLE `muc_registered` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; ALTER TABLE `muc_registered` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `sm` DROP INDEX `i_node`; @@ -94,16 +93,13 @@ BEGIN 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 `privacy_list` ADD INDEX `i_privacy_list_sh_username` USING BTREE (`server_host`, `username`); 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_jid` (`server_host`, `jid`); ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_grp` (`server_host`, `grp`); - ALTER TABLE `sr_user` ADD PRIMARY KEY (`server_host`, `jid`, `grp`); 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; @@ -119,7 +115,6 @@ BEGIN 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 `mix_pam` ADD INDEX `i_mix_pam_us` (`username`(191), `server_host`); 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`; diff --git a/sql/mysql.sql b/sql/mysql.sql index ae4a73312..630c4a557 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,12 +17,14 @@ -- CREATE TABLE users ( - username varchar(191) PRIMARY KEY, + username varchar(191) NOT NULL, + type smallint 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 + 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: @@ -51,7 +53,6 @@ CREATE TABLE rosterusers ( ) 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 ( @@ -77,7 +78,6 @@ CREATE TABLE sr_user ( ) 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 ( @@ -100,6 +100,7 @@ CREATE TABLE archive ( 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; @@ -108,6 +109,12 @@ CREATE INDEX i_username_timestamp USING BTREE ON archive(username(191), timestam 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, @@ -174,7 +181,6 @@ CREATE TABLE privacy_list ( created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) 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 ( @@ -199,7 +205,6 @@ CREATE TABLE private_storage ( created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) 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 @@ -307,7 +312,6 @@ CREATE TABLE muc_online_users ( ) 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 INDEX i_muc_online_users_us USING BTREE ON muc_online_users(username(75), server(75)); CREATE TABLE muc_room_subscribers ( room varchar(191) NOT NULL, @@ -374,7 +378,6 @@ CREATE TABLE route ( ) 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 INDEX i_route_domain ON route(domain(75)); CREATE TABLE bosh ( sid text NOT NULL, @@ -433,7 +436,6 @@ CREATE TABLE mix_participant ( ) 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 INDEX i_mix_participant_chan_serv ON mix_participant (channel(191), service(191)); CREATE TABLE mix_subscription ( channel text NOT NULL, @@ -445,9 +447,7 @@ CREATE TABLE mix_subscription ( ) 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_ud ON mix_subscription (channel(191), service(191), username(191), domain(191)); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); -CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel(191), service(191)); CREATE TABLE mix_pam ( username text NOT NULL, @@ -458,7 +458,6 @@ CREATE TABLE mix_pam ( ) 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 INDEX i_mix_pam_u ON mix_pam (username(191)); CREATE TABLE mqtt_pub ( username varchar(191) NOT NULL, diff --git a/sql/pg.new.sql b/sql/pg.new.sql index 6700a4771..1e59ec571 100644 --- a/sql/pg.new.sql +++ b/sql/pg.new.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,7 +20,7 @@ -- 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); +-- 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 ''; @@ -30,10 +30,8 @@ -- ALTER TABLE rosterusers ADD COLUMN server_host text NOT NULL DEFAULT ''; -- DROP INDEX i_rosteru_user_jid; --- DROP INDEX i_rosteru_username; -- 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_username ON rosterusers USING btree (server_host, username); -- CREATE INDEX i_rosteru_sh_jid ON rosterusers USING btree (server_host, jid); -- ALTER TABLE rosterusers ALTER COLUMN server_host DROP DEFAULT; @@ -43,15 +41,15 @@ -- 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_jid; -- DROP INDEX i_sr_user_grp; -- ALTER TABLE sr_user ADD PRIMARY KEY (server_host, jid, grp); --- CREATE INDEX i_sr_user_sh_jid ON sr_user USING btree (server_host, jid); -- 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; @@ -94,7 +92,7 @@ -- 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, username); +-- 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); @@ -114,17 +112,13 @@ -- 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; -- DROP INDEX i_privacy_list_username_name; --- CREATE INDEX i_privacy_list_sh_username ON privacy_list USING btree (server_host, username); -- 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; -- DROP INDEX i_private_storage_username_namespace; -- ALTER TABLE private_storage ADD PRIMARY KEY (server_host, username, namespace); --- CREATE INDEX i_private_storage_sh_username ON private_storage USING btree (server_host, username); -- ALTER TABLE private_storage ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE roster_version ADD COLUMN server_host text NOT NULL DEFAULT ''; @@ -161,20 +155,14 @@ -- 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; --- DROP INDEX i_mix_pam_us; -- CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); --- CREATE INDEX i_mix_pam_us ON mix_pam (username, server_host); -- ALTER TABLE mix_pam ALTER COLUMN server_host DROP DEFAULT; --- ALTER TABLE route ADD COLUMN server_host text NOT NULL DEFAULT ''; --- DROP INDEX i_route; --- CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); --- ALTER TABLE i_route 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); @@ -184,12 +172,13 @@ 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) + PRIMARY KEY (server_host, username, "type") ); -- Add support for SCRAM auth to a database created before ejabberd 16.03: @@ -221,7 +210,6 @@ CREATE TABLE rosterusers ( ); CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers USING btree (server_host, username, jid); -CREATE INDEX i_rosteru_sh_username ON rosterusers USING btree (server_host, username); CREATE INDEX i_rosteru_sh_jid ON rosterusers USING btree (server_host, jid); @@ -238,8 +226,7 @@ CREATE TABLE sr_group ( name text NOT NULL, server_host text NOT NULL, opts text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - PRIMARY KEY (server_host, name) + created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group USING btree (server_host, name); @@ -248,19 +235,17 @@ CREATE TABLE sr_user ( jid text NOT NULL, server_host text NOT NULL, grp text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - PRIMARY KEY (server_host, jid, grp) + 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_jid ON sr_user USING btree (server_host, jid); 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 SERIAL, + seq BIGSERIAL, created_at TIMESTAMP NOT NULL DEFAULT now() ); @@ -274,9 +259,10 @@ CREATE TABLE archive ( bare_peer text NOT NULL, xml text NOT NULL, txt text, - id SERIAL, + id BIGSERIAL, kind text, nick text, + origin_id text, created_at TIMESTAMP NOT NULL DEFAULT now() ); @@ -284,6 +270,12 @@ CREATE INDEX i_archive_sh_username_timestamp ON archive USING btree (server_host 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, @@ -355,11 +347,10 @@ CREATE TABLE privacy_list ( username text NOT NULL, server_host 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_sh_username ON privacy_list USING btree (server_host, username); CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list USING btree (server_host, username, name); CREATE TABLE privacy_list_data ( @@ -382,12 +373,10 @@ CREATE TABLE private_storage ( server_host text NOT NULL, namespace text NOT NULL, data text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - PRIMARY KEY (server_host, username, namespace) + created_at TIMESTAMP NOT NULL DEFAULT now() ); -CREATE INDEX i_private_storage_sh_username ON private_storage USING btree (server_host, username); - +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, @@ -413,7 +402,7 @@ CREATE TABLE pubsub_node ( node text NOT NULL, parent text NOT NULL DEFAULT '', plugin text NOT NULL, - nodeid SERIAL UNIQUE + 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); @@ -436,7 +425,7 @@ CREATE TABLE pubsub_state ( jid text NOT NULL, affiliation character(1), subscriptions text NOT NULL DEFAULT '', - stateid SERIAL UNIQUE + 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); @@ -502,7 +491,6 @@ CREATE TABLE muc_online_users ( ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); -CREATE INDEX i_muc_online_users_us ON muc_online_users USING btree (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, @@ -574,7 +562,6 @@ CREATE TABLE route ( ); CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); -CREATE INDEX i_route_domain ON route USING btree (domain); CREATE TABLE bosh ( sid text NOT NULL, @@ -607,6 +594,7 @@ CREATE TABLE push_session ( ); 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, @@ -634,7 +622,6 @@ CREATE TABLE mix_participant ( ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); -CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, @@ -646,9 +633,7 @@ CREATE TABLE mix_subscription ( ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); -CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); -CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, @@ -660,7 +645,6 @@ CREATE TABLE mix_pam ( ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); -CREATE INDEX i_mix_pam_us ON mix_pam (username, server_host); CREATE TABLE mqtt_pub ( username text NOT NULL, diff --git a/sql/pg.sql b/sql/pg.sql index 4cf4f0cbd..dd83e087e 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2022 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,12 +17,14 @@ -- CREATE TABLE users ( - username text PRIMARY KEY, + username 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() + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (username, "type") ); -- Add support for SCRAM auth to a database created before ejabberd 16.03: @@ -51,7 +53,6 @@ CREATE TABLE rosterusers ( ); 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); @@ -78,13 +79,12 @@ 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() ); @@ -97,9 +97,10 @@ CREATE TABLE archive ( bare_peer text NOT NULL, xml text NOT NULL, txt text, - id SERIAL, + id BIGSERIAL, kind text, nick text, + origin_id text, created_at TIMESTAMP NOT NULL DEFAULT now() ); @@ -107,6 +108,12 @@ 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, @@ -169,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 ( @@ -198,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); @@ -224,7 +229,7 @@ CREATE TABLE pubsub_node ( node text NOT NULL, parent text NOT NULL DEFAULT '', plugin text NOT NULL, - nodeid SERIAL UNIQUE + 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); @@ -247,7 +252,7 @@ CREATE TABLE pubsub_state ( jid text NOT NULL, affiliation character(1), subscriptions text NOT NULL DEFAULT '', - stateid SERIAL UNIQUE + 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); @@ -309,7 +314,6 @@ CREATE TABLE muc_online_users ( ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); -CREATE INDEX i_muc_online_users_us ON muc_online_users USING btree (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, @@ -378,7 +382,6 @@ CREATE TABLE route ( ); CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); -CREATE INDEX i_route_domain ON route USING btree (domain); CREATE TABLE bosh ( sid text NOT NULL, @@ -409,7 +412,7 @@ CREATE TABLE push_session ( ); CREATE UNIQUE INDEX i_push_usn ON push_session USING btree (username, service, node); -CREATE UNIQUE INDEX i_push_ut ON push_session USING btree (username, timestamp); +CREATE INDEX i_push_ut ON push_session USING btree (username, timestamp); CREATE TABLE mix_channel ( channel text NOT NULL, @@ -437,7 +440,6 @@ CREATE TABLE mix_participant ( ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); -CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, @@ -449,9 +451,7 @@ CREATE TABLE mix_subscription ( ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); -CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); -CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, @@ -462,7 +462,6 @@ CREATE TABLE mix_pam ( ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); -CREATE INDEX i_mix_pam_us ON mix_pam (username); CREATE TABLE mqtt_pub ( username text NOT NULL, diff --git a/src/acl.erl b/src/acl.erl index 7e03298ba..eaa0aa50f 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -39,14 +39,14 @@ -type acl_rule() :: {user, {binary(), binary()} | binary()} | {server, binary()} | {resource, binary()} | - {user_regexp, {re:mp(), binary()} | re:mp()} | - {server_regexp, re:mp()} | - {resource_regexp, re:mp()} | - {node_regexp, {re:mp(), re:mp()}} | - {user_glob, {re:mp(), binary()} | re:mp()} | - {server_glob, re:mp()} | - {resource_glob, re:mp()} | - {node_glob, {re:mp(), re:mp()}} | + {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()]}]. @@ -106,8 +106,8 @@ match_acl(_Host, {shared_group, {G, H}}, #{usr := {U, S, _}}) -> undefined -> false; Mod -> Mod:is_user_in_group({U, S}, G, H) end; -match_acl(Host, {shared_group, G}, Map) -> - match_acl(Host, {shared_group, {G, Host}}, Map); +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, _}}) -> @@ -348,7 +348,7 @@ node_validator(UV, SV) -> %%%=================================================================== %%% Aux %%%=================================================================== --spec match_regexp(iodata(), re:mp()) -> boolean(). +-spec match_regexp(iodata(), misc:re_mp()) -> boolean(). match_regexp(Data, RegExp) -> re:run(Data, RegExp) /= nomatch. diff --git a/src/econf.erl b/src/econf.erl index 1a8711b48..5bc4f4599 100644 --- a/src/econf.erl +++ b/src/econf.erl @@ -3,7 +3,7 @@ %%% Purpose : Validator for ejabberd configuration options %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -197,6 +197,11 @@ 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). @@ -477,6 +482,8 @@ domain() -> 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, _} -> @@ -500,10 +507,15 @@ db_type(M) -> and_then( atom(), fun(T) -> - case code:ensure_loaded(db_module(M, T)) of - {module, _} -> T; - {error, _} -> fail({bad_db_type, M, T}) - end + 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). @@ -536,13 +548,10 @@ sip_uri() -> -spec host() -> yconf:validator(binary()). host() -> fun(Domain) -> - Host = ejabberd_config:get_myname(), Hosts = ejabberd_config:get_option(hosts), - Domain1 = (binary())(Domain), - Domain2 = misc:expand_keyword(<<"@HOST@">>, Domain1, Host), - Domain3 = (domain())(Domain2), + Domain3 = (domain())(Domain), case lists:member(Domain3, Hosts) of - true -> fail({route_conflict, Domain3}); + true -> fail({route_conflict, Domain}); false -> Domain3 end end. diff --git a/src/ejabberd.app.src.script b/src/ejabberd.app.src.script index 4c8745146..a4e245461 100644 --- a/src/ejabberd.app.src.script +++ b/src/ejabberd.app.src.script @@ -1,13 +1,33 @@ -Vars = case file:consult(filename:join([filename:dirname(SCRIPT), "..", "vars.config"])) of +{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), - [lists:keyfind(description, 1, Terms), + 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, @@ -27,7 +47,7 @@ Vars = case file:consult(filename:join([filename:dirname(SCRIPT), "..", "vars.co pkix, stringprep, yconf, - xmpp]}, + xmpp | ElixirApps]}, {mod, {ejabberd_app, []}}]}. %% Local Variables: diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 048eb7d98..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-2022 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,15 +27,21 @@ -author('alexey@process-one.net'). -compile({no_auto_import, [{halt, 0}]}). --protocol({xep, 4, '2.9'}). --protocol({xep, 86, '1.0'}). --protocol({xep, 106, '1.1'}). --protocol({xep, 170, '1.0'}). --protocol({xep, 205, '1.0'}). --protocol({xep, 212, '1.0'}). --protocol({xep, 216, '1.0'}). --protocol({xep, 243, '1.0'}). --protocol({xep, 270, '1.0'}). +-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]). @@ -43,7 +49,10 @@ -include("logger.hrl"). start() -> - application:ensure_all_started(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). @@ -129,7 +138,7 @@ check_apps() -> fun() -> Apps = [ejabberd | [App || {App, _, _} <- application:which_applications(), - App /= ejabberd]], + App /= ejabberd, App /= hex]], ?DEBUG("Checking consistency of applications: ~ts", [misc:join_atoms(Apps, <<", ">>)]), misc:peach( @@ -152,11 +161,11 @@ exit_or_halt(Reason, StartFlag) -> 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 -> @@ -166,6 +175,15 @@ module_name([Dir, _, <> | _] = Mod) when H >= 65, H =< 90 -> 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); diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index d4c9dd018..57b3637e3 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -5,7 +5,7 @@ %%% Created : 7 Sep 2016 by Paweł Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -82,7 +82,7 @@ can_access(Cmd, CallerInfo) -> case matches_definition(Def, Cmd, CallerModule, Tag, Host, CallerInfo) of true -> ?DEBUG("Command '~p' execution allowed by rule " - "'~ts' (CallerInfo=~p)", [Cmd, Name, CallerInfo]), + "'~ts'~n (CallerInfo=~p)", [Cmd, Name, CallerInfo]), allow; _ -> none @@ -93,7 +93,7 @@ can_access(Cmd, CallerInfo) -> case Res of allow -> allow; _ -> - ?DEBUG("Command '~p' execution denied " + ?DEBUG("Command '~p' execution denied~n " "(CallerInfo=~p)", [Cmd, CallerInfo]), deny end. @@ -344,10 +344,20 @@ validator(from) -> fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; - (A) -> (econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl]))(A) + (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_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl]))(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( @@ -377,6 +387,6 @@ validator() -> fun(Os) -> {proplists:get_value(from, Os, []), proplists:get_value(who, Os, none), - proplists:get_value(what, Os, [])} + proplists:get_value(what, Os, {none, none})} end), [unique]). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index c67b7a0d7..8b16fc727 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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,12 +31,17 @@ 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, format_status/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)). @@ -108,6 +113,8 @@ init([]) -> 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{}}. @@ -153,14 +160,13 @@ terminate(_Reason, _State) -> 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}. -format_status(_Opt, Status) -> - Status. - %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -450,11 +456,11 @@ delete_obsolete_data() -> %%%=================================================================== get_commands_spec() -> [#ejabberd_commands{name = request_certificate, tags = [acme], - desc = "Requests certificates for all or the specified " - "domains: all | domain1,domain2,...", + 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 = ["all | domain.tld,conference.domain.tld,..."], + args_example = ["example.com,domain.tld,conference.domain.tld"], args = [{domains, string}], result = {res, restuple}}, #ejabberd_commands{name = list_certificates, tags = [acme], @@ -550,6 +556,21 @@ list_certificates() -> {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 %%%=================================================================== diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 97e217cbc..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-2022 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,13 +33,17 @@ 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, list_cluster/0, + 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/0, %% Accounts @@ -50,7 +54,7 @@ %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia - 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, @@ -61,13 +65,27 @@ clear_cache/0, gc/0, get_commands_spec/0, - delete_old_messages_batch/4, delete_old_messages_status/1, delete_old_messages_abort/1]). + 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("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 -record(state, {}). @@ -77,6 +95,10 @@ start_link() -> 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) -> @@ -92,6 +114,10 @@ handle_info(Info, State) -> {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) -> @@ -116,28 +142,52 @@ get_commands_spec() -> desc = "Stop ejabberd gracefully", 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 = ?MODULE, function = restart, args = [], result = {res, rescode}}, #ejabberd_commands{name = reopen_log, tags = [logs], - desc = "Reopen the log files after being renamed", - longdesc = "This can be useful when an external tool is " + 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 " - "https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files", + "_`../../admin/guide/troubleshooting.md#log-files|Log Files`_.", policy = admin, module = ?MODULE, function = reopen_log, args = [], result = {res, rescode}}, #ejabberd_commands{name = rotate_log, tags = [logs], - desc = "Rotate the log files", + 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 = stop_kindly, tags = [server], - desc = "Inform users and rooms, wait, and stop the server", - longdesc = "Provide the delay in seconds, and the " + #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 stop_kindly 60 " - "\\\"The server will stop in one minute.\\\"", + "`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 = 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.">>], @@ -152,9 +202,10 @@ get_commands_spec() -> 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: none | emergency | alert | critical " - "| error | warning | notice | info | debug"], + args_desc = ["Desired logging level"], args_example = ["debug"], args = [{loglevel, string}], result = {res, rescode}}, @@ -166,10 +217,13 @@ get_commands_spec() -> 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 = ["mod_vcard"], + args_example = ["all"], args = [{module, string}], + result_example = {ok, <<"Updated modules: mod_configure, mod_vcard">>}, result = {res, restuple}}, #ejabberd_commands{name = register, tags = [accounts], @@ -213,21 +267,39 @@ get_commands_spec() -> result = {res, rescode}}, #ejabberd_commands{name = join_cluster, tags = [cluster], - desc = "Join this node into the cluster handled by Node", - longdesc = "This command works only with ejabberdctl, " - "not mod_http_api or other code that runs inside the " - "same ejabberd node that will be joined.", + 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, rescode}}, + 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 " + "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"], @@ -236,11 +308,27 @@ get_commands_spec() -> result = {res, rescode}}, #ejabberd_commands{name = list_cluster, tags = [cluster], - desc = "List nodes that are part of the cluster handled by Node", + 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", @@ -288,8 +376,9 @@ get_commands_spec() -> args = [{host, binary}], result = {res, rescode}}, #ejabberd_commands{name = import_prosody, tags = [mnesia, sql], desc = "Import data from Prosody", - longdesc = "Note: this requires ejabberd compiled with --enable-lua " - "and include the optional 'luerl' library.", + 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/"], @@ -310,17 +399,17 @@ get_commands_spec() -> 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 = delete_old_messages_batch, tags = [purge], + #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, @@ -333,7 +422,7 @@ get_commands_spec() -> result = {res, restuple}, result_desc = "Result tuple", result_example = {ok, <<"Removal of 5000 messages in progress">>}}, - #ejabberd_commands{name = delete_old_messages_status, tags = [purge], + #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, @@ -343,7 +432,7 @@ get_commands_spec() -> result = {status, string}, result_desc = "Status test", result_example = "Operation in progress, delete 5000 messages"}, - #ejabberd_commands{name = abort_delete_old_messages, tags = [purge], + #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, @@ -359,15 +448,21 @@ get_commands_spec() -> 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 http://./#delete-mnesia[delete_mnesia] command.", + "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 = 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"], @@ -395,7 +490,7 @@ get_commands_spec() -> "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'.", + "_`install_fallback`_ API.", module = ?MODULE, function = restore_mnesia, args_desc = ["Full path to the backup file"], args_example = ["/var/lib/ejabberd/database.backup"], @@ -417,8 +512,9 @@ get_commands_spec() -> longdesc = "Restore immediately. This is not " "recommended for big databases, as it will " "consume much time, memory and processor. In " - "that case it's preferable to use 'backup' and " - "'install_fallback'.", + "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"], @@ -440,8 +536,8 @@ get_commands_spec() -> "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'.", + "command requires less memory than " + "_`restore`_ API.", module = ?MODULE, function = install_fallback_mnesia, args_desc = ["Full path to the fallback file"], args_example = ["/var/lib/ejabberd/database.fallback"], @@ -459,9 +555,103 @@ get_commands_spec() -> desc = "Generate Unix manpage for current ejabberd version", note = "added in 20.01", module = ejabberd_doc, function = man, - args = [], result = {res, restuple}} - ]. + 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 @@ -478,7 +668,7 @@ 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), @@ -513,39 +703,49 @@ set_loglevel(LogLevel) -> %%% Stop Kindly %%% +evacuate_kindly(DelaySeconds, AnnouncementTextString) -> + perform_kindly(DelaySeconds, AnnouncementTextString, evacuate). + stop_kindly(DelaySeconds, AnnouncementTextString) -> - Subject = (str:format("Server stop in ~p seconds!", [DelaySeconds])), - WaitingDesc = (str: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, - [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]}, - {"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] ~ts... ", - [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) -> @@ -570,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). @@ -580,7 +786,10 @@ update_module(ModuleNameBin) when is_binary(ModuleNameBin) -> update_module(ModuleNameString) -> ModuleName = list_to_atom(ModuleNameString), case ejabberd_update:update([ModuleName]) of - {ok, _Res} -> {ok, []}; + {ok, []} -> + {ok, "Not updated: "++ModuleNameString}; + {ok, [ModuleName]} -> + {ok, "Updated: "++ModuleNameString}; {error, Reason} -> {error, Reason} end. @@ -590,8 +799,9 @@ update() -> Mods = ejabberd_admin:update_list(), io:format("Updating modules: ~p~n", [Mods]), ejabberd_admin:update("all"), - io:format("Updated modules: ", []), - Mods -- ejabberd_admin:update_list(). + Mods2 = Mods -- ejabberd_admin:update_list(), + io:format("Updated modules: ~p~n", [Mods2]), + ok. %%% %%% Account management @@ -666,15 +876,84 @@ convert_to_yaml(In, Out) -> %%% Cluster management %%% -join_cluster(NodeBin) -> - ejabberd_cluster:join(list_to_atom(binary_to_list(NodeBin))). +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). -leave_cluster(NodeBin) -> - ejabberd_cluster:leave(list_to_atom(binary_to_list(NodeBin))). +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 %%% @@ -777,6 +1056,12 @@ delete_old_messages_abort(Server) -> %%% Mnesia management %%% +get_master() -> + case mnesia:table_info(session, master_nodes) of + [] -> none; + [Node] -> Node + end. + set_master("self") -> set_master(node()); set_master(NodeString) when is_list(NodeString) -> @@ -784,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]), @@ -994,3 +1279,243 @@ 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 45e92d064..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-2022 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 @@ -32,7 +32,7 @@ -export([start/2, prep_stop/1, stop/1]). -include("logger.hrl"). --include("ejabberd_stacktrace.hrl"). + %%% %%% Application API @@ -44,6 +44,7 @@ start(normal, _Args) -> 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 @@ -59,10 +60,14 @@ start(normal, _Args) -> 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]), @@ -103,6 +108,8 @@ prep_stop(State) -> ejabberd_sm:stop(), ejabberd_service:stop(), ejabberd_s2s:stop(), + ejabberd_system_monitor:stop(), + gen_mod:prep_stop(), gen_mod:stop(), State. @@ -171,10 +178,18 @@ file_queue_init() -> Err -> throw({?MODULE, Err}) end. +%%% +%%% Elixir +%%% + -ifdef(ELIXIR_ENABLED). is_using_elixir_config() -> Config = ejabberd_config:path(), - 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config). + 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 @@ -193,8 +208,19 @@ start_elixir_application() -> 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 c64c508d9..0c5d2fc69 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -5,7 +5,7 @@ %%% Created : 23 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +28,8 @@ -author('alexey@process-one.net'). +-protocol({rfc, 5802}). + %% External exports -export([start_link/0, host_up/1, host_down/1, config_reloaded/0, set_password/3, check_password/4, @@ -46,7 +48,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([auth_modules/1, convert_to_scram/1]). +-export([auth_modules/1, convert_to_scram/1, drop_password_type/2, set_password_instance/3]). -include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). @@ -74,26 +76,38 @@ -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()} | error}. +-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(). -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]). @@ -223,6 +237,7 @@ 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. @@ -234,14 +249,16 @@ check_password_with_authmodule(User, AuthzId, Server, Password) -> -spec check_password_with_authmodule( binary(), binary(), binary(), binary(), binary(), - digest_fun() | undefined) -> false | {true, atom()}. + 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) of - error -> + case {jid:nodeprep(AuthzId), get_is_banned(LUser, LServer)} of + {error, _} -> false; - LAuthzId -> + {_, {is_banned, BanReason}} -> + {false, 'account-disabled', BanReason}; + {LAuthzId, _} -> untag_stop( lists:foldl( fun(Mod, false) -> @@ -261,15 +278,41 @@ check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGe false end. +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 set_password(binary(), binary(), password()) -> ok | {error, db_failure | not_allowed | invalid_jid | invalid_password}. set_password(User, Server, Password) -> 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, Password, M); + db_set_password(LUser, LServer, Plain, Passwords, M); (_, ok) -> ok end, {error, not_allowed}, auth_modules(LServer)); @@ -277,6 +320,32 @@ set_password(User, Server, Password) -> Err end. +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. + -spec try_register(binary(), binary(), password()) -> ok | {error, db_failure | not_allowed | exists | invalid_jid | invalid_password}. @@ -289,19 +358,25 @@ try_register(User, Server, Password) -> false -> case ejabberd_router:is_my_host(LServer) of true -> - case lists:foldl( - fun(_, ok) -> - ok; - (Mod, _) -> - db_try_register( - LUser, LServer, Password, Mod) - end, {error, not_allowed}, - auth_modules(LServer)) of - ok -> - ejabberd_hooks:run( - register_user, LServer, [LUser, LServer]); - {error, _} = Err -> - Err + 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} @@ -347,44 +422,46 @@ count_users(Server, Opts) -> auth_modules(LServer))) end. --spec get_password(binary(), binary()) -> false | password(). +-spec get_password(binary(), binary()) -> false | [password()]. get_password(User, Server) -> - case validate_credentials(User, Server) of - {ok, LUser, LServer} -> - case lists:foldl( - fun(M, error) -> db_get_password(LUser, LServer, M); - (_M, Acc) -> Acc - end, error, auth_modules(LServer)) of - {ok, Password} -> - Password; - error -> - false - end; - _ -> - false - end. + {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. --spec get_password_with_authmodule(binary(), binary()) -> {false | password(), module()}. +-spec get_password_with_authmodule(binary(), binary()) -> + {false | {false, atom(), binary()} | [password()], module()}. get_password_with_authmodule(User, Server) -> 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} -> + {{ok, Password}, Module} when is_list(Password) -> {Password, Module}; + {{ok, Password}, Module} -> + {[Password], Module}; {error, Module} -> {false, Module} + end end; _ -> {false, undefined} @@ -396,20 +473,34 @@ user_exists(_User, <<"">>) -> user_exists(User, Server) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> - lists:any( - fun(M) -> - case db_user_exists(LUser, LServer, M) of - {error, _} -> - false; - Else -> - Else - end - end, auth_modules(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. +-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). @@ -418,14 +509,38 @@ 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, _} -> true; - false -> + {false, _} -> user_exists_in_other_modules_loop(AuthModules, User, Server); - {error, _} -> - maybe + {{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( @@ -551,57 +666,98 @@ backend_type(Mod) -> 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(), password(), module()) -> ok | {error, exists | db_failure | not_allowed}. -db_try_register(User, Server, Password, Mod) -> - case erlang:function_exported(Mod, try_register, 3) of +-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 -> - Password1 = case Mod:store_type(Server) of - scram -> password_to_scram(Server, Password); - _ -> Password - end, - Ret = case use_cache(Mod, Server) of - true -> - ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, Password}, - fun() -> Mod:try_register(User, Server, Password1) end, - cache_nodes(Mod, Server)); - false -> - ets_cache:untag(Mod:try_register(User, Server, Password1)) - end, - case Ret of - {ok, _} -> ok; - {error, _} = Err -> Err + 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; - false -> - {error, not_allowed} + _ -> + 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(), password(), module()) -> ok | {error, db_failure | not_allowed}. -db_set_password(User, Server, Password, Mod) -> - case erlang:function_exported(Mod, set_password, 3) of - true -> - Password1 = case Mod:store_type(Server) of - scram -> password_to_scram(Server, Password); - _ -> Password - end, - Ret = case use_cache(Mod, Server) of +-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, Password}, - fun() -> Mod:set_password(User, Server, Password1) end, - cache_nodes(Mod, Server)); + 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(User, Server, Password1)) - end, - case Ret of - {ok, _} -> ok; - {error, _} = Err -> Err - end; - false -> - {error, not_allowed} + 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) -> @@ -611,24 +767,36 @@ db_get_password(User, Server, Mod) -> 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() -> Mod:get_password(User, Server) end); + 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 -> - ets_cache:untag(Mod:get_password(User, Server)) + 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; + {true, false}; not_found -> - false; + {false, false}; error -> case {Mod:store_type(Server), use_cache(Mod, Server)} of {external, true} -> @@ -647,26 +815,26 @@ db_user_exists(User, Server, Mod) -> end, case Val of {ok, _} -> - true; + {true, Mod /= ejabberd_auth_anonymous}; not_found -> - false; + {false, Mod /= ejabberd_auth_anonymous}; error -> - false; + {false, Mod /= ejabberd_auth_anonymous}; {error, _} = Err -> - Err + {Err, Mod /= ejabberd_auth_anonymous} end; {external, false} -> - ets_cache:untag(Mod:user_exists(User, Server)); + {ets_cache:untag(Mod:user_exists(User, Server)), Mod /= ejabberd_auth_anonymous}; _ -> - false + {false, false} end end. db_check_password(User, AuthzId, Server, ProvidedPassword, Digest, DigestFun, Mod) -> case db_get_password(User, Server, Mod) of - {ok, ValidPassword} -> - match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun); + {ok, ValidPasswords} -> + match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun); error -> case {Mod:store_type(Server), use_cache(Mod, Server)} of {external, true} -> @@ -771,7 +939,9 @@ password_to_scram(Host, Password) -> password_to_scram(_Host, #scram{} = Password, _IterationCount) -> Password; password_to_scram(Host, Password, IterationCount) -> - Hash = ejabberd_option:auth_scram_hash(Host), + 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)), @@ -855,15 +1025,23 @@ auth_modules() -> auth_modules(Server) -> LServer = jid:nameprep(Server), Methods = ejabberd_option:auth_method(LServer), - [ejabberd:module_name([<<"ejabberd">>, <<"auth">>, + [ejabberd:module_name([<<"auth">>, misc:atom_to_binary(M)]) || M <- Methods]. --spec match_passwords(password(), password(), +-spec match_passwords(password(), [password()], binary(), digest_fun() | undefined) -> boolean(). -match_passwords(Password, #scram{} = Scram, <<"">>, undefined) -> +match_passwords(Provided, Passwords, Digest, DigestFun) -> + lists:any( + fun(Pass) -> + match_password(Provided, Pass, Digest, DigestFun) + end, Passwords). + +-spec match_password(password(), password(), + binary(), digest_fun() | undefined) -> boolean(). +match_password(Password, #scram{} = Scram, <<"">>, undefined) -> is_password_scram_valid(Password, Scram); -match_passwords(Password, #scram{} = Scram, Digest, DigestFun) -> +match_password(Password, #scram{} = Scram, Digest, DigestFun) -> StoredKey = base64:decode(Scram#scram.storedkey), DigRes = if Digest /= <<"">> -> Digest == DigestFun(StoredKey); @@ -874,9 +1052,9 @@ match_passwords(Password, #scram{} = Scram, Digest, DigestFun) -> true -> StoredKey == Password andalso Password /= <<"">> end; -match_passwords(ProvidedPassword, ValidPassword, <<"">>, undefined) -> +match_password(ProvidedPassword, ValidPassword, <<"">>, undefined) -> ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>; -match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun) -> +match_password(ProvidedPassword, ValidPassword, Digest, DigestFun) -> DigRes = if Digest /= <<"">> -> Digest == DigestFun(ValidPassword); true -> false @@ -945,7 +1123,7 @@ convert_to_scram(Server) -> lists:foreach( fun({U, S}) -> case get_password(U, S) of - Pass when is_binary(Pass) -> + [Pass] when is_binary(Pass) -> SPass = password_to_scram(Server, Pass), set_password(U, S, SPass); _ -> diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index bfe4e0cac..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-2022 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,6 +28,8 @@ -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, @@ -153,7 +155,7 @@ check_password(User, _AuthzId, Server, _Password) -> %% 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; + maybe_exists -> false; false -> login(User, Server) end}. diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index a5320b7bc..1b69a9a10 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_auth_jwt.erl b/src/ejabberd_auth_jwt.erl index d1fe4d15a..7fac3e4f7 100644 --- a/src/ejabberd_auth_jwt.erl +++ b/src/ejabberd_auth_jwt.erl @@ -5,7 +5,7 @@ %%% Created : 16 Mar 2019 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -44,9 +44,9 @@ %%%---------------------------------------------------------------------- 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 + %% 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 diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 9195d2498..091e567a8 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index bb1aa92bf..996dd620f 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,11 +29,11 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, set_password/3, try_register/3, +-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]). + plain_password_required/1, use_cache/1, drop_password_type/2, set_password_instance/3]). -export([need_transform/1, transform/1]). -include("logger.hrl"). @@ -86,30 +86,58 @@ plain_password_required(Server) -> store_type(Server) -> ejabberd_auth:password_format(Server). -set_password(User, Server, Password) -> - US = {User, Server}, - F = fun () -> - mnesia:write(#passwd{us = US, password = Password}) +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, Password}}; + {cache, {ok, Passwords}}; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {nocache, {error, db_failure}} end. -try_register(User, Server, Password) -> - US = {User, Server}, - F = fun () -> - case mnesia:read({passwd, US}) of - [] -> - mnesia:write(#passwd{us = US, password = Password}), - mnesia:dirty_update_counter(reg_users_counter, Server, 1), - {ok, Password}; - [_] -> - {error, exists} - 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} -> @@ -120,9 +148,10 @@ try_register(User, Server, Password) -> end. get_users(Server, []) -> - mnesia:dirty_select(passwd, + Users = mnesia:dirty_select(passwd, [{#passwd{us = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, Server}], ['$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}]); @@ -179,22 +208,48 @@ count_users(Server, _) -> count_users(Server, []). get_password(User, Server) -> - case mnesia:dirty_read(passwd, {User, Server}) of - [{passwd, _, {scram, SK, SEK, Salt, IC}}] -> - {cache, {ok, #scram{storedkey = SK, serverkey = SEK, - salt = Salt, hash = sha, iterationcount = IC}}}; - [#passwd{password = Password}] -> - {cache, {ok, Password}}; + 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) -> - US = {User, Server}, F = fun () -> - mnesia:delete({passwd, US}), - mnesia:dirty_update_counter(reg_users_counter, Server, -1), - ok + 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} -> @@ -206,45 +261,10 @@ remove_user(User, Server) -> need_transform(#reg_users_counter{}) -> false; -need_transform({passwd, {U, S}, Pass}) -> - case Pass of - _ when is_binary(Pass) -> - case store_type(S) of - scram -> - ?INFO_MSG("Passwords in Mnesia table 'passwd' " - "will be SCRAM'ed", []), - true; - plain -> - false - end; - {scram, _, _, _, _} -> - case store_type(S) of - scram -> - false; - plain -> - ?WARNING_MSG("Some passwords were stored in the database " - "as SCRAM, but 'auth_password_format' " - "is not configured as 'scram': some " - "authentication mechanisms such as DIGEST-MD5 " - "would *fail*", []), - false - end; - #scram{} -> - case store_type(S) of - scram -> - false; - plain -> - ?WARNING_MSG("Some passwords were stored in the database " - "as SCRAM, but 'auth_password_format' " - "is not configured as 'scram': some " - "authentication mechanisms such as DIGEST-MD5 " - "would *fail*", []), - false - end; - _ when is_list(U) orelse is_list(S) orelse is_list(Pass) -> - ?INFO_MSG("Mnesia table 'passwd' will be converted to binary", []), - true - end. +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) -> @@ -263,24 +283,14 @@ transform({passwd, {U, S}, Pass}) transform(#passwd{us = NewUS, password = NewPass}); transform(#passwd{us = {U, S}, password = Password} = P) when is_binary(Password) -> - case store_type(S) of - scram -> - case jid:resourceprep(Password) of - error -> - ?ERROR_MSG("SASLprep failed for password of user ~ts@~ts", - [U, S]), - P; - _ -> - Scram = ejabberd_auth:password_to_scram(S, Password), - P#passwd{password = Scram} - end; - plain -> - P - end; -transform({passwd, _, {scram, _, _, _, _}} = P) -> - P; -transform(#passwd{password = #scram{}} = P) -> - P. + 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( diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index aa48a1931..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-2022 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/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index ca7fd0889..8ce78bc18 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,22 +30,72 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, set_password/3, try_register/3, +-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]). + 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"). --define(SALT_LENGTH, 16). - %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -start(_Host) -> ok. +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. @@ -55,42 +105,87 @@ plain_password_required(Server) -> store_type(Server) -> ejabberd_auth:password_format(Server). -set_password(User, Server, Password) -> +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() -> - case Password of - #scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC} -> - SK2 = scram_hash_encode(Hash, SK), + 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, - SK2, SEK, Salt, IC); - _ -> - set_password_t(User, Server, Password) - end + 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, Password}}; + {cache, {ok, Passwords}}; {aborted, _} -> {nocache, {error, db_failure}} end. -try_register(User, Server, Password) -> - Res = - case Password of - #scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC} -> - SK2 = scram_hash_encode(Hash, SK), - add_user_scram( - Server, User, - SK2, SEK, Salt, IC); - _ -> - add_user(Server, User, Password) - end, - case Res of - {updated, 1} -> {cache, {ok, Password}}; - _ -> {nocache, {error, exists}} +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) -> @@ -109,21 +204,41 @@ count_users(Server, Opts) -> get_password(User, Server) -> case get_password_scram(Server, User) of - {selected, [{Password, <<>>, <<>>, 0}]} -> - {cache, {ok, Password}}; - {selected, [{StoredKey, ServerKey, Salt, IterationCount}]} -> - {Hash, SK} = case StoredKey of - <<"sha256:", Rest/binary>> -> {sha256, Rest}; - <<"sha512:", Rest/binary>> -> {sha512, Rest}; - Other -> {sha, Other} - end, - {cache, {ok, #scram{storedkey = SK, - serverkey = ServerKey, - salt = Salt, - hash = Hash, - iterationcount = IterationCount}}}; {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. @@ -136,21 +251,21 @@ remove_user(User, Server) -> {error, db_failure} end. --define(BATCH_SIZE, 1000). +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")). -scram_hash_encode(Hash, StoreKey) -> - case Hash of - sha -> StoreKey; - sha256 -> <<"sha256:", StoreKey/binary>>; - sha512 -> <<"sha512:", StoreKey/binary>> - end. - -set_password_scram_t(LUser, LServer, +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", @@ -161,37 +276,31 @@ set_password_t(LUser, LServer, Password) -> "users", ["!username=%(LUser)s", "!server_host=%(LServer)s", - "password=%(Password)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 @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" + ?SQL("select @(type)d, @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" " from users" " where username=%(LUser)s and %(LServer)H")). -add_user_scram(LServer, LUser, - StoredKey, ServerKey, Salt, IterationCount) -> - ejabberd_sql:sql_query( - LServer, - ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "password=%(StoredKey)s", - "serverkey=%(ServerKey)s", - "salt=%(Salt)s", - "iterationcount=%(IterationCount)d"])). - -add_user(LServer, LUser, Password) -> - ejabberd_sql:sql_query( - LServer, - ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "password=%(Password)s"])). - del_user(LServer, LUser) -> ejabberd_sql:sql_query( LServer, @@ -200,7 +309,7 @@ del_user(LServer, LUser) -> list_users(LServer, []) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users where %(LServer)H")); + ?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, @@ -216,7 +325,7 @@ list_users(LServer, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users " + ?SQL("select @(distinct username)s from users " "where %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")); @@ -228,7 +337,7 @@ list_users(LServer, SPrefix2 = <>, ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users " + ?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")). @@ -245,11 +354,11 @@ users_number(LServer) -> " where oid = 'users'::regclass::oid")); _ -> ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users where %(LServer)H")) + ?SQL("select @(count(distinct username))d from users where %(LServer)H")) end; (_Type, _) -> ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users where %(LServer)H")) + ?SQL("select @(count(distinct username))d from users where %(LServer)H")) end). users_number(LServer, [{prefix, Prefix}]) @@ -258,7 +367,7 @@ users_number(LServer, [{prefix, Prefix}]) SPrefix2 = <>, ejabberd_sql:sql_query( LServer, - ?SQL("select @(count(*))d from users " + ?SQL("select @(count(distinct username))d from users " "where username like %(SPrefix2)s %ESCAPE and %(LServer)H")); users_number(LServer, []) -> users_number(LServer). @@ -266,7 +375,7 @@ users_number(LServer, []) -> which_users_exists(LServer, LUsers) when length(LUsers) =< 100 -> try ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users where username in %(LUsers)ls")) of + ?SQL("select @(distinct username)s from users where username in %(LUsers)ls")) of {selected, Matching} -> [U || {U} <- Matching]; {error, _} = E -> @@ -290,40 +399,44 @@ which_users_exists(LServer, LUsers) -> export(_Server) -> [{passwd, - fun(Host, #passwd{us = {LUser, LServer}, password = Password}) + fun(Host, #passwd{us = {LUser, LServer, plain}, password = Password}) when LServer == Host, is_binary(Password) -> - [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + [?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, StoredKey1, ServerKey, Salt, IterationCount}}) + (Host, {passwd, {LUser, LServer, _}, + {scram, StoredKey, ServerKey, Salt, IterationCount}}) when LServer == Host -> Hash = sha, - StoredKey = scram_hash_encode(Hash, StoredKey1), - [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + 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}) + (Host, #passwd{us = {LUser, LServer, _}, password = #scram{} = Scram}) when LServer == Host -> - StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey), + StoredKey = Scram#scram.storedkey, ServerKey = Scram#scram.serverkey, Salt = Scram#scram.salt, IterationCount = Scram#scram.iterationcount, - [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + 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", diff --git a/src/ejabberd_backend_sup.erl b/src/ejabberd_backend_sup.erl index 575e66e40..1b3495e36 100644 --- a/src/ejabberd_backend_sup.erl +++ b/src/ejabberd_backend_sup.erl @@ -2,7 +2,7 @@ %%% Created : 24 Feb 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_batch.erl b/src/ejabberd_batch.erl index 406a79e21..5a907c74b 100644 --- a/src/ejabberd_batch.erl +++ b/src/ejabberd_batch.erl @@ -5,7 +5,7 @@ %%% Created : 8 mar 2022 by Paweł Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_bosh.erl b/src/ejabberd_bosh.erl index 3bcf4f6f6..8d1dbd595 100644 --- a/src/ejabberd_bosh.erl +++ b/src/ejabberd_bosh.erl @@ -5,7 +5,7 @@ %%% Created : 20 Jul 2011 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,8 +25,8 @@ -module(ejabberd_bosh). -behaviour(xmpp_socket). -behaviour(p1_fsm). --protocol({xep, 124, '1.11'}). --protocol({xep, 206, '1.4'}). +-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]). @@ -308,9 +308,9 @@ init([#body{attrs = Attrs}, IP, SID]) -> ignore end. -wait_for_session(_Event, State) -> +wait_for_session(Event, State) -> ?ERROR_MSG("Unexpected event in 'wait_for_session': ~p", - [_Event]), + [Event]), {next_state, wait_for_session, State}. wait_for_session(#body{attrs = Attrs} = Req, From, @@ -367,16 +367,16 @@ wait_for_session(#body{attrs = Attrs} = Req, From, reply_next_state(State4, Resp#body{els = RespEls}, RID, From) end; -wait_for_session(_Event, _From, State) -> +wait_for_session(Event, _From, State) -> ?ERROR_MSG("Unexpected sync event in 'wait_for_session': ~p", - [_Event]), + [Event]), {reply, {error, badarg}, wait_for_session, State}. active({#body{} = Body, From}, State) -> active1(Body, From, State); -active(_Event, State) -> +active(Event, State) -> ?ERROR_MSG("Unexpected event in 'active': ~p", - [_Event]), + [Event]), {next_state, active, State}. active(#body{attrs = Attrs, size = Size} = Req, From, @@ -408,9 +408,9 @@ active(#body{attrs = Attrs, size = Size} = Req, From, end; true -> active1(Req, From, State1) end; -active(_Event, _From, State) -> +active(Event, _From, State) -> ?ERROR_MSG("Unexpected sync event in 'active': ~p", - [_Event]), + [Event]), {reply, {error, badarg}, active, State}. active1(#body{attrs = Attrs} = Req, From, State) -> @@ -517,9 +517,9 @@ handle_event({activate, C2SPid}, StateName, handle_event({change_shaper, Shaper}, StateName, State) -> {next_state, StateName, State#state{shaper_state = Shaper}}; -handle_event(_Event, StateName, State) -> +handle_event(Event, StateName, State) -> ?ERROR_MSG("Unexpected event in '~ts': ~p", - [StateName, _Event]), + [StateName, Event]), {next_state, StateName, State}. handle_sync_event({send_xml, @@ -557,9 +557,9 @@ handle_sync_event(deactivate_socket, _From, StateName, StateData) -> {reply, ok, StateName, StateData#state{c2s_pid = undefined}}; -handle_sync_event(_Event, _From, StateName, State) -> +handle_sync_event(Event, _From, StateName, State) -> ?ERROR_MSG("Unexpected sync event in '~ts': ~p", - [StateName, _Event]), + [StateName, Event]), {reply, {error, badarg}, StateName, State}. handle_info({timeout, TRef, wait_timeout}, StateName, @@ -583,9 +583,9 @@ handle_info({timeout, TRef, shaper_timeout}, StateName, {stop, normal, State}; _ -> {next_state, StateName, State} end; -handle_info(_Info, StateName, State) -> +handle_info(Info, StateName, State) -> ?ERROR_MSG("Unexpected info:~n** Msg: ~p~n** StateName: ~p", - [_Info, StateName]), + [Info, StateName]), {next_state, StateName, State}. terminate(_Reason, _StateName, State) -> diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index ec5c610e8..ef9312ef5 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -2,7 +2,7 @@ %%% Created : 8 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +22,12 @@ -module(ejabberd_c2s). -behaviour(xmpp_stream_in). -behaviour(ejabberd_listener). + +-protocol({rfc, 3920}). +-protocol({rfc, 3921}). +-protocol({rfc, 6120}). -protocol({rfc, 6121}). +-protocol({xep, 138, '2.1', '1.1.0', "complete", ""}). %% ejabberd_listener callbacks -export([start/3, start_link/3, accept/1, listen_opt_type/1, listen_options/0]). @@ -30,22 +35,29 @@ -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, - 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]). + 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, reject_unauthenticated_packet/2, - process_closed/2, process_terminated/2, process_info/2]). + 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]). + host_up/1, host_down/1, send_ws_ping/1, bounce_message_queue/2, + reset_vcard_xupdate_resend_presence/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). @@ -101,6 +113,10 @@ resend_presence(Pid) -> resend_presence(Pid, To) -> route(Pid, {resend_presence, To}). +-spec reset_vcard_xupdate_resend_presence(pid()) -> boolean(). +reset_vcard_xupdate_resend_presence(Pid) -> + route(Pid, reset_vcard_xupdate_resend_presence). + -spec close(pid()) -> ok; (state()) -> state(). close(Ref) -> @@ -150,6 +166,7 @@ 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, @@ -166,6 +183,7 @@ 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, @@ -208,7 +226,12 @@ open_session(#{user := U, server := S, resource := R, Pres -> get_priority_from_presence(Pres) end, Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}], - ejabberd_sm:open_session(SID, U, S, R, Prio, Info), + 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). %%%=================================================================== @@ -234,6 +257,13 @@ process_info(#{lserver := LServer} = State, {route, Packet}) -> true -> State1 end; +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; @@ -258,6 +288,11 @@ 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). @@ -265,6 +300,7 @@ reject_unauthenticated_packet(State, _Pkt) -> 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), @@ -366,6 +402,9 @@ tls_enabled(#{tls_enabled := TLSEnabled, tls_verify := TLSVerify}) -> TLSEnabled or TLSRequired or TLSVerify. +allow_unencrypted_sasl2(#{allow_unencrypted_sasl2 := AllowUnencryptedSasl2}) -> + AllowUnencryptedSasl2. + compress_methods(#{zlib := true}) -> [<<"zlib">>]; compress_methods(_) -> @@ -377,31 +416,62 @@ unauthenticated_stream_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), - ScramHash = ejabberd_option:auth_scram_hash(LServer), - ShaAv = Type == plain orelse (Type == scram andalso ScramHash == sha), - Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256), - Sha512Av = Type == plain orelse (Type == scram andalso ScramHash == sha512), + {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 - lists:filter( - fun(<<"ANONYMOUS">>) -> - ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); - (<<"DIGEST-MD5">>) -> Type == plain; - (<<"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). + 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) -> @@ -426,33 +496,74 @@ check_password_digest_fun(_Mech, #{lserver := LServer}) -> ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG) end. -bind(<<"">>, State) -> - bind(new_uniq_id(), State); -bind(R, #{user := U, server := S, access := Access, lang := Lang, - lserver := LServer, socket := Socket, - ip := IP} = State) -> - case resource_conflict_action(U, S, R) of - closenew -> - {error, xmpp:err_conflict(), State}; - {accept_resource, Resource} -> - JID = jid:make(U, S, Resource), - case acl:match_rule(LServer, Access, - #{usr => jid:split(JID), ip => IP}) of - allow -> - State1 = open_session(State#{resource => Resource, - sid => ejabberd_sm:make_sid()}), - State2 = ejabberd_hooks:run_fold( - c2s_session_opened, LServer, State1, []), - ?INFO_MSG("(~ts) Opened c2s session for ~ts", - [xmpp_socket:pp(Socket), jid:encode(JID)]), - {ok, State2}; - 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), State} - 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) -> @@ -529,6 +640,30 @@ 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]). @@ -553,12 +688,14 @@ init([State, Opts]) -> 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(), @@ -567,6 +704,7 @@ init([State, Opts]) -> 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) -> @@ -950,7 +1088,7 @@ get_conn_type(State) -> websocket -> websocket end. --spec fix_from_to(xmpp_element(), state()) -> stanza(). +-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 @@ -995,6 +1133,8 @@ 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) -> @@ -1017,6 +1157,7 @@ listen_options() -> {tls_compression, false}, {starttls, false}, {starttls_required, false}, + {allow_unencrypted_sasl2, false}, {tls_verify, false}, {zlib, false}, {max_stanza_size, infinity}, diff --git a/src/ejabberd_c2s_config.erl b/src/ejabberd_c2s_config.erl index 4100ae23c..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-2022 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/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index 69b14915f..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-2022 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,7 +25,8 @@ -module(ejabberd_captcha). --protocol({xep, 158, '1.0'}). +-protocol({xep, 158, '1.0', '2.1.0', "complete", ""}). +-protocol({xep, 231, '1.0', '2.1.0', "complete", ""}). -behaviour(gen_server). @@ -77,6 +78,11 @@ mk_ocr_field(Lang, CID, Type) -> [_, 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()} | @@ -243,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">>}, @@ -288,7 +295,7 @@ init([]) -> case check_captcha_setup() of true -> register_handlers(), - ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 70), {ok, #state{enabled = true}}; false -> {ok, #state{enabled = false}}; @@ -355,7 +362,7 @@ terminate(_Reason, #state{enabled = Enabled}) -> if Enabled -> unregister_handlers(); true -> ok end, - ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 70). register_handlers() -> ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), @@ -392,6 +399,18 @@ create_image(Limiter, Key) -> {error, image_error()}. do_create_image(Key) -> FileName = get_prog_name(), + 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, @@ -423,18 +442,40 @@ do_create_image(Key) -> get_prog_name() -> 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. +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(), <>; @@ -459,6 +500,40 @@ parse_captcha_host() -> <<"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 = binary_to_integer(PortString), PortListeners = get_port_listeners(PortNumber), @@ -544,7 +619,7 @@ return(Port, TRef, Result) -> is_feature_available() -> case get_prog_name() of - Prog when is_binary(Prog) -> true; + PathOrModule when is_binary(PathOrModule) -> true; false -> false end. diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl index 0a5364beb..38a378d30 100644 --- a/src/ejabberd_cluster.erl +++ b/src/ejabberd_cluster.erl @@ -3,7 +3,7 @@ %%% Created : 5 Jul 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_cluster_mnesia.erl b/src/ejabberd_cluster_mnesia.erl index 81bf010d7..ada0703be 100644 --- a/src/ejabberd_cluster_mnesia.erl +++ b/src/ejabberd_cluster_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 7 Oct 2015 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_commands.erl b/src/ejabberd_commands.erl index 5371f4da4..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-2022 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,6 +33,7 @@ -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, @@ -42,7 +43,9 @@ get_tags_commands/1, register_commands/1, register_commands/2, + register_commands/3, unregister_commands/1, + unregister_commands/3, get_commands_spec/0, get_commands_definition/0, get_commands_definition/1, @@ -56,8 +59,6 @@ -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). --define(POLICY_ACCESS, '$policy'). - -type auth() :: {binary(), binary(), binary() | {oauth, binary()}, boolean()} | map(). -record(state, {}). @@ -73,7 +74,7 @@ get_commands_spec() -> "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)" + "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"], @@ -86,8 +87,9 @@ get_commands_spec() -> 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 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"], @@ -143,27 +145,64 @@ code_change(_OldVsn, State, _Extra) -> 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) -> - %% XXX check if command exists - mnesia:dirty_write(Command#ejabberd_commands{definer = Definer}) - %% ?DEBUG("This command is already defined:~n~p", [Command]) + 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), 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. unregister_commands(Commands) -> lists:foreach( fun(Command) -> - mnesia:dirty_delete_object(Command) + mnesia:dirty_delete(ejabberd_commands, Command#ejabberd_commands.name) end, 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()}]. list_commands() -> @@ -175,7 +214,19 @@ list_commands(Version) -> Commands = get_commands_definition(Version), [{Name, Args, Desc} || #ejabberd_commands{name = Name, args = Args, - desc = Desc} <- Commands]. + 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()}. @@ -251,10 +302,16 @@ execute_command2(Name, Arguments, CallerInfo) -> execute_command2(Name, Arguments, CallerInfo, Version) -> Command = get_command_definition(Name, Version), - case ejabberd_access_permissions:can_access(Name, CallerInfo) of - allow -> + 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. @@ -276,7 +333,8 @@ get_tags_commands() -> get_tags_commands(Version) -> CommandTags = [{Name, Tags} || #ejabberd_commands{name = Name, tags = Tags} - <- get_commands_definition(Version)], + <- get_commands_definition(Version), + not lists:member(internal, Tags)], Dict = lists:foldl( fun({CommandNameAtom, CTags}, D) -> CommandName = atom_to_list(CommandNameAtom), diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl index 0fe5f6402..79bfe6147 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -5,7 +5,7 @@ %%% Created : 20 May 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,9 +33,11 @@ -include("ejabberd_commands.hrl"). -define(RAW(V), if HTMLOutput -> fxml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end). --define(TAG(N), if HTMLOutput -> [<<"<", ??N, "/>">>]; true -> md_tag(N, <<"">>) end). --define(TAG(N, V), if HTMLOutput -> [<<"<", ??N, ">">>, V, <<"">>]; true -> md_tag(N, V) end). --define(TAG(N, C, V), if HTMLOutput -> [<<"<", ??N, " class='", C, "'>">>, V, <<"">>]; true -> md_tag(N, 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)). @@ -85,7 +87,7 @@ md_tag(h2, V) -> md_tag(strong, V) -> [<<"*">>, V, <<"*">>]; md_tag('div', V) -> - [<<"
">>, V, <<"
">>]; + [<<"*Note* about this command: ">>, V, <<".">>]; md_tag(_, V) -> V. @@ -235,7 +237,7 @@ json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> [?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, + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<"">>, <<"~~~ json\n">>} end, {Code, ResultStr} = case {ResultDesc, Result} of {{_, rescode}, V} when V == true; V == ok -> {200, [?STR_L("")]}; @@ -245,13 +247,8 @@ json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> {200, [?STR(Text1)]}; {{_, restuple}, {_, Text2}} -> {500, [?STR(Text2)]}; - {{_, {list, _}}, _} -> - {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; - {{_, {tuple, _}}, _} -> - {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; - {{Name0, _}, _} -> - {200, [Indent, ?OP_L("{"), ?STR_A(Name0), ?OP_L(": "), - json_gen(ResultDesc, Result, Indent, HTMLOutput), ?OP_L("}")]} + {{_, _}, _} -> + {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)} end, CodeStr = case Code of 200 -> <<" 200 OK">>; @@ -367,7 +364,7 @@ make_tags(HTMLOutput) -> -dialyzer({no_match, gen_tags/2}). gen_tags({TagName, Commands}, HTMLOutput) -> - [?TAG(h1, TagName) | [?TAG(p, ?RAW("* *`"++C++"`*")) || C <- Commands]]. + [?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, @@ -384,7 +381,7 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, ResultText = case Result of {res,rescode} -> [?TAG(dl, [gen_param(res, integer, - "Status code (0 on success, 1 otherwise)", + "Status code (`0` on success, `1` otherwise)", HTMLOutput)])]; {res,restuple} -> [?TAG(dl, [gen_param(res, string, @@ -398,14 +395,14 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] end end, - TagsText = [?RAW("*`"++atom_to_list(Tag)++"`* ") || Tag <- Tags], + TagsText = ?RAW(string:join(["_`"++atom_to_list(Tag)++"`_" || Tag <- Tags], ", ")), IsDefinerMod = case Definer of - unknown -> true; - _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes))) + 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)++"`*"))]; + [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("_`"++atom_to_list(Definer)++"`_"))]; false -> [] end, @@ -413,14 +410,19 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, "" -> []; _ -> ?TAG('div', "note-down", ?RAW(Note)) end, + {NotePre, NotePost} = + if HTMLOutput -> {[], NoteEl}; + true -> {NoteEl, []} + end, - [NoteEl, - ?TAG(h1, atom_to_list(Name)), + [?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)] @@ -433,26 +435,32 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, [Name, Ex]))) end. -find_commands_definitions() -> - case code:lib_dir(ejabberd, ebin) of - {error, _} -> - lists:map(fun({N, _, _}) -> - ejabberd_commands:get_command_definition(N) - end, ejabberd_commands:list_commands()); - Path -> - lists:flatmap(fun(P) -> - Mod = list_to_atom(filename:rootname(P)), - 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, filelib:wildcard("*.beam", Path)) +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), @@ -472,13 +480,21 @@ generate_html_output(File, RegExp, Languages) -> ok. maybe_add_policy_arguments(#ejabberd_commands{args=Args1, policy=user}=Cmd) -> - Args2 = [{user, binary}, {server, binary} | Args1], + 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 @@ -489,21 +505,22 @@ generate_md_output(File, RegExp, Languages) -> end, Cmds2), Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], Langs = binary:split(Languages, <<",">>, [global]), - Header = <<"---\ntitle: Administration API reference\ntoc: true\nmenu: API Reference\norder: 1\n" - "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_commands'\n---\n\n" - "This section describes API of ejabberd.\n">>, + 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]), + {ok, Fh} = file:open(File, [write, {encoding, utf8}]), io:format(Fh, "~ts~ts", [Header, Out]), file:close(Fh), ok. generate_tags_md(File) -> - Header = <<"---\ntitle: API Tags\ntoc: true\nmenu: API Tags\norder: 2\n" - "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_tags'\n---\n\n" - "This section enumerates the tags and their associated API.\n">>, + 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]), + {ok, Fh} = file:open(File, [write, {encoding, utf8}]), io:format(Fh, "~ts~ts", [Header, Tags]), file:close(Fh), ok. diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index c5d8c4494..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-2022 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,20 +37,22 @@ -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, set_option/2]). +-export([get_option/2]). -export([get_version/0, get_myhosts/0]). -export([get_mylang/0, get_lang/1]). -deprecated([{get_option, 2}, - {set_option, 2}, {get_version, 0}, {get_myhosts, 0}, {get_mylang, 0}, {get_lang, 1}]). -include("logger.hrl"). --include("ejabberd_stacktrace.hrl"). + -type option() :: atom() | {atom(), global | binary()}. -type error_reason() :: {merge_conflict, atom(), binary()} | @@ -68,6 +70,10 @@ -optional_callbacks([globals/0]). +-ifndef(OTP_BELOW_28). +-dialyzer([no_opaque_union]). +-endif. + %%%=================================================================== %%% API %%%=================================================================== @@ -111,6 +117,9 @@ reload() -> 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 -> @@ -160,14 +169,14 @@ get_option({O, Host} = Opt) -> T -> T end, try ets:lookup_element(Tab, Opt, 2) - catch ?EX_RULE(error, badarg, St) when Host /= global -> - StackTrace = ?EX_STACK(St), - 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 + 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 set_option(option(), term()) -> ok. @@ -203,7 +212,7 @@ get_lang(Host) -> -spec get_uri() -> binary(). get_uri() -> - <<"http://www.process-one.net/en/ejabberd/">>. + <<"https://www.process-one.net/ejabberd/">>. -spec get_copyright() -> binary(). get_copyright() -> @@ -255,30 +264,31 @@ version() -> -spec default_db(binary() | global, module()) -> atom(). default_db(Host, Module) -> - default_db(default_db, Host, Module, mnesia). + 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, 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, Host, Module, mnesia). + 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, Host, Module, Default). + default_db(default_ram_db, ram_db_type, Host, Module, Default). --spec default_db(default_db | default_ram_db, binary() | global, module(), atom()) -> atom(). -default_db(Opt, Host, Mod, 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 option '~ts', using " - "'~ts' as fallback", [Mod, Type, Opt, Default]), + "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. @@ -297,14 +307,21 @@ beams(external) -> end end, ExtMods), case application:get_env(ejabberd, external_beams) of - {ok, Path} -> - case lists:member(Path, code:get_path()) of - true -> ok; - false -> code:add_patha(Path) - end, - Beams = filelib:wildcard(filename:join(Path, "*\.beam")), - CustMods = [list_to_atom(filename:rootname(filename:basename(Beam))) - || Beam <- Beams], + {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 @@ -325,7 +342,12 @@ may_hide_data(Data) -> -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 - application:load(Application), + 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), @@ -335,12 +357,20 @@ env_binary_to_list(Application, Parameter) -> 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)) + maps:merge(Vs, validators(M, Disallowed, DK)) end, #{}, Modules), Required = lists:flatmap( fun(M) -> @@ -397,6 +427,145 @@ format_error({error, {exception, Class, Reason, St}}) -> "file attached and the following stacktrace included:~n** ~ts", [misc:format_exception(2, Class, Reason, St)])). +%% @format-begin + +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; +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. + +get_defined_keywords(Host) -> + Tab = case get_tmp_config() of + undefined -> + ejabberd_options; + T -> + T + end, + get_defined_keywords(Tab, Host). + +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). + +get_defined_keywords_yaml_config(Y) -> + [{erlang:atom_to_binary(KwAtom, latin1), KwValue} + || {KwAtom, KwValue} <- proplists:get_value(define_keyword, Y, [])]. + +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())}]. + +resolve_host_alias(Host) -> + case lists:member(Host, ejabberd_option:hosts()) of + true -> + Host; + false -> + resolve_host_alias2(Host) + end. + +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. + +%% 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)). +%% @format-end + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -463,21 +632,29 @@ callback_modules(external) -> end end, beams(external)); callback_modules(all) -> - callback_modules(local) ++ callback_modules(external). + misc:lists_uniq(callback_modules(local) ++ callback_modules(external)). --spec validators(module(), [atom()]) -> econf:validators(). -validators(Mod, Disallowed) -> +-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 -> - {true, - try {O, Mod:opt_type(O)} + Type = + try Mod:opt_type(O) catch _:_ -> - {O, ejabberd_options:opt_type(O)} - end} + 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()))). @@ -502,13 +679,34 @@ read_file(File, Opts) -> end, case Ret of {ok, Y} -> - validate(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 | lists:flatten(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 @@ -527,21 +725,35 @@ read_erlang_file(File, _) -> 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([]), + {Validators, Required} = validators([], DK), Validator = econf:options(Validators, [{required, Required}, unique]), @@ -590,8 +802,9 @@ load_file(File) -> Err -> abort(Err) end - catch ?EX_RULE(Class, Reason, St) -> - {error, {exception, Class, Reason, ?EX_STACK(St)}} + catch + Class:Reason:Stack -> + {error, {exception, Class, Reason, Stack}} end. -spec commit() -> ok. diff --git a/src/ejabberd_config_transformer.erl b/src/ejabberd_config_transformer.erl index 7a1a03506..1aed7c6a8 100644 --- a/src/ejabberd_config_transformer.erl +++ b/src/ejabberd_config_transformer.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -128,6 +128,11 @@ transform(Host, s2s_use_starttls, required_trusted, Acc) -> 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}. @@ -225,6 +230,8 @@ filter(_Host, captcha_host, _, _) -> 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( @@ -271,25 +278,25 @@ replace_request_handlers(Opts) -> Handlers = proplists:get_value(request_handlers, Opts, []), Handlers1 = lists:foldl( - fun({captcha, true}, Acc) -> + fun({captcha, IsEnabled}, Acc) -> Handler = {<<"/captcha">>, ejabberd_captcha}, - warn_replaced_handler(captcha, Handler), + warn_replaced_handler(captcha, Handler, IsEnabled), [Handler|Acc]; - ({register, true}, Acc) -> + ({register, IsEnabled}, Acc) -> Handler = {<<"/register">>, mod_register_web}, - warn_replaced_handler(register, Handler), + warn_replaced_handler(register, Handler, IsEnabled), [Handler|Acc]; - ({web_admin, true}, Acc) -> + ({web_admin, IsEnabled}, Acc) -> Handler = {<<"/admin">>, ejabberd_web_admin}, - warn_replaced_handler(web_admin, Handler), + warn_replaced_handler(web_admin, Handler, IsEnabled), [Handler|Acc]; - ({http_bind, true}, Acc) -> + ({http_bind, IsEnabled}, Acc) -> Handler = {<<"/bosh">>, mod_bosh}, - warn_replaced_handler(http_bind, Handler), + warn_replaced_handler(http_bind, Handler, IsEnabled), [Handler|Acc]; - ({xmlrpc, true}, Acc) -> + ({xmlrpc, IsEnabled}, Acc) -> Handler = {<<"/">>, ejabberd_xmlrpc}, - warn_replaced_handler(xmlrpc, Handler), + warn_replaced_handler(xmlrpc, Handler, IsEnabled), Acc ++ [Handler]; (_, Acc) -> Acc @@ -538,7 +545,12 @@ 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}) -> +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", diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index e715aac00..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-2022 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,22 +23,23 @@ %%% %%%---------------------------------------------------------------------- -%%% Does not support commands that have arguments with ctypes: list, tuple - -module(ejabberd_ctl). -behaviour(gen_server). -author('alexey@process-one.net'). --export([start/0, start_link/0, process/1, process2/2]). +-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_http.hrl"). -include("logger.hrl"). --include("ejabberd_stacktrace.hrl"). + -define(DEFAULT_VERSION, 1000000). @@ -92,6 +93,7 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), {ok, #state{}}. handle_call(Request, From, State) -> @@ -107,21 +109,51 @@ handle_info(Info, State) -> {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(). --spec process([string()], non_neg_integer()) -> 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. @@ -144,20 +176,12 @@ process(["status"], _Version) -> %% 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"], _Version) -> - print("~p~n", [mnesia:system_info(all)]), - ?STATUS_SUCCESS; - -process(["mnesia", "info"], _Version) -> +process(["mnesia_info_ctl"], _Version) -> mnesia:info(), ?STATUS_SUCCESS; -process(["mnesia", Arg], _Version) -> - 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 @@ -238,7 +262,7 @@ process2(Args, AccessCommands, Auth, Version) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, process(["help" | [CommandString]], Version), - {lists:flatten(String), ?STATUS_ERROR}; + {lists:flatten(String), ?STATUS_USAGE}; {String, Code} when is_list(String) and is_integer(Code) -> {lists:flatten(String), Code}; @@ -277,7 +301,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> try_call_command(Args, Auth, AccessCommands, Version); false -> print_usage(Version), - {"", ?STATUS_USAGE}; + {"", ?STATUS_BADRPC}; Status -> {"", Status} catch @@ -294,7 +318,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> try_call_command(Args, Auth, AccessCommands, Version) -> try call_command(Args, Auth, AccessCommands, Version) of {Reason, wrong_command_arguments} -> - {Reason, ?STATUS_ERROR}; + {Reason, ?STATUS_USAGE}; Res -> Res catch @@ -307,11 +331,10 @@ try_call_command(Args, Auth, AccessCommands, Version) -> ?STATUS_ERROR}; throw:Error -> {io_lib:format("~p", [Error]), ?STATUS_ERROR}; - ?EX_RULE(A, Why, Stack) -> - StackTrace = ?EX_STACK(Stack), - {io_lib:format("Unhandled exception occurred executing the command:~n** ~ts", - [misc:format_exception(2, A, Why, StackTrace)]), - ?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 call_command(Args::[string()], @@ -335,14 +358,14 @@ call_command([CmdString | Args], Auth, _AccessCommands, Version) -> ArgsFormatted, CI2, Version), - format_result(Result, ResultFormat); - {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> + 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]]), + process(["help" | [CmdString]], Version), {io_lib:format("Error: the command '~ts' requires ~p ~ts.", [CmdString, NumCompa, TextCompa]), wrong_command_arguments} @@ -372,9 +395,16 @@ format_arg(Arg, string) -> NumChars = integer_to_list(length(Arg)), Parse = "~" ++ NumChars ++ "c", 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 = jiffy:decode(S), + JSON = misc:json_decode(S), mod_http_api:format_arg(JSON, Format). format_arg2(Arg, Parse)-> @@ -385,52 +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)}; %% 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}, _) -> +format_result({error, ErrorAtom, Code, Msg}, _, _Version) -> {io_lib:format("Error: ~p: ~s", [ErrorAtom, Msg]), make_status(Code)}; -format_result(Atom, {_Name, atom}) -> +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([A|_]=String, {_Name, string}) when is_list(String) and is_integer(A) -> +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("~ts", [binary_to_list(Binary)]); +format_result(Binary, {_Name, binary}, _Version) when is_binary(Binary) -> + io_lib:format("~ts", [Binary]); -format_result(Atom, {_Name, string}) when is_atom(Atom) -> +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}) when is_integer(Integer) -> +format_result(Integer, {_Name, string}, _Version) when is_integer(Integer) -> io_lib:format("~ts", [integer_to_list(Integer)]); -format_result(Other, {_Name, string}) -> +format_result(Other, {_Name, string}, _Version) -> io_lib:format("~p", [Other]); -format_result(Code, {_Name, rescode}) -> +format_result(Code, {_Name, rescode}, _Version) -> make_status(Code); -format_result({Code, Text}, {_Name, restuple}) -> +format_result({Code, Text}, {_Name, restuple}, _Version) -> {io_lib:format("~ts", [Text]), make_status(Code)}; -%% The result is a list of something: [something()] -format_result([], {_Name, {list, _ElementsDef}}) -> +format_result([], {_Name, {top_result_list, _ElementsDef}}, _Version) -> ""; -format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> +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}}, _Version) -> + ""; +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)]; @@ -438,17 +493,17 @@ 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)]; -format_result(404, {_Name, _}) -> +format_result(404, {_Name, _}, _Version) -> make_status(not_found). make_status(ok) -> ?STATUS_SUCCESS; @@ -462,11 +517,7 @@ make_status(Error) -> 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:_ -> [] @@ -476,19 +527,24 @@ get_list_commands(Version) -> 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). + +has_list_args(Args) -> + lists:any( + fun({_Name, list}) -> true; + ({_Name, {list, _}}) -> true; + (_) -> false end, Args). @@ -498,7 +554,7 @@ is_supported_args(Args) -> %% Commands are Bold -define(B1, "\e[1m"). --define(B2, "\e[21m"). +-define(B2, "\e[22m"). -define(C(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end). %% Arguments are Dim @@ -520,17 +576,11 @@ print_usage(Version) -> {MaxC, ShCode} = get_shell_info(), print_usage(dual, MaxC, ShCode, Version). print_usage(HelpMode, MaxC, ShCode, Version) -> - AllCommands = - [ - {"help", ["[arguments]"], "Get help"}, - {"status", [], "Get ejabberd status"}, - {"stop", [], "Stop ejabberd"}, - {"restart", [], "Restart ejabberd"}, - {"mnesia", ["[info]"], "show information of Mnesia system"}] ++ - get_list_commands(Version), + AllCommands = get_list_commands(Version), print( - ["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("nodename"), "] [--version ", ?A("api_version"), "] ", + ["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"], []), @@ -578,17 +628,9 @@ get_shell_info() -> _:_ -> {78, false} end. -%% Erlang/OTP 20.0 introduced string:find/2, but we must support old 19.3 -string_find([], _SearchPattern) -> - nomatch; -string_find([A | String], [A]) -> - String; -string_find([_ | String], SearchPattern) -> - string_find(String, SearchPattern). - %% Split this command description in several lines of proper length prepare_description(DescInit, MaxC, Desc) -> - case string_find(Desc, "\n") of + case string:find(Desc, "\n") of nomatch -> prepare_description2(DescInit, MaxC, Desc); _ -> @@ -753,9 +795,13 @@ print_usage_help(MaxC, ShCode) -> " ejabberdctl ", ?C("help"), " ", ?C("register"), "\n", " ejabberdctl ", ?C("help"), " ", ?C("regist*"), "\n", "\n", - "Please note that 'ejabberdctl' shows all ejabberd commands,\n", - "even those that cannot be used in the shell with ejabberdctl.\n", - "Those commands can be identified because their 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{ name = help, @@ -763,7 +809,7 @@ print_usage_help(MaxC, ShCode) -> longdesc = lists:flatten(LongDesc), args = ArgsDef, result = {help, string}}, - print_usage_command2("help", C, MaxC, ShCode). + print(get_usage_command2("help", C, MaxC, ShCode), []). %%----------------------------- @@ -817,26 +863,42 @@ filter_commands_regexp(All, Glob) -> end, All). +maybe_add_policy_arguments(Args, user) -> + [{user, binary}, {host, binary} | Args]; +maybe_add_policy_arguments(Args, _) -> + Args. + -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), - print_usage_command2(Cmd, C, MaxC, ShCode). + get_usage_command2(Cmd, C, MaxC, ShCode). -print_usage_command2(Cmd, C, MaxC, ShCode) -> +get_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, definer = Definer, desc = Desc, - args = ArgsDef, + args = ArgsDefPreliminary, + args_desc = ArgsDesc, + args_example = ArgsExample, + result_example = ResultExample, + policy = Policy, longdesc = LongDesc, + note = Note, result = ResultDef} = C, 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"; @@ -846,21 +908,34 @@ print_usage_command2(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, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])], IsDefinerMod = case Definer of unknown -> true; - _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes))) + _ -> 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 @@ -868,24 +943,69 @@ print_usage_command2(Cmd, C, MaxC, ShCode) -> _ -> ["", 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, - case Cmd of - "help" -> ok; - _ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, - "\n\n", XmlrpcFmt, TagsFmt, "\n\n", ModuleFmt, DescFmt, "\n\n"], []) + First = case Cmd of + "help" -> ""; + _ -> [NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, + "\n\n", ExampleFmt, TagsFmt, "\n\n", ModuleFmt, NoteFmt, DescFmt, "\n\n"] end, - print([LongDescFmt, NoteEjabberdctl], []). + [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) -> @@ -898,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 @@ -919,3 +1040,137 @@ disable_logging() -> disable_logging() -> logger:set_primary_config(level, none). -endif. + +%%----------------------------- +%% Format Example Help +%%----------------------------- + +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 index 1701779ec..192c355c3 100644 --- a/src/ejabberd_db_sup.erl +++ b/src/ejabberd_db_sup.erl @@ -2,7 +2,7 @@ %%% Created : 13 June 2019 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_doc.erl b/src/ejabberd_doc.erl index ba28c8116..c2bf33922 100644 --- a/src/ejabberd_doc.erl +++ b/src/ejabberd_doc.erl @@ -2,7 +2,7 @@ %%% File : ejabberd_doc.erl %%% Purpose : Options documentation generator %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +24,7 @@ %% API -export([man/0, man/1, have_a2x/0]). +-include("ejabberd_commands.hrl"). -include("translate.hrl"). %%%=================================================================== @@ -45,7 +46,9 @@ man(Lang) -> #{desc := Descr} = Map -> DocOpts = maps:get(opts, Map, []), Example = maps:get(example, Map, []), - {[{M, Descr, DocOpts, #{example => Example}}|Mods], SubMods}; + 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)}; @@ -72,10 +75,12 @@ man(Lang) -> catch _:undef -> [] end end, ejabberd_config:callback_modules(all)), + Version = binary_to_list(ejabberd_config:version()), Options = ["TOP LEVEL OPTIONS", "-----------------", - tr(Lang, ?T("This section describes top level options of ejabberd.")), + "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) -> @@ -95,26 +100,30 @@ man(Lang) -> "MODULES", "-------", "[[modules]]", - tr(Lang, ?T("This section describes options of all ejabberd 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(), - ModName, + 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) + format_example(0, Lang, Example) ++ [io_lib:nl()] ++ + format_apitags(Lang, Example) end, lists:keysort(1, ModDoc1)), ListenOptions = [io_lib:nl(), "LISTENERS", "-------", "[[listeners]]", - tr(Lang, ?T("This section describes options of all ejabberd listeners.")), + "This section describes listeners options of ejabberd " ++ Version ++ ".", io_lib:nl(), "TODO"], AsciiData = @@ -151,7 +160,7 @@ opts_to_man(Lang, Backends) -> end, Backends). opt_to_man(Lang, {Option, Options}, Level) -> - [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++ + [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)] ++ @@ -162,16 +171,56 @@ opt_to_man(Lang, {Option, Options, Children}, Level) -> lists:keysort(1, Children))]) ++ [io_lib:nl()|format_example(Level, Lang, Options)]. -format_option(Lang, Option, #{note := Note, value := Val}) -> - "\n\n_Note_ about the next option: " ++ Note ++ ":\n\n"++ - "*" ++ atom_to_list(Option) ++ "*: 'pass:[" ++ - tr(Lang, Val) ++ "]'::"; -format_option(Lang, Option, #{value := Val}) -> - "*" ++ atom_to_list(Option) ++ "*: 'pass:[" ++ +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). @@ -194,7 +243,7 @@ format_example(Level, Lang, #{example := [_|_] = Example}) -> false -> lists:flatmap( fun(Block) -> - ["+", "''''", "+"|Block] + ["+", "*Examples*:", "+"|Block] end, lists:map( fun({Text, Lines}) -> @@ -388,7 +437,7 @@ run_a2x(Cwd, AsciiDocFile) -> {error, "a2x was not found: do you have 'asciidoc' installed?"}; {true, Path} -> Cmd = lists:flatten( - io_lib:format("~ts -f manpage ~ts -D ~ts", + io_lib:format("~ts --no-xmllint -f manpage ~ts -D ~ts", [Path, AsciiDocFile, Cwd])), case os:cmd(Cmd) of "" -> ok; diff --git a/src/ejabberd_hooks.erl b/src/ejabberd_hooks.erl index 3a440e455..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-2022 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,6 +34,10 @@ delete/3, delete/4, delete/5, + subscribe/4, + subscribe/5, + unsubscribe/4, + unsubscribe/5, run/2, run/3, run_fold/3, @@ -56,9 +60,11 @@ ). -include("logger.hrl"). --include("ejabberd_stacktrace.hrl"). + -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'). @@ -74,7 +80,7 @@ start_link() -> add(Hook, Function, Seq) when is_function(Function) -> add(Hook, global, undefined, Function, Seq). --spec add(atom(), HostOrModule :: binary() | atom(), fun() | atom() , integer()) -> ok. +-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); @@ -87,6 +93,34 @@ add(Hook, Module, Function, Seq) -> add(Hook, Host, Module, Function, Seq) -> gen_server:call(?MODULE, {add, Hook, Host, Module, Function, Seq}). +-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). + +-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) -> @@ -105,8 +139,20 @@ delete(Hook, Module, Function, Seq) -> delete(Hook, Host, Module, Function, Seq) -> gen_server:call(?MODULE, {delete, Hook, Host, Module, Function, Seq}). + + +-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}). + + -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). @@ -114,17 +160,28 @@ run(Hook, Args) -> -spec run(atom(), binary() | global, list()) -> ok. run(Hook, Host, Args) -> try ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> + [{_, Ls, Subs}] -> case erlang:get(?TRACE_HOOK_KEY) of - undefined -> + 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 -> - run1(Ls, Hook, Args); + 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), - run2(Ls, Hook, Args, Host, TracingOpts) + 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; [] -> @@ -134,7 +191,7 @@ run(Hook, Host, Args) -> end. -spec run_fold(atom(), T, list()) -> T. -%% @doc Run the calls of this hook in order. +%% @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. @@ -145,17 +202,28 @@ run_fold(Hook, Val, Args) -> -spec run_fold(atom(), binary() | global, T, list()) -> T. run_fold(Hook, Host, Val, Args) -> try ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> + [{_, Ls, Subs}] -> case erlang:get(?TRACE_HOOK_KEY) of - undefined -> + 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 -> - run_fold1(Ls, Hook, Val, Args); + 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]), - run_fold2(Ls, Hook, Val, Args, Host, TracingOpts) + 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; [] -> @@ -230,6 +298,14 @@ handle_call({delete, Hook, Host, Module, Function, Seq}, _From, State) -> HookFormat = {Seq, Module, Function}, Reply = handle_delete(Hook, Host, HookFormat), {reply, Reply, State}; +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({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}. @@ -237,27 +313,53 @@ handle_call(Request, From, State) -> -spec handle_add(atom(), atom(), hook()) -> ok. handle_add(Hook, Host, El) -> case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> + [{_, Ls, Subs}] -> case lists:member(El, Ls) of true -> ok; false -> NewLs = lists:merge(Ls, [El]), - ets:insert(hooks, {{Hook, Host}, NewLs}), + ets:insert(hooks, {{Hook, Host}, NewLs, Subs}), ok end; [] -> NewLs = [El], - ets:insert(hooks, {{Hook, Host}, NewLs}), + 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}] -> + [{_, Ls, Subs}] -> NewLs = lists:delete(El, Ls), - ets:insert(hooks, {{Hook, Host}, NewLs}), + 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 @@ -310,6 +412,40 @@ run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) -> 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", @@ -319,17 +455,47 @@ safe_apply(Hook, Module, Function, Args) -> true -> apply(Module, Function, Args) end - catch ?EX_RULE(E, R, St) when E /= exit; R /= normal -> - Stack = ?EX_STACK(St), - ?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' + 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. %%%---------------------------------------------------------------------- @@ -453,41 +619,45 @@ do_get_tracing_options(Hook, Host, MaybeMap) -> end end. -run2([], Hook, Args, Host, Opts) -> +run2([], Hook, Args, Host, Opts, SubscriberList) -> foreach_stop_hook_tracing(Opts, Hook, Host, Args, undefined), - ok; -run2([{Seq, Module, Function} | Ls], Hook, Args, Host, TracingOpts) -> + 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); + run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3); stop -> foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, {Module, Function, Seq, Ls}), - ok; + SubscriberList3; _ -> - run2(Ls, Hook, Args, Host, TracingOpts) + run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3) end. -run_fold2([], Hook, Val, Args, Host, Opts) -> +run_fold2([], Hook, Val, Args, Host, Opts, SubscriberList) -> fold_stop_hook_tracing(Opts, Hook, Host, [Val | Args], undefined), - Val; -run_fold2([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, TracingOpts) -> + {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); + 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; + {Val, SubscriberList3}; {stop, NewVal} -> fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {new, NewVal}, Ls}), - NewVal; + {NewVal, SubscriberList3}; NewVal -> - run_fold2(Ls, Hook, NewVal, Args, Host, TracingOpts) + run_fold2(Ls, Hook, NewVal, Args, Host, TracingOpts, SubscriberList3) end. foreach_start_hook_tracing(TracingOpts, Hook, Host, Args) -> @@ -543,13 +713,11 @@ run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) -> _ -> ok catch - ?EX_RULE(E, R, St) -> - Stack = ?EX_STACK(St), - ?ERROR_MSG( + 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 + [Hook, Host, erlang:self(), EventHandler, E, R, Stack]), + ok end end, EventHandlerList @@ -719,8 +887,7 @@ tracing_output(#{output_function := OutputF}, Text, Args) -> _ -> ok catch - ?EX_RULE(E, R, St) -> - Stack = ?EX_STACK(St), + E:R:Stack -> ?ERROR_MSG("Tracing output function exception(~0p): ~0p: ~0p", [E, R, Stack]), ok end; diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index fe13868f4..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-2022 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 @@ -39,6 +39,7 @@ -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). + -include_lib("kernel/include/file.hrl"). -record(state, {sockmod, @@ -65,9 +66,9 @@ request_headers = [], end_of_request = false, options = [], - default_host, custom_headers, trail = <<>>, + allow_unencrypted_sasl2, addr_re, sock_peer_name = none }). @@ -132,10 +133,12 @@ init(SockMod, Socket, Opts) -> CustomHeaders = proplists:get_value(custom_headers, Opts, []), + AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts), State = #state{sockmod = SockMod1, socket = Socket1, custom_headers = CustomHeaders, options = Opts, + allow_unencrypted_sasl2 = AllowUnencryptedSasl2, request_handlers = RequestHandlers, sock_peer_name = SockPeer, addr_re = RE}, @@ -166,9 +169,8 @@ send_file(State, Fd, Size, FileName) -> try case State#state.sockmod of gen_tcp -> - case file:sendfile(Fd, State#state.socket, 0, Size, []) of - {ok, _} -> ok - end; + {ok, _} = file:sendfile(Fd, State#state.socket, 0, Size, []), + ok; _ -> case file:read(Fd, ?SEND_BUF) of {ok, Data} -> @@ -271,7 +273,7 @@ process_header(State, Data) -> request_headers = add_header(Name, Langs, State)}; {ok, {http_header, _, 'Host' = Name, _, Value}} -> {Host, Port, TP} = get_transfer_protocol(State#state.addr_re, SockMod, Value), - State#state{request_host = Host, + State#state{request_host = ejabberd_config:resolve_host_alias(Host), request_port = Port, request_tp = TP, request_headers = add_header(Name, Value, State)}; @@ -297,7 +299,6 @@ process_header(State, Data) -> #state{sockmod = SockMod, socket = Socket, trail = State3#state.trail, options = State#state.options, - default_host = State#state.default_host, custom_headers = State#state.custom_headers, request_handlers = State#state.request_handlers, addr_re = State#state.addr_re}; @@ -305,7 +306,6 @@ process_header(State, Data) -> #state{end_of_request = true, trail = State3#state.trail, options = State#state.options, - default_host = State#state.default_host, custom_headers = State#state.custom_headers, request_handlers = State#state.request_handlers, addr_re = State#state.addr_re} @@ -313,7 +313,6 @@ process_header(State, Data) -> _ -> #state{end_of_request = true, options = State#state.options, - default_host = State#state.default_host, custom_headers = State#state.custom_headers, request_handlers = State#state.request_handlers, addr_re = State#state.addr_re} @@ -370,7 +369,15 @@ process(Handlers, Request) -> HandlerModule:socket_handoff( LocalPath, Request, HandlerOpts); false -> - HandlerModule:process(LocalPath, Request) + 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; @@ -711,7 +718,7 @@ file_format_error(Reason) -> url_decode_q_split_normalize(Path) -> {NPath, Query} = url_decode_q_split(Path), LPath = normalize_path([NPE - || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), + || NPE <- str:tokens(misc:uri_decode(NPath), <<"/">>)]), {LPath, Query}. % Code below is taken (with some modifications) from the yaws webserver, which @@ -739,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 = list_to_integer([Hi, Lo], 16), - 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). @@ -907,23 +901,18 @@ normalize_path([Part | Path], Norm) -> 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(default_host) -> - econf:domain(); listen_opt_type(custom_headers) -> econf:map( econf:binary(), - econf:and_then( - econf:binary(), - fun(V) -> - misc:expand_keyword(<<"@VERSION@">>, V, - ejabberd_option:version()) - end)). + econf:binary()). listen_options() -> [{ciphers, undefined}, @@ -932,7 +921,7 @@ listen_options() -> {protocol_options, undefined}, {tls, false}, {tls_compression, false}, + {allow_unencrypted_sasl2, false}, {request_handlers, []}, {tag, <<>>}, - {default_host, undefined}, {custom_headers, []}]. diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index fec53c210..c14ed2d58 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -5,7 +5,7 @@ %%% Created : 09-10-2010 by Eric Cestari %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -50,8 +50,7 @@ input = [] :: list(), active = false :: boolean(), c2s_pid :: pid(), - ws :: {#ws{}, pid()}, - rfc_compliant = undefined :: boolean() | undefined}). + ws :: {#ws{}, pid()}}). %-define(DBGFSM, true). @@ -123,6 +122,7 @@ init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) -> ({max_ack_queue, _}) -> true; ({ack_timeout, _}) -> true; ({resume_timeout, _}) -> true; + ({allow_unencrypted_sasl2, _}) -> true; ({max_resume_timeout, _}) -> true; ({resend_on_timeout, _}) -> true; ({access, _}) -> true; @@ -166,57 +166,38 @@ handle_event({new_shaper, Shaper}, StateName, #state{ws = {_, WsPid}} = StateDat {next_state, StateName, StateData}. handle_sync_event({send_xml, Packet}, _From, StateName, - #state{ws = {_, WsPid}, rfc_compliant = R} = StateData) -> - Packet2 = case {case R of undefined -> true; V -> V end, Packet} of - {true, {xmlstreamstart, _, Attrs}} -> - Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} | - lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], - {xmlstreamelement, #xmlel{name = <<"open">>, attrs = Attrs2}}; - {true, {xmlstreamend, _}} -> - {xmlstreamelement, #xmlel{name = <<"close">>, - attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}}; - {true, {xmlstreamraw, <<"\r\n\r\n">>}} -> % cdata ping - skip; - {true, {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, - {xmlstreamelement , El3}; - _ -> - Packet - end, - case Packet2 of - {xmlstreamstart, Name, Attrs3} -> - B = fxml:element_to_binary(#xmlel{name = Name, attrs = Attrs3}), - route_text(WsPid, <<(binary:part(B, 0, byte_size(B)-2))/binary, ">">>); - {xmlstreamend, Name} -> - route_text(WsPid, <<"">>); - {xmlstreamelement, El} -> - route_text(WsPid, fxml:element_to_binary(El)); - {xmlstreamraw, Bin} -> - route_text(WsPid, Bin); - {xmlstreamcdata, Bin2} -> - route_text(WsPid, Bin2); - skip -> - ok - end, - SN2 = case Packet2 of - {xmlstreamelement, #xmlel{name = <<"close">>}} -> - stream_end_sent; - _ -> - StateName - end, + #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}, rfc_compliant = true} = StateData) - when StateName /= stream_end_sent -> +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)), @@ -230,7 +211,7 @@ handle_info({received, Packet}, StateName, StateDataI) -> {StateData, Parsed} = parse(StateDataI, Packet), SD = case StateData#state.active of false -> - Input = StateData#state.input ++ if is_binary(Parsed) -> [Parsed]; true -> Parsed end, + Input = StateData#state.input ++ Parsed, StateData#state{input = Input}; true -> StateData#state.c2s_pid ! {tcp, StateData#state.socket, Parsed}, @@ -313,53 +294,19 @@ get_human_html_xmlel() -> "client that supports it.">>}]}]}]}. -parse(#state{rfc_compliant = C} = State, Data) -> - case C of - undefined -> - P = fxml_stream:new(self()), - P2 = fxml_stream:parse(P, Data), - fxml_stream:close(P2), - case parsed_items([]) of - error -> - {State#state{rfc_compliant = true}, <<"parse error">>}; - [] -> - {State#state{rfc_compliant = true}, <<"parse error">>}; - [{xmlstreamstart, <<"open">>, _} | _] -> - parse(State#state{rfc_compliant = true}, Data); - _ -> - parse(State#state{rfc_compliant = false}, Data) - end; - true -> - 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, <<"parse error">>}; - _ -> - {State, [El]} - end; - false -> - {State, Data} - end. - -parsed_items(List) -> - receive - {'$gen_event', El} - when element(1, El) == xmlel; - element(1, El) == xmlstreamstart; - element(1, El) == xmlstreamelement; - element(1, El) == xmlstreamcdata; - element(1, El) == xmlstreamend -> - parsed_items([El | List]); - {'$gen_event', {xmlstreamerror, _}} -> - error - after 0 -> - lists:reverse(List) +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. @@ -369,3 +316,7 @@ route_text(Pid, Data) -> {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 index be25dcb9d..5db539cab 100644 --- a/src/ejabberd_iq.erl +++ b/src/ejabberd_iq.erl @@ -5,7 +5,7 @@ %%% Created : 10 Nov 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -36,7 +36,7 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). --include("ejabberd_stacktrace.hrl"). + -record(state, {expire = infinity :: timeout()}). -type state() :: #state{}. @@ -174,11 +174,11 @@ calc_checksum(Data) -> -spec callback(atom() | pid(), #iq{} | timeout, term()) -> any(). callback(undefined, IQRes, Fun) -> try Fun(IQRes) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to process iq response:~n~ts~n** ~ts", - [xmpp:pp(IQRes), - misc:format_exception(2, Class, Reason, StackTrace)]) + 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 diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 41eec8c1e..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-2022 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 @@ -60,8 +60,6 @@ -optional_callbacks([listen_opt_type/1, tcp_init/2, udp_init/2]). --define(TCP_SEND_TIMEOUT, 15000). - start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -108,17 +106,20 @@ 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, + case {gen_udp:open(Port2, [binary, {active, false}, {reuseaddr, true} | - ExtraOpts2]) of - {ok, Socket} -> + 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()}), @@ -138,20 +139,22 @@ init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) -> {error, _} -> ok end; - {error, Reason} = Err -> - report_socket_error(Reason, EndPoint, Module), - proc_lib:init_ack(Err) + {error, Reason} -> + return_socket_error(Reason, EndPoint, Module) end; - {error, Reason} = Err -> - report_socket_error(Reason, EndPoint, Module), - proc_lib:init_ack(Err) + {{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) of - {ok, ListenSocket} -> + 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), @@ -171,13 +174,13 @@ init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) -> {error, _} -> ok end; - {error, Reason} = Err -> - report_socket_error(Reason, EndPoint, Module), - proc_lib:init_ack(Err) + {error, Reason} -> + return_socket_error(Reason, EndPoint, Module) end; - {error, Reason} = Err -> - report_socket_error(Reason, EndPoint, Module), - proc_lib:init_ack(Err) + {{error, Reason}, _} -> + return_socket_error(Reason, EndPoint, Module); + {_, {error, Reason}} -> + return_socket_error(Reason, EndPoint, Module) end. -spec listen_tcp(inet:port_number(), [gen_tcp:option()]) -> @@ -186,8 +189,10 @@ 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), - {0, [{ip, {local, Path}} | SO]}; + file:delete(Prov), + {0, [{ip, {local, Prov}} | SO]}; _ -> {Port, SockOpts} end, @@ -205,17 +210,121 @@ listen_tcp(Port, SockOpts) -> Err end. +%%% +%%% Unix Domain Socket utility functions +%%% + +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 OptVal = {Opt, Val} of + case {Opt, Val} of {ip, _} -> - {ModOpts, [OptVal|SockOpts]}; + {ModOpts, [{Opt, Val} | SockOpts]}; {backlog, _} when Transport == tcp -> - {ModOpts, [OptVal|SockOpts]}; + {ModOpts, [{Opt, Val} | SockOpts]}; {backlog, _} -> {ModOpts, SockOpts}; + {send_timeout, _} -> + {ModOpts, [{Opt, Val} | SockOpts]}; _ -> {ModOpts#{Opt => Val}, SockOpts} end @@ -270,11 +379,20 @@ accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) -> gen_tcp:close(Socket), none end, - ?INFO_MSG("(~p) Accepted connection ~ts -> ~ts", - [Receiver, - ejabberd_config:may_hide_data( - format_endpoint({PPort, PAddr, tcp})), - format_endpoint({Port, Addr, tcp})]); + 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; _ -> gen_tcp:close(Socket) end, @@ -285,6 +403,16 @@ accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) -> accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity) end. +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 @@ -399,12 +527,13 @@ stop_listeners() -> Ports). -spec stop_listener(endpoint(), module(), opts()) -> ok | {error, any()}. -stop_listener({_, _, Transport} = EndPoint, Module, Opts) -> +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 -> @@ -476,10 +605,20 @@ config_reloaded() -> end end, New). --spec report_socket_error(inet:posix(), endpoint(), module()) -> ok. -report_socket_error(Reason, EndPoint, Module) -> +-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)]). + [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) -> @@ -491,10 +630,15 @@ format_error(Reason) -> end. -spec format_endpoint(endpoint()) -> string(). -format_endpoint({Port, IP, _Transport}) -> +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) -> - <<"unix:", Unix/binary>>; + Def = get_definitive_udsocket_path(Unix), + <<"unix:", Def/binary>>; _ -> IPStr = case tuple_size(IP) of 4 -> inet:ntoa(IP); @@ -564,13 +708,21 @@ validator(M, T) -> true -> [] end, + Keywords = ejabberd_config:get_defined_keywords(global) ++ ejabberd_config:get_predefined_keywords(global), Validator = maps:from_list( lists:map( fun(Opt) -> - try {Opt, M:listen_opt_type(Opt)} + Type = try M:listen_opt_type(Opt) catch _:_ when M /= ?MODULE -> - {Opt, listen_opt_type(Opt)} - end + 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, @@ -699,6 +851,12 @@ 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(). @@ -708,6 +866,7 @@ listen_options() -> {ip, {0,0,0,0}}, {accept_interval, 0}, {send_timeout, 15000}, - {backlog, 5}, + {backlog, 128}, + {unix_socket, #{}}, {use_proxy_protocol, false}, {supervisor, true}]. diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 4a12b18f9..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-2022 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 @@ -48,7 +48,7 @@ -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). --include("ejabberd_stacktrace.hrl"). + -include("translate.hrl"). -record(state, {}). diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index 3d6c08650..c002914bf 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -5,7 +5,7 @@ %%% Created : 12 May 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2022 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 @@ -27,7 +27,7 @@ %% API -export([start/0, get/0, set/1, get_log_path/0, flush/0]). --export([convert_loglevel/1, loglevels/0]). +-export([convert_loglevel/1, loglevels/0, set_modules_fully_logged/1, config_reloaded/0]). -ifndef(LAGER). -export([progress_filter/2]). -endif. @@ -47,6 +47,8 @@ -export_type([loglevel/0]). +-include("logger.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -185,6 +187,9 @@ restart() -> application:stop(lager), start(Level). +config_reloaded() -> + ok. + reopen_log() -> ok. @@ -249,6 +254,8 @@ get_lager_version() -> false -> "0.0.0" end. +set_modules_fully_logged(_) -> ok. + flush() -> application:stop(lager), application:stop(sasl). @@ -278,7 +285,7 @@ start(Level) -> burst_limit_window_time => LogBurstLimitWindowTime, burst_limit_max_count => LogBurstLimitCount}, FmtConfig = #{legacy_header => false, - time_designator => $ , + time_designator => $\s, max_size => 100*1024, single_line => false}, FileFmtConfig = FmtConfig#{template => file_template()}, @@ -328,6 +335,27 @@ get_default_handlerid() -> restart() -> 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 -> @@ -338,17 +366,40 @@ progress_filter(#{level:=info,msg:={report,#{label:={_,progress}}}} = Event, _) progress_filter(Event, _) -> Event. +-ifdef(ELIXIR_ENABLED). console_template() -> - [time, " [", level, "] " | msg()]. + 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()]. -msg() -> - [{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []}, - msg, io_lib:nl()]. - -spec reopen_log() -> ok. reopen_log() -> ok. @@ -378,6 +429,10 @@ set(Level) when ?is_loglevel(Level) -> end end. +set_modules_fully_logged(Modules) -> + logger:unset_module_level(), + logger:set_module_level(Modules, all). + -spec flush() -> ok. flush() -> lists:foreach( diff --git a/src/ejabberd_mnesia.erl b/src/ejabberd_mnesia.erl index 88762746c..d9db27219 100644 --- a/src/ejabberd_mnesia.erl +++ b/src/ejabberd_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 17 Nov 2016 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -39,11 +39,10 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(STORAGE_TYPES, [disc_copies, disc_only_copies, ram_copies]). -define(NEED_RESET, [local_content, type]). -include("logger.hrl"). --include("ejabberd_stacktrace.hrl"). + -record(state, {tables = #{} :: tables(), schema = [] :: [{atom(), custom_schema()}]}). @@ -81,10 +80,11 @@ init([]) -> Schema = read_schema_file(), {ok, #state{schema = Schema}}; false -> - ?CRITICAL_MSG("Node name mismatch: I'm [~ts], " - "the database is owned by ~p", [MyNode, DbNodes]), + ?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", []), + "or change node name in Mnesia by running: " + "ejabberdctl mnesia_change ~ts", [hd(DbNodes)]), {stop, node_name_mismatch} end. @@ -377,14 +377,15 @@ do_transform(OldAttrs, Attrs, Old) -> transform_fun(Module, Name) -> fun(Obj) -> try Module:transform(Obj) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?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) + 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. diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 51676ac5d..a18596d46 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -5,7 +5,7 @@ %%% Created : 20 Mar 2015 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -54,6 +54,8 @@ 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"). @@ -81,7 +83,7 @@ get_commands_spec() -> [ #ejabberd_commands{name = oauth_issue_token, tags = [oauth], - desc = "Issue an oauth token for the given jid", + desc = "Issue an OAuth token for the given jid", module = ?MODULE, function = oauth_issue_token, args = [{jid, string},{ttl, integer}, {scopes, string}], policy = restricted, @@ -91,16 +93,29 @@ get_commands_spec() -> "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 tokens, their user and scope, and how many seconds remain until expirity", + 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 a token", + desc = "Revoke authorization for an OAuth token", note = "changed in 22.05", module = ?MODULE, function = oauth_revoke_token, args = [{token, binary}], @@ -109,7 +124,7 @@ get_commands_spec() -> result_desc = "Result code" }, #ejabberd_commands{name = oauth_add_client_password, tags = [oauth], - desc = "Add OAUTH client_id with password grant type", + desc = "Add OAuth client_id with password grant type", module = ?MODULE, function = oauth_add_client_password, args = [{client_id, binary}, {client_name, binary}, @@ -118,7 +133,7 @@ get_commands_spec() -> result = {res, restuple} }, #ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth], - desc = "Add OAUTH client_id with implicit grant type", + desc = "Add OAuth client_id with implicit grant type", module = ?MODULE, function = oauth_add_client_implicit, args = [{client_id, binary}, {client_name, binary}, @@ -127,7 +142,7 @@ get_commands_spec() -> result = {res, restuple} }, #ejabberd_commands{name = oauth_remove_client, tags = [oauth], - desc = "Remove OAUTH client_id", + desc = "Remove OAuth client_id", module = ?MODULE, function = oauth_remove_client, args = [{client_id, binary}], policy = restricted, @@ -135,8 +150,10 @@ get_commands_spec() -> } ]. -oauth_issue_token(Jid, TTLSeconds, ScopesString) -> +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}, @@ -215,6 +232,8 @@ init([]) -> 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}. @@ -240,12 +259,15 @@ handle_info(Info, State) -> {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({client, ClientID}, Ctx) -> +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) -> @@ -498,6 +520,10 @@ process(_Handlers, 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, <<"">>), @@ -505,8 +531,8 @@ process(_Handlers, ?XAE(<<"form">>, [{<<"action">>, <<"authorization_token">>}, {<<"method">>, <<"post">>}], - [?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)]), - ?INPUTID(<<"email">>, <<"username">>, <<"">>), + [?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)]) + ] ++ JidEls ++ [ ?BR, ?LABEL(<<"password">>, [?CT(?T("Password")), ?C(<<": ">>)]), ?INPUTID(<<"password">>, <<"password">>, <<"">>), @@ -569,61 +595,71 @@ process(_Handlers, RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), SScope = proplists:get_value(<<"scope">>, Q, <<"">>), StringJID = proplists:get_value(<<"username">>, Q, <<"">>), - #jid{user = Username, server = Server} = jid:decode(StringJID), - 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">>)])} + 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, @@ -675,39 +711,42 @@ process(_Handlers, password -> SScope = proplists:get_value(<<"scope">>, Q, <<"">>), StringJID = proplists:get_value(<<"username">>, Q, <<"">>), - #jid{user = Username, server = Server} = jid:decode(StringJID), - 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) + 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">>, @@ -736,19 +775,21 @@ json_response(Code, Body) -> {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, {<<"Cache-Control">>, <<"no-store">>}, {<<"Pragma">>, <<"no-cache">>}], - jiffy:encode(Body)}. + 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}]}, + 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_scope) -> <<"Invalid scope">>; +json_error_desc(invalid_jid) -> <<"Invalid JID">>. web_head() -> [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, @@ -774,3 +815,30 @@ logo() -> {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 index edb6dd52c..37fa3285c 100644 --- a/src/ejabberd_oauth_mnesia.erl +++ b/src/ejabberd_oauth_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 20 Jul 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl index a170826fb..b7200872a 100644 --- a/src/ejabberd_oauth_rest.erl +++ b/src/ejabberd_oauth_rest.erl @@ -5,7 +5,7 @@ %%% Created : 26 Jul 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -50,11 +50,11 @@ store(R) -> 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 + #{<<"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 -> @@ -65,14 +65,23 @@ store(R) -> lookup(Token) -> Path = path(<<"lookup">>), case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], - {[{<<"token">>, Token}]}], + #{<<"token">> => Token}], 2, 500) of - {ok, 200, {Data}} -> - SJID = proplists:get_value(<<"user">>, Data, <<>>), + {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 = proplists:get_value(<<"scope">>, Data, []), - Expire = proplists:get_value(<<"expire">>, Data, 0), + 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, @@ -113,11 +122,11 @@ store_client(#oauth_client{client_id = ClientID, case rest:with_retry( post, [ejabberd_config:get_myname(), Path, [], - {[{<<"client_id">>, ClientID}, - {<<"client_name">>, ClientName}, - {<<"grant_type">>, SGrantType}, - {<<"options">>, SOptions} - ]}], 2, 500) of + #{<<"client_id">> => ClientID, + <<"client_name">> => ClientName, + <<"grant_type">> => SGrantType, + <<"options">> => SOptions + }], 2, 500) of {ok, Code, _} when Code == 200 orelse Code == 201 -> ok; Err -> @@ -128,17 +137,26 @@ store_client(#oauth_client{client_id = ClientID, lookup_client(ClientID) -> Path = path(<<"lookup_client">>), case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], - {[{<<"client_id">>, ClientID}]}], + #{<<"client_id">> => ClientID}], 2, 500) of - {ok, 200, {Data}} -> - ClientName = proplists:get_value(<<"client_name">>, Data, <<>>), - SGrantType = proplists:get_value(<<"grant_type">>, Data, <<>>), + {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 = proplists:get_value(<<"options">>, Data, <<>>), + 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, diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl index b73f56b78..fe0a159ad 100644 --- a/src/ejabberd_oauth_sql.erl +++ b/src/ejabberd_oauth_sql.erl @@ -5,7 +5,7 @@ %%% Created : 27 Jul 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +34,7 @@ lookup_client/1, store_client/1, remove_client/1, revoke/1]). +-export([sql_schemas/0]). -include("ejabberd_oauth.hrl"). -include("ejabberd_sql_pt.hrl"). @@ -41,8 +42,35 @@ -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, diff --git a/src/ejabberd_old_config.erl b/src/ejabberd_old_config.erl index 47812d894..670dd7158 100644 --- a/src/ejabberd_old_config.erl +++ b/src/ejabberd_old_config.erl @@ -1,7 +1,7 @@ %%%---------------------------------------------------------------------- %%% Purpose: Transform old-style Erlang config to YAML config %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -619,7 +619,7 @@ strings_to_binary(L) when is_list(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}; + {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) -> diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl index 588721895..775ea14c9 100644 --- a/src/ejabberd_option.erl +++ b/src/ejabberd_option.erl @@ -14,10 +14,13 @@ -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]). @@ -37,8 +40,10 @@ -export([cluster_nodes/0]). -export([default_db/0, default_db/1]). -export([default_ram_db/0, default_ram_db/1]). --export([define_macro/0, define_macro/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]). @@ -51,7 +56,9 @@ -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]). @@ -74,6 +81,7 @@ -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]). @@ -113,6 +121,10 @@ -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]). @@ -141,6 +153,7 @@ -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]). @@ -158,6 +171,8 @@ -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]). @@ -221,6 +236,13 @@ auth_cache_missed() -> 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). @@ -242,6 +264,13 @@ auth_password_format() -> 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). @@ -249,6 +278,13 @@ auth_scram_hash() -> 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). @@ -316,7 +352,7 @@ cache_size() -> cache_size(Host) -> ejabberd_config:get_option({cache_size, Host}). --spec captcha_cmd() -> any(). +-spec captcha_cmd() -> 'undefined' | binary(). captcha_cmd() -> ejabberd_config:get_option({captcha_cmd, global}). @@ -328,7 +364,7 @@ captcha_host() -> captcha_limit() -> ejabberd_config:get_option({captcha_limit, global}). --spec captcha_url() -> 'undefined' | binary(). +-spec captcha_url() -> 'auto' | 'undefined' | binary(). captcha_url() -> ejabberd_config:get_option({captcha_url, global}). @@ -358,12 +394,16 @@ default_ram_db() -> 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() -> - define_macro(global). --spec define_macro(global | binary()) -> any(). -define_macro(Host) -> - ejabberd_config:get_option({define_macro, Host}). + ejabberd_config:get_option({define_macro, global}). -spec disable_sasl_mechanisms() -> [binary()]. disable_sasl_mechanisms() -> @@ -372,6 +412,13 @@ disable_sasl_mechanisms() -> 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}). @@ -441,6 +488,10 @@ host_config() -> 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). @@ -448,6 +499,10 @@ include_config_file() -> 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). @@ -593,6 +648,10 @@ log_burst_limit_count() -> 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}). @@ -791,6 +850,34 @@ resource_conflict() -> 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}). @@ -954,6 +1041,13 @@ sql_database() -> 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). @@ -1070,6 +1164,17 @@ sql_username(Host) -> 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). diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl index 1eff76575..609d75b93 100644 --- a/src/ejabberd_options.erl +++ b/src/ejabberd_options.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -77,10 +77,16 @@ opt_type(auth_opts) -> {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) -> @@ -110,20 +116,15 @@ opt_type(cache_missed) -> opt_type(cache_size) -> econf:pos_int(infinity); opt_type(captcha_cmd) -> - econf:and_then( - econf:binary(), - fun(V) -> - V2 = misc:expand_keyword(<<"@SEMVER@">>, V, - ejabberd_option:version()), - misc:expand_keyword(<<"@VERSION">>, V2, - misc:semver_to_xxyy(ejabberd_option:version())) - end); + econf:binary(); opt_type(captcha_host) -> econf:binary(); opt_type(captcha_limit) -> econf:pos_int(infinity); opt_type(captcha_url) -> - econf:url(); + econf:either( + econf:url(), + econf:enum([auto, undefined])); opt_type(certfiles) -> econf:list(econf:binary()); opt_type(cluster_backend) -> @@ -134,8 +135,12 @@ 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:any(); + 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( @@ -179,8 +184,17 @@ opt_type(host_config) -> [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) -> @@ -233,6 +247,8 @@ 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( @@ -252,6 +268,10 @@ 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) -> @@ -322,6 +342,14 @@ 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) -> @@ -418,6 +446,8 @@ 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) -> @@ -493,6 +523,7 @@ opt_type(jwt_auth_only_rule) -> {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()}]. @@ -513,6 +544,7 @@ options() -> {access_rules, []}, {acme, #{}}, {allow_contrib_modules, true}, + {install_contrib_modules, []}, {allow_multiple_connections, false}, {anonymous_protocol, sasl_anon}, {api_permissions, @@ -533,6 +565,9 @@ options() -> {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}, @@ -544,11 +579,13 @@ options() -> {captcha_cmd, undefined}, {captcha_host, <<"">>}, {captcha_limit, infinity}, - {captcha_url, undefined}, + {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, <<>>}, @@ -560,6 +597,7 @@ options() -> {extauth_program, undefined}, {fqdn, fun fqdn/1}, {hide_sensitive_log_data, false}, + {hosts_alias, []}, {host_config, []}, {include_config_file, []}, {language, <<"en">>}, @@ -589,11 +627,14 @@ options() -> {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(30)}, + {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}, @@ -611,7 +652,7 @@ options() -> {oom_killer, true}, {oom_queue, 10000}, {oom_watermark, 80}, - {outgoing_s2s_families, [inet, inet6]}, + {outgoing_s2s_families, [inet6, inet]}, {outgoing_s2s_ipv4_address, undefined}, {outgoing_s2s_ipv6_address, undefined}, {outgoing_s2s_port, 5269}, @@ -630,6 +671,10 @@ options() -> {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, @@ -700,6 +745,7 @@ options() -> {sql_start_interval, timer:seconds(30)}, {sql_username, <<"ejabberd">>}, {sql_prepared_statements, true}, + {sql_flags, []}, {trusted_proxies, []}, {validate_stream, false}, {websocket_origin, []}, @@ -726,20 +772,25 @@ globals() -> 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, diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl index c1404871d..56e2633c3 100644 --- a/src/ejabberd_options_doc.erl +++ b/src/ejabberd_options_doc.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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,27 +30,28 @@ doc() -> [{hosts, #{value => ?T("[Domain1, Domain2, ...]"), desc => - ?T("The option defines a list containing one or more " - "domains that 'ejabberd' will serve. This is a " + ?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 " - "http://../listen/[Listen Modules] section " + "_`listen.md|Listen Modules`_ section " "for details.")}}, {modules, #{value => "{Module: Options}", desc => - ?T("The option for modules configuration. See " - "http://../modules/[Modules] section " - "for details.")}}, + ?T("Set all the " + "_`modules.md|modules`_ configuration options.")}}, {loglevel, #{value => "none | emergency | alert | critical | " "error | warning | notice | info | debug", desc => - ?T("Verbosity of log files generated by ejabberd. " + ?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 " @@ -106,15 +107,22 @@ doc() -> {default_db, #{value => "mnesia | sql", desc => - ?T("Default persistent storage for ejabberd. " - "Modules and other components (e.g. authentication) " - "may have its own value. The default value is 'mnesia'.")}}, + ?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. " - "Modules and other components (e.g. session management) " - "may have its own value. The default value is 'mnesia'.")}}, + "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 => @@ -135,7 +143,9 @@ doc() -> {acl, #{value => "{ACLName: {ACLType: ACLValue}}", desc => - ?T("The option defines access control lists: named sets " + ?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' " @@ -218,10 +228,10 @@ doc() -> "specified 'Pattern' according to the rules used by the " "Unix shell.")}}]}, {access_rules, - #{value => "{AccessName: {allow|deny: ACLRules|ACLName}}", + #{value => "{AccessName: {allow|deny: ACLName|ACLDefinition}}", desc => ?T("This option defines " - "http://../basic/#access-rules[Access Rules]. " + "_`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 " @@ -255,7 +265,7 @@ doc() -> {acme, #{value => ?T("Options"), desc => - ?T("http://../basic/#acme[ACME] configuration, to automatically " + ?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 " @@ -303,6 +313,8 @@ doc() -> #{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", @@ -315,7 +327,9 @@ doc() -> {anonymous_protocol, #{value => "login_anon | sasl_anon | both", desc => - [?T("Define what anonymous protocol will be used: "), "", + [?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 " @@ -326,15 +340,12 @@ doc() -> desc => ?T("Define the permissions for API access. Please consult the " "ejabberd Docs web -> For Developers -> ejabberd ReST API -> " - "https://docs.ejabberd.im/developer/ejabberd-api/permissions/" - "[API Permissions].")}}, + "_`../../developer/ejabberd-api/permissions.md|API Permissions`_.")}}, {append_host_config, #{value => "{Host: Options}", desc => - ?T("To define specific ejabberd modules in a virtual host, " - "you can define the global 'modules' option with the common modules, " - "and later add specific modules to certain virtual hosts. " - "To accomplish that, 'append_host_config' option can be used.")}}, + ?T("Add a few specific options to a certain " + "_`../configuration/basic.md#virtual-hosting|virtual host`_.")}}, {auth_cache_life_time, #{value => "timeout()", desc => @@ -353,7 +364,7 @@ doc() -> {auth_method, #{value => "[mnesia | sql | anonymous | external | jwt | ldap | pam, ...]", desc => - ?T("A list of authentication methods to use. " + ?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. " @@ -363,7 +374,7 @@ doc() -> desc => ?T("This is used by the contributed module " "'ejabberd_auth_http' that can be installed from the " - "https://github.com/processone/ejabberd-contrib[ejabberd-contrib] " + "_`../../admin/guide/modules.md#ejabberd-contrib|ejabberd-contrib`_ " "Git repository. Please refer to that " "module's README file for details.")}}, {auth_password_format, @@ -371,28 +382,57 @@ doc() -> note => "improved in 20.01", desc => [?T("The option defines in what format the users passwords " - "are stored:"), "", + "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. "), "", + "SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1/256/512(-PLUS). "), "", ?T("* 'scram': The password is not stored, only some information " - "that allows to verify the hash provided by the client. " + "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."), - ?T("The default value is 'plain'.")]}}, + "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 SCRAM format. " + ?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 => @@ -406,8 +446,8 @@ doc() -> "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 http://../toplevel/#host-config[host_config] to specify this option per-vhost."), "", - ?T("To set a specific file per listener, use the listener's http://../listen-options/#cafile[cafile] option. Please notice that 'c2s_cafile' overrides the listener's 'cafile' option."), "" + ?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, ...]", @@ -426,8 +466,8 @@ doc() -> 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, " + "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, @@ -451,16 +491,17 @@ doc() -> 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 http://../toplevel/#s2s-cafile[s2s_cafile] option."), "" + ?T("For server connections, this 'ca_file' option is overridden by the _`s2s_cafile`_ option."), "" ]}}, {captcha_cmd, - #{value => ?T("Path"), - note => "improved in 21.10", + #{value => ?T("Path | ModuleName"), + note => "improved in 23.01", desc => - ?T("Full path to a script that generates http://../basic/#captcha[CAPTCHA] images. " - "@VERSION@ is replaced with ejabberd version number in XX.YY format. " - "@SEMVER@ is replaced with ejabberd version number in semver format " + ?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 => @@ -469,18 +510,24 @@ doc() -> {captcha_limit, #{value => "pos_integer() | infinity", desc => - ?T("Maximum number of http://../basic/#captcha[CAPTCHA] generated images per minute for " + ?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"), + #{value => ?T("URL | auto | undefined"), + note => "improved in 23.04", desc => - ?T("An URL where http://../basic/#captcha[CAPTCHA] requests should be sent. NOTE: you need " + ?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. There is no default value.")}}, + "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 => @@ -513,16 +560,28 @@ doc() -> ?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_macro, - #{value => "{MacroName: MacroValue}", + {define_keyword, + #{value => "{NAME: Value}", + note => "added in 25.03", desc => - ?T("Defines a macro. The value can be any valid arbitrary " - "YAML value. For convenience, it's recommended to define " - "a 'MacroName' in capital letters. Duplicated macros are not allowed. " - "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 is possible to use a 'MacroValue' in the definition of another macro."), + ?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", @@ -534,6 +593,16 @@ doc() -> "", "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 => @@ -545,7 +614,9 @@ doc() -> {domain_balancing, #{value => "{Domain: Options}", desc => - ?T("An algorithm to load balance the components that are plugged " + ?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 " @@ -560,17 +631,25 @@ doc() -> " transport.example.org:", " type: bare_source"]}, [{type, - #{value => "random | source | destination | bare_source | bare_destination", + #{value => ?T("Value"), desc => - ?T("How to deliver stanzas to connected components: " - "'random' - an instance is chosen at random; " - "'destination' - an instance is chosen by the full JID of " - "the packet's 'to' attribute; " - "'source' - by the full JID of the packet's 'from' attribute; " - "'bare_destination' - by the the bare JID (without resource) " - "of the packet's 'to' attribute; " - "'bare_source' - by the bare JID (without resource) of the " - "packet's 'from' attribute is used. The default value is 'random'.")}}, + ?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 => @@ -578,19 +657,23 @@ doc() -> {extauth_pool_name, #{value => ?T("Name"), desc => - ?T("Define the pool name appendix, so the full pool name will be " + ?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 " - "external program to start for better load balancing. " + "_`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 external " - "authentication script. The script must be executable by ejabberd.")}}, + ?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 => @@ -628,8 +711,10 @@ doc() -> {host_config, #{value => "{Host: Options}", desc => - ?T("The option is used to redefine 'Options' for virtual host " - "'Host'. In the example below LDAP authentication method " + ?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 => @@ -644,10 +729,29 @@ doc() -> " 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 additional configuration from 'Filename'. If the " + ?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, @@ -663,10 +767,19 @@ doc() -> "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 this auth " + ?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 " @@ -676,36 +789,44 @@ doc() -> #{value => ?T("FieldName"), desc => ?T("By default, the JID is defined in the '\"jid\"' JWT field. " - "This option allows to specify other JWT field name " + "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 JWK Key. " + ?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("The option defines the default language of server strings " + ?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\"'.")}}, + "The default value is '\"en\"'. ")}}, {ldap_servers, #{value => "[Host, ...]", desc => - ?T("A list of IP addresses or DNS names of your LDAP servers. " + ?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. " + ?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 will try to connect to these backup servers. " + "ejabberd connects to these backup servers. " "The default is an empty list, i.e. no backup servers specified. " - "WARNING: ejabberd doesn't try to reconnect back to the main " - "servers when they become operational again, so the only way " - "to restore these connections is to restart ejabberd. This " - "limitation might be fixed in future releases.")}}, + "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 => @@ -777,18 +898,18 @@ doc() -> "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\".")}}, + "'\"%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))\". " + "'\"(&(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 " + "whitespaces. Also you must not use '\"uid\"' attribute in the " "filter because this attribute will be appended to the filter " "automatically.")}}, {ldap_dn_filter, @@ -798,11 +919,11 @@ doc() -> "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\" " + "'\"%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 " + "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: " @@ -821,20 +942,26 @@ doc() -> 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 '10485760' (that is, 10 Mb).")}}, + "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")}}, + "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.")}}, + "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 => @@ -854,7 +981,7 @@ doc() -> 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 '30' seconds.")}}, + "The default value is '120' seconds.")}}, {net_ticktime, #{value => "timeout()", desc => @@ -868,11 +995,13 @@ doc() -> {new_sql_schema, #{value => "true | false", desc => - {?T("Whether to use 'new' SQL schema. All schemas are located " + {?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 " - "allows to store one XMPP domain into one ejabberd database. " - "The 'new' schema allows to handle several XMPP domains in a " + "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 " @@ -880,6 +1009,21 @@ doc() -> "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. " @@ -949,7 +1093,7 @@ doc() -> ?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.")}}, + "to 'false' or if _`oom_watermark`_ is not reached yet.")}}, {oom_watermark, #{value => ?T("Percent"), desc => @@ -959,17 +1103,20 @@ doc() -> "memory drops below this 'Percent', OOM killer is deactivated. " "The default value is '80' percents.")}}, {outgoing_s2s_families, - #{value => "[ipv4 | ipv6, ...]", + #{value => "[ipv6 | ipv4, ...]", + note => "changed in 23.01", desc => ?T("Specify which address families to try, in what order. " - "The default is '[ipv4, ipv6]' which means it first tries " - "connecting with IPv4, if that fails it tries using IPv6.")}}, + "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\". " + "an outgoing S2S IPv4 connection, for example '\"127.0.0.1\"'. " "The default value is 'undefined'.")}}, {outgoing_s2s_ipv6_address, #{value => "Address", @@ -977,7 +1124,7 @@ doc() -> 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'.")}}, + "'\"::FFFF:127.0.0.1\"'. The default value is 'undefined'.")}}, {outgoing_s2s_port, #{value => "1..65535", desc => @@ -991,14 +1138,18 @@ doc() -> {pam_service, #{value => ?T("Name"), desc => - ?T("This option defines the PAM service name. Refer to the PAM " + ?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 PAM service: only the username, " + "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", @@ -1015,37 +1166,45 @@ doc() -> #{value => "timeout()", desc => ?T("A timeout to wait for the connection to be re-established " - "to the Redis server. The default is '1 second'.")}}, + "to the _`database.md#redis|Redis`_ " + "server. The default is '1 second'.")}}, {redis_db, #{value => ?T("Number"), - desc => ?T("Redis database number. The default is '0'.")}}, + desc => ?T("_`database.md#redis|Redis`_ " + "database number. The default is '0'.")}}, {redis_password, #{value => ?T("Password"), desc => - ?T("The password to the Redis server. " + ?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 Redis server. " + ?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 Redis server is accepting connections. " + ?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 Redis server. " + ?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 => ?T("Hostname"), + #{value => "Host | IP Address | Unix Socket Path", + note => "improved in 24.12", desc => - ?T("A hostname or an IP address of the Redis server. " - "The default is 'localhost'.")}}, + ?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 => @@ -1069,6 +1228,26 @@ doc() -> "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 => @@ -1105,7 +1284,7 @@ doc() -> {s2s_access, #{value => ?T("Access"), desc => - ?T("This http://../basic/#access-rules[Access Rule] defines to " + ?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.")}}, @@ -1114,8 +1293,8 @@ doc() -> 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 http://../toplevel/#ca-file[ca_file] will be used."), "", - ?T("You can use http://../toplevel/#host-config[host_config] to specify this option per-vhost."), "" + "the value of _`ca_file`_ will be used."), "", + ?T("You can use _`host_config`_ to specify this option per-vhost."), "" ]}}, {s2s_ciphers, #{value => "[Cipher, ...]", @@ -1134,8 +1313,8 @@ doc() -> 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, " + "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, @@ -1199,7 +1378,9 @@ doc() -> {shaper, #{value => "{ShaperName: Rate}", desc => - ?T("The option defines a set of shapers. Every shaper is assigned " + ?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 " @@ -1214,9 +1395,11 @@ doc() -> " normal: 1000", " fast: 50000"]}}, {shaper_rules, - #{value => "{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}", + #{value => "{ShaperRuleName: {Number|ShaperName: ACLName|ACLDefinition}}", desc => - ?T("An entry allowing to declaring shaper to use for matching user/hosts. " + ?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."), @@ -1283,9 +1466,9 @@ doc() -> note => "added in 20.12", desc => ?T("Path to the ODBC driver to use to connect to a Microsoft SQL " - "Server database. This option is only valid if the _`sql_type`_ " - "option is set to 'mssql'. " - "The default value is: 'libtdsodbc.so'")}}, + "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 => @@ -1294,7 +1477,7 @@ doc() -> #{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: " + "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, @@ -1308,7 +1491,13 @@ doc() -> 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.")}}, + "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 => @@ -1322,16 +1511,20 @@ doc() -> "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {sql_server, - #{value => ?T("Host"), + #{value => "Host | IP Address | ODBC Connection String | Unix Socket Path", + note => "improved in 24.06", desc => - ?T("A hostname or an IP address of the SQL server. " + ?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 and " + "SQL server. The option is only available for MySQL, MS SQL and " "PostgreSQL. The default value is 'false'.")}}, {sql_ssl_cafile, #{value => ?T("Path"), @@ -1340,7 +1533,8 @@ doc() -> "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.")}}, + "certificate verification is disabled. " + "This option has no effect for MS SQL.")}}, {sql_ssl_certfile, #{value => ?T("Path"), desc => @@ -1348,13 +1542,15 @@ doc() -> "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.")}}, + "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()", @@ -1373,9 +1569,9 @@ doc() -> "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. " - "This allows, if enabled, to be able to know the real IP " + "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 " + "(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 " @@ -1398,7 +1594,7 @@ doc() -> "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\".")}}, + "'\"https://test.example.org:8081\"'.")}}, {websocket_ping_interval, #{value => "timeout()", desc => diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 8f45efa99..789be7359 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -5,7 +5,7 @@ %%% Created : 17 Jul 2008 by Pablo Polvorin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -32,7 +32,7 @@ -module(ejabberd_piefxis). --protocol({xep, 227, '1.1'}). +-protocol({xep, 227, '1.1', '2.1.0', "partial", ""}). -export([import_file/1, export_server/1, export_host/2]). diff --git a/src/ejabberd_pkix.erl b/src/ejabberd_pkix.erl index c5a945588..b699454dd 100644 --- a/src/ejabberd_pkix.erl +++ b/src/ejabberd_pkix.erl @@ -3,7 +3,7 @@ %%% Created : 4 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +34,7 @@ -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, format_status/2]). + terminate/2, code_change/3]). -include("logger.hrl"). -define(CALL_TIMEOUT, timer:minutes(1)). @@ -225,10 +225,6 @@ terminate(_Reason, State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. --spec format_status(normal | terminate, list()) -> term(). -format_status(_Opt, Status) -> - Status. - %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/src/ejabberd_redis.erl b/src/ejabberd_redis.erl index 597c57cfb..c0e61c0e6 100644 --- a/src/ejabberd_redis.erl +++ b/src/ejabberd_redis.erl @@ -4,7 +4,7 @@ %%% Created : 8 May 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -42,15 +42,13 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(SERVER, ?MODULE). --define(PROCNAME, 'ejabberd_redis_client'). -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"). --include("ejabberd_stacktrace.hrl"). + -record(state, {connection :: pid() | undefined, num :: pos_integer(), @@ -71,6 +69,14 @@ -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 %%%=================================================================== @@ -108,9 +114,10 @@ multi(F) -> {error, _} = Err -> Err; Result -> get_result(Result) end - catch ?EX_RULE(E, R, St) -> - erlang:erase(?TR_STACK), - erlang:raise(E, R, ?EX_STACK(St)) + catch + E:R:St -> + erlang:erase(?TR_STACK), + erlang:raise(E, R, St) end; _ -> erlang:error(nested_transaction) @@ -459,11 +466,12 @@ code_change(_OldVsn, State, _Extra) -> %%%=================================================================== -spec connect(state()) -> {ok, pid()} | {error, any()}. connect(#state{num = Num}) -> - Server = ejabberd_option:redis_server(), + 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", @@ -483,16 +491,33 @@ connect(#state{num = Num}) -> {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 - Res = eredis_sub:start_link(Server, Port, Pass, no_reconnect, infinity, drop), + 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) -> - eredis:start_link(Server, Port, DB, Pass, no_reconnect, 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(); diff --git a/src/ejabberd_redis_sup.erl b/src/ejabberd_redis_sup.erl index 6906ef937..8d49f4632 100644 --- a/src/ejabberd_redis_sup.erl +++ b/src/ejabberd_redis_sup.erl @@ -3,7 +3,7 @@ %%% Created : 6 Apr 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_regexp.erl b/src/ejabberd_regexp.erl index 5841dd4da..0d18deac6 100644 --- a/src/ejabberd_regexp.erl +++ b/src/ejabberd_regexp.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_regexp.erl -%%% Author : Badlop +%%% Author : Badlop %%% Purpose : Frontend to Re OTP module -%%% Created : 8 Dec 2011 by Badlop +%%% Created : 8 Dec 2011 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_router.erl b/src/ejabberd_router.erl index 7be9475ec..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-2022 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 @@ -70,7 +70,8 @@ -include("logger.hrl"). -include("ejabberd_router.hrl"). -include_lib("xmpp/include/xmpp.hrl"). --include("ejabberd_stacktrace.hrl"). + + -callback init() -> any(). -callback register_route(binary(), binary(), local_hint(), @@ -90,11 +91,11 @@ start_link() -> -spec route(stanza()) -> ok. route(Packet) -> try do_route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + 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. -spec route(jid(), jid(), xmlel() | stanza()) -> ok. @@ -380,8 +381,9 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%-------------------------------------------------------------------- -spec do_route(stanza()) -> ok. -do_route(OrigPacket) -> - ?DEBUG("Route:~n~ts", [xmpp:pp(OrigPacket)]), +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; @@ -405,6 +407,22 @@ do_route(OrigPacket) -> 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) -> diff --git a/src/ejabberd_router_mnesia.erl b/src/ejabberd_router_mnesia.erl index 53adca533..66ae02208 100644 --- a/src/ejabberd_router_mnesia.erl +++ b/src/ejabberd_router_mnesia.erl @@ -2,7 +2,7 @@ %%% Created : 11 Jan 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_router_multicast.erl b/src/ejabberd_router_multicast.erl index f3aba8407..df8473c2b 100644 --- a/src/ejabberd_router_multicast.erl +++ b/src/ejabberd_router_multicast.erl @@ -5,7 +5,7 @@ %%% Created : 11 Aug 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_router_redis.erl b/src/ejabberd_router_redis.erl index 450aa4b6b..0ddd63aa7 100644 --- a/src/ejabberd_router_redis.erl +++ b/src/ejabberd_router_redis.erl @@ -3,7 +3,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_router_sql.erl b/src/ejabberd_router_sql.erl index 07a3c27f8..2d7631476 100644 --- a/src/ejabberd_router_sql.erl +++ b/src/ejabberd_router_sql.erl @@ -3,7 +3,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,16 +27,20 @@ %% 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"). --include("ejabberd_stacktrace.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( @@ -48,6 +52,23 @@ init() -> 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), @@ -121,13 +142,13 @@ row_to_route(Domain, {ServerHost, NodeS, PidS, LocalHintS} = Row) -> local_hint = dec_local_hint(LocalHintS)}] catch _:{bad_node, _} -> []; - ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?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)]), - [] + 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 b2b078098..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-2022 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,8 +25,6 @@ -module(ejabberd_s2s). --protocol({xep, 220, '1.1'}). - -author('alexey@process-one.net'). -behaviour(gen_server). @@ -43,7 +41,7 @@ external_host_overloaded/1, is_temporarly_blocked/1, 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]). + host_up/1, host_down/1, queue_type/1, register_connection/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, @@ -55,7 +53,7 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). --include("ejabberd_stacktrace.hrl"). + -include("translate.hrl"). -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER, 1). @@ -130,6 +128,10 @@ get_connections_pids(FromTo) -> [] 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). @@ -228,6 +230,8 @@ init([]) -> 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}. @@ -237,15 +241,19 @@ handle_cast(Msg, State) -> {noreply, State}. 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({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 ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + 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({'DOWN', _Ref, process, Pid, _Reason}, State) -> @@ -474,6 +482,20 @@ new_connection_int(MyServer, Server, From, FromTo, [] 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 ejabberd_shaper:match(From, max_s2s_connections, jid:make(To)) of diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index b3ff0c2b4..c985e3afc 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -2,7 +2,7 @@ %%% Created : 12 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -138,7 +138,7 @@ process_closed(#{server := LServer} = State, Reason) -> %%% xmpp_stream_in callbacks %%%=================================================================== tls_options(#{tls_options := TLSOpts, lserver := LServer, server_host := ServerHost}) -> - ejabberd_s2s:tls_options(LServer, ServerHost, TLSOpts). + [override_cert_purpose | ejabberd_s2s:tls_options(LServer, ServerHost, TLSOpts)]. tls_required(#{server_host := ServerHost}) -> ejabberd_s2s:tls_required(ServerHost). diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index f7998240c..0bb384732 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -2,7 +2,7 @@ %%% Created : 16 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +34,7 @@ terminate/2, code_change/3]). %% Hooks -export([process_auth_result/2, process_closed/2, handle_unexpected_info/2, - handle_unexpected_cast/2, process_downgraded/2]). + handle_unexpected_cast/2, process_downgraded/2, handle_unauthenticated_features/2]). %% API -export([start/3, start_link/3, connect/1, close/1, close/2, stop_async/1, send/2, route/2, establish/1, update_state/2, host_up/1, host_down/1]). @@ -216,6 +216,9 @@ dns_retries(#{server_host := ServerHost}) -> dns_timeout(#{server_host := ServerHost}) -> ejabberd_option:s2s_dns_timeout(ServerHost). +handle_unauthenticated_features(Features, #{server_host := ServerHost} = State) -> + ejabberd_hooks:run_fold(s2s_out_unauthenticated_features, ServerHost, State, [Features]). + handle_auth_success(Mech, #{socket := Socket, ip := IP, remote_server := RServer, server_host := ServerHost, @@ -360,10 +363,17 @@ bounce_message_queue(FromTo, State) -> -spec bounce_packet(xmpp_element(), state()) -> state(). bounce_packet(Pkt, State) when ?is_stanza(Pkt) -> - Lang = xmpp:get_lang(Pkt), - Err = mk_bounce_error(Lang, State), - ejabberd_router:route_error(Pkt, Err), - State; + #{server_host := Host} = State, + case ejabberd_hooks:run_fold( + s2s_out_bounce_packet, Host, State, [Pkt]) of + ignore -> + State; + State2 -> + Lang = xmpp:get_lang(Pkt), + Err = mk_bounce_error(Lang, State2), + ejabberd_router:route_error(Pkt, Err), + State2 + end; bounce_packet(_, State) -> State. diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 5e386ed7d..5947fea2b 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -2,7 +2,7 @@ %%% Created : 11 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +23,7 @@ -behaviour(xmpp_stream_in). -behaviour(ejabberd_listener). --protocol({xep, 114, '1.6'}). +-protocol({xep, 114, '1.6', '0.1.0', "complete", ""}). %% ejabberd_listener callbacks -export([start/3, start_link/3, stop/0, accept/1]). diff --git a/src/ejabberd_shaper.erl b/src/ejabberd_shaper.erl index 4bd5229fa..d45f1dcd8 100644 --- a/src/ejabberd_shaper.erl +++ b/src/ejabberd_shaper.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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,6 +21,7 @@ -export([start_link/0, new/1, update/2, match/3, get_max_rate/1]). -export([reload_from_config/0]). +-export([read_shaper_rules/2]). -export([validator/1, shaper_rules_validator/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -36,6 +37,10 @@ -export_type([shaper/0, shaper_rule/0, shaper_rate/0]). +-ifndef(OTP_BELOW_28). +-dialyzer([no_opaque_union]). +-endif. + %%%=================================================================== %%% API %%%=================================================================== diff --git a/src/ejabberd_sip.erl b/src/ejabberd_sip.erl index a85b139e0..d39aa5d30 100644 --- a/src/ejabberd_sip.erl +++ b/src/ejabberd_sip.erl @@ -5,7 +5,7 @@ %%% Created : 30 Apr 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2022 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 diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 231e4351e..d6d81fbbe 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -5,7 +5,7 @@ %%% Created : 24 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -39,6 +39,7 @@ route/2, open_session/5, open_session/6, + open_session/7, close_session/4, check_in_subscription/2, bounce_offline_message/1, @@ -55,11 +56,14 @@ get_vh_session_number/1, get_vh_by_backend/1, force_update_presence/1, + reset_vcard_xupdate_resend_presence/1, connected_users/0, connected_users_number/0, user_resources/2, kick_user/2, kick_user/3, + kick_user_restuple/2, + kick_users/1, get_session_pid/3, get_session_sid/3, get_session_sids/2, @@ -88,7 +92,7 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). -include("ejabberd_sm.hrl"). --include("ejabberd_stacktrace.hrl"). + -include("translate.hrl"). -callback init() -> ok | {error, any()}. @@ -127,13 +131,14 @@ stop() -> %% @doc route arbitrary term to c2s process(es) route(To, Term) -> try do_route(To, Term), ok - catch ?EX_RULE(E, R, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route term to ~ts:~n" - "** Term = ~p~n" - "** ~ts", - [jid:encode(To), Term, - misc:format_exception(2, E, R, StackTrace)]) + catch + E:R:StackTrace -> + ?ERROR_MSG("Failed to route term to ~ts:~n" + "** Term = ~p~n" + "** ~ts", + [jid:encode(To), + Term, + misc:format_exception(2, E, R, StackTrace)]) end. -spec route(stanza()) -> ok. @@ -147,15 +152,21 @@ route(Packet) -> ok end. --spec open_session(sid(), binary(), binary(), binary(), prio(), info()) -> ok. -open_session(SID, User, Server, Resource, Priority, Info) -> +-spec open_session(sid(), binary(), binary(), binary(), prio(), info(), + {binary(), binary()} | undefined) -> ok. +open_session(SID, User, Server, Resource, Priority, Info, Bind2Tag) -> set_session(SID, User, Server, Resource, Priority, Info), - check_for_sessions_to_replace(User, Server, Resource), + check_for_sessions_to_replace(User, Server, Resource, Bind2Tag), JID = jid:make(User, Server, Resource), ejabberd_hooks:run(sm_register_connection_hook, JID#jid.lserver, [SID, JID, Info]). +-spec open_session(sid(), binary(), binary(), binary(), prio(), info()) -> ok. + +open_session(SID, User, Server, Resource, Priority, Info) -> + open_session(SID, User, Server, Resource, Priority, Info, undefined). + -spec open_session(sid(), binary(), binary(), binary(), info()) -> ok. open_session(SID, User, Server, Resource, Info) -> @@ -197,6 +208,18 @@ bounce_offline_message(Acc) -> Acc. -spec bounce_sm_packet({bounce | term(), stanza()}) -> any(). +bounce_sm_packet({bounce, #message{meta = #{ignore_sm_bounce := true}} = Packet} = Acc) -> + ?DEBUG("Dropping packet to unavailable resource:~n~ts", + [xmpp:pp(Packet)]), + Acc; +bounce_sm_packet({bounce, #iq{meta = #{ignore_sm_bounce := true}} = Packet} = Acc) -> + ?DEBUG("Dropping packet to unavailable resource:~n~ts", + [xmpp:pp(Packet)]), + Acc; +bounce_sm_packet({bounce, #presence{meta = #{ignore_sm_bounce := true}} = Packet} = Acc) -> + ?DEBUG("Dropping packet to unavailable resource:~n~ts", + [xmpp:pp(Packet)]), + Acc; bounce_sm_packet({bounce, Packet} = Acc) -> Lang = xmpp:get_lang(Packet), Txt = ?T("User session not found"), @@ -452,8 +475,17 @@ c2s_handle_info(#{lang := Lang} = State, replaced) -> State1 = State#{replaced => true}, Err = xmpp:serr_conflict(?T("Replaced by new connection"), Lang), {stop, ejabberd_c2s:send(State1, Err)}; -c2s_handle_info(#{lang := Lang} = State, kick) -> +c2s_handle_info(#{lang := Lang, bind2_session_id := {Tag, _}} = State, + {replaced_with_bind_tag, Bind2Tag}) when Tag == Bind2Tag -> + State1 = State#{replaced => true}, + Err = xmpp:serr_conflict(?T("Replaced by new connection"), Lang), + {stop, ejabberd_c2s:send(State1, Err)}; +c2s_handle_info(State, {replaced_with_bind_tag, _}) -> + State; +c2s_handle_info(#{lang := Lang, jid := JID} = State, kick) -> Err = xmpp:serr_policy_violation(?T("has been kicked"), Lang), + ejabberd_hooks:run(sm_kick_user, JID#jid.lserver, + [JID#jid.luser, JID#jid.lserver]), {stop, ejabberd_c2s:send(State, Err)}; c2s_handle_info(#{lang := Lang} = State, {exit, Reason}) -> Err = xmpp:serr_conflict(Reason, Lang), @@ -826,16 +858,18 @@ clean_session_list([S1, S2 | Rest], Res) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% On new session, check if some existing connections need to be replace --spec check_for_sessions_to_replace(binary(), binary(), binary()) -> ok | replaced. -check_for_sessions_to_replace(User, Server, Resource) -> +-spec check_for_sessions_to_replace(binary(), binary(), binary(), + {binary(), binary()} | undefined) -> ok | replaced. +check_for_sessions_to_replace(User, Server, Resource, Bind2Tag) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), - check_existing_resources(LUser, LServer, LResource), + check_existing_resources(LUser, LServer, LResource, Bind2Tag), check_max_sessions(LUser, LServer). --spec check_existing_resources(binary(), binary(), binary()) -> ok. -check_existing_resources(LUser, LServer, LResource) -> +-spec check_existing_resources(binary(), binary(), binary(), + {binary(), binary()} | undefined) -> ok. +check_existing_resources(LUser, LServer, LResource, undefined) -> Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer, LResource), if Ss == [] -> ok; @@ -847,7 +881,20 @@ check_existing_resources(LUser, LServer, LResource) -> (_) -> ok end, SIDs) - end. + end; +check_existing_resources(LUser, LServer, LResource, {Tag, Hash}) -> + Mod = get_sm_backend(LServer), + Ss = get_sessions(Mod, LUser, LServer), + lists:foreach( + fun(#session{sid = {_, Pid}, usr = {_, _, Res}}) + when Pid /= self(), Res == LResource -> + ejabberd_c2s:route(Pid, replaced); + (#session{sid = {_, Pid}, usr = {_, _, Res}}) + when Pid /= self(), binary_part(Res, size(Res), -size(Hash)) == Hash -> + ejabberd_c2s:route(Pid, {replaced_with_bind_tag, Tag}); + (_) -> + ok + end, Ss). -spec is_existing_resource(binary(), binary(), binary()) -> boolean(). @@ -897,6 +944,15 @@ force_update_presence({LUser, LServer}) -> end, Ss). +-spec reset_vcard_xupdate_resend_presence({binary(), binary()}) -> ok. +reset_vcard_xupdate_resend_presence({LUser, LServer}) -> + Mod = get_sm_backend(LServer), + Ss = get_sessions(Mod, LUser, LServer), + lists:foreach( + fun(#session{sid = {_, Pid}}) -> + ejabberd_c2s:reset_vcard_xupdate_resend_presence(Pid) + end, Ss). + -spec get_sm_backend(binary()) -> module(). get_sm_backend(Host) -> @@ -982,8 +1038,8 @@ get_commands_spec() -> desc = "List all established sessions", policy = admin, module = ?MODULE, function = connected_users, args = [], - result_desc = "List of users sessions", - result_example = [<<"user1@example.com">>, <<"user2@example.com">>], + result_desc = "List of users sessions full JID", + result_example = [<<"user1@example.com/Home">>, <<"user2@example.com/54134">>], result = {connected_users, {list, {sessions, string}}}}, #ejabberd_commands{name = connected_users_number, tags = [session, statistics], desc = "Get the number of established sessions", @@ -1008,7 +1064,30 @@ get_commands_spec() -> args_example = [<<"user1">>, <<"example.com">>], result_desc = "Number of resources that were kicked", result_example = 3, - result = {num_resources, integer}}]. + result = {num_resources, integer}}, + + #ejabberd_commands{name = kick_user, tags = [session], + desc = "Disconnect user's active sessions", + module = ?MODULE, function = kick_user_restuple, + version = 2, + note = "modified in 24.06", + args = [{user, binary}, {host, binary}], + args_desc = ["User name", "Server name"], + args_example = [<<"user1">>, <<"example.com">>], + result_desc = "The result text indicates the number of sessions that were kicked", + result_example = {ok, <<"Kicked sessions: 2">>}, + result = {res, restuple}}, + + #ejabberd_commands{name = kick_users, tags = [session], + desc = "Disconnect all given host users' active sessions", + module = ?MODULE, function = kick_users, + note = "added in 25.04", + args = [{host, binary}], + args_desc = ["Server name"], + args_example = [<<"example.com">>], + result_desc = "Number of sessions that were kicked", + result_example = 3, + result = {num_sessions, integer}}]. -spec connected_users() -> [binary()]. @@ -1043,5 +1122,14 @@ kick_user(User, Server, Resource) -> Pid -> ejabberd_c2s:route(Pid, kick) end. +kick_user_restuple(User, Server) -> + NumberBin = integer_to_binary(kick_user(User, Server)), + {ok, <<"Kicked sessions: ", NumberBin/binary>>}. + +-spec kick_users(binary()) -> non_neg_integer(). +kick_users(Server) -> + length([kick_user(U, S, R) || {U, S, R} <-get_vh_session_list(Server)]). + + make_sid() -> {misc:unique_timestamp(), self()}. diff --git a/src/ejabberd_sm_mnesia.erl b/src/ejabberd_sm_mnesia.erl index ec321271b..a82687963 100644 --- a/src/ejabberd_sm_mnesia.erl +++ b/src/ejabberd_sm_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 9 Mar 2015 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -112,6 +112,7 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + ?INFO_MSG("Node ~p has left our Mnesia SM tables", [Node]), Sessions = ets:select( session, @@ -125,6 +126,9 @@ handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> mnesia:dirty_delete_object(S) end, Sessions), {noreply, State}; +handle_info({mnesia_system_event, {mnesia_up, Node}}, State) -> + ?INFO_MSG("Node ~p joined our Mnesia SM tables", [Node]), + {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl index 5f612b926..8e63cdcec 100644 --- a/src/ejabberd_sm_redis.erl +++ b/src/ejabberd_sm_redis.erl @@ -4,7 +4,7 @@ %%% Created : 11 Mar 2015 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_sm_sql.erl b/src/ejabberd_sm_sql.erl index b58b66f0d..e3f35f505 100644 --- a/src/ejabberd_sm_sql.erl +++ b/src/ejabberd_sm_sql.erl @@ -4,7 +4,7 @@ %%% Created : 9 Mar 2015 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +34,7 @@ get_sessions/0, get_sessions/1, get_sessions/2]). +-export([sql_schemas/0]). -include("ejabberd_sm.hrl"). -include("logger.hrl"). @@ -48,6 +49,7 @@ init() -> ?DEBUG("Cleaning SQL SM table...", []), lists:foldl( fun(Host, ok) -> + ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), case ejabberd_sql:sql_query( Host, ?SQL("delete from sm where node=%(Node)s")) of {updated, _} -> @@ -60,6 +62,29 @@ init() -> Err end, ok, ejabberd_sm:get_vh_by_backend(?MODULE)). +sql_schemas() -> + [#sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"sm">>, + columns = + [#sql_column{name = <<"usec">>, type = bigint}, + #sql_column{name = <<"pid">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"priority">>, type = text}, + #sql_column{name = <<"info">>, type = text}], + indices = [#sql_index{ + columns = [<<"usec">>, <<"pid">>], + unique = true}, + #sql_index{ + columns = [<<"node">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"username">>]}]}]}]. + set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, priority = Priority, info = Info}) -> InfoS = misc:term_to_expr(Info), @@ -84,7 +109,7 @@ set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, delete_session(#session{usr = {_, LServer, _}, sid = {Now, Pid}}) -> TS = now_to_timestamp(Now), - PidS = list_to_binary(erlang:pid_to_list(Pid)), + PidS = misc:encode_pid(Pid), case ejabberd_sql:sql_query( LServer, ?SQL("delete from sm where usec=%(TS)d and pid=%(PidS)s")) of diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index 3cd2dc345..922fec14a 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -32,9 +32,12 @@ %% External exports -export([start_link/2, sql_query/2, + sql_query/3, sql_query_t/1, sql_transaction/2, + sql_transaction/4, sql_bloc/2, + sql_bloc/3, abort/1, restart/1, use_new_schema/0, @@ -56,7 +59,8 @@ init_mssql/1, keep_alive/2, to_list/2, - to_array/2]). + to_array/2, + parse_mysql_version/2]). %% gen_fsm callbacks -export([init/1, handle_event/3, handle_sync_event/4, @@ -66,18 +70,36 @@ -export([connecting/2, connecting/3, session_established/2, session_established/3]). +-ifdef(OTP_BELOW_28). +-ifdef(OTP_BELOW_26). +%% OTP 25 or lower +-type(odbc_connection_reference() :: pid()). +-type(db_ref_pid() :: pid()). +-else. +%% OTP 26 or 27 +-type(odbc_connection_reference() :: odbc:connection_reference()). +-type(db_ref_pid() :: pid()). +-endif. +-else. +%% OTP 28 or higher +-nominal(odbc_connection_reference() :: odbc:connection_reference()). +-nominal(db_ref_pid() :: pid()). +-dialyzer([no_opaque_union]). +-endif. + -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). --include("ejabberd_stacktrace.hrl"). + -record(state, - {db_ref :: undefined | pid(), + {db_ref :: undefined | db_ref_pid() | odbc_connection_reference(), db_type = odbc :: pgsql | mysql | sqlite | odbc | mssql, - db_version :: undefined | non_neg_integer(), + db_version :: undefined | non_neg_integer() | {non_neg_integer(), atom(), non_neg_integer()}, reconnect_count = 0 :: non_neg_integer(), host :: binary(), pending_requests :: p1_queue:queue(), - overload_reported :: undefined | integer()}). + overload_reported :: undefined | integer(), + timeout :: pos_integer()}). -define(STATE_KEY, ejabberd_sql_state). -define(NESTING_KEY, ejabberd_sql_nesting_level). @@ -93,15 +115,16 @@ -endif. -type state() :: #state{}. --type sql_query_simple() :: [sql_query() | binary()] | #sql_query{} | - fun(() -> any()) | fun((atom(), _) -> any()). --type sql_query() :: sql_query_simple() | - [{atom() | {atom(), any()}, sql_query_simple()}]. --type sql_query_result() :: {updated, non_neg_integer()} | - {error, binary() | atom()} | - {selected, [binary()], [[binary()]]} | - {selected, [any()]} | - ok. +-type sql_query_simple(T) :: [sql_query(T) | binary()] | binary() | + #sql_query{} | + fun(() -> T) | fun((atom(), _) -> T). +-type sql_query(T) :: sql_query_simple(T) | + [{atom() | {atom(), any()}, sql_query_simple(T)}]. +-type sql_query_result(T) :: {updated, non_neg_integer()} | + {error, binary() | atom()} | + {selected, [binary()], [[binary()]]} | + {selected, [any()]} | + T. %%%---------------------------------------------------------------------- %%% API @@ -112,35 +135,48 @@ start_link(Host, I) -> p1_fsm:start_link({local, Proc}, ?MODULE, [Host], fsm_limit_opts() ++ ?FSMOPTS). --spec sql_query(binary(), sql_query()) -> sql_query_result(). +-spec sql_query(binary(), sql_query(T), pos_integer()) -> sql_query_result(T). +sql_query(Host, Query, Timeout) -> + sql_call(Host, {sql_query, Query}, Timeout). + +-spec sql_query(binary(), sql_query(T)) -> sql_query_result(T). sql_query(Host, Query) -> - sql_call(Host, {sql_query, Query}). + sql_query(Host, Query, query_timeout(Host)). %% SQL transaction based on a list of queries %% This function automatically --spec sql_transaction(binary(), [sql_query()] | fun(() -> any())) -> - {atomic, any()} | +-spec sql_transaction(binary(), [sql_query(T)] | fun(() -> T), pos_integer(), pos_integer()) -> + {atomic, T} | {aborted, any()}. -sql_transaction(Host, Queries) +sql_transaction(Host, Queries, Timeout, Restarts) when is_list(Queries) -> F = fun () -> lists:foreach(fun (Query) -> sql_query_t(Query) end, Queries) end, - sql_transaction(Host, F); + sql_transaction(Host, F, Timeout, Restarts); %% SQL transaction, based on a erlang anonymous function (F = fun) -sql_transaction(Host, F) when is_function(F) -> - case sql_call(Host, {sql_transaction, F}) of +sql_transaction(Host, F, Timeout, Restarts) when is_function(F) -> + case sql_call(Host, {sql_transaction, F, Restarts}, Timeout) of {atomic, _} = Ret -> Ret; {aborted, _} = Ret -> Ret; Err -> {aborted, Err} end. -%% SQL bloc, based on a erlang anonymous function (F = fun) -sql_bloc(Host, F) -> sql_call(Host, {sql_bloc, F}). +-spec sql_transaction(binary(), [sql_query(T)] | fun(() -> T)) -> + {atomic, T} | + {aborted, any()}. +sql_transaction(Host, Queries) -> + sql_transaction(Host, Queries, query_timeout(Host), ?MAX_TRANSACTION_RESTARTS). -sql_call(Host, Msg) -> - Timeout = query_timeout(Host), +%% SQL bloc, based on a erlang anonymous function (F = fun) +sql_bloc(Host, F, Timeout) -> + sql_call(Host, {sql_bloc, F}, Timeout). + +sql_bloc(Host, F) -> + sql_bloc(Host, F, query_timeout(Host)). + +sql_call(Host, Msg, Timeout) -> case get(?STATE_KEY) of undefined -> sync_send_event(Host, @@ -158,8 +194,8 @@ keep_alive(Host, Proc) -> Timeout) of {selected,_,[[<<"1">>]]} -> ok; - _Err -> - ?ERROR_MSG("Keep alive query failed, closing connection: ~p", [_Err]), + Err -> + ?ERROR_MSG("Keep alive query failed, closing connection: ~p", [Err]), sync_send_event(Proc, force_timeout, Timeout) end. @@ -177,7 +213,7 @@ sync_send_event(Proc, Msg, Timeout) -> {error, Reason} end. --spec sql_query_t(sql_query()) -> sql_query_result(). +-spec sql_query_t(sql_query(T)) -> sql_query_result(T). %% This function is intended to be used from inside an sql_transaction: sql_query_t(Query) -> QRes = sql_query_internal(Query), @@ -347,7 +383,8 @@ init([Host]) -> QueueType = ejabberd_option:sql_queue_type(Host), {ok, connecting, #state{db_type = DBType, host = Host, - pending_requests = p1_queue:new(QueueType, max_fsm_queue())}}. + pending_requests = p1_queue:new(QueueType, max_fsm_queue()), + timeout = query_timeout(Host)}}. connecting(connect, #state{host = Host} = State) -> ConnectRes = case db_opts(Host) of @@ -488,10 +525,12 @@ handle_reconnect(Reason, #state{host = Host, reconnect_count = RC} = State) -> _ -> ok end, p1_fsm:send_event_after(StartInterval, connect), - {next_state, connecting, State#state{reconnect_count = RC + 1}}. + {next_state, connecting, State#state{reconnect_count = RC + 1, + timeout = query_timeout(Host)}}. run_sql_cmd(Command, From, State, Timestamp) -> - case current_time() >= Timestamp of + CT = current_time(), + case CT >= Timestamp of true -> State1 = report_overload(State), {next_state, session_established, State1}; @@ -502,30 +541,31 @@ run_sql_cmd(Command, From, State, Timestamp) -> State#state.pending_requests), handle_reconnect(Reason, State#state{pending_requests = PR}) after 0 -> + Timeout = min(query_timeout(State#state.host), Timestamp - CT), put(?NESTING_KEY, ?TOP_LEVEL_TXN), - put(?STATE_KEY, State), + put(?STATE_KEY, State#state{timeout = Timeout}), abort_on_driver_error(outer_op(Command), From, Timestamp) end end. %% @doc Only called by handle_call, only handles top level operations. --spec outer_op(Op::{atom(), binary()}) -> +-spec outer_op(Op::{atom(), binary()} | {sql_transaction, binary(), pos_integer()}) -> {error, Reason::binary()} | {aborted, Reason::binary()} | {atomic, Result::any()}. outer_op({sql_query, Query}) -> sql_query_internal(Query); -outer_op({sql_transaction, F}) -> - outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, <<"">>); +outer_op({sql_transaction, F, Restarts}) -> + outer_transaction(F, 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}) -> +nested_op({sql_transaction, F, Restarts}) -> NestingLevel = get(?NESTING_KEY), if NestingLevel =:= (?TOP_LEVEL_TXN) -> - outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, <<"">>); - true -> inner_transaction(F) + outer_transaction(F, Restarts, <<"">>); + true -> inner_transaction(F) end; nested_op({sql_bloc, F}) -> execute_bloc(F). @@ -576,19 +616,20 @@ outer_transaction(F, NRestarts, _Reason) -> {atomic, Res} end catch - ?EX_RULE(throw, {aborted, Reason}, _) when NRestarts > 0 -> - maybe_restart_transaction(F, NRestarts, Reason, true); - ?EX_RULE(throw, {aborted, Reason}, Stack) when NRestarts =:= 0 -> - StackTrace = ?EX_STACK(Stack), - ?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, - StackTrace, get(?STATE_KEY)]), - maybe_restart_transaction(F, NRestarts, Reason, true); - ?EX_RULE(exit, Reason, _) -> - maybe_restart_transaction(F, 0, Reason, true) + throw:{aborted, Reason}:_ when NRestarts > 0 -> + maybe_restart_transaction(F, NRestarts, Reason, true); + throw:{aborted, Reason}:StackTrace 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, + StackTrace, + get(?STATE_KEY)]), + maybe_restart_transaction(F, NRestarts, Reason, true); + _:Reason:_ -> + maybe_restart_transaction(F, 0, Reason, true) end end. @@ -683,7 +724,14 @@ sql_query_internal(#sql_query{} = Query) -> pgsql_sql_query(Query) end; mysql -> - generic_sql_query(Query); + case {Query#sql_query.flags, ejabberd_option:sql_prepared_statements(State#state.host)} of + {1, _} -> + generic_sql_query(Query); + {_, false} -> + generic_sql_query(Query); + _ -> + mysql_prepared_execute(Query, State) + end; sqlite -> sqlite_sql_query(Query) end @@ -695,10 +743,9 @@ sql_query_internal(#sql_query{} = Query) -> {error, <<"terminated unexpectedly">>}; exit:{shutdown, _} -> {error, <<"shutdown">>}; - ?EX_RULE(Class, Reason, Stack) -> - StackTrace = ?EX_STACK(Stack), + Class:Reason:StackTrace -> ?ERROR_MSG("Internal error while processing SQL query:~n** ~ts", - [misc:format_exception(2, Class, Reason, StackTrace)]), + [misc:format_exception(2, Class, Reason, StackTrace)]), {error, <<"internal error">>} end, check_error(Res, Query); @@ -711,7 +758,7 @@ sql_query_internal(F) when is_function(F) -> sql_query_internal(Query) -> State = get(?STATE_KEY), ?DEBUG("SQL: \"~ts\"", [Query]), - QueryTimeout = query_timeout(State#state.host), + QueryTimeout = State#state.timeout, Res = case State#state.db_type of odbc -> to_odbc(odbc:sql_query(State#state.db_ref, [Query], @@ -862,6 +909,24 @@ pgsql_execute_sql_query(SQLQuery, State) -> Res = pgsql_execute_to_odbc(ExecuteRes), sql_query_format_res(Res, SQLQuery). +mysql_prepared_execute(#sql_query{hash = Hash} = Query, State) -> + ValEsc = #sql_escape{like_escape = fun() -> ignore end, _ = fun(X) -> X end}, + TypesEsc = #sql_escape{string = fun(_) -> string end, + integer = fun(_) -> integer end, + boolean = fun(_) -> bool end, + in_array_string = fun(_) -> string end, + like_escape = fun() -> ignore end}, + Val = [X || X <- (Query#sql_query.args)(ValEsc), X /= ignore], + Types = [X || X <- (Query#sql_query.args)(TypesEsc), X /= ignore], + QueryFn = fun() -> + PrepEsc = #sql_escape{like_escape = fun() -> <<>> end, _ = fun(_) -> <<"?">> end}, + (Query#sql_query.format_query)((Query#sql_query.args)(PrepEsc)) + end, + QueryTimeout = query_timeout(State#state.host), + Res = p1_mysql_conn:prepared_query(State#state.db_ref, QueryFn, Hash, Val, Types, + self(), [{timeout, QueryTimeout - 1000}]), + Res2 = mysql_to_odbc(Res), + sql_query_format_res(Res2, Query). sql_query_format_res({selected, _, Rows}, SQLQuery) -> Res = @@ -870,12 +935,11 @@ sql_query_format_res({selected, _, Rows}, SQLQuery) -> try [(SQLQuery#sql_query.format_res)(Row)] catch - ?EX_RULE(Class, Reason, Stack) -> - StackTrace = ?EX_STACK(Stack), + Class:Reason:StackTrace -> ?ERROR_MSG("Error while processing SQL query result:~n" "** Row: ~p~n** ~ts", [Row, - misc:format_exception(2, Class, Reason, StackTrace)]), + misc:format_exception(2, Class, Reason, StackTrace)]), [] end end, Rows), @@ -1005,22 +1069,14 @@ sqlite_to_odbc(_Host, _) -> %% Open a database connection to PostgreSQL pgsql_connect(Server, Port, DB, Username, Password, ConnectTimeout, Transport, SSLOpts) -> - case pgsql:connect([{host, Server}, - {database, DB}, - {user, Username}, - {password, Password}, - {port, Port}, - {transport, Transport}, - {connect_timeout, ConnectTimeout}, - {as_binary, true}|SSLOpts]) of - {ok, Ref} -> - pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>, - <<"standard_conforming_strings='off';">>]), - pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]), - {ok, Ref}; - Err -> - Err - end. + pgsql:connect([{host, Server}, + {database, DB}, + {user, Username}, + {password, Password}, + {port, Port}, + {transport, Transport}, + {connect_timeout, ConnectTimeout}, + {as_binary, true}|SSLOpts]). %% Convert PostgreSQL query result to Erlang ODBC result formalism pgsql_to_odbc({ok, PGSQLResult}) -> @@ -1061,10 +1117,10 @@ pgsql_execute_to_odbc(_) -> {updated, undefined}. %% part of init/1 %% Open a database connection to MySQL -mysql_connect(Server, Port, DB, Username, Password, ConnectTimeout, Transport, _) -> +mysql_connect(Server, Port, DB, Username, Password, ConnectTimeout, Transport, SSLOpts0) -> SSLOpts = case Transport of ssl -> - [ssl_required]; + [ssl_required|SSLOpts0]; _ -> [] end, @@ -1103,19 +1159,48 @@ mysql_to_odbc(ok) -> mysql_item_to_odbc(Columns, Recs) -> {selected, [element(2, Column) || Column <- Columns], Recs}. -to_odbc({selected, Columns, Recs}) -> - Rows = [lists:map( - fun(I) when is_integer(I) -> - integer_to_binary(I); - (B) -> - B - end, Row) || Row <- Recs], - {selected, [list_to_binary(C) || C <- Columns], Rows}; +to_odbc({selected, Columns, Rows}) -> + Rows2 = lists:map( + fun(Row) -> + Row2 = if is_tuple(Row) -> tuple_to_list(Row); + is_list(Row) -> Row + end, + lists:map( + fun(I) when is_integer(I) -> integer_to_binary(I); + (B) -> B + end, Row2) + end, Rows), + {selected, [list_to_binary(C) || C <- Columns], Rows2}; to_odbc({error, Reason}) when is_list(Reason) -> {error, list_to_binary(Reason)}; to_odbc(Res) -> Res. +parse_mysql_version(SVersion, DefaultUpsert) -> + case re:run(SVersion, <<"(\\d+)\\.(\\d+)(?:\\.(\\d+))?(?:-([^-]*))?">>, + [{capture, all_but_first, binary}]) of + {match, [V1, V2, V3, Type]} -> + V = ((bin_to_int(V1)*1000)+bin_to_int(V2))*1000+bin_to_int(V3), + TypeA = binary_to_atom(Type, utf8), + Flags = case TypeA of + 'MariaDB' -> DefaultUpsert; + _ when V >= 5007026 andalso V < 8000000 -> 1; + _ when V >= 8000020 -> 1; + _ -> DefaultUpsert + end, + {ok, {V, TypeA, Flags}}; + {match, [V1, V2, V3]} -> + V = ((bin_to_int(V1)*1000)+bin_to_int(V2))*1000+bin_to_int(V3), + Flags = case V of + _ when V >= 5007026 andalso V < 8000000 -> 1; + _ when V >= 8000020 -> 1; + _ -> DefaultUpsert + end, + {ok, {V, unknown, Flags}}; + _ -> + error + end. + get_db_version(#state{db_type = pgsql} = State) -> case pgsql:squery(State#state.db_ref, <<"select current_setting('server_version_num')">>) of @@ -1131,9 +1216,33 @@ get_db_version(#state{db_type = pgsql} = State) -> ?WARNING_MSG("Error getting pgsql version: ~p", [Res]), State end; +get_db_version(#state{db_type = mysql, host = Host} = State) -> + DefaultUpsert = case lists:member(mysql_alternative_upsert, ejabberd_option:sql_flags(Host)) of + true -> 1; + _ -> 0 + end, + case mysql_to_odbc(p1_mysql_conn:squery(State#state.db_ref, + [<<"select version();">>], self(), + [{timeout, 5000}, + {result_type, binary}])) of + {selected, _, [SVersion]} -> + case parse_mysql_version(SVersion, DefaultUpsert) of + {ok, V} -> + State#state{db_version = V}; + error -> + ?WARNING_MSG("Error parsing mysql version: ~p", [SVersion]), + State + end; + Res -> + ?WARNING_MSG("Error getting mysql version: ~p", [Res]), + State + end; get_db_version(State) -> State. +bin_to_int(<<>>) -> 0; +bin_to_int(V) -> binary_to_integer(V). + log(Level, Format, Args) -> case Level of debug -> ?DEBUG(Format, Args); @@ -1167,9 +1276,19 @@ db_opts(Host) -> SSLOpts = get_ssl_opts(Transport, Host), case Type of mssql -> - [mssql, <<"DRIVER=ODBC;SERVER=", Server/binary, ";UID=", User/binary, - ";DATABASE=", DB/binary ,";PWD=", Pass/binary, - ";PORT=", (integer_to_binary(Port))/binary ,";CLIENT_CHARSET=UTF-8;">>, Timeout]; + case odbc_server_is_connstring(Server) of + true -> + [mssql, Server, Timeout]; + false -> + Encryption = case Transport of + tcp -> <<"">>; + ssl -> <<";ENCRYPTION=require;ENCRYPT=yes">> + end, + [mssql, <<"DRIVER=ODBC;SERVER=", Server/binary, ";DATABASE=", DB/binary, + ";UID=", User/binary, ";PWD=", Pass/binary, + ";PORT=", (integer_to_binary(Port))/binary, Encryption/binary, + ";CLIENT_CHARSET=UTF-8;">>, Timeout] + end; _ -> [Type, Server, Port, DB, User, Pass, Timeout, Transport, SSLOpts] end @@ -1179,6 +1298,8 @@ warn_if_ssl_unsupported(tcp, _) -> ok; warn_if_ssl_unsupported(ssl, pgsql) -> ok; +warn_if_ssl_unsupported(ssl, mssql) -> + ok; warn_if_ssl_unsupported(ssl, mysql) -> ok; warn_if_ssl_unsupported(ssl, Type) -> @@ -1206,12 +1327,12 @@ get_ssl_opts(ssl, Host) -> Opts2 end; false -> - Opts2 + [{verify, verify_none}|Opts2] end; get_ssl_opts(tcp, _) -> []. -init_mssql(Host) -> +init_mssql_odbcinst(Host) -> Driver = ejabberd_option:sql_odbc_driver(Host), ODBCINST = io_lib:fwrite("[ODBC]~n" "Driver = ~s~n", [Driver]), @@ -1233,6 +1354,19 @@ init_mssql(Host) -> Err end. +init_mssql(Host) -> + Server = ejabberd_option:sql_server(Host), + case odbc_server_is_connstring(Server) of + true -> ok; + false -> init_mssql_odbcinst(Host) + end. + +odbc_server_is_connstring(Server) -> + case binary:match(Server, <<"=">>) of + nomatch -> false; + _ -> true + end. + write_file_if_new(File, Payload) -> case filelib:is_file(File) of true -> ok; @@ -1241,7 +1375,7 @@ write_file_if_new(File, Payload) -> tmp_dir() -> case os:type() of - {win32, _} -> filename:join([os:getenv("HOME"), "conf"]); + {win32, _} -> filename:join([misc:get_home(), "conf"]); _ -> filename:join(["/tmp", "ejabberd"]) end. diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl index 0f8465942..365cc2b47 100644 --- a/src/ejabberd_sql_pt.erl +++ b/src/ejabberd_sql_pt.erl @@ -5,7 +5,7 @@ %%% Created : 20 Jan 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -42,7 +42,8 @@ used_vars = [], use_new_schema, need_timestamp_pass = false, - need_array_pass = false}). + need_array_pass = false, + has_list = false}). -define(QUERY_RECORD, "sql_query"). @@ -268,9 +269,12 @@ parse1([$@, $( | S], Acc, State) -> Convert = case Type of integer -> - erl_syntax:application( - erl_syntax:atom(binary_to_integer), - [EVar]); + erl_syntax:if_expr([ + erl_syntax:clause( + [erl_syntax:application(erl_syntax:atom(is_binary), [EVar])], + [erl_syntax:application(erl_syntax:atom(binary_to_integer), [EVar])]), + erl_syntax:clause([erl_syntax:atom(true)], [EVar]) + ]); string -> EVar; timestamp -> @@ -339,6 +343,7 @@ parse1([$%, $( | S], Acc, State) -> erl_syntax:variable(Name)]), State2#state{'query' = [[{var, Var, Type}] | State2#state.'query'], need_array_pass = true, + has_list = true, args = [[Convert, ConvertArr] | State2#state.args], params = [Var | State2#state.params], param_pos = State2#state.param_pos + 1, @@ -467,6 +472,7 @@ make_sql_query(State, Type) -> Hash = erlang:phash2(State#state{loc = undefined, use_new_schema = true}), SHash = <<"Q", (integer_to_binary(Hash))/binary>>, Query = pack_query(State#state.'query'), + Flags = case State#state.has_list of true -> 1; _ -> 0 end, EQuery = lists:flatmap( fun({str, S}) -> @@ -515,6 +521,9 @@ make_sql_query(State, Type) -> none, [erl_syntax:tuple(State#state.res)] )])), + erl_syntax:record_field( + erl_syntax:atom(flags), + erl_syntax:abstract(Flags)), erl_syntax:record_field( erl_syntax:atom(loc), erl_syntax:abstract({get(?MOD), State#state.loc})) @@ -567,7 +576,6 @@ parse_upsert_field1([$= | S], Acc, ParamPos, Loc) -> parse_upsert_field1([C | S], Acc, ParamPos, Loc) -> parse_upsert_field1(S, [C | Acc], ParamPos, Loc). - make_sql_upsert(Table, ParseRes, Pos) -> check_upsert(ParseRes, Pos), erl_syntax:fun_expr( @@ -587,6 +595,11 @@ make_sql_upsert(Table, ParseRes, Pos) -> erl_syntax:integer(90100))], [make_sql_upsert_pgsql901(Table, ParseRes), erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:atom(mysql), erl_syntax:tuple([erl_syntax:underscore(), erl_syntax:underscore(), erl_syntax:integer(1)])], + [], + [make_sql_upsert_mysql_select(Table, ParseRes), + erl_syntax:atom(ok)]), erl_syntax:clause( [erl_syntax:atom(mysql), erl_syntax:underscore()], [], @@ -682,6 +695,66 @@ make_sql_upsert_insert(Table, ParseRes) -> ]), State. +make_sql_upsert_select(Table, ParseRes) -> + {Fields0, Where0} = + lists:foldl( + fun({Field, key, ST}, {Fie, Whe}) -> + {Fie, [ST#state{ + 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query'}] ++ Whe}; + ({Field, {true}, ST}, {Fie, Whe}) -> + {[ST#state{ + 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query'}] ++ Fie, Whe}; + (_, Acc) -> + Acc + end, {[], []}, ParseRes), + Fields = join_states(Fields0, " AND "), + Where = join_states(Where0, " AND "), + State = + concat_states( + [#state{'query' = [{str, "SELECT "}], + res_vars = [erl_syntax:variable("__VSel")], + res = [erl_syntax:application( + erl_syntax:atom(ejabberd_sql), + erl_syntax:atom(to_bool), + [erl_syntax:variable("__VSel")])]}, + Fields, + #state{'query' = [{str, " FROM "}, {str, Table}, {str, " WHERE "}]}, + Where + ]), + State. + +make_sql_upsert_mysql_select(Table, ParseRes) -> + Select = make_sql_query(make_sql_upsert_select(Table, ParseRes)), + Insert = make_sql_query(make_sql_upsert_insert(Table, ParseRes)), + Update = make_sql_query(make_sql_upsert_update(Table, ParseRes)), + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ejabberd_sql), + erl_syntax:atom(sql_query_t), + [Select]), + [erl_syntax:clause( + [erl_syntax:tuple([erl_syntax:atom(selected), erl_syntax:list([])])], + none, + [erl_syntax:application( + erl_syntax:atom(ejabberd_sql), + erl_syntax:atom(sql_query_t), + [Insert])]), + erl_syntax:clause( + [erl_syntax:abstract({selected, [{true}]})], + [], + [erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:tuple([erl_syntax:atom(selected), erl_syntax:underscore()])], + none, + [erl_syntax:application( + erl_syntax:atom(ejabberd_sql), + erl_syntax:atom(sql_query_t), + [Update])]), + erl_syntax:clause( + [erl_syntax:variable("__SelectRes")], + none, + [erl_syntax:variable("__SelectRes")])]). + make_sql_upsert_mysql(Table, ParseRes) -> Vals = lists:map( diff --git a/src/ejabberd_sql_schema.erl b/src/ejabberd_sql_schema.erl new file mode 100644 index 000000000..8d2830101 --- /dev/null +++ b/src/ejabberd_sql_schema.erl @@ -0,0 +1,1279 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_sql.erl +%%% Author : Alexey Shchepin +%%% Purpose : SQL schema versioning +%%% Created : 15 Aug 2023 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_sql_schema). + +-author('alexey@process-one.net'). + +-export([start/1, update_schema/3, + get_table_schema/2, get_table_indices/2, print_schema/3, + test/0]). + +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("ejabberd_ctl.hrl"). + +start(Host) -> + case should_update_schema(Host) of + true -> + case table_exists(Host, <<"schema_version">>) of + true -> + ok; + false -> + SchemaInfo = + ejabberd_sql:sql_query( + Host, + fun(DBType, DBVersion) -> + #sql_schema_info{ + db_type = DBType, + db_version = DBVersion, + new_schema = ejabberd_sql:use_new_schema()} + end), + Table = filter_table_sh(SchemaInfo, schema_table()), + Res = create_table(Host, SchemaInfo, Table), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to create table ~s: ~p", + [Table#sql_table.name, Error]), + {error, Error}; + _ -> + ok + end + end; + false -> + ok + end. + +schema_table() -> + #sql_table{ + name = <<"schema_version">>, + columns = [#sql_column{name = <<"module">>, type = text}, + #sql_column{name = <<"version">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"module">>], + unique = true}]}. + +get_table_schema(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(pgsql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select " + " @(a.attname)s, " + " @(pg_catalog.format_type(a.atttypid, a.atttypmod))s " + " from " + " pg_class t, " + " pg_attribute a " + " where " + " a.attrelid = t.oid and " + " a.attnum > 0 and " + " a.atttypid > 0 and " + " t.relkind = 'r' and " + " t.relname=%(Table)s")) + of + {selected, Cols} -> + [{Col, string_to_type(SType)} || {Col, SType} <- Cols] + end; + (sqlite, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(i.name)s, @(i.type)s" + " from pragma_table_info(%(Table)s) as i")) + of + {selected, Cols} -> + [{Col, string_to_type(SType)} || {Col, SType} <- Cols] + end; + (mysql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(column_name)s, @(column_type)s" + " from information_schema.columns" + " where table_name=%(Table)s and" + " table_schema=schema()" + " order by ordinal_position")) + of + {selected, Cols} -> + [{Col, string_to_type(SType)} || {Col, SType} <- Cols] + end + end). + +get_table_indices(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(pgsql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select " + " @(i.relname)s, " + " @(a.attname)s " + " from " + " pg_class t, " + " pg_class i, " + " pg_index ix, " + " pg_attribute a " + " where " + " t.oid = ix.indrelid and " + " i.oid = ix.indexrelid and " + " a.attrelid = t.oid and " + " a.attnum = ANY(ix.indkey) and " + " t.relkind = 'r' and " + " t.relname=%(Table)s " + " order by " + " i.relname, " + " array_position(ix.indkey, a.attnum)")) + of + {selected, Cols} -> + Indices = + lists:foldr( + fun({IdxName, ColName}, Acc) -> + maps:update_with( + IdxName, + fun(Cs) -> [ColName | Cs] end, + [ColName], + Acc) + end, #{}, Cols), + maps:to_list(Indices) + end; + (sqlite, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(i.name)s, @(c.name)s " + " from pragma_index_list(%(Table)s) as i," + " pragma_index_xinfo(i.name) as c" + " where c.cid >= 0" + " order by i.name, c.seqno")) + of + {selected, Cols} -> + Indices = + lists:foldr( + fun({IdxName, ColName}, Acc) -> + maps:update_with( + IdxName, + fun(Cs) -> [ColName | Cs] end, + [ColName], + Acc) + end, #{}, Cols), + maps:to_list(Indices) + end; + (mysql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @(index_name)s, @(column_name)s" + " from information_schema.statistics" + " where table_name=%(Table)s and" + " table_schema=schema()" + " order by index_name, seq_in_index")) + of + {selected, Cols} -> + Indices = + lists:foldr( + fun({IdxName, ColName}, Acc) -> + maps:update_with( + IdxName, + fun(Cs) -> [ColName | Cs] end, + [ColName], + Acc) + end, #{}, Cols), + maps:to_list(Indices) + end + end). + +find_index_name(Host, Table, Columns) -> + Indices = get_table_indices(Host, Table), + case lists:keyfind(Columns, 2, Indices) of + false -> + false; + {Name, _} -> + {ok, Name} + end. + +get_version(Host, Module) -> + SModule = misc:atom_to_binary(Module), + ejabberd_sql:sql_query( + Host, + ?SQL("select @(version)d" + " from schema_version" + " where module=%(SModule)s")). + +store_version(Host, Module, Version) -> + SModule = misc:atom_to_binary(Module), + ?SQL_UPSERT( + Host, + "schema_version", + ["!module=%(SModule)s", + "version=%(Version)d"]). + +store_version_t(Module, Version) -> + SModule = misc:atom_to_binary(Module), + ?SQL_UPSERT_T( + "schema_version", + ["!module=%(SModule)s", + "version=%(Version)d"]). + +table_exists(Host, Table) -> + ejabberd_sql:sql_query( + Host, + fun(pgsql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @()b exists (select * from pg_tables " + " where tablename=%(Table)s)")) + of + {selected, [{Res}]} -> + Res + end; + (sqlite, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @()b exists" + " (select 0 from pragma_table_info(%(Table)s))")) + of + {selected, [{Res}]} -> + Res + end; + (mysql, _) -> + case + ejabberd_sql:sql_query_t( + ?SQL("select @()b exists" + " (select 0 from information_schema.tables" + " where table_name=%(Table)s and" + " table_schema=schema())")) + of + {selected, [{Res}]} -> + Res + end + end). + +filter_table_sh(SchemaInfo, Table) -> + case {SchemaInfo#sql_schema_info.new_schema, Table#sql_table.name} of + {true, _} -> + Table; + {_, <<"route">>} -> + Table; + {false, _} -> + Table#sql_table{ + columns = + lists:keydelete( + <<"server_host">>, #sql_column.name, Table#sql_table.columns), + indices = + lists:map( + fun(Idx) -> + Idx#sql_index{ + columns = + lists:delete( + <<"server_host">>, Idx#sql_index.columns) + } + end, Table#sql_table.indices) + } + end. + +string_to_type(SType) -> + case string:lowercase(SType) of + <<"text">> -> text; + <<"mediumtext">> -> text; + <<"bigint">> -> bigint; + <<"bigint ", _/binary>> -> bigint; + <<"bigint(", _/binary>> -> bigint; + <<"integer">> -> integer; + <<"int">> -> integer; + <<"int(", _/binary>> -> integer; + <<"int ", _/binary>> -> integer; + <<"smallint">> -> smallint; + <<"smallint(", _/binary>> -> smallint; + <<"numeric">> -> numeric; + <<"decimal", _/binary>> -> numeric; + <<"bigserial">> -> bigserial; + <<"boolean">> -> boolean; + <<"tinyint(1)">> -> boolean; + <<"tinyint", _/binary>> -> smallint; + <<"bytea">> -> blob; + <<"blob">> -> blob; + <<"timestamp", _/binary>> -> timestamp; + <<"character(", R/binary>> -> + {ok, [N], []} = io_lib:fread("~d)", binary_to_list(R)), + {char, N}; + <<"char(", R/binary>> -> + {ok, [N], []} = io_lib:fread("~d)", binary_to_list(R)), + {char, N}; + <<"varchar(", _/binary>> -> text; + <<"character varying(", _/binary>> -> text; + T -> + ?ERROR_MSG("Unknown SQL type '~s'", [T]), + {undefined, T} + end. + +check_columns_compatibility(RequiredColumns, Columns) -> + lists:all( + fun(#sql_column{name = Name, type = Type}) -> + %io:format("col ~p~n", [{Name, Type}]), + case lists:keyfind(Name, 1, Columns) of + false -> + false; + {_, Type2} -> + %io:format("tt ~p~n", [{Type, Type2}]), + case {Type, Type2} of + {T, T} -> true; + {text, blob} -> true; + {{text, _}, blob} -> true; + {{text, _}, text} -> true; + {{text, _}, {varchar, _}} -> true; + {text, {varchar, _}} -> true; + {{char, _}, text} -> true; + {{varchar, _}, text} -> true; + {smallint, integer} -> true; + {smallint, bigint} -> true; + {smallint, numeric} -> true; + {integer, bigint} -> true; + {integer, numeric} -> true; + {bigint, numeric} -> true; + %% a workaround for MySQL definition of mqtt_pub + {bigint, integer} -> true; + {bigserial, integer} -> true; + {bigserial, bigint} -> true; + {bigserial, numeric} -> true; + _ -> false + end + end + end, RequiredColumns). + +guess_version(Host, Schemas) -> + LastSchema = lists:max(Schemas), + SomeTablesExist = + lists:any( + fun(Table) -> + table_exists(Host, Table#sql_table.name) + end, LastSchema#sql_schema.tables), + if + SomeTablesExist -> + CompatibleSchemas = + lists:filter( + fun(Schema) -> + lists:all( + fun(Table) -> + CurrentColumns = + get_table_schema( + Host, Table#sql_table.name), + check_columns_compatibility( + Table#sql_table.columns, + CurrentColumns) + end, Schema#sql_schema.tables) + end, Schemas), + case CompatibleSchemas of + [] -> -1; + _ -> + (lists:max(CompatibleSchemas))#sql_schema.version + end; + true -> + 0 + end. + +get_current_version(Host, Module, Schemas) -> + case get_version(Host, Module) of + {selected, [{Version}]} -> + Version; + {selected, []} -> + Version = guess_version(Host, Schemas), + if + Version > 0 -> + store_version(Host, Module, Version); + true -> + ok + end, + Version + end. + +sqlite_table_copy_t(SchemaInfo, Table) -> + TableName = Table#sql_table.name, + NewTableName = <<"new_", TableName/binary>>, + NewTable = Table#sql_table{name = NewTableName}, + create_table_t(SchemaInfo, NewTable), + Columns = lists:join(<<",">>, + lists:map(fun(C) -> escape_name(SchemaInfo, C#sql_column.name) end, + Table#sql_table.columns)), + SQL2 = [<<"INSERT INTO ">>, NewTableName, + <<" SELECT ">>, Columns, <<" FROM ">>, TableName], + ?INFO_MSG("Copying table ~s to ~s:~n~s~n", + [TableName, NewTableName, SQL2]), + ejabberd_sql:sql_query_t(SQL2), + SQL3 = <<"DROP TABLE ", TableName/binary>>, + ?INFO_MSG("Droping old table ~s:~n~s~n", + [TableName, SQL3]), + ejabberd_sql:sql_query_t(SQL3), + SQL4 = <<"ALTER TABLE ", NewTableName/binary, + " RENAME TO ", TableName/binary>>, + ?INFO_MSG("Renaming table ~s to ~s:~n~s~n", + [NewTableName, TableName, SQL4]), + ejabberd_sql:sql_query_t(SQL4). + +format_type(#sql_schema_info{db_type = pgsql}, Column) -> + case Column#sql_column.type of + text -> <<"text">>; + {text, _} -> <<"text">>; + bigint -> <<"bigint">>; + integer -> <<"integer">>; + smallint -> <<"smallint">>; + numeric -> <<"numeric">>; + boolean -> <<"boolean">>; + blob -> <<"bytea">>; + timestamp -> <<"timestamp">>; + {char, N} -> [<<"character(">>, integer_to_binary(N), <<")">>]; + bigserial -> <<"bigserial">> + end; +format_type(#sql_schema_info{db_type = sqlite}, Column) -> + case Column#sql_column.type of + text -> <<"text">>; + {text, _} -> <<"text">>; + bigint -> <<"bigint">>; + integer -> <<"integer">>; + smallint -> <<"smallint">>; + numeric -> <<"numeric">>; + boolean -> <<"boolean">>; + blob -> <<"blob">>; + timestamp -> <<"timestamp">>; + {char, N} -> [<<"character(">>, integer_to_binary(N), <<")">>]; + bigserial -> <<"integer primary key autoincrement">> + end; +format_type(#sql_schema_info{db_type = mysql}, Column) -> + case Column#sql_column.type of + text -> <<"text">>; + {text, big} -> <<"mediumtext">>; + {text, N} when is_integer(N), N < 191 -> + [<<"varchar(">>, integer_to_binary(N), <<")">>]; + {text, _} -> <<"text">>; + bigint -> <<"bigint">>; + integer -> <<"integer">>; + smallint -> <<"smallint">>; + numeric -> <<"numeric">>; + boolean -> <<"boolean">>; + blob -> <<"blob">>; + timestamp -> <<"timestamp">>; + {char, N} -> [<<"character(">>, integer_to_binary(N), <<")">>]; + bigserial -> <<"bigint auto_increment primary key">> + end. + +format_default(#sql_schema_info{db_type = pgsql}, Column) -> + case Column#sql_column.type of + text -> <<"''">>; + {text, _} -> <<"''">>; + bigint -> <<"0">>; + integer -> <<"0">>; + smallint -> <<"0">>; + numeric -> <<"0">>; + boolean -> <<"false">>; + blob -> <<"''">>; + timestamp -> <<"now()">> + %{char, N} -> <<"''">>; + %bigserial -> <<"0">> + end; +format_default(#sql_schema_info{db_type = sqlite}, Column) -> + case Column#sql_column.type of + text -> <<"''">>; + {text, _} -> <<"''">>; + bigint -> <<"0">>; + integer -> <<"0">>; + smallint -> <<"0">>; + numeric -> <<"0">>; + boolean -> <<"false">>; + blob -> <<"''">>; + timestamp -> <<"CURRENT_TIMESTAMP">> + %{char, N} -> <<"''">>; + %bigserial -> <<"0">> + end; +format_default(#sql_schema_info{db_type = mysql}, Column) -> + case Column#sql_column.type of + text -> <<"('')">>; + {text, _} -> <<"('')">>; + bigint -> <<"0">>; + integer -> <<"0">>; + smallint -> <<"0">>; + numeric -> <<"0">>; + boolean -> <<"false">>; + blob -> <<"('')">>; + timestamp -> <<"CURRENT_TIMESTAMP">> + %{char, N} -> <<"''">>; + %bigserial -> <<"0">> + end. + +escape_name(#sql_schema_info{db_type = pgsql}, <<"type">>) -> + <<"\"type\"">>; +escape_name(_SchemaInfo, ColumnName) -> + ColumnName. + +format_column_def(SchemaInfo, Column) -> + [<<" ">>, + escape_name(SchemaInfo, Column#sql_column.name), <<" ">>, + format_type(SchemaInfo, Column), + <<" NOT NULL">>, + case Column#sql_column.default of + false -> []; + true -> + [<<" DEFAULT ">>, format_default(SchemaInfo, Column)] + end, + case lists:keyfind(sql_references, 1, Column#sql_column.opts) of + false -> []; + #sql_references{table = T, column = C} -> + [<<" REFERENCES ">>, T, <<"(">>, C, <<") ON DELETE CASCADE">>] + end]. + +format_mysql_index_column(Table, ColumnName) -> + {value, Column} = + lists:keysearch( + ColumnName, #sql_column.name, Table#sql_table.columns), + NeedsSizeLimit = + case Column#sql_column.type of + {text, N} when is_integer(N), N < 191 -> false; + {text, _} -> true; + text -> true; + _ -> false + end, + if + NeedsSizeLimit -> + [ColumnName, <<"(191)">>]; + true -> + ColumnName + end. + +format_create_index(#sql_schema_info{db_type = pgsql}, Table, Index) -> + TableName = Table#sql_table.name, + Unique = + case Index#sql_index.unique of + true -> <<"UNIQUE ">>; + false -> <<"">> + end, + Name = [<<"i_">>, TableName, <<"_">>, + lists:join( + <<"_">>, + Index#sql_index.columns)], + [<<"CREATE ">>, Unique, <<"INDEX ">>, Name, <<" ON ">>, TableName, + <<" USING btree (">>, + lists:join( + <<", ">>, + Index#sql_index.columns), + <<");">>]; +format_create_index(#sql_schema_info{db_type = sqlite}, Table, Index) -> + TableName = Table#sql_table.name, + Unique = + case Index#sql_index.unique of + true -> <<"UNIQUE ">>; + false -> <<"">> + end, + Name = [<<"i_">>, TableName, <<"_">>, + lists:join( + <<"_">>, + Index#sql_index.columns)], + [<<"CREATE ">>, Unique, <<"INDEX ">>, Name, <<" ON ">>, TableName, + <<"(">>, + lists:join( + <<", ">>, + Index#sql_index.columns), + <<");">>]; +format_create_index(#sql_schema_info{db_type = mysql}, Table, Index) -> + TableName = Table#sql_table.name, + Unique = + case Index#sql_index.unique of + true -> <<"UNIQUE ">>; + false -> <<"">> + end, + Name = [<<"i_">>, TableName, <<"_">>, + lists:join( + <<"_">>, + Index#sql_index.columns)], + [<<"CREATE ">>, Unique, <<"INDEX ">>, Name, + <<" USING BTREE ON ">>, TableName, + <<"(">>, + lists:join( + <<", ">>, + lists:map( + fun(Col) -> + format_mysql_index_column(Table, Col) + end, Index#sql_index.columns)), + <<");">>]. + +format_primary_key(#sql_schema_info{db_type = mysql}, Table) -> + case lists:filter( + fun(#sql_index{meta = #{primary_key := true}}) -> true; + (_) -> false + end, Table#sql_table.indices) of + [] -> []; + [I] -> + [[<<" ">>, + <<"PRIMARY KEY (">>, + lists:join( + <<", ">>, + lists:map( + fun(Col) -> + format_mysql_index_column(Table, Col) + end, I#sql_index.columns)), + <<")">>]] + end; +format_primary_key(_SchemaInfo, Table) -> + case lists:filter( + fun(#sql_index{meta = #{primary_key := true}}) -> true; + (_) -> false + end, Table#sql_table.indices) of + [] -> []; + [I] -> + [[<<" ">>, + <<"PRIMARY KEY (">>, + lists:join(<<", ">>, I#sql_index.columns), + <<")">>]] + end. + +format_add_primary_key(#sql_schema_info{db_type = sqlite} = SchemaInfo, + Table, Index) -> + format_create_index(SchemaInfo, Table, Index); +format_add_primary_key(#sql_schema_info{db_type = pgsql}, Table, Index) -> + TableName = Table#sql_table.name, + [<<"ALTER TABLE ">>, TableName, <<" ADD PRIMARY KEY (">>, + lists:join( + <<", ">>, + Index#sql_index.columns), + <<");">>]; +format_add_primary_key(#sql_schema_info{db_type = mysql}, Table, Index) -> + TableName = Table#sql_table.name, + [<<"ALTER TABLE ">>, TableName, <<" ADD PRIMARY KEY (">>, + lists:join( + <<", ">>, + lists:map( + fun(Col) -> + format_mysql_index_column(Table, Col) + end, Index#sql_index.columns)), + <<");">>]. + +format_create_table(#sql_schema_info{db_type = pgsql} = SchemaInfo, Table) -> + TableName = Table#sql_table.name, + [iolist_to_binary( + [<<"CREATE TABLE ">>, TableName, <<" (\n">>, + lists:join( + <<",\n">>, + lists:map( + fun(C) -> format_column_def(SchemaInfo, C) end, + Table#sql_table.columns) ++ + format_primary_key(SchemaInfo, Table)), + <<"\n);\n">>])] ++ + lists:flatmap( + fun(#sql_index{meta = #{primary_key := true}}) -> + []; + (#sql_index{meta = #{ignore := true}}) -> + []; + (I) -> + [iolist_to_binary( + [format_create_index(SchemaInfo, Table, I), + <<"\n">>])] + end, + Table#sql_table.indices); +format_create_table(#sql_schema_info{db_type = sqlite} = SchemaInfo, Table) -> + TableName = Table#sql_table.name, + [iolist_to_binary( + [<<"CREATE TABLE ">>, TableName, <<" (\n">>, + lists:join( + <<",\n">>, + lists:map( + fun(C) -> format_column_def(SchemaInfo, C) end, + Table#sql_table.columns) ++ + format_primary_key(SchemaInfo, Table)), + <<"\n);\n">>])] ++ + lists:flatmap( + fun(#sql_index{meta = #{primary_key := true}}) -> + []; + (#sql_index{meta = #{ignore := true}}) -> + []; + (I) -> + [iolist_to_binary( + [format_create_index(SchemaInfo, Table, I), + <<"\n">>])] + end, + Table#sql_table.indices); +format_create_table(#sql_schema_info{db_type = mysql} = SchemaInfo, Table) -> + TableName = Table#sql_table.name, + [iolist_to_binary( + [<<"CREATE TABLE ">>, TableName, <<" (\n">>, + lists:join( + <<",\n">>, + lists:map( + fun(C) -> format_column_def(SchemaInfo, C) end, + Table#sql_table.columns) ++ + format_primary_key(SchemaInfo, Table)), + <<"\n) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n">>])] ++ + lists:flatmap( + fun(#sql_index{meta = #{primary_key := true}}) -> + []; + (#sql_index{meta = #{ignore := true}}) -> + []; + (I) -> + [iolist_to_binary( + [format_create_index(SchemaInfo, Table, I), + <<"\n">>])] + end, + Table#sql_table.indices). + +create_table(Host, SchemaInfo, Table) -> + ejabberd_sql:sql_query(Host, + fun() -> + create_table_t(SchemaInfo, Table) + end). + +create_table_t(SchemaInfo, Table) -> + SQLs = format_create_table(SchemaInfo, Table), + ?INFO_MSG("Creating table ~s:~n~s~n", + [Table#sql_table.name, SQLs]), + lists:foreach( + fun(SQL) -> ejabberd_sql:sql_query_t(SQL) end, SQLs), + case Table#sql_table.post_create of + undefined -> + ok; + F when is_function(F, 1) -> + PostSQLs = F(SchemaInfo), + lists:foreach( + fun(SQL) -> ejabberd_sql:sql_query_t(SQL) end, + PostSQLs) + end. + +create_tables(Host, Module, SchemaInfo, Schema) -> + lists:foreach( + fun(Table) -> + Res = create_table(Host, SchemaInfo, Table), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to create table ~s: ~p", + [Table#sql_table.name, Error]), + error(Error); + _ -> + ok + end + end, Schema#sql_schema.tables), + store_version(Host, Module, Schema#sql_schema.version). + +should_update_schema(Host) -> + SupportedDB = + case ejabberd_option:sql_type(Host) of + pgsql -> true; + sqlite -> true; + mysql -> true; + _ -> false + end, + case ejabberd_option:update_sql_schema() andalso SupportedDB of + true -> + case ejabberd_sql:use_new_schema() of + true -> + lists:member(sql, ejabberd_option:auth_method(Host)); + false -> + true + end; + false -> + false + end. + +preprocess_table(SchemaInfo, Table) -> + Table1 = filter_table_sh(SchemaInfo, Table), + ImplicitPK = + case SchemaInfo#sql_schema_info.db_type of + pgsql -> false; + sqlite -> + case lists:keyfind(bigserial, #sql_column.type, + Table1#sql_table.columns) of + false -> false; + #sql_column{name = Name} -> {ok, Name} + end; + mysql -> + case lists:keyfind(bigserial, #sql_column.type, + Table1#sql_table.columns) of + false -> false; + #sql_column{name = Name} -> {ok, Name} + end + end, + Indices = + case ImplicitPK of + false -> + {Inds, _} = + lists:mapfoldl( + fun(#sql_index{unique = true} = I, false) -> + {I#sql_index{ + meta = (I#sql_index.meta)#{primary_key => true}}, + true}; + (I, Acc) -> + {I, Acc} + end, false, Table1#sql_table.indices), + Inds; + {ok, CN} -> + lists:map( + fun(#sql_index{columns = [CN1]} = I) when CN == CN1 -> + I#sql_index{ + meta = (I#sql_index.meta)#{ignore => true}}; + (I) -> I + end, + Table1#sql_table.indices) + end, + Table1#sql_table{indices = Indices}. + +preprocess_schemas(SchemaInfo, Schemas) -> + lists:map( + fun(Schema) -> + Schema#sql_schema{ + tables = lists:map( + fun(T) -> + preprocess_table(SchemaInfo, T) + end, + Schema#sql_schema.tables)} + end, Schemas). + +update_schema(Host, Module, RawSchemas) -> + case should_update_schema(Host) of + true -> + SchemaInfo = + ejabberd_sql:sql_query( + Host, + fun(DBType, DBVersion) -> + #sql_schema_info{ + db_type = DBType, + db_version = DBVersion, + new_schema = ejabberd_sql:use_new_schema()} + end), + Schemas = preprocess_schemas(SchemaInfo, RawSchemas), + Version = get_current_version(Host, Module, Schemas), + LastSchema = lists:max(Schemas), + LastVersion = LastSchema#sql_schema.version, + case Version of + _ when Version < 0 -> + ?ERROR_MSG("Can't update SQL schema for module ~p, please do it manually", [Module]); + 0 -> + create_tables(Host, Module, SchemaInfo, LastSchema); + LastVersion -> + ok; + _ when LastVersion < Version -> + ?ERROR_MSG("The current SQL schema for module ~p is ~p, but the latest known schema in the module is ~p", [Module, Version, LastVersion]); + _ -> + lists:foreach( + fun(Schema) -> + if + Schema#sql_schema.version > Version -> + do_update_schema(Host, Module, + SchemaInfo, Schema); + true -> + ok + end + end, lists:sort(Schemas)) + end; + false -> + ok + end. + +do_update_schema(Host, Module, SchemaInfo, Schema) -> + F = fun() -> + lists:foreach( + fun({add_column, TableName, ColumnName}) -> + {value, Table} = + lists:keysearch( + TableName, #sql_table.name, Schema#sql_schema.tables), + {value, Column} = + lists:keysearch( + ColumnName, #sql_column.name, Table#sql_table.columns), + Res = + ejabberd_sql:sql_query_t( + fun(DBType, _DBVersion) -> + Def = format_column_def(SchemaInfo, Column), + Default = format_default(SchemaInfo, Column), + SQLs = + [[<<"ALTER TABLE ">>, + TableName, + <<" ADD COLUMN\n">>, + Def, + <<" DEFAULT ">>, + Default, <<";\n">>]] ++ + case Column#sql_column.default of + false when DBType /= sqlite -> + [[<<"ALTER TABLE ">>, + TableName, + <<" ALTER COLUMN ">>, + ColumnName, + <<" DROP DEFAULT;">>]]; + _ -> + [] + end, + ?INFO_MSG("Add column ~s/~s:~n~s~n", + [TableName, + ColumnName, + SQLs]), + lists:foreach( + fun(SQL) -> ejabberd_sql:sql_query_t(SQL) end, + SQLs) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end; + ({drop_column, TableName, ColumnName}) -> + Res = + ejabberd_sql:sql_query_t( + fun(_DBType, _DBVersion) -> + SQL = [<<"ALTER TABLE ">>, + TableName, + <<" DROP COLUMN ">>, + ColumnName, + <<";">>], + ?INFO_MSG("Drop column ~s/~s:~n~s~n", + [TableName, + ColumnName, + SQL]), + ejabberd_sql:sql_query_t(SQL) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end; + ({create_index, TableName, Columns1}) -> + Columns = + case ejabberd_sql:use_new_schema() of + true -> + Columns1; + false -> + lists:delete( + <<"server_host">>, Columns1) + end, + {value, Table} = + lists:keysearch( + TableName, #sql_table.name, Schema#sql_schema.tables), + {value, Index} = + lists:keysearch( + Columns, #sql_index.columns, Table#sql_table.indices), + case Index#sql_index.meta of + #{ignore := true} -> ok; + _ -> + Res = + ejabberd_sql:sql_query_t( + fun() -> + case Index#sql_index.meta of + #{primary_key := true} -> + SQL1 = format_add_primary_key( + SchemaInfo, Table, Index), + SQL = iolist_to_binary(SQL1), + ?INFO_MSG("Add primary key ~s/~p:~n~s~n", + [Table#sql_table.name, + Index#sql_index.columns, + SQL]), + ejabberd_sql:sql_query_t(SQL); + _ -> + SQL1 = format_create_index( + SchemaInfo, Table, Index), + SQL = iolist_to_binary(SQL1), + ?INFO_MSG("Create index ~s/~p:~n~s~n", + [Table#sql_table.name, + Index#sql_index.columns, + SQL]), + ejabberd_sql:sql_query_t(SQL) + end + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end + end; + ({update_primary_key, TableName, Columns1}) -> + Columns = + case ejabberd_sql:use_new_schema() of + true -> + Columns1; + false -> + lists:delete( + <<"server_host">>, Columns1) + end, + {value, Table} = + lists:keysearch( + TableName, #sql_table.name, Schema#sql_schema.tables), + {value, Index} = + lists:keysearch( + Columns, #sql_index.columns, Table#sql_table.indices), + Res = + case SchemaInfo#sql_schema_info.db_type of + sqlite -> + sqlite_table_copy_t(SchemaInfo, Table); + pgsql -> + TableName = Table#sql_table.name, + SQL1 = [<<"ALTER TABLE ">>, TableName, <<" DROP CONSTRAINT ", + TableName/binary, "_pkey, ", + "ADD PRIMARY KEY (">>, + lists:join( + <<", ">>, + Index#sql_index.columns), + <<");">>], + SQL = iolist_to_binary(SQL1), + ?INFO_MSG("Update primary key ~s/~p:~n~s~n", + [Table#sql_table.name, + Index#sql_index.columns, + SQL]), + ejabberd_sql:sql_query_t( + fun(_DBType, _DBVersion) -> + ejabberd_sql:sql_query_t(SQL) + end); + mysql -> + TableName = Table#sql_table.name, + SQL1 = [<<"ALTER TABLE ">>, TableName, <<" DROP PRIMARY KEY, " + "ADD PRIMARY KEY (">>, + lists:join( + <<", ">>, + lists:map( + fun(Col) -> + format_mysql_index_column(Table, Col) + end, Index#sql_index.columns)), + <<");">>], + SQL = iolist_to_binary(SQL1), + ?INFO_MSG("Update primary key ~s/~p:~n~s~n", + [Table#sql_table.name, + Index#sql_index.columns, + SQL]), + ejabberd_sql:sql_query_t( + fun(_DBType, _DBVersion) -> + ejabberd_sql:sql_query_t(SQL) + end) + end, + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end; + ({drop_index, TableName, Columns1}) -> + Columns = + case ejabberd_sql:use_new_schema() of + true -> + Columns1; + false -> + lists:delete( + <<"server_host">>, Columns1) + end, + case find_index_name(Host, TableName, Columns) of + false -> + ?ERROR_MSG("Can't find an index to drop for ~s/~p", + [TableName, Columns]); + {ok, IndexName} -> + Res = + ejabberd_sql:sql_query_t( + fun(DBType, _DBVersion) -> + SQL = + case DBType of + mysql -> + [<<"DROP INDEX ">>, + IndexName, + <<" ON ">>, + TableName, + <<";">>]; + _ -> + [<<"DROP INDEX ">>, + IndexName, <<";">>] + end, + ?INFO_MSG("Drop index ~s/~p:~n~s~n", + [TableName, + Columns, + SQL]), + ejabberd_sql:sql_query_t(SQL) + end), + case Res of + {error, Error} -> + ?ERROR_MSG("Failed to update table ~s: ~p", + [TableName, Error]), + error(Error); + _ -> + ok + end + end + end, Schema#sql_schema.update), + store_version_t(Module, Schema#sql_schema.version) + end, + ejabberd_sql:sql_transaction(Host, F, ejabberd_option:update_sql_schema_timeout(), 1). + +print_schema(SDBType, SDBVersion, SNewSchema) -> + {DBType, DBVersion} = + case SDBType of + "pgsql" -> + case string:split(SDBVersion, ".") of + [SMajor, SMinor] -> + try {list_to_integer(SMajor), list_to_integer(SMinor)} of + {Major, Minor} -> + {pgsql, Major * 10000 + Minor} + catch _:_ -> + io:format("pgsql version be in the form of " + "Major.Minor, e.g. 16.1~n"), + {error, error} + end; + _ -> + io:format("pgsql version be in the form of " + "Major.Minor, e.g. 16.1~n"), + {error, error} + end; + "mysql" -> + case ejabberd_sql:parse_mysql_version(SDBVersion, 0) of + {ok, V} -> + {mysql, V}; + error -> + io:format("mysql version be in the same form as " + "SELECT VERSION() returns, e.g. 8.2.0~n"), + {error, error} + end; + "sqlite" -> + {sqlite, undefined}; + _ -> + io:format("db_type must be one of the following: " + "'pgsql', 'mysql', 'sqlite'~n"), + {error, error} + end, + NewSchema = + case SNewSchema of + "0" -> false; + "1" -> true; + "false" -> false; + "true" -> true; + _ -> + io:format("new_schema must be one of the following: " + "'0', '1', 'false', 'true'~n"), + error + end, + case {DBType, NewSchema} of + {error, _} -> ?STATUS_ERROR; + {_, error} -> ?STATUS_ERROR; + _ -> + SchemaInfo = + #sql_schema_info{ + db_type = DBType, + db_version = DBVersion, + new_schema = NewSchema}, + Mods = ejabberd_config:beams(all), + lists:foreach( + fun(Mod) -> + case erlang:function_exported(Mod, sql_schemas, 0) of + true -> + Schemas = Mod:sql_schemas(), + Schemas2 = preprocess_schemas(SchemaInfo, Schemas), + Schema = lists:max(Schemas2), + SQLs = + lists:flatmap( + fun(Table) -> + SQLs = format_create_table(SchemaInfo, Table), + PostSQLs = + case Table#sql_table.post_create of + undefined -> + []; + F when is_function(F, 1) -> + PSQLs = F(SchemaInfo), + lists:map( + fun(S) -> + [S, <<"\n">>] + end, PSQLs) + end, + SQLs ++ PostSQLs + end, Schema#sql_schema.tables), + io:format("~s~n", [SQLs]); + false -> + ok + end + end, Mods), + ?STATUS_SUCCESS + end. + + +test() -> + Schemas = + [#sql_schema{ + version = 2, + tables = + [#sql_table{ + name = <<"archive2">>, + 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}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"origin_id">>, type = text}, + #sql_column{name = <<"type">>, type = text}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{columns = [<<"id">>], + unique = true}, + #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">>, <<"origin_id">>]}, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>]} + ]}], + update = + [{add_column, <<"archive2">>, <<"origin_id">>}, + {create_index, <<"archive2">>, + [<<"server_host">>, <<"origin_id">>]}, + {drop_index, <<"archive2">>, + [<<"server_host">>, <<"origin_id">>]}, + {drop_column, <<"archive2">>, <<"origin_id">>}, + {create_index, <<"archive2">>, [<<"id">>]} + ]}, + #sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"archive2">>, + 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">>]} + ]}]}], + update_schema(<<"localhost">>, mod_foo, Schemas). diff --git a/src/ejabberd_sql_sup.erl b/src/ejabberd_sql_sup.erl index 659189c33..414828358 100644 --- a/src/ejabberd_sql_sup.erl +++ b/src/ejabberd_sql_sup.erl @@ -5,7 +5,7 @@ %%% Created : 22 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,28 +37,36 @@ start(Host) -> case is_started(Host) of true -> ok; false -> - App = case ejabberd_option:sql_type(Host) of - mysql -> p1_mysql; - pgsql -> p1_pgsql; - sqlite -> sqlite3; - _ -> odbc - end, - ejabberd:start_app(App), - Spec = #{id => gen_mod:get_module_proc(Host, ?MODULE), - start => {ejabberd_sql_sup, start_link, [Host]}, - restart => transient, - shutdown => infinity, - type => supervisor, - modules => [?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 + case lists:member(Host, ejabberd_option:hosts()) of + false -> + ?WARNING_MSG("Rejecting start of sql worker for unknown host: ~ts", [Host]), + {error, invalid_host}; + true -> + App = case ejabberd_option:sql_type(Host) of + mysql -> p1_mysql; + pgsql -> p1_pgsql; + sqlite -> sqlite3; + _ -> odbc + end, + ejabberd:start_app(App), + Spec = #{id => gen_mod:get_module_proc(Host, ?MODULE), + start => {ejabberd_sql_sup, start_link, [Host]}, + restart => transient, + shutdown => infinity, + type => supervisor, + modules => [?MODULE]}, + case supervisor:start_child(ejabberd_db_sup, Spec) of + {ok, _} -> + ejabberd_sql_schema:start(Host), + 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 end. diff --git a/src/ejabberd_stun.erl b/src/ejabberd_stun.erl index b41baca12..cc7bb472f 100644 --- a/src/ejabberd_stun.erl +++ b/src/ejabberd_stun.erl @@ -5,7 +5,7 @@ %%% Created : 8 May 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2022 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 @@ -26,7 +26,6 @@ -module(ejabberd_stun). -behaviour(ejabberd_listener). -protocol({rfc, 5766}). --protocol({xep, 176, '1.0'}). -ifndef(STUN). -include("logger.hrl"). @@ -106,7 +105,6 @@ prepare_turn_opts(Opts) -> prepare_turn_opts(Opts, _UseTurn = false) -> set_certfile(Opts); prepare_turn_opts(Opts, _UseTurn = true) -> - NumberOfMyHosts = length(ejabberd_option:hosts()), TurnIP = case proplists:get_value(turn_ipv4_address, Opts) of undefined -> MyIP = misc:get_my_ipv4_address(), @@ -129,18 +127,9 @@ prepare_turn_opts(Opts, _UseTurn = true) -> AuthType = proplists:get_value(auth_type, Opts, user), Realm = case proplists:get_value(auth_realm, Opts) of undefined when AuthType == user -> - if NumberOfMyHosts > 1 -> - ?INFO_MSG("You have several virtual hosts " - "configured, but option 'auth_realm' is " - "undefined and 'auth_type' is set to " - "'user', so the TURN relay might not be " - "working properly. Using ~ts as a " - "fallback", - [ejabberd_config:get_myname()]); - true -> - ok - end, - [{auth_realm, ejabberd_config:get_myname()}]; + MyName = ejabberd_config:get_myname(), + ?DEBUG("Using ~ts as TURN realm", [MyName]), + [{auth_realm, MyName}]; _ -> [] end, diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index e15e658c4..3fa0fee0f 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -42,8 +42,8 @@ init([]) -> worker(ejabberd_cluster), worker(translate), worker(ejabberd_access_permissions), - worker(ejabberd_ctl), worker(ejabberd_commands), + worker(ejabberd_ctl), worker(ejabberd_admin), supervisor(ejabberd_listener), worker(ejabberd_pkix), @@ -61,9 +61,9 @@ init([]) -> simple_supervisor(ejabberd_s2s_out), worker(ejabberd_s2s), simple_supervisor(ejabberd_service), - worker(ejabberd_captcha), worker(ext_mod), supervisor(ejabberd_gen_mod_sup, gen_mod), + worker(ejabberd_captcha), worker(ejabberd_acme), worker(ejabberd_auth), worker(ejabberd_oauth), diff --git a/src/ejabberd_system_monitor.erl b/src/ejabberd_system_monitor.erl index d03e3bbd8..35a1f0370 100644 --- a/src/ejabberd_system_monitor.erl +++ b/src/ejabberd_system_monitor.erl @@ -5,7 +5,7 @@ %%% Created : 21 Mar 2007 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +30,7 @@ -author('ekhramtsov@process-one.net'). %% API --export([start/0, config_reloaded/0]). +-export([start/0, config_reloaded/0, stop/0]). %% gen_event callbacks -export([init/1, handle_event/2, handle_call/2, @@ -68,6 +68,10 @@ start() -> ejabberd:start_app(os_mon), set_oom_watermark(). +-spec stop() -> term(). +stop() -> + gen_event:delete_handler(alarm_handler, ?MODULE, []). + excluded_apps() -> [os_mon, mnesia, sasl, stdlib, kernel]. @@ -78,7 +82,9 @@ config_reloaded() -> %%%=================================================================== %%% gen_event callbacks %%%=================================================================== -init([]) -> +init({[], _}) -> % Called by gen_event:swap_handler + {ok, #state{}}; +init([]) -> % Called by gen_event:add_handler ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), {ok, #state{}}. @@ -115,7 +121,8 @@ handle_info(Info, State) -> ?WARNING_MSG("unexpected info: ~p~n", [Info]), {ok, State}. -terminate(_Reason, _State) -> +terminate(_Reason, State) -> + misc:cancel_timer(State#state.tref), ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). code_change(_OldVsn, State, _Extra) -> diff --git a/src/ejabberd_systemd.erl b/src/ejabberd_systemd.erl index 6a15c56c2..30ed6e4ea 100644 --- a/src/ejabberd_systemd.erl +++ b/src/ejabberd_systemd.erl @@ -44,9 +44,8 @@ {socket :: gen_udp:socket() | undefined, destination :: inet:local_address() | undefined, interval :: pos_integer() | undefined, - last_ping :: integer() | undefined}). + timer :: reference() | undefined}). --type watchdog_timeout() :: pos_integer() | hibernate. -type state() :: #state{}. %%-------------------------------------------------------------------- @@ -71,8 +70,7 @@ stopping() -> %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- --spec init(any()) - -> {ok, state()} | {ok, state(), watchdog_timeout()} | {stop, term()}. +-spec init(any()) -> {ok, state()} | {stop, term()}. init(_Opts) -> process_flag(trap_exit, true), case os:getenv("NOTIFY_SOCKET") of @@ -84,17 +82,10 @@ init(_Opts) -> Destination = {local, Path}, case gen_udp:open(0, [local]) of {ok, Socket} -> - Interval = get_watchdog_interval(), State = #state{socket = Socket, destination = Destination, - interval = Interval}, - if is_integer(Interval), Interval > 0 -> - ?INFO_MSG("Watchdog notifications enabled", []), - {ok, set_last_ping(State), Interval}; - true -> - ?INFO_MSG("Watchdog notifications disabled", []), - {ok, State} - end; + interval = get_watchdog_interval()}, + {ok, maybe_start_timer(State)}; {error, Reason} -> ?CRITICAL_MSG("Cannot open IPC socket: ~p", [Reason]), {stop, Reason} @@ -105,47 +96,48 @@ init(_Opts) -> end. -spec handle_call(term(), {pid(), term()}, state()) - -> {reply, {error, badarg}, state(), watchdog_timeout()}. + -> {reply, {error, badarg}, state()}. handle_call(Request, From, State) -> ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), - {reply, {error, badarg}, State, get_timeout(State)}. + {reply, {error, badarg}, State}. --spec handle_cast({notify, binary()} | term(), state()) - -> {noreply, state(), watchdog_timeout()}. +-spec handle_cast({notify, binary()} | term(), state()) -> {noreply, state()}. handle_cast({notify, Notification}, #state{destination = undefined} = State) -> ?DEBUG("No NOTIFY_SOCKET, dropping ~s notification", [Notification]), - {noreply, State, get_timeout(State)}; + {noreply, State}; handle_cast({notify, Notification}, State) -> try notify(State, Notification) catch _:Err -> ?ERROR_MSG("Cannot send ~s notification: ~p", [Notification, Err]) end, - {noreply, State, get_timeout(State)}; + {noreply, State}; handle_cast(Msg, State) -> ?ERROR_MSG("Got unexpected message: ~p", [Msg]), - {noreply, State, get_timeout(State)}. + {noreply, State}. --spec handle_info(timeout | term(), state()) - -> {noreply, state(), watchdog_timeout()}. -handle_info(timeout, #state{interval = Interval} = State) +-spec handle_info(ping_watchdog | term(), state()) -> {noreply, state()}. +handle_info(ping_watchdog, #state{interval = Interval} = State) when is_integer(Interval), Interval > 0 -> try notify(State, <<"WATCHDOG=1">>) catch _:Err -> ?ERROR_MSG("Cannot ping watchdog: ~p", [Err]) end, - {noreply, set_last_ping(State), Interval}; + {noreply, start_timer(State)}; handle_info(Info, State) -> ?ERROR_MSG("Got unexpected info: ~p", [Info]), - {noreply, State, get_timeout(State)}. + {noreply, State}. -spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. -terminate(Reason, #state{socket = undefined}) -> +terminate(Reason, #state{socket = Socket} = State) -> ?DEBUG("Terminating ~s (~p)", [?MODULE, Reason]), - ok; -terminate(Reason, #state{socket = Socket}) -> - ?DEBUG("Closing socket and terminating ~s (~p)", [?MODULE, Reason]), - ok = gen_udp:close(Socket). + cancel_timer(State), + case Socket of + undefined -> + ok; + _Socket -> + gen_udp:close(Socket) + end. -spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> @@ -166,24 +158,24 @@ get_watchdog_interval() -> undefined end. --spec get_timeout(state()) -> watchdog_timeout(). -get_timeout(#state{interval = undefined}) -> - ?DEBUG("Watchdog interval is undefined, hibernating", []), - hibernate; -get_timeout(#state{interval = Interval, last_ping = LastPing}) -> - case Interval - (erlang:monotonic_time(millisecond) - LastPing) of - Timeout when Timeout > 0 -> - ?DEBUG("Calculated new timeout value: ~B", [Timeout]), - Timeout; - _ -> - ?DEBUG("Calculated new timeout value: 1", []), - 1 - end. +-spec maybe_start_timer(state()) -> state(). +maybe_start_timer(#state{interval = Interval} = State) + when is_integer(Interval), Interval > 0 -> + ?INFO_MSG("Watchdog notifications enabled", []), + start_timer(State); +maybe_start_timer(State) -> + ?INFO_MSG("Watchdog notifications disabled", []), + State. --spec set_last_ping(state()) -> state(). -set_last_ping(State) -> - LastPing = erlang:monotonic_time(millisecond), - State#state{last_ping = LastPing}. +-spec start_timer(state()) -> state(). +start_timer(#state{interval = Interval} = State) -> + ?DEBUG("Pinging watchdog in ~B milliseconds", [Interval]), + State#state{timer = erlang:send_after(Interval, self(), ping_watchdog)}. + +-spec cancel_timer(state()) -> ok. +cancel_timer(#state{timer = Timer}) -> + ?DEBUG("Cancelling watchdog timer", []), + misc:cancel_timer(Timer). -spec notify(state(), binary()) -> ok. notify(#state{socket = Socket, destination = Destination}, @@ -193,4 +185,5 @@ notify(#state{socket = Socket, destination = Destination}, -spec cast_notification(binary()) -> ok. cast_notification(Notification) -> + ?DEBUG("Closing NOTIFY_SOCKET", []), gen_server:cast(?MODULE, {notify, Notification}). diff --git a/src/ejabberd_tmp_sup.erl b/src/ejabberd_tmp_sup.erl index f23b51289..7d26e6154 100644 --- a/src/ejabberd_tmp_sup.erl +++ b/src/ejabberd_tmp_sup.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jul 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_update.erl b/src/ejabberd_update.erl index 0ec129944..6c80fe8e7 100644 --- a/src/ejabberd_update.erl +++ b/src/ejabberd_update.erl @@ -5,7 +5,7 @@ %%% Created : 27 Jan 2006 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,13 +38,17 @@ %% Update all the modified modules update() -> case update_info() of - {ok, Dir, _UpdatedBeams, _Script, LowLevelScript, _Check} -> - Eval = - eval_script( - LowLevelScript, [], - [{ejabberd, "", filename:join(Dir, "..")}]), - ?DEBUG("Eval: ~p~n", [Eval]), - Eval; + {ok, Dir, UpdatedBeams, _Script, LowLevelScript, _Check} -> + case eval_script( + LowLevelScript, [], + [{ejabberd, "", filename:join(Dir, "..")}]) of + {ok, _} -> + ?DEBUG("Updated: ~p~n", [UpdatedBeams]), + {ok, UpdatedBeams}; + Eval -> + ?DEBUG("Eval: ~p~n", [Eval]), + Eval + end; {error, Reason} -> {error, Reason} end. @@ -56,12 +60,16 @@ update(ModulesToUpdate) -> UpdatedBeamsNow = [A || A <- UpdatedBeamsAll, B <- ModulesToUpdate, A == B], {_, LowLevelScript, _} = build_script(Dir, UpdatedBeamsNow), - Eval = - eval_script( - LowLevelScript, [], - [{ejabberd, "", filename:join(Dir, "..")}]), - ?DEBUG("Eval: ~p~n", [Eval]), - Eval; + case eval_script( + LowLevelScript, [], + [{ejabberd, "", filename:join(Dir, "..")}]) of + {ok, _} -> + ?DEBUG("Updated: ~p~n", [UpdatedBeamsNow]), + {ok, UpdatedBeamsNow}; + Eval -> + ?DEBUG("Eval: ~p~n", [Eval]), + Eval + end; {error, Reason} -> {error, Reason} end. diff --git a/src/ejabberd_web.erl b/src/ejabberd_web.erl index 431ae8ef3..e6090f2d8 100644 --- a/src/ejabberd_web.erl +++ b/src/ejabberd_web.erl @@ -1,12 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_web.erl %%% Author : Alexey Shchepin -%%% Purpose : %%% Purpose : %%% Created : 28 Feb 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 46598e1ef..ea63d817a 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -5,7 +5,7 @@ %%% Created : 9 Apr 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,18 +29,20 @@ -author('alexey@process-one.net'). --export([process/2, list_users/4, - list_users_in_diapason/4, pretty_print_xml/1, - term_to_id/1]). +-export([process/2, pretty_print_xml/1, + make_command/2, make_command/4, make_command_raw_value/3, + make_table/2, make_table/4, + term_to_id/1, id_to_term/1]). --include("logger.hrl"). +%% Internal commands +-export([webadmin_host_last_activity/3, + webadmin_node_db_table_page/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"). -define(INPUTATTRS(Type, Name, Value, Attrs), @@ -61,25 +63,27 @@ get_acl_rule([<<"style.css">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"logo.png">>], _) -> {<<"localhost">>, [all]}; -get_acl_rule([<<"logo-fill.png">>], _) -> - {<<"localhost">>, [all]}; get_acl_rule([<<"favicon.ico">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"additions.js">>], _) -> {<<"localhost">>, [all]}; +get_acl_rule([<<"sortable.min.css">>], _) -> + {<<"localhost">>, [all]}; +get_acl_rule([<<"sortable.min.js">>], _) -> + {<<"localhost">>, [all]}; %% This page only displays vhosts that the user is admin: get_acl_rule([<<"vhosts">>], _) -> {<<"localhost">>, [all]}; %% The pages of a vhost are only accessible if the user is admin of that vhost: get_acl_rule([<<"server">>, VHost | _RPath], Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> - {VHost, [configure, webadmin_view]}; + {VHost, [configure]}; get_acl_rule([<<"server">>, VHost | _RPath], 'POST') -> {VHost, [configure]}; %% Default rule: only global admins can access any other random page get_acl_rule(_RPath, Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> - {global, [configure, webadmin_view]}; + {global, [configure]}; get_acl_rule(_RPath, 'POST') -> {global, [configure]}. @@ -104,7 +108,7 @@ get_menu_items(global, cluster, Lang, JID, Level) -> end, Items); get_menu_items(Host, cluster, Lang, JID, Level) -> - {_Base, _, Items} = make_host_menu(Host, [], Lang, JID, Level), + {_Base, _, Items} = make_host_menu(Host, [], [], Lang, JID, Level), lists:map(fun ({URI, Name}) -> {<>, Name}; ({URI, Name, _SubMenu}) -> @@ -156,6 +160,12 @@ process(Path, #request{raw_path = RawPath} = Request) -> {301, [{<<"Location">>, <>}], <<>>} end. +process2([<<"logout">> | _], #request{lang = Lang}) -> + Text = [?XCT(<<"h1">>, ?T("Logged Out")), + ?XE(<<"p">>, [?C(<<"Your web browser has logout from WebAdmin. Close this window, or login again in: ">>), + ?AC(<<"../">>, <<"ejabberd WebAdmin">>)])], + {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], + ejabberd_web:make_xhtml(Text)}; process2([<<"server">>, SHost | RPath] = Path, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = @@ -174,8 +184,7 @@ process2([<<"server">>, SHost | RPath] = Path, {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], - ejabberd_web:make_xhtml([?XCT(<<"h1">>, - ?T("Unauthorized"))])}; + ejabberd_web:make_xhtml(make_unauthorized(Lang))}; {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, @@ -186,8 +195,7 @@ process2([<<"server">>, SHost | RPath] = Path, [{<<"WWW-Authenticate">>, <<"basic realm=\"auth error, retry login " "to ejabberd\"">>}], - ejabberd_web:make_xhtml([?XCT(<<"h1">>, - ?T("Unauthorized"))])} + ejabberd_web:make_xhtml(make_unauthorized(Lang))} end; false -> ejabberd_web:error(not_found) end; @@ -206,8 +214,7 @@ process2(RPath, {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], - ejabberd_web:make_xhtml([?XCT(<<"h1">>, - ?T("Unauthorized"))])}; + ejabberd_web:make_xhtml(make_unauthorized(Lang))}; {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, @@ -218,16 +225,20 @@ process2(RPath, [{<<"WWW-Authenticate">>, <<"basic realm=\"auth error, retry login " "to ejabberd\"">>}], - ejabberd_web:make_xhtml([?XCT(<<"h1">>, - ?T("Unauthorized"))])} + ejabberd_web:make_xhtml(make_unauthorized(Lang))} end. +make_unauthorized(Lang) -> + [?XCT(<<"h1">>, ?T("Unauthorized")), + ?XE(<<"p">>, [?C(<<"There was some problem authenticating, or the account doesn't have privilege.">>)]), + ?XE(<<"p">>, [?C(<<"Please check the log file for a more precise error message.">>)])]. + get_auth_admin(Auth, HostHTTP, RPath, Method) -> case Auth of {SJID, Pass} -> {HostOfRule, AccessRule} = get_acl_rule(RPath, Method), try jid:decode(SJID) of - #jid{user = <<"">>, server = User} -> + #jid{luser = <<"">>, lserver = User} -> case ejabberd_router:is_my_host(HostHTTP) of true -> get_auth_account(HostOfRule, AccessRule, User, HostHTTP, @@ -235,7 +246,7 @@ get_auth_admin(Auth, HostHTTP, RPath, Method) -> _ -> {unauthorized, <<"missing-server">>} end; - #jid{user = User, server = Server} -> + #jid{luser = User, lserver = Server} -> get_auth_account(HostOfRule, AccessRule, User, Server, Pass) catch _:{bad_jid, _} -> @@ -272,19 +283,28 @@ get_auth_account2(HostOfRule, AccessRule, User, Server, %%%================================== %%%% make_xhtml -make_xhtml(Els, Host, Lang, JID, Level) -> - make_xhtml(Els, Host, cluster, Lang, JID, Level). +make_xhtml(Els, Host, Request, JID, Level) -> + make_xhtml(Els, Host, cluster, unspecified, Request, JID, Level). + +make_xhtml(Els, Host, Username, Request, JID, Level) when + (Username == unspecified) or (is_binary(Username)) -> + make_xhtml(Els, Host, cluster, Username, Request, JID, Level); + +make_xhtml(Els, Host, Node, Request, JID, Level) -> + make_xhtml(Els, Host, Node, unspecified, Request, JID, Level). -spec make_xhtml([xmlel()], Host::global | binary(), Node::cluster | atom(), - Lang::binary(), + Username::unspecified | binary(), + Request::http_request(), jid(), Level::integer()) -> {200, [html], xmlel()}. -make_xhtml(Els, Host, Node, Lang, JID, Level) -> +make_xhtml(Els, Host, Node, Username, #request{lang = Lang} = R, JID, Level) -> Base = get_base_path_sum(0, 0, Level), - MenuItems = make_navigation(Host, Node, Lang, JID, Level), + MenuItems = make_navigation(Host, Node, Username, Lang, JID, Level) + ++ make_login_items(R, Level), {200, [html], #xmlel{name = <<"html">>, attrs = @@ -319,7 +339,20 @@ make_xhtml(Els, Host, Node, Lang, JID, Level) -> <>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}], - children = []}]}, + children = []}, + #xmlel{name = <<"link">>, + attrs = + [{<<"href">>, + <>}, + {<<"type">>, <<"text/css">>}, + {<<"rel">>, <<"stylesheet">>}], + children = []}, + #xmlel{name = <<"script">>, + attrs = + [{<<"src">>, + <>}, + {<<"type">>, <<"text/javascript">>}], + children = [?C(<<" ">>)]}]}, ?XE(<<"body">>, [?XAE(<<"div">>, [{<<"id">>, <<"container">>}], [?XAE(<<"div">>, [{<<"id">>, <<"header">>}], @@ -336,7 +369,7 @@ make_xhtml(Els, Host, Node, Lang, JID, Level) -> [?XE(<<"p">>, [?AC(<<"https://www.ejabberd.im/">>, <<"ejabberd">>), ?C(<<" ">>), ?C(ejabberd_option:version()), - ?C(<<" (c) 2002-2022 ">>), + ?C(<<" (c) 2002-2025 ">>), ?AC(<<"https://www.process-one.net/">>, <<"ProcessOne, leader in messaging and push solutions">>)] )])])])]}}. @@ -387,31 +420,67 @@ logo() -> {error, _} -> <<>> end. -logo_fill() -> - case misc:read_img("admin-logo-fill.png") of - {ok, Img} -> Img; +sortable_css() -> + case misc:read_css("sortable.min.css") of + {ok, CSS} -> CSS; + {error, _} -> <<>> + end. + +sortable_js() -> + case misc:read_js("sortable.min.js") of + {ok, JS} -> JS; {error, _} -> <<>> end. %%%================================== %%%% process_admin -process_admin(global, #request{path = [], lang = Lang}, AJID) -> - make_xhtml((?H1GL((translate:translate(Lang, ?T("Administration"))), <<"">>, - <<"Contents">>)) - ++ - [?XE(<<"ul">>, - [?LI([?ACT(MIU, MIN)]) - || {MIU, MIN} - <- get_menu_items(global, cluster, Lang, AJID, 0)])], - global, Lang, AJID, 0); -process_admin(Host, #request{path = [], lang = Lang}, AJID) -> +process_admin(global, #request{path = [], lang = Lang} = Request, AJID) -> + Title = ?H1GLraw(<<"">>, <<"">>, <<"home">>), + MenuItems = get_menu_items(global, cluster, Lang, AJID, 0), + Disclaimer = maybe_disclaimer_not_admin(MenuItems, AJID, Lang), + WelcomeText = + [?BR, + ?XAE(<<"p">>, [{<<"align">>, <<"center">>}], + [?XA(<<"img">>, [{<<"src">>, <<"logo.png">>}, + {<<"style">>, <<"border-radius:10px; background:#49cbc1; padding: 1.1em;">>}]) + ]) + ] ++ Title ++ [ + ?XAE(<<"blockquote">>, + [{<<"id">>, <<"welcome">>}], + [?XC(<<"p">>, <<"Welcome to ejabberd's WebAdmin!">>), + ?XC(<<"p">>, <<"Browse the menu to navigate your XMPP virtual hosts, " + "Erlang nodes, and other global server pages...">>), + ?XC(<<"p">>, <<"Some pages have a link in the top right corner " + "to relevant documentation in ejabberd Docs.">>), + ?X(<<"hr">>), + ?XE(<<"p">>, + [?C(<<"Many pages use ejabberd's API commands to show information " + "and to allow you perform administrative tasks. " + "Click on a command name to view its details. " + "You can also execute those same API commands " + "using other interfaces, see: ">>), + ?AC(<<"https://docs.ejabberd.im/developer/ejabberd-api/">>, + <<"ejabberd Docs: API">>) + ]), + ?XC(<<"p">>, <<"For example, this is the 'stats' command, " + "it accepts an argument and returns an integer:">>), + make_command(stats, Request)]), + ?BR], + make_xhtml(Disclaimer ++ WelcomeText ++ + [?XE(<<"ul">>, + [?LI([?ACT(MIU, MIN)]) + || {MIU, MIN} + <- MenuItems])], + global, Request, AJID, 0); +process_admin(Host, #request{path = [], lang = Lang} = R, AJID) -> make_xhtml([?XCT(<<"h1">>, ?T("Administration")), ?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(Host, cluster, Lang, AJID, 2)])], - Host, Lang, AJID, 2); + Host, R, AJID, 2); + process_admin(Host, #request{path = [<<"style.css">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), @@ -427,378 +496,461 @@ process_admin(_Host, #request{path = [<<"logo.png">>]}, _) -> [{<<"Content-Type">>, <<"image/png">>}, last_modified(), cache_control_public()], logo()}; -process_admin(_Host, #request{path = [<<"logo-fill.png">>]}, _) -> - {200, - [{<<"Content-Type">>, <<"image/png">>}, last_modified(), - cache_control_public()], - logo_fill()}; process_admin(_Host, #request{path = [<<"additions.js">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/javascript">>}, last_modified(), cache_control_public()], additions_js()}; -process_admin(global, #request{path = [<<"vhosts">>], lang = Lang}, AJID) -> - Res = list_vhosts(Lang, AJID), - make_xhtml((?H1GL((translate:translate(Lang, ?T("Virtual Hosts"))), - <<"basic/#xmpp-domains">>, ?T("XMPP Domains"))) - ++ Res, - global, Lang, AJID, 1); -process_admin(Host, #request{path = [<<"users">>], q = Query, - lang = Lang}, AJID) +process_admin(_Host, #request{path = [<<"sortable.min.css">>]}, _) -> + {200, + [{<<"Content-Type">>, <<"text/css">>}, last_modified(), + cache_control_public()], + sortable_css()}; +process_admin(_Host, #request{path = [<<"sortable.min.js">>]}, _) -> + {200, + [{<<"Content-Type">>, <<"text/javascript">>}, + last_modified(), cache_control_public()], + sortable_js()}; + +%% @format-begin + +process_admin(global, #request{path = [<<"vhosts">> | RPath], lang = Lang} = R, AJID) -> + Hosts = + case make_command_raw_value(registered_vhosts, R, []) of + Hs when is_list(Hs) -> + Hs; + _ -> + {User, Server} = R#request.us, + ?INFO_MSG("Access to WebAdmin page vhosts/ for account ~s@~s was denied", + [User, Server]), + [] + end, + Level = 1 + length(RPath), + HostsAllowed = [Host || Host <- Hosts, can_user_access_host(Host, R)], + Table = + make_table(20, + RPath, + [<<"host">>, {<<"registered users">>, right}, {<<"online users">>, right}], + [{make_command(echo, + R, + [{<<"sentence">>, Host}], + [{only, value}, + {result_links, [{sentence, host, Level, <<"">>}]}]), + make_command(stats_host, + R, + [{<<"name">>, <<"registeredusers">>}, {<<"host">>, Host}], + [{only, value}, + {result_links, [{stat, arg_host, Level, <<"users/">>}]}]), + make_command(stats_host, + R, + [{<<"name">>, <<"onlineusers">>}, {<<"host">>, Host}], + [{only, value}, + {result_links, [{stat, arg_host, Level, <<"online-users/">>}]}])} + || Host <- HostsAllowed]), + VhostsElements = + [make_command(registered_vhosts, R, [], [{only, presentation}]), + make_command(stats_host, R, [], [{only, presentation}]), + ?XE(<<"blockquote">>, [Table])], + make_xhtml(?H1GL(translate:translate(Lang, ?T("Virtual Hosts")), + <<"basic/#xmpp-domains">>, + ?T("XMPP Domains")) + ++ VhostsElements, + global, + R, + AJID, + Level); +process_admin(Host, + #request{path = [<<"users">>, <<"diapason">>, Diap | RPath], lang = Lang} = R, + AJID) when is_binary(Host) -> - Res = list_users(Host, Query, Lang, fun url_func/1), - make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, - Lang, AJID, 3); -process_admin(Host, #request{path = [<<"users">>, Diap], - lang = Lang}, AJID) + Level = 5 + length(RPath), + RegisterEl = make_command(register, R, [{<<"host">>, Host}], []), + Res = list_users_in_diapason(Host, Level, 30, RPath, R, Diap, RegisterEl), + make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, R, AJID, Level); +process_admin(Host, + #request{path = [<<"users">>, <<"top">>, Attribute | RPath], lang = Lang} = R, + AJID) when is_binary(Host) -> - Res = list_users_in_diapason(Host, Diap, Lang, - fun url_func/1), - make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, - Lang, AJID, 4); -process_admin(Host, #request{path = [<<"online-users">>], - lang = Lang}, AJID) + Level = 5 + length(RPath), + RegisterEl = make_command(register, R, [{<<"host">>, Host}], []), + Res = list_users_top(Host, Level, 30, RPath, R, Attribute, RegisterEl), + make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, R, AJID, Level); +process_admin(Host, #request{path = [<<"users">> | RPath], lang = Lang} = R, AJID) when is_binary(Host) -> - Res = list_online_users(Host, Lang), - make_xhtml([?XCT(<<"h1">>, ?T("Online Users"))] ++ Res, - Host, Lang, AJID, 3); -process_admin(Host, #request{path = [<<"last-activity">>], - q = Query, lang = Lang}, AJID) + Level = 3 + length(RPath), + RegisterEl = make_command(register, R, [{<<"host">>, Host}], []), + Res = list_users(Host, Level, 30, RPath, R, RegisterEl), + make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, R, AJID, Level); +process_admin(Host, #request{path = [<<"online-users">> | RPath], lang = Lang} = R, AJID) when is_binary(Host) -> - ?DEBUG("Query: ~p", [Query]), - Month = case lists:keysearch(<<"period">>, 1, Query) of - {value, {_, Val}} -> Val; - _ -> <<"month">> - end, - Res = case lists:keysearch(<<"ordinary">>, 1, Query) of - {value, {_, _}} -> - list_last_activity(Host, Lang, false, Month); - _ -> list_last_activity(Host, Lang, true, Month) - end, - PageH1 = ?H1GL(translate:translate(Lang, ?T("Users Last Activity")), <<"modules/#mod-last">>, <<"mod_last">>), - make_xhtml(PageH1 ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?CT(?T("Period: ")), - ?XAE(<<"select">>, [{<<"name">>, <<"period">>}], - (lists:map(fun ({O, V}) -> - Sel = if O == Month -> - [{<<"selected">>, - <<"selected">>}]; - true -> [] - end, - ?XAC(<<"option">>, - (Sel ++ - [{<<"value">>, O}]), - V) - end, - [{<<"month">>, translate:translate(Lang, ?T("Last month"))}, - {<<"year">>, translate:translate(Lang, ?T("Last year"))}, - {<<"all">>, - translate:translate(Lang, ?T("All activity"))}]))), - ?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"ordinary">>, - ?T("Show Ordinary Table")), - ?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"integral">>, - ?T("Show Integral Table"))])] - ++ Res, - Host, Lang, AJID, 3); -process_admin(Host, #request{path = [<<"stats">>], lang = Lang}, AJID) -> - Res = get_stats(Host, Lang), - PageH1 = ?H1GL(translate:translate(Lang, ?T("Statistics")), <<"modules/#mod-stats">>, <<"mod_stats">>), - Level = case Host of - global -> 1; - _ -> 3 - end, - make_xhtml(PageH1 ++ Res, Host, Lang, AJID, Level); -process_admin(Host, #request{path = [<<"user">>, U], - q = Query, lang = Lang}, AJID) -> + Level = 3 + length(RPath), + Set = [make_command(kick_users, + R, + [{<<"host">>, Host}], + [{style, danger}, {force_execution, false}])], + timer:sleep(200), % small delay after kicking users before getting the updated list + Get = [make_command(connected_users_vhost, + R, + [{<<"host">>, Host}], + [{table_options, {100, RPath}}, + {result_links, [{sessions, user, Level, <<"">>}]}])], + make_xhtml([?XCT(<<"h1">>, ?T("Online Users"))] ++ Set ++ Get, Host, R, AJID, Level); +process_admin(Host, + #request{path = [<<"last-activity">>], + q = Query, + lang = Lang} = + R, + AJID) + when is_binary(Host) -> + PageH1 = + ?H1GL(translate:translate(Lang, ?T("Users Last Activity")), + <<"modules/#mod_last">>, + <<"mod_last">>), + Res = make_command(webadmin_host_last_activity, + R, + [{<<"host">>, Host}, {<<"query">>, Query}, {<<"lang">>, Lang}], + []), + make_xhtml(PageH1 ++ [Res], Host, R, AJID, 3); +process_admin(Host, #request{path = [<<"user">>, U], lang = Lang} = R, AJID) -> case ejabberd_auth:user_exists(U, Host) of - true -> - Res = user_info(U, Host, Query, Lang), - make_xhtml(Res, Host, Lang, AJID, 4); - false -> - make_xhtml([?XCT(<<"h1">>, ?T("Not Found"))], Host, - Lang, AJID, 4) + true -> + Res = user_info(U, Host, R), + make_xhtml(Res, Host, U, R, AJID, 4); + false -> + make_xhtml([?XCT(<<"h1">>, ?T("Not Found"))], Host, R, AJID, 4) end; -process_admin(Host, #request{path = [<<"nodes">>], lang = Lang}, AJID) -> - Res = get_nodes(Lang), - Level = case Host of - global -> 1; - _ -> 3 - end, - make_xhtml(Res, Host, Lang, AJID, Level); -process_admin(Host, #request{path = [<<"node">>, SNode | NPath], - q = Query, lang = Lang}, AJID) -> +process_admin(Host, #request{path = [<<"nodes">>]} = R, AJID) -> + Level = + case Host of + global -> + 1; + _ -> + 3 + end, + Res = ?H1GLraw(<<"Nodes">>, <<"admin/guide/clustering/">>, <<"Clustering">>) + ++ [make_command(list_cluster, R, [], [{result_links, [{node, node, 1, <<"">>}]}])], + make_xhtml(Res, Host, R, AJID, Level); +process_admin(Host, + #request{path = [<<"node">>, SNode | NPath], lang = Lang} = Request, + AJID) -> case search_running_node(SNode) of - false -> - make_xhtml([?XCT(<<"h1">>, ?T("Node not found"))], Host, - Lang, AJID, 2); - Node -> - Res = get_node(Host, Node, NPath, Query, Lang), - Level = case Host of - global -> 2 + length(NPath); - _ -> 4 + length(NPath) - end, - make_xhtml(Res, Host, Node, Lang, AJID, Level) + false -> + make_xhtml([?XCT(<<"h1">>, ?T("Node not found"))], Host, Request, AJID, 2); + Node -> + Res = get_node(Host, Node, NPath, Request#request{path = NPath}), + Level = + case Host of + global -> + 2 + length(NPath); + _ -> + 4 + length(NPath) + end, + make_xhtml(Res, Host, Node, Request, AJID, Level) end; %%%================================== %%%% process_admin default case -process_admin(Host, #request{lang = Lang} = Request, AJID) -> - Res = case Host of - global -> - ejabberd_hooks:run_fold( - webadmin_page_main, Host, [], [Request]); - _ -> - ejabberd_hooks:run_fold( - webadmin_page_host, Host, [], [Host, Request]) - end, - Level = case Host of - global -> length(Request#request.path); - _ -> 2 + length(Request#request.path) - end, +process_admin(Host, #request{path = Path} = Request, AJID) -> + {Username, RPath} = + case Path of + [<<"user">>, U | UPath] -> + {U, UPath}; + _ -> + {unspecified, Path} + end, + Request2 = Request#request{path = RPath}, + Res = case {Host, Username} of + {global, _} -> + ejabberd_hooks:run_fold(webadmin_page_main, Host, [], [Request2]); + {_, unspecified} -> + ejabberd_hooks:run_fold(webadmin_page_host, Host, [], [Host, Request2]); + {_Host, Username} -> + ejabberd_hooks:run_fold(webadmin_page_hostuser, + Host, + [], + [Host, Username, Request2]) + end, + Level = + case Host of + global -> + length(Request#request.path); + _ -> + 2 + length(Request#request.path) + end, case Res of - [] -> - setelement(1, - make_xhtml([?XC(<<"h1">>, <<"Not Found">>)], Host, Lang, - AJID, Level), - 404); - _ -> make_xhtml(Res, Host, Lang, AJID, Level) + [] -> + setelement(1, + make_xhtml([?XC(<<"h1">>, <<"Not Found">>)], Host, Request, AJID, Level), + 404); + _ -> + make_xhtml(Res, Host, Username, Request, AJID, Level) + end. +%% @format-end + +term_to_id([]) -> <<>>; +term_to_id(T) -> base64:encode((term_to_binary(T))). +id_to_term(<<>>) -> []; +id_to_term(I) -> binary_to_term(base64:decode(I)). + +can_user_access_host(Host, #request{auth = Auth, + host = HostHTTP, + method = Method}) -> + Path = [<<"server">>, Host], + case get_auth_admin(Auth, HostHTTP, Path, Method) of + {ok, _} -> + true; + {unauthorized, _Error} -> + false end. -term_to_id(T) -> base64:encode((term_to_binary(T))). %%%================================== %%%% list_vhosts -list_vhosts(Lang, JID) -> +list_vhosts_allowed(JID) -> Hosts = ejabberd_option:hosts(), - HostsAllowed = lists:filter(fun (Host) -> + lists:filter(fun (Host) -> any_rules_allowed(Host, - [configure, webadmin_view], + [configure], JID) end, - Hosts), - list_vhosts2(Lang, HostsAllowed). + Hosts). -list_vhosts2(Lang, Hosts) -> - SHosts = lists:sort(Hosts), - [?XE(<<"table">>, - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Host")), - ?XACT(<<"td">>, - [{<<"class">>, <<"alignright">>}], - ?T("Registered Users")), - ?XACT(<<"td">>, - [{<<"class">>, <<"alignright">>}], - ?T("Online Users"))])]), - ?XE(<<"tbody">>, - (lists:map(fun (Host) -> - OnlineUsers = - length(ejabberd_sm:get_vh_session_list(Host)), - RegisteredUsers = - ejabberd_auth:count_users(Host), - ?XE(<<"tr">>, - [?XE(<<"td">>, - [?AC(<<"../server/", Host/binary, - "/">>, - Host)]), - ?XAE(<<"td">>, - [{<<"class">>, <<"alignright">>}], - [?AC(<<"../server/", Host/binary, "/users/">>, - pretty_string_int(RegisteredUsers))]), - ?XAE(<<"td">>, - [{<<"class">>, <<"alignright">>}], - [?AC(<<"../server/", Host/binary, "/online-users/">>, - pretty_string_int(OnlineUsers))])]) - end, - SHosts)))])]. +maybe_disclaimer_not_admin(MenuItems, AJID, _Lang) -> + case {MenuItems, list_vhosts_allowed(AJID)} of + {[_], []} -> + [?BR, + ?DIVRES([?C(<<"Apparently your account has no administration rights in " + "this server. Please check how to grant admin rights: ">>), + ?AC(<<"https://docs.ejabberd.im/admin/install/next-steps/#administration-account">>, + <<"ejabberd Docs: Administration Account">>)]) + ]; + _ -> + [] + end. %%%================================== %%%% list_users -list_users(Host, Query, Lang, URLFunc) -> - Res = list_users_parse_query(Query, Host), - Users = ejabberd_auth:get_users(Host), - SUsers = lists:sort([{S, U} || {U, S} <- Users]), - FUsers = case length(SUsers) of - N when N =< 100 -> - [list_given_users(Host, SUsers, <<"../">>, Lang, - URLFunc)]; - N -> - NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) - + 1, - M = trunc(N / NParts) + 1, - lists:flatmap(fun (K) -> - L = K + M - 1, - Last = if L < N -> - su_to_list(lists:nth(L, - SUsers)); - true -> - su_to_list(lists:last(SUsers)) - end, - Name = <<(su_to_list(lists:nth(K, - SUsers)))/binary, - $\s, 226, 128, 148, $\s, - Last/binary>>, - [?AC((URLFunc({user_diapason, K, L})), - Name), - ?BR] - end, - lists:seq(1, N, M)) - end, - case Res of -%% Parse user creation query and try register: - ok -> [?XREST(?T("Submitted"))]; - error -> [?XREST(?T("Bad format"))]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - ([?XE(<<"table">>, - [?XE(<<"tr">>, - [?XC(<<"td">>, <<(translate:translate(Lang, ?T("User")))/binary, ":">>), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"newusername">>, <<"">>)]), - ?XE(<<"td">>, [?C(<<" @ ", Host/binary>>)])]), - ?XE(<<"tr">>, - [?XC(<<"td">>, <<(translate:translate(Lang, ?T("Password")))/binary, ":">>), - ?XE(<<"td">>, - [?INPUT(<<"password">>, <<"newuserpassword">>, - <<"">>)]), - ?X(<<"td">>)]), - ?XE(<<"tr">>, - [?X(<<"td">>), - ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], - [?INPUTT(<<"submit">>, <<"addnewuser">>, - ?T("Add User"))]), - ?X(<<"td">>)])]), - ?P] - ++ FUsers))]. +%% @format-begin -list_users_parse_query(Query, Host) -> - case lists:keysearch(<<"addnewuser">>, 1, Query) of - {value, _} -> - {value, {_, Username}} = - lists:keysearch(<<"newusername">>, 1, Query), - {value, {_, Password}} = - lists:keysearch(<<"newuserpassword">>, 1, Query), - try jid:decode(<>) - of - #jid{user = User, server = Server} -> - case ejabberd_auth:try_register(User, Server, Password) - of - {error, _Reason} -> error; - _ -> ok - end - catch _:{bad_jid, _} -> - error - end; - false -> nothing +list_users(Host, Level, PageSize, RPath, R, RegisterEl) -> + Usernames = + case make_command_raw_value(registered_users, R, [{<<"host">>, Host}]) of + As when is_list(As) -> + As; + _ -> + {Aser, Aerver} = R#request.us, + ?INFO_MSG("Access to WebAdmin page users/ for account ~s@~s was denied", + [Aser, Aerver]), + [] + end, + case length(Usernames) of + N when N =< 10 -> + list_users(Host, Level, PageSize, RPath, R, Usernames, RegisterEl); + N when N > 10 -> + list_users_diapason(Host, R, Usernames, N, RegisterEl) end. -list_users_in_diapason(Host, Diap, Lang, URLFunc) -> - Users = ejabberd_auth:get_users(Host), - SUsers = lists:sort([{S, U} || {U, S} <- Users]), +list_users(Host, Level, PageSize, RPath, R, Usernames, RegisterEl) -> + IsOffline = gen_mod:is_loaded(Host, mod_offline), + IsMam = gen_mod:is_loaded(Host, mod_mam), + IsRoster = gen_mod:is_loaded(Host, mod_roster), + IsLast = gen_mod:is_loaded(Host, mod_last), + Columns = + [<<"user">>, + list_users_element(IsOffline, column, offline, {}), + list_users_element(IsMam, column, mam, {}), + list_users_element(IsRoster, column, roster, {}), + list_users_element(IsLast, column, timestamp, {}), + list_users_element(IsLast, column, status, {})], + Rows = + [list_to_tuple(lists:flatten([make_command(echo, + R, + [{<<"sentence">>, + jid:encode( + jid:make(Username, Host))}], + [{only, raw_and_value}, + {result_links, + [{sentence, user, Level, <<"">>}]}]), + list_users_element(IsOffline, + row, + offline, + {R, Username, Host, Level}), + list_users_element(IsMam, + row, + mam, + {R, Username, Host, Level}), + list_users_element(IsRoster, + row, + roster, + {R, Username, Host, Level}), + list_users_element(IsLast, row, last, {R, Username, Host})])) + || Username <- Usernames], + Table = make_table(PageSize, RPath, lists:flatten(Columns), Rows), + Result = + [RegisterEl, + make_command(registered_users, R, [], [{only, presentation}]), + list_users_element(IsOffline, presentation, offline, R), + list_users_element(IsMam, presentation, mam, R), + list_users_element(IsRoster, presentation, roster, R), + list_users_element(IsLast, presentation, last, R), + Table], + lists:flatten(Result). + +list_users_element(false, _, _, _) -> + []; +list_users_element(_, column, offline, _) -> + {<<"offline">>, right}; +list_users_element(_, column, mam, _) -> + {<<"mam">>, right}; +list_users_element(_, column, roster, _) -> + {<<"roster">>, right}; +list_users_element(_, column, timestamp, _) -> + {<<"timestamp">>, left}; +list_users_element(_, column, status, _) -> + {<<"status">>, left}; +list_users_element(_, row, offline, {R, Username, Host, Level}) -> + make_command(get_offline_count, + R, + [{<<"user">>, Username}, {<<"host">>, Host}], + [{only, raw_and_value}, + {result_links, + [{value, arg_host, Level, <<"user/", Username/binary, "/queue/">>}]}]); +list_users_element(_, row, mam, {R, Username, Host, Level}) -> + make_command(get_mam_count, + R, + [{<<"user">>, Username}, {<<"host">>, Host}], + [{only, raw_and_value}, + {result_links, + [{value, arg_host, Level, <<"user/", Username/binary, "/mam/">>}]}]); +list_users_element(_, row, roster, {R, Username, Host, Level}) -> + make_command(get_roster_count, + R, + [{<<"user">>, Username}, {<<"host">>, Host}], + [{only, raw_and_value}, + {result_links, + [{value, arg_host, Level, <<"user/", Username/binary, "/roster/">>}]}]); +list_users_element(_, row, last, {R, Username, Host}) -> + [?C(element(1, + make_command_raw_value(get_last, R, [{<<"user">>, Username}, {<<"host">>, Host}]))), + ?C(element(2, + make_command_raw_value(get_last, + R, + [{<<"user">>, Username}, {<<"host">>, Host}])))]; +list_users_element(_, presentation, offline, R) -> + make_command(get_offline_count, R, [], [{only, presentation}]); +list_users_element(_, presentation, mam, R) -> + make_command(get_mam_count, R, [], [{only, presentation}]); +list_users_element(_, presentation, roster, R) -> + make_command(get_roster_count, R, [], [{only, presentation}]); +list_users_element(_, presentation, last, R) -> + make_command(get_last, R, [], [{only, presentation}]). + +list_users_diapason(Host, R, Usernames, N, RegisterEl) -> + URLFunc = fun url_func/1, + SUsers = [{Host, U} || U <- Usernames], + NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, + M = trunc(N / NParts) + 1, + FUsers = + lists:flatmap(fun(K) -> + L = K + M - 1, + Last = + if L < N -> + su_to_list(lists:nth(L, SUsers)); + true -> + su_to_list(lists:last(SUsers)) + end, + Name = + <<(su_to_list(lists:nth(K, SUsers)))/binary, + $\s, + 226, + 128, + 148, + $\s, + Last/binary>>, + [?AC(URLFunc({user_diapason, K, L}), Name), ?BR] + end, + lists:seq(1, N, M)), + [RegisterEl, + make_command(get_offline_count, R, [], [{only, presentation}]), + ?AC(<<"top/offline/">>, <<"View Top Offline Queues">>), + make_command(get_roster_count, R, [], [{only, presentation}]), + ?AC(<<"top/roster/">>, <<"View Top Rosters">>), + make_command(get_last, R, [], [{only, presentation}]), + ?AC(<<"top/last/">>, <<"View Top-Oldest Last Activity">>), + make_command(registered_users, R, [], [{only, presentation}])] + ++ FUsers. + +list_users_in_diapason(Host, Level, PageSize, RPath, R, Diap, RegisterEl) -> + Usernames = + case make_command_raw_value(registered_users, R, [{<<"host">>, Host}]) of + As when is_list(As) -> + As; + _ -> + {Aser, Aerver} = R#request.us, + ?INFO_MSG("Access to WebAdmin page users/ for account ~s@~s was denied", + [Aser, Aerver]), + [] + end, + SUsers = lists:sort([{Host, U} || U <- Usernames]), [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), N1 = binary_to_integer(S1), N2 = binary_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), - [list_given_users(Host, Sub, <<"../../">>, Lang, - URLFunc)]. + Usernames2 = [U || {_, U} <- Sub], + list_users(Host, Level, PageSize, RPath, R, Usernames2, RegisterEl). -list_given_users(Host, Users, Prefix, Lang, URLFunc) -> - ModOffline = get_offlinemsg_module(Host), - ?XE(<<"table">>, - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("User")), - ?XACT(<<"td">>, - [{<<"class">>, <<"alignright">>}], - ?T("Offline Messages")), - ?XCT(<<"td">>, ?T("Last Activity"))])]), - ?XE(<<"tbody">>, - (lists:map(fun (_SU = {Server, User}) -> - US = {User, Server}, - QueueLenStr = get_offlinemsg_length(ModOffline, - User, - Server), - FQueueLen = [?AC((URLFunc({users_queue, Prefix, - User, Server})), - QueueLenStr)], - FLast = case - ejabberd_sm:get_user_resources(User, - Server) - of - [] -> - case get_last_info(User, Server) of - not_found -> translate:translate(Lang, ?T("Never")); - {ok, Shift, _Status} -> - TimeStamp = {Shift div - 1000000, - Shift rem - 1000000, - 0}, - {{Year, Month, Day}, - {Hour, Minute, Second}} = - calendar:now_to_local_time(TimeStamp), - (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Year, - Month, - Day, - Hour, - Minute, - Second])) - end; - _ -> translate:translate(Lang, ?T("Online")) - end, - ?XE(<<"tr">>, - [?XE(<<"td">>, - [?AC((URLFunc({user, Prefix, - misc:url_encode(User), - Server})), - (us_to_list(US)))]), - ?XAE(<<"td">>, - [{<<"class">>, <<"alignright">>}], - FQueueLen), - ?XC(<<"td">>, FLast)]) - end, - Users)))]). - -get_offlinemsg_length(ModOffline, User, Server) -> - case ModOffline of - none -> <<"disabled">>; - _ -> - pretty_string_int(ModOffline:count_offline_messages(User,Server)) - end. - -get_offlinemsg_module(Server) -> - case gen_mod:is_loaded(Server, mod_offline) of - true -> mod_offline; - false -> none - end. +list_users_top(Host, Level, PageSize, RPath, R, Operation, RegisterEl) -> + Usernames = + case make_command_raw_value(registered_users, R, [{<<"host">>, Host}]) of + As when is_list(As) -> + As; + _ -> + {Aser, Aerver} = R#request.us, + ?INFO_MSG("Access to WebAdmin page users/ for account ~s@~s was denied", + [Aser, Aerver]), + [] + end, + {Command, Reverse} = + case Operation of + <<"roster">> -> + {get_roster_count, true}; + <<"offline">> -> + {get_offline_count, true}; + <<"last">> -> + {get_last, false} + end, + UsernamesCounts = + [{U, + make_command(Command, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{only, raw_value}, + {result_links, + [{value, arg_host, Level, <<"user/", U/binary, "/roster/">>}]}])} + || U <- Usernames], + USorted = lists:keysort(2, UsernamesCounts), + UReversed = + case Reverse of + true -> + lists:reverse(USorted); + false -> + USorted + end, + Usernames2 = [U || {U, _} <- lists:sublist(UReversed, 100)], + list_users(Host, Level, PageSize, RPath, R, Usernames2, RegisterEl). get_lastactivity_menuitem_list(Server) -> case gen_mod:is_loaded(Server, mod_last) of - true -> - case mod_last_opt:db_type(Server) of - mnesia -> [{<<"last-activity">>, ?T("Last Activity")}]; - _ -> [] - end; - false -> - [] - end. - -get_last_info(User, Server) -> - case gen_mod:is_loaded(Server, mod_last) of - true -> - mod_last:get_last_info(User, Server); - false -> - not_found + true -> + case mod_last_opt:db_type(Server) of + mnesia -> + [{<<"last-activity">>, ?T("Last Activity")}]; + _ -> + [] + end; + false -> + [] end. us_to_list({User, Server}) -> @@ -806,157 +958,69 @@ us_to_list({User, Server}) -> su_to_list({Server, User}) -> jid:encode({User, Server, <<"">>}). +%% @format-end + +%%%================================== +%%%% last-activity + +webadmin_host_last_activity(Host, Query, Lang) -> + ?DEBUG("Query: ~p", [Query]), + Month = case lists:keysearch(<<"period">>, 1, Query) of + {value, {_, Val}} -> Val; + _ -> <<"month">> + end, + Res = case lists:keysearch(<<"ordinary">>, 1, Query) of + {value, {_, _}} -> + list_last_activity(Host, Lang, false, Month); + _ -> list_last_activity(Host, Lang, true, Month) + end, + [?XAE(<<"form">>, + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [?CT(?T("Period: ")), + ?XAE(<<"select">>, [{<<"name">>, <<"period">>}], + (lists:map(fun ({O, V}) -> + Sel = if O == Month -> + [{<<"selected">>, + <<"selected">>}]; + true -> [] + end, + ?XAC(<<"option">>, + (Sel ++ + [{<<"value">>, O}]), + V) + end, + [{<<"month">>, translate:translate(Lang, ?T("Last month"))}, + {<<"year">>, translate:translate(Lang, ?T("Last year"))}, + {<<"all">>, + translate:translate(Lang, ?T("All activity"))}]))), + ?C(<<" ">>), + ?INPUTT(<<"submit">>, <<"ordinary">>, + ?T("Show Ordinary Table")), + ?C(<<" ">>), + ?INPUTT(<<"submit">>, <<"integral">>, + ?T("Show Integral Table"))])] + ++ Res. %%%================================== %%%% get_stats -get_stats(global, Lang) -> - OnlineUsers = ejabberd_sm:connected_users_number(), - RegisteredUsers = lists:foldl(fun (Host, Total) -> - ejabberd_auth:count_users(Host) - + Total - end, - 0, ejabberd_option:hosts()), - OutS2SNumber = ejabberd_s2s:outgoing_s2s_number(), - InS2SNumber = ejabberd_s2s:incoming_s2s_number(), - [?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Registered Users:")), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(RegisteredUsers)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Online Users:")), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(OnlineUsers)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Outgoing s2s Connections:")), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(OutS2SNumber)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Incoming s2s Connections:")), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(InS2SNumber)))])])])]; -get_stats(Host, Lang) -> - OnlineUsers = - length(ejabberd_sm:get_vh_session_list(Host)), - RegisteredUsers = - ejabberd_auth:count_users(Host), - [?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Registered Users:")), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(RegisteredUsers)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Online Users:")), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(OnlineUsers)))])])])]. - -list_online_users(Host, _Lang) -> - Users = [{S, U} - || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Host)], - SUsers = lists:usort(Users), - lists:flatmap(fun ({_S, U} = SU) -> - [?AC(<<"../user/", - (misc:url_encode(U))/binary, "/">>, - (su_to_list(SU))), - ?BR] - end, - SUsers). - -user_info(User, Server, Query, Lang) -> +user_info(User, Server, #request{q = Query, lang = Lang} = R) -> LServer = jid:nameprep(Server), US = {jid:nodeprep(User), LServer}, Res = user_parse_query(User, Server, Query), - Resources = ejabberd_sm:get_user_resources(User, - Server), - FResources = - case Resources of - [] -> [?CT(?T("None"))]; - _ -> - [?XE(<<"ul">>, - (lists:map( - fun (R) -> - FIP = case - ejabberd_sm:get_user_info(User, - Server, - R) - of - offline -> <<"">>; - Info - when - is_list(Info) -> - Node = - proplists:get_value(node, - Info), - Conn = - proplists:get_value(conn, - Info), - {IP, Port} = - proplists:get_value(ip, - Info), - ConnS = case Conn of - c2s -> - <<"plain">>; - c2s_tls -> - <<"tls">>; - c2s_compressed -> - <<"zlib">>; - c2s_compressed_tls -> - <<"tls+zlib">>; - http_bind -> - <<"http-bind">>; - websocket -> - <<"websocket">>; - _ -> - <<"unknown">> - end, - <> - end, - case direction(Lang) of - [{_, <<"rtl">>}] -> ?LI([?C((<>))]); - _ -> ?LI([?C((<>))]) - end - end, - lists:sort(Resources))))] - end, - FPassword = [?INPUT(<<"text">>, <<"password">>, <<"">>), - ?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"chpassword">>, - ?T("Change Password"))], UserItems = ejabberd_hooks:run_fold(webadmin_user, - LServer, [], [User, Server, Lang]), - LastActivity = case ejabberd_sm:get_user_resources(User, - Server) - of - [] -> - case get_last_info(User, Server) of - not_found -> translate:translate(Lang, ?T("Never")); - {ok, Shift, _Status} -> - TimeStamp = {Shift div 1000000, - Shift rem 1000000, 0}, - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:now_to_local_time(TimeStamp), - (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Year, Month, Day, - Hour, Minute, - Second])) - end; - _ -> translate:translate(Lang, ?T("Online")) - end, + LServer, [], [User, Server, R]), + Lasts = case gen_mod:is_loaded(Server, mod_last) of + true -> + [make_command(get_last, R, + [{<<"user">>, User}, {<<"host">>, Server}], + []), + make_command(set_last, R, + [{<<"user">>, User}, {<<"host">>, Server}], + [])]; + false -> + [] + end, [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("User ~ts"), [us_to_list(US)])))] ++ @@ -968,16 +1032,19 @@ user_info(User, Server, Query, Lang) -> ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - ([?XCT(<<"h3">>, ?T("Connected Resources:"))] ++ - FResources ++ - [?XCT(<<"h3">>, ?T("Password:"))] ++ - FPassword ++ - [?XCT(<<"h3">>, ?T("Last Activity"))] ++ - [?C(LastActivity)] ++ - UserItems ++ - [?P, - ?INPUTTD(<<"submit">>, <<"removeuser">>, - ?T("Remove User"))]))]. + ([make_command(user_sessions_info, R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, [{node, node, 4, <<>>}]}]), + make_command(change_password, R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{style, danger}])] ++ + Lasts ++ + UserItems ++ + [?P, + make_command(unregister, R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{style, danger}]) + ]))]. user_parse_query(User, Server, Query) -> lists:foldl(fun ({Action, _Value}, Acc) @@ -987,19 +1054,6 @@ user_parse_query(User, Server, Query) -> end, nothing, Query). -user_parse_query1(<<"password">>, _User, _Server, - _Query) -> - nothing; -user_parse_query1(<<"chpassword">>, User, Server, - Query) -> - case lists:keysearch(<<"password">>, 1, Query) of - {value, {_, Password}} -> - ejabberd_auth:set_password(User, Server, Password), ok; - _ -> error - end; -user_parse_query1(<<"removeuser">>, User, Server, - _Query) -> - ejabberd_auth:remove_user(User, Server), ok; user_parse_query1(Action, User, Server, Query) -> case ejabberd_hooks:run_fold(webadmin_user_parse_query, Server, [], [Action, User, Server, Query]) @@ -1071,33 +1125,6 @@ histogram([], _Integral, _Current, Count, Hist) -> %%%================================== %%%% get_nodes -get_nodes(Lang) -> - RunningNodes = ejabberd_cluster:get_nodes(), - StoppedNodes = ejabberd_cluster:get_known_nodes() - -- RunningNodes, - FRN = if RunningNodes == [] -> ?CT(?T("None")); - true -> - ?XE(<<"ul">>, - (lists:map(fun (N) -> - S = iolist_to_binary(atom_to_list(N)), - ?LI([?AC(<<"../node/", S/binary, "/">>, - S)]) - end, - lists:sort(RunningNodes)))) - end, - FSN = if StoppedNodes == [] -> ?CT(?T("None")); - true -> - ?XE(<<"ul">>, - (lists:map(fun (N) -> - S = iolist_to_binary(atom_to_list(N)), - ?LI([?C(S)]) - end, - lists:sort(StoppedNodes)))) - end, - [?XCT(<<"h1">>, ?T("Nodes")), - ?XCT(<<"h3">>, ?T("Running Nodes")), FRN, - ?XCT(<<"h3">>, ?T("Stopped Nodes")), FSN]. - search_running_node(SNode) -> RunningNodes = ejabberd_cluster:get_nodes(), search_running_node(SNode, RunningNodes). @@ -1109,601 +1136,104 @@ search_running_node(SNode, [Node | Nodes]) -> _ -> search_running_node(SNode, Nodes) end. -get_node(global, Node, [], Query, Lang) -> - Res = node_parse_query(Node, Query), +get_node(global, Node, [], #request{lang = Lang}) -> Base = get_base_path(global, Node, 2), - MenuItems2 = make_menu_items(global, Node, Base, Lang), + BaseItems = [{<<"db">>, <<"Mnesia Tables">>}, + {<<"backup">>, <<"Mnesia Backup">>}], + MenuItems = make_menu_items(global, Node, Base, Lang, BaseItems), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node])))] ++ - case Res of - ok -> [?XREST(?T("Submitted"))]; - error -> [?XREST(?T("Bad format"))]; - nothing -> [] - end - ++ - [?XE(<<"ul">>, - ([?LI([?ACT(<<"db/">>, ?T("Database"))]), - ?LI([?ACT(<<"backup/">>, ?T("Backup"))]), - ?LI([?ACT(<<"stats/">>, ?T("Statistics"))]), - ?LI([?ACT(<<"update/">>, ?T("Update"))])] - ++ MenuItems2)), - ?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?INPUTT(<<"submit">>, <<"restart">>, ?T("Restart")), - ?C(<<" ">>), - ?INPUTTD(<<"submit">>, <<"stop">>, ?T("Stop"))])]; -get_node(Host, Node, [], _Query, Lang) -> + [?XE(<<"ul">>, MenuItems)]; +get_node(Host, Node, [], #request{lang = Lang}) -> Base = get_base_path(Host, Node, 4), - MenuItems2 = make_menu_items(Host, Node, Base, Lang), + MenuItems2 = make_menu_items(Host, Node, Base, Lang, []), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node]))), ?XE(<<"ul">>, MenuItems2)]; -get_node(global, Node, [<<"db">>], Query, Lang) -> - case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of - {badrpc, _Reason} -> - [?XCT(<<"h1">>, ?T("RPC Call Error"))]; - Tables -> - ResS = case node_db_parse_query(Node, Tables, Query) of - nothing -> []; - ok -> [?XREST(?T("Submitted"))] - end, - STables = lists:sort(Tables), - Rows = lists:map(fun (Table) -> - STable = - iolist_to_binary(atom_to_list(Table)), - TInfo = case ejabberd_cluster:call(Node, mnesia, - table_info, - [Table, all]) - of - {badrpc, _} -> []; - I -> I - end, - {Type, Size, Memory} = case - {lists:keysearch(storage_type, - 1, - TInfo), - lists:keysearch(size, - 1, - TInfo), - lists:keysearch(memory, - 1, - TInfo)} - of - {{value, - {storage_type, - T}}, - {value, {size, S}}, - {value, - {memory, M}}} -> - {T, S, M}; - _ -> {unknown, 0, 0} - end, - MemoryB = Memory*erlang:system_info(wordsize), - ?XE(<<"tr">>, - [?XE(<<"td">>, - [?AC(<<"./", STable/binary, - "/">>, - STable)]), - ?XE(<<"td">>, - [db_storage_select(STable, Type, - Lang)]), - ?XAE(<<"td">>, - [{<<"class">>, <<"alignright">>}], - [?AC(<<"./", STable/binary, - "/1/">>, - (pretty_string_int(Size)))]), - ?XAC(<<"td">>, - [{<<"class">>, <<"alignright">>}], - (pretty_string_int(MemoryB)))]) - end, - STables), - [?XC(<<"h1">>, - (str:translate_and_format(Lang, ?T("Database Tables at ~p"), - [Node])) - )] - ++ - ResS ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XAE(<<"table">>, [], - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Name")), - ?XCT(<<"td">>, ?T("Storage Type")), - ?XACT(<<"td">>, - [{<<"class">>, <<"alignright">>}], - ?T("Elements")), - ?XACT(<<"td">>, - [{<<"class">>, <<"alignright">>}], - ?T("Memory"))])]), - ?XE(<<"tbody">>, - (Rows ++ - [?XE(<<"tr">>, - [?XAE(<<"td">>, - [{<<"colspan">>, <<"4">>}, - {<<"class">>, <<"alignright">>}], - [?INPUTT(<<"submit">>, - <<"submit">>, - ?T("Submit"))])])]))])])] - end; -get_node(global, Node, [<<"db">>, TableName], _Query, Lang) -> - make_table_view(Node, TableName, Lang); -get_node(global, Node, [<<"db">>, TableName, PageNumber], _Query, Lang) -> - make_table_elements_view(Node, TableName, Lang, binary_to_integer(PageNumber)); -get_node(global, Node, [<<"backup">>], Query, Lang) -> - HomeDirRaw = case {os:getenv("HOME"), os:type()} of - {EnvHome, _} when is_list(EnvHome) -> list_to_binary(EnvHome); - {false, {win32, _Osname}} -> <<"C:/">>; - {false, _} -> <<"/tmp/">> - end, - HomeDir = filename:nativename(HomeDirRaw), - ResS = case node_backup_parse_query(Node, Query) of - nothing -> []; - ok -> [?XREST(?T("Submitted"))]; - {error, Error} -> - [?XRES(<<(translate:translate(Lang, ?T("Error")))/binary, ": ", - ((str:format("~p", [Error])))/binary>>)] - end, - [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Backup of ~p"), [Node])))] - ++ - ResS ++ - [?XCT(<<"p">>, - ?T("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.")), - ?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Store binary backup:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"storepath">>, - (filename:join(HomeDir, - "ejabberd.backup")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"store">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Restore binary backup immediately:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"restorepath">>, - (filename:join(HomeDir, - "ejabberd.backup")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"restore">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Restore binary backup after next ejabberd " - "restart (requires less memory):")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"fallbackpath">>, - (filename:join(HomeDir, - "ejabberd.backup")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"fallback">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Store plain text backup:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"dumppath">>, - (filename:join(HomeDir, - "ejabberd.dump")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"dump">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Restore plain text backup immediately:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"loadpath">>, - (filename:join(HomeDir, - "ejabberd.dump")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"load">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Import users data from a PIEFXIS file " - "(XEP-0227):")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, - <<"import_piefxis_filepath">>, - (filename:join(HomeDir, - "users.xml")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, - <<"import_piefxis_file">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Export data of all users in the server " - "to PIEFXIS files (XEP-0227):")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, - <<"export_piefxis_dirpath">>, - HomeDir)]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, - <<"export_piefxis_dir">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XE(<<"td">>, - [?CT(?T("Export data of users in a host to PIEFXIS " - "files (XEP-0227):")), - ?C(<<" ">>), - make_select_host(Lang, <<"export_piefxis_host_dirhost">>)]), - ?XE(<<"td">>, - [?INPUT(<<"text">>, - <<"export_piefxis_host_dirpath">>, - HomeDir)]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, - <<"export_piefxis_host_dir">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XE(<<"td">>, - [?CT(?T("Export all tables as SQL queries " - "to a file:")), - ?C(<<" ">>), - make_select_host(Lang, <<"export_sql_filehost">>)]), - ?XE(<<"td">>, - [?INPUT(<<"text">>, - <<"export_sql_filepath">>, - (filename:join(HomeDir, - "db.sql")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"export_sql_file">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Import user data from jabberd14 spool " - "file:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"import_filepath">>, - (filename:join(HomeDir, - "user1.xml")))]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"import_file">>, - ?T("OK"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, - ?T("Import users data from jabberd14 spool " - "directory:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"import_dirpath">>, - <<"/var/spool/jabber/">>)]), - ?XE(<<"td">>, - [?INPUTT(<<"submit">>, <<"import_dir">>, - ?T("OK"))])])])])])]; -get_node(global, Node, [<<"stats">>], _Query, Lang) -> - UpTime = ejabberd_cluster:call(Node, erlang, statistics, - [wall_clock]), - UpTimeS = (str:format("~.3f", - [element(1, UpTime) / 1000])), - UpTimeDate = uptime_date(Node), - CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]), - CPUTimeS = (str:format("~.3f", - [element(1, CPUTime) / 1000])), - OnlineUsers = ejabberd_sm:connected_users_number(), - TransactionsCommitted = ejabberd_cluster:call(Node, mnesia, - system_info, [transaction_commits]), - TransactionsAborted = ejabberd_cluster:call(Node, mnesia, - system_info, [transaction_failures]), - TransactionsRestarted = ejabberd_cluster:call(Node, mnesia, - system_info, [transaction_restarts]), - TransactionsLogged = ejabberd_cluster:call(Node, mnesia, system_info, - [transaction_log_writes]), - [?XC(<<"h1">>, - (str:translate_and_format(Lang, ?T("Statistics of ~p"), [Node]))), - ?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Uptime:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - UpTimeS)]), - ?XE(<<"tr">>, - [?X(<<"td">>), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - UpTimeDate)]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("CPU Time:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - CPUTimeS)]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Online Users:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(OnlineUsers)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Transactions Committed:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(TransactionsCommitted)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Transactions Aborted:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(TransactionsAborted)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Transactions Restarted:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(TransactionsRestarted)))]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Transactions Logged:")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(TransactionsLogged)))])])])]; -get_node(global, Node, [<<"update">>], Query, Lang) -> - ejabberd_cluster:call(Node, code, purge, [ejabberd_update]), - Res = node_update_parse_query(Node, Query), - ejabberd_cluster:call(Node, code, load_file, [ejabberd_update]), - {ok, _Dir, UpdatedBeams, Script, LowLevelScript, - Check} = - ejabberd_cluster:call(Node, ejabberd_update, update_info, []), - Mods = case UpdatedBeams of - [] -> ?CT(?T("None")); - _ -> - BeamsLis = lists:map(fun (Beam) -> - BeamString = - iolist_to_binary(atom_to_list(Beam)), - ?LI([?INPUT(<<"checkbox">>, - <<"selected">>, - BeamString), - ?C(BeamString)]) - end, - UpdatedBeams), - SelectButtons = [?BR, - ?INPUTATTRS(<<"button">>, <<"selectall">>, - ?T("Select All"), - [{<<"onClick">>, - <<"selectAll()">>}]), - ?C(<<" ">>), - ?INPUTATTRS(<<"button">>, <<"unselectall">>, - ?T("Unselect All"), - [{<<"onClick">>, - <<"unSelectAll()">>}])], - ?XAE(<<"ul">>, [{<<"class">>, <<"nolistyle">>}], - (BeamsLis ++ SelectButtons)) - end, - FmtScript = (?XC(<<"pre">>, - (str:format("~p", [Script])))), - FmtLowLevelScript = (?XC(<<"pre">>, - (str:format("~p", [LowLevelScript])))), - [?XC(<<"h1">>, - (str:translate_and_format(Lang, ?T("Update ~p"), [Node])))] - ++ - case Res of - ok -> [?XREST(?T("Submitted"))]; - {error, ErrorText} -> - [?XREST(<<"Error: ", ErrorText/binary>>)]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XCT(<<"h2">>, ?T("Update plan")), - ?XCT(<<"h3">>, ?T("Modified modules")), Mods, - ?XCT(<<"h3">>, ?T("Update script")), FmtScript, - ?XCT(<<"h3">>, ?T("Low level update script")), - FmtLowLevelScript, ?XCT(<<"h3">>, ?T("Script check")), - ?XC(<<"pre">>, (misc:atom_to_binary(Check))), - ?BR, - ?INPUTT(<<"submit">>, <<"update">>, ?T("Update"))])]; -get_node(Host, Node, NPath, Query, Lang) -> + +get_node(global, Node, [<<"db">> | RPath], R) -> + PageTitle = <<"Mnesia Tables">>, + Title = ?XC(<<"h1">>, PageTitle), + Level = length(RPath), + [Title, ?BR | webadmin_db(Node, RPath, R, Level)]; + +get_node(global, Node, [<<"backup">>], #request{lang = Lang} = R) -> + Types = [{<<"#binary">>, <<"Binary">>}, + {<<"#plaintext">>, <<"Plain Text">>}, + {<<"#piefxis">>, <<"PIEXFIS (XEP-0227)">>}, + {<<"#sql">>, <<"SQL">>}, + {<<"#prosody">>, <<"Prosody">>}, + {<<"#jabberd14">>, <<"jabberd 1.4">>}], + + [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Backup of ~p"), [Node]))), + ?XCT(<<"p">>, + ?T("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.")), + ?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types]), + + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"binary">>}], <<"Binary">>), + ?XCT(<<"p">>, ?T("Store binary backup:")), + ?XE(<<"blockquote">>, [make_command(backup, R)]), + ?XCT(<<"p">>, ?T("Restore binary backup immediately:")), + ?XE(<<"blockquote">>, [make_command(restore, R, [], [{style, danger}])]), + ?XCT(<<"p">>, ?T("Restore binary backup after next ejabberd " + "restart (requires less memory):")), + ?XE(<<"blockquote">>, [make_command(install_fallback, R, [], [{style, danger}])]), + + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"plaintext">>}], <<"Plain Text">>), + ?XCT(<<"p">>, ?T("Store plain text backup:")), + ?XE(<<"blockquote">>, [make_command(dump, R)]), + ?XCT(<<"p">>, ?T("Restore plain text backup immediately:")), + ?XE(<<"blockquote">>, [make_command(load, R, [], [{style, danger}])]), + + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"piefxis">>}], <<"PIEFXIS (XEP-0227)">>), + ?XCT(<<"p">>, ?T("Import users data from a PIEFXIS file (XEP-0227):")), + ?XE(<<"blockquote">>, [make_command(import_piefxis, R)]), + ?XCT(<<"p">>, ?T("Export data of all users in the server to PIEFXIS files (XEP-0227):")), + ?XE(<<"blockquote">>, [make_command(export_piefxis, R)]), + ?XCT(<<"p">>, ?T("Export data of users in a host to PIEFXIS files (XEP-0227):")), + ?XE(<<"blockquote">>, [make_command(export_piefxis_host, R)]), + + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"sql">>}], <<"SQL">>), + ?XCT(<<"p">>, ?T("Export all tables as SQL queries to a file:")), + ?XE(<<"blockquote">>, [make_command(export2sql, R)]), + + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"prosody">>}], <<"Prosody">>), + ?XCT(<<"p">>, <<"Import data from Prosody:">>), + ?XE(<<"blockquote">>, [make_command(import_prosody, R)]), + + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"jabberd14">>}], <<"jabberd 1.4">>), + ?XCT(<<"p">>, ?T("Import user data from jabberd14 spool file:")), + ?XE(<<"blockquote">>, [make_command(import_file, R)]), + ?XCT(<<"p">>, ?T("Import users data from jabberd14 spool directory:")), + ?XE(<<"blockquote">>, [make_command(import_dir, R)]) + ]; +get_node(Host, Node, _NPath, Request) -> Res = case Host of global -> ejabberd_hooks:run_fold(webadmin_page_node, Host, [], - [Node, NPath, Query, Lang]); + [Node, Request]); _ -> ejabberd_hooks:run_fold(webadmin_page_hostnode, Host, [], - [Host, Node, NPath, Query, Lang]) + [Host, Node, Request]) end, case Res of [] -> [?XC(<<"h1">>, <<"Not Found">>)]; _ -> Res end. -uptime_date(Node) -> - Localtime = ejabberd_cluster:call(Node, erlang, localtime, []), - Now = calendar:datetime_to_gregorian_seconds(Localtime), - {Wall, _} = ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]), - LastRestart = Now - (Wall div 1000), - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:gregorian_seconds_to_datetime(LastRestart), - str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Year, Month, Day, Hour, Minute, Second]). - %%%================================== %%%% node parse -node_parse_query(Node, Query) -> - case lists:keysearch(<<"restart">>, 1, Query) of - {value, _} -> - case ejabberd_cluster:call(Node, init, restart, []) of - {badrpc, _Reason} -> error; - _ -> ok - end; - _ -> - case lists:keysearch(<<"stop">>, 1, Query) of - {value, _} -> - case ejabberd_cluster:call(Node, init, stop, []) of - {badrpc, _Reason} -> error; - _ -> ok - end; - _ -> nothing - end - end. - -make_select_host(Lang, Name) -> - ?XAE(<<"select">>, - [{<<"name">>, Name}], - (lists:map(fun (Host) -> - ?XACT(<<"option">>, - ([{<<"value">>, Host}]), Host) - end, - ejabberd_config:get_option(hosts)))). - -db_storage_select(ID, Opt, Lang) -> - ?XAE(<<"select">>, - [{<<"name">>, <<"table", ID/binary>>}], - (lists:map(fun ({O, Desc}) -> - Sel = if O == Opt -> - [{<<"selected">>, <<"selected">>}]; - true -> [] - end, - ?XACT(<<"option">>, - (Sel ++ - [{<<"value">>, - iolist_to_binary(atom_to_list(O))}]), - Desc) - end, - [{ram_copies, ?T("RAM copy")}, - {disc_copies, ?T("RAM and disc copy")}, - {disc_only_copies, ?T("Disc only copy")}, - {unknown, ?T("Remote copy")}, - {delete_content, ?T("Delete content")}, - {delete_table, ?T("Delete table")}]))). - -node_db_parse_query(_Node, _Tables, [{nokey, <<>>}]) -> - nothing; -node_db_parse_query(Node, Tables, Query) -> - lists:foreach(fun (Table) -> - STable = iolist_to_binary(atom_to_list(Table)), - case lists:keysearch(<<"table", STable/binary>>, 1, - Query) - of - {value, {_, SType}} -> - Type = case SType of - <<"unknown">> -> unknown; - <<"ram_copies">> -> ram_copies; - <<"disc_copies">> -> disc_copies; - <<"disc_only_copies">> -> - disc_only_copies; - <<"delete_content">> -> delete_content; - <<"delete_table">> -> delete_table; - _ -> false - end, - if Type == false -> ok; - Type == delete_content -> - mnesia:clear_table(Table); - Type == delete_table -> - mnesia:delete_table(Table); - 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; - _ -> ok - end - end, - Tables), - ok. - -node_backup_parse_query(_Node, [{nokey, <<>>}]) -> - nothing; -node_backup_parse_query(Node, Query) -> - lists:foldl(fun (Action, nothing) -> - case lists:keysearch(Action, 1, Query) of - {value, _} -> - case lists:keysearch(<>, 1, - Query) - of - {value, {_, Path}} -> - Res = case Action of - <<"store">> -> - ejabberd_cluster:call(Node, mnesia, backup, - [binary_to_list(Path)]); - <<"restore">> -> - ejabberd_cluster:call(Node, ejabberd_admin, - restore, [Path]); - <<"fallback">> -> - ejabberd_cluster:call(Node, mnesia, - install_fallback, - [binary_to_list(Path)]); - <<"dump">> -> - ejabberd_cluster:call(Node, ejabberd_admin, - dump_to_textfile, - [Path]); - <<"load">> -> - ejabberd_cluster:call(Node, mnesia, - load_textfile, - [binary_to_list(Path)]); - <<"import_piefxis_file">> -> - ejabberd_cluster:call(Node, ejabberd_piefxis, - import_file, [Path]); - <<"export_piefxis_dir">> -> - ejabberd_cluster:call(Node, ejabberd_piefxis, - export_server, [Path]); - <<"export_piefxis_host_dir">> -> - {value, {_, Host}} = - lists:keysearch(<>, - 1, Query), - ejabberd_cluster:call(Node, ejabberd_piefxis, - export_host, - [Path, Host]); - <<"export_sql_file">> -> - {value, {_, Host}} = - lists:keysearch(<>, - 1, Query), - ejabberd_cluster:call(Node, ejd2sql, - export, [Host, Path]); - <<"import_file">> -> - ejabberd_cluster:call(Node, ejabberd_admin, - import_file, [Path]); - <<"import_dir">> -> - ejabberd_cluster:call(Node, ejabberd_admin, - import_dir, [Path]) - end, - case Res of - {error, Reason} -> {error, Reason}; - {badrpc, Reason} -> {badrpc, Reason}; - _ -> ok - end; - OtherError -> {error, OtherError} - end; - _ -> nothing - end; - (_Action, Res) -> Res - end, - nothing, - [<<"store">>, <<"restore">>, <<"fallback">>, <<"dump">>, - <<"load">>, <<"import_file">>, <<"import_dir">>, - <<"import_piefxis_file">>, <<"export_piefxis_dir">>, - <<"export_piefxis_host_dir">>, <<"export_sql_file">>]). - -node_update_parse_query(Node, Query) -> - case lists:keysearch(<<"update">>, 1, Query) of - {value, _} -> - ModulesToUpdateStrings = - proplists:get_all_values(<<"selected">>, Query), - ModulesToUpdate = [misc:binary_to_atom(M) - || M <- ModulesToUpdateStrings], - case ejabberd_cluster:call(Node, ejabberd_update, update, - [ModulesToUpdate]) - of - {ok, _} -> ok; - {error, Error} -> - ?ERROR_MSG("~p~n", [Error]), - {error, (str:format("~p", [Error]))}; - {badrpc, Error} -> - ?ERROR_MSG("Bad RPC: ~p~n", [Error]), - {error, - <<"Bad RPC: ", ((str:format("~p", [Error])))/binary>>} - end; - _ -> nothing - end. - pretty_print_xml(El) -> list_to_binary(pretty_print_xml(El, <<"">>)). @@ -1714,7 +1244,7 @@ pretty_print_xml({xmlcdata, CData}, Prefix) -> ($\n) -> true; ($\t) -> true; ($\v) -> true; - ($ ) -> true; + ($\s) -> true; (_) -> false end, binary_to_list(CData)), if IsBlankCData -> @@ -1762,12 +1292,8 @@ pretty_print_xml(#xmlel{name = Name, attrs = Attrs, end]. url_func({user_diapason, From, To}) -> - <<(integer_to_binary(From))/binary, "-", - (integer_to_binary(To))/binary, "/">>; -url_func({users_queue, Prefix, User, _Server}) -> - <>; -url_func({user, Prefix, User, _Server}) -> - <>. + <<"diapason/", (integer_to_binary(From))/binary, "-", + (integer_to_binary(To))/binary, "/">>. last_modified() -> {<<"Last-Modified">>, @@ -1791,49 +1317,7 @@ pretty_string_int(String) when is_binary(String) -> %%%================================== %%%% mnesia table view -make_table_view(Node, STable, Lang) -> - Table = misc:binary_to_atom(STable), - TInfo = ejabberd_cluster:call(Node, 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), - TableInfo = str:format("~p", [TInfo]), - [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"), - [Node]))), - ?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Name")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - STable - )]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Node")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - misc:atom_to_binary(Node) - )]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Storage Type")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - misc:atom_to_binary(Type) - )]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Elements")), - ?XAE(<<"td">>, - [{<<"class">>, <<"alignright">>}], - [?AC(<<"1/">>, - (pretty_string_int(Size)))]) - ]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Memory")), - ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(MemoryB)) - )]) - ])]), - ?XC(<<"pre">>, TableInfo)]. - -make_table_elements_view(Node, STable, Lang, PageNumber) -> +webadmin_node_db_table_page(Node, STable, PageNumber) -> Table = misc:binary_to_atom(STable), TInfo = ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]), {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), @@ -1842,10 +1326,7 @@ make_table_elements_view(Node, STable, Lang, PageNumber) -> TableContentErl = get_table_content(Node, Table, Type, PageNumber, PageSize), TableContent = str:format("~p", [TableContentErl]), PagesLinks = build_elements_pages_list(Size, PageNumber, PageSize), - [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"), - [Node]))), - ?P, ?AC(<<"../">>, STable), ?P - ] ++ PagesLinks ++ [?XC(<<"pre">>, TableContent)]. + [?P] ++ PagesLinks ++ [?XC(<<"pre">>, TableContent)]. build_elements_pages_list(Size, PageNumber, PageSize) -> PagesNumber = calculate_pages_number(Size, PageSize), @@ -1877,39 +1358,147 @@ get_table_content(Node, Table, _Type, PageNumber, PageSize) -> || Key <- Keys], lists:flatten(Res). +%% @format-begin + +webadmin_db(Node, [<<"table">>, TableName, <<"details">> | RPath], R, Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = + make_breadcrumb({table_section, Level, Service, TableName, <<"Details">>, RPath}), + Get = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [mnesia_table_details, R, [{<<"table">>, TableName}], []])], + Breadcrumb ++ Get; +webadmin_db(Node, + [<<"table">>, TableName, <<"elements">>, PageNumber | RPath], + R, + Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = + make_breadcrumb({table_section, Level, Service, TableName, <<"Elements">>, RPath}), + Get = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [webadmin_node_db_table_page, + R, + [{<<"node">>, Node}, + {<<"table">>, TableName}, + {<<"page">>, PageNumber}], + []])], + Breadcrumb ++ Get; +webadmin_db(Node, [<<"table">>, TableName, <<"change-storage">> | RPath], R, Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = + make_breadcrumb({table_section, Level, Service, TableName, <<"Change Storage">>, RPath}), + Set = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [mnesia_table_change_storage, R, [{<<"table">>, TableName}], []])], + Breadcrumb ++ Set; +webadmin_db(Node, [<<"table">>, TableName, <<"clear">> | RPath], R, Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = + make_breadcrumb({table_section, Level, Service, TableName, <<"Clear Content">>, RPath}), + Set = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [mnesia_table_clear, + R, + [{<<"table">>, TableName}], + [{style, danger}]])], + Breadcrumb ++ Set; +webadmin_db(Node, [<<"table">>, TableName, <<"destroy">> | RPath], R, Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = + make_breadcrumb({table_section, Level, Service, TableName, <<"Destroy Table">>, RPath}), + Set = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [mnesia_table_destroy, + R, + [{<<"table">>, TableName}], + [{style, danger}]])], + Breadcrumb ++ Set; +webadmin_db(_Node, [<<"table">>, TableName | _RPath], _R, Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = make_breadcrumb({table, Level, Service, TableName}), + MenuItems = + [{<<"details/">>, <<"Details">>}, + {<<"elements/1/">>, <<"Elements">>}, + {<<"change-storage/">>, <<"Change Storage">>}, + {<<"clear/">>, <<"Clear Content">>}, + {<<"destroy/">>, <<"Destroy Table">>}], + Get = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Breadcrumb ++ Get; +webadmin_db(Node, _RPath, R, _Level) -> + Service = <<"Mnesia Tables">>, + Breadcrumb = make_breadcrumb({service, Service}), + Get = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [mnesia_list_tables, + R, + [], + [{result_links, [{name, mnesia_table, 3, <<"">>}]}]])], + Breadcrumb ++ Get. + +make_breadcrumb({service, Service}) -> + make_breadcrumb([Service]); +make_breadcrumb({table, Level, Service, Name}) -> + make_breadcrumb([{Level, Service}, separator, Name]); +make_breadcrumb({table_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 + %%%================================== %%%% navigation menu -make_navigation(Host, Node, Lang, JID, Level) -> - Menu = make_navigation_menu(Host, Node, Lang, JID, Level), +make_navigation(Host, Node, Username, Lang, JID, Level) -> + Menu = make_navigation_menu(Host, Node, Username, Lang, JID, Level), make_menu_items(Lang, Menu). -spec make_navigation_menu(Host::global | binary(), Node::cluster | atom(), + Username::unspecified | binary(), Lang::binary(), JID::jid(), Level::integer()) -> Menu::{URL::binary(), Title::binary()} | {URL::binary(), Title::binary(), [Menu::any()]}. -make_navigation_menu(Host, Node, Lang, JID, Level) -> +make_navigation_menu(Host, Node, Username, Lang, JID, Level) -> HostNodeMenu = make_host_node_menu(Host, Node, Lang, JID, Level), - HostMenu = make_host_menu(Host, HostNodeMenu, Lang, + HostUserMenu = make_host_user_menu(Host, Username, Lang, + JID, Level), + HostMenu = make_host_menu(Host, HostNodeMenu, HostUserMenu, Lang, JID, Level), NodeMenu = make_node_menu(Host, Node, Lang, Level), make_server_menu(HostMenu, NodeMenu, Lang, JID, Level). -make_menu_items(global, cluster, Base, Lang) -> - HookItems = get_menu_items_hook(server, Lang), - make_menu_items(Lang, {Base, <<"">>, HookItems}); -make_menu_items(global, Node, Base, Lang) -> - HookItems = get_menu_items_hook({node, Node}, Lang), - make_menu_items(Lang, {Base, <<"">>, HookItems}); -make_menu_items(Host, cluster, Base, Lang) -> - HookItems = get_menu_items_hook({host, Host}, Lang), - make_menu_items(Lang, {Base, <<"">>, HookItems}); -make_menu_items(Host, Node, Base, Lang) -> - HookItems = get_menu_items_hook({hostnode, Host, Node}, - Lang), - make_menu_items(Lang, {Base, <<"">>, HookItems}). +make_menu_items(Host, Node, Base, Lang, Acc) -> + Place = case {Host, Node} of + {global, cluster} -> server; + {global, Node} -> {node, Node}; + {Host, cluster} -> {host, Host}; + {Host, Node} -> {hostnode, Host, Node} + end, + HookItems = get_menu_items_hook(Place, Lang), + Items = lists:keysort(2, HookItems ++ Acc), + make_menu_items(Lang, {Base, <<"">>, Items}). make_host_node_menu(global, _, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; @@ -1922,21 +1511,34 @@ make_host_node_menu(Host, Node, Lang, JID, Level) -> || Tuple <- HostNodeFixed, is_allowed_path(Host, Tuple, JID)], {HostNodeBase, iolist_to_binary(atom_to_list(Node)), - HostNodeFixed2}. + lists:keysort(2, HostNodeFixed2)}. -make_host_menu(global, _HostNodeMenu, _Lang, _JID, _Level) -> +make_host_user_menu(global, _, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; -make_host_menu(Host, HostNodeMenu, Lang, JID, Level) -> +make_host_user_menu(_, unspecified, _Lang, _JID, _Level) -> + {<<"">>, <<"">>, []}; +make_host_user_menu(Host, Username, Lang, JID, Level) -> + HostNodeBase = get_base_path(Host, Username, Level), + HostNodeFixed = get_menu_items_hook({hostuser, Host, Username}, Lang), + HostNodeFixed2 = [Tuple + || Tuple <- HostNodeFixed, + is_allowed_path(Host, Tuple, JID)], + {HostNodeBase, Username, + lists:keysort(2, HostNodeFixed2)}. + +make_host_menu(global, _HostNodeMenu, _HostUserMenu, _Lang, _JID, _Level) -> + {<<"">>, <<"">>, []}; +make_host_menu(Host, HostNodeMenu, HostUserMenu, Lang, JID, Level) -> HostBase = get_base_path(Host, cluster, Level), - HostFixed = [{<<"users">>, ?T("Users")}, - {<<"online-users">>, ?T("Online Users")}] - ++ + HostFixed = [{<<"users">>, ?T("Users"), HostUserMenu}, + {<<"online-users">>, ?T("Online Users")}], + HostFixedAdditional = get_lastactivity_menuitem_list(Host) ++ - [{<<"nodes">>, ?T("Nodes"), HostNodeMenu}, - {<<"stats">>, ?T("Statistics")}] + [{<<"nodes">>, ?T("Nodes"), HostNodeMenu}] ++ get_menu_items_hook({host, Host}, Lang), + HostFixedAll = HostFixed ++ lists:keysort(2, HostFixedAdditional), HostFixed2 = [Tuple - || Tuple <- HostFixed, + || Tuple <- HostFixedAll, is_allowed_path(Host, Tuple, JID)], {HostBase, Host, HostFixed2}. @@ -1944,30 +1546,31 @@ make_node_menu(_Host, cluster, _Lang, _Level) -> {<<"">>, <<"">>, []}; make_node_menu(global, Node, Lang, Level) -> NodeBase = get_base_path(global, Node, Level), - NodeFixed = [{<<"db">>, ?T("Database")}, - {<<"backup">>, ?T("Backup")}, - {<<"stats">>, ?T("Statistics")}, - {<<"update">>, ?T("Update")}] + NodeFixed = [{<<"db">>, <<"Mnesia Tables">>}, + {<<"backup">>, <<"Mnesia Backup">>}] ++ get_menu_items_hook({node, Node}, Lang), {NodeBase, iolist_to_binary(atom_to_list(Node)), - NodeFixed}; + lists:keysort(2, NodeFixed)}; make_node_menu(_Host, _Node, _Lang, _Level) -> {<<"">>, <<"">>, []}. make_server_menu(HostMenu, NodeMenu, Lang, JID, Level) -> Base = get_base_path(global, cluster, Level), Fixed = [{<<"vhosts">>, ?T("Virtual Hosts"), HostMenu}, - {<<"nodes">>, ?T("Nodes"), NodeMenu}, - {<<"stats">>, ?T("Statistics")}] - ++ get_menu_items_hook(server, Lang), + {<<"nodes">>, ?T("Nodes"), NodeMenu}], + FixedAdditional = get_menu_items_hook(server, Lang), + FixedAll = Fixed ++ lists:keysort(2, FixedAdditional), Fixed2 = [Tuple - || Tuple <- Fixed, + || Tuple <- FixedAll, is_allowed_path(global, Tuple, JID)], {Base, <<"">>, Fixed2}. get_menu_items_hook({hostnode, Host, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_hostnode, Host, [], [Host, Node, Lang]); +get_menu_items_hook({hostuser, Host, Username}, Lang) -> + ejabberd_hooks:run_fold(webadmin_menu_hostuser, Host, + [], [Host, Username, Lang]); get_menu_items_hook({host, Host}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_host, Host, [], [Host, Lang]); @@ -2036,4 +1639,960 @@ any_rules_allowed(Host, Access, Entity) -> allow == acl:match_rule(Host, Rule, Entity) end, Access). +%%%================================== +%%%% login box + +%%% @format-begin + +make_login_items(#request{us = {Username, Host}} = R, Level) -> + UserBin = + jid:encode( + jid:make(Username, Host, <<"">>)), + UserEl = + make_command(echo, + R, + [{<<"sentence">>, UserBin}], + [{only, value}, {result_links, [{sentence, user, Level, <<"">>}]}]), + UserEl2 = + case UserEl of + {xmlcdata, <<>>} -> + {xmlel, <<"code">>, [], [{xmlel, <<"a">>, [], [{xmlcdata, UserBin}]}]}; + _ -> + UserEl + end, + MenuPost = + case ejabberd_hooks:run_fold(webadmin_menu_system_post, [], [R]) of + [] -> + []; + PostElements -> + [{xmlel, + <<"div">>, + [{<<"id">>, <<"navitemlogin">>}], + [?XE(<<"ul">>, PostElements)]}] + end, + [{xmlel, + <<"li">>, + [{<<"id">>, <<"navitemlogin-start">>}], + [{xmlel, + <<"div">>, + [{<<"id">>, <<"navitemlogin">>}], + [?XE(<<"ul">>, + [?LI([?C(unicode:characters_to_binary("👤")), UserEl2]), + ?LI([?C(unicode:characters_to_binary("🏭")), + make_command(echo, + R, + [{<<"sentence">>, misc:atom_to_binary(node())}], + [{only, value}, + {result_links, [{sentence, node, Level, <<"">>}]}])])] + ++ ejabberd_hooks:run_fold(webadmin_menu_system_inside, [], [R]) + ++ [?LI([?C(unicode:characters_to_binary("📤")), + ?AC(<<(binary:copy(<<"../">>, Level))/binary, "logout/">>, + <<"Logout">>)])])]}] + ++ MenuPost}]. + +%%%================================== + +%%%% make_command: API + +-spec make_command(Name :: atom(), Request :: http_request()) -> xmlel(). +make_command(Name, Request) -> + make_command2(Name, Request, [], []). + +-spec make_command(Name :: atom(), + Request :: http_request(), + BaseArguments :: [{ArgName :: binary(), ArgValue :: binary()}], + [Option]) -> + xmlel() | {xmlcdata, binary()} | {raw_and_value, any(), xmlel()} + when Option :: + {only, presentation | without_presentation | button | result | value | raw_and_value} | + {input_name_append, [binary()]} | + {force_execution, boolean()} | + {table_options, {PageSize :: integer(), RemainingPath :: [binary()]}} | + {result_named, boolean()} | + {result_links, + [{ResultName :: atom(), + LinkType :: host | node | user | room | shared_roster | arg_host | paragraph, + Level :: integer(), + Append :: binary()}]} | + {style, normal | danger}. +make_command(Name, Request, BaseArguments, Options) -> + make_command2(Name, Request, BaseArguments, Options). + +-spec make_command_raw_value(Name :: atom(), + Request :: http_request(), + BaseArguments :: [{ArgName :: binary(), ArgValue :: binary()}]) -> + any(). +make_command_raw_value(Name, Request, BaseArguments) -> + make_command2(Name, Request, BaseArguments, [{only, raw_value}]). + +%%%================================== +%%%% make_command: main + +-spec make_command2(Name :: atom(), + Request :: http_request(), + BaseArguments :: [{ArgName :: binary(), ArgValue :: binary()}], + [Option]) -> + xmlel() | any() + when Option :: + {only, + presentation | + without_presentation | + button | + result | + value | + raw_value | + raw_and_value} | + {input_name_append, [binary()]} | + {force_execution, boolean() | undefined} | + {table_options, {PageSize :: integer(), RemainingPath :: [binary()]}} | + {result_named, boolean()} | + {result_links, + [{ResultName :: atom(), + LinkType :: host | node | user | room | shared_roster | arg_host | paragraph, + Level :: integer(), + Append :: binary()}]} | + {style, normal | danger}. +make_command2(Name, Request, BaseArguments, Options) -> + Only = proplists:get_value(only, Options, all), + ForceExecution = proplists:get_value(force_execution, Options, undefined), + InputNameAppend = proplists:get_value(input_name_append, Options, []), + Resultnamed = proplists:get_value(result_named, Options, false), + ResultLinks = proplists:get_value(result_links, Options, []), + TO = proplists:get_value(table_options, Options, {999999, []}), + Style = proplists:get_value(style, Options, normal), + #request{us = {RUser, RServer}, ip = RIp} = Request, + CallerInfo = + #{usr => {RUser, RServer, <<"">>}, + ip => RIp, + caller_host => RServer, + caller_module => ?MODULE}, + try {ejabberd_commands:get_command_definition(Name), + ejabberd_access_permissions:can_access(Name, CallerInfo)} + of + {C, allow} -> + make_command2(Name, + Request, + CallerInfo, + BaseArguments, + C, + Only, + ForceExecution, + InputNameAppend, + Resultnamed, + ResultLinks, + Style, + TO); + {_C, deny} -> + ?DEBUG("Blocked access to command ~p for~n CallerInfo: ~p", [Name, CallerInfo]), + ?C(<<"">>) + catch + A:B -> + ?INFO_MSG("Problem preparing command ~p: ~p", [Name, {A, B}]), + ?C(<<"">>) + end. + +make_command2(Name, + Request, + CallerInfo, + BaseArguments, + C, + Only, + ForceExecution, + InputNameAppend, + Resultnamed, + ResultLinks, + Style, + TO) -> + {ArgumentsFormat, _Rename, ResultFormatApi} = ejabberd_commands:get_command_format(Name), + Method = + case {ForceExecution, ResultFormatApi} of + {true, _} -> + auto; + {false, _} -> + manual; + {_, {_, rescode}} -> + manual; + {_, {_, restuple}} -> + manual; + _ -> + auto + end, + PresentationEls = make_command_presentation(Name, C#ejabberd_commands.tags), + Query = Request#request.q, + {ArgumentsUsed1, ExecRes} = + execute_command(Name, + Query, + BaseArguments, + Method, + ArgumentsFormat, + CallerInfo, + InputNameAppend), + ArgumentsFormatDetailed = + add_arguments_details(ArgumentsFormat, + C#ejabberd_commands.args_desc, + C#ejabberd_commands.args_example), + ArgumentsEls = + make_command_arguments(Name, + Query, + Only, + Method, + Style, + ArgumentsFormatDetailed, + BaseArguments, + InputNameAppend), + Automated = + case ArgumentsEls of + [] -> + true; + _ -> + false + end, + ArgumentsUsed = + (catch lists:zip( + lists:map(fun({A, _}) -> A end, ArgumentsFormat), ArgumentsUsed1)), + ResultEls = + make_command_result(ExecRes, + ArgumentsUsed, + ResultFormatApi, + Automated, + Resultnamed, + ResultLinks, + TO), + make_command3(Only, ExecRes, PresentationEls, ArgumentsEls, ResultEls). + +make_command3(presentation, _ExecRes, PresentationEls, _ArgumentsEls, _ResultEls) -> + ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], PresentationEls); +make_command3(button, _ExecRes, _PresentationEls, [Button], _ResultEls) -> + Button; +make_command3(result, + _ExecRes, + _PresentationEls, + _ArgumentsEls, + [{xmlcdata, _}, Xmlel]) -> + ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], [Xmlel]); +make_command3(value, _ExecRes, _PresentationEls, _ArgumentsEls, [{xmlcdata, _}, Xmlel]) -> + Xmlel; +make_command3(value, + _ExecRes, + _PresentationEls, + _ArgumentsEls, + [{xmlel, _, _, _} = Xmlel]) -> + Xmlel; +make_command3(raw_and_value, + ExecRes, + _PresentationEls, + _ArgumentsEls, + [{xmlel, _, _, _} = Xmlel]) -> + {raw_and_value, ExecRes, Xmlel}; +make_command3(raw_value, ExecRes, _PresentationEls, _ArgumentsEls, _ResultEls) -> + ExecRes; +make_command3(without_presentation, + _ExecRes, + _PresentationEls, + ArgumentsEls, + ResultEls) -> + ?XAE(<<"p">>, + [{<<"class">>, <<"api">>}], + [?XE(<<"blockquote">>, ArgumentsEls ++ ResultEls)]); +make_command3(all, _ExecRes, PresentationEls, ArgumentsEls, ResultEls) -> + ?XAE(<<"p">>, + [{<<"class">>, <<"api">>}], + PresentationEls ++ [?XE(<<"blockquote">>, ArgumentsEls ++ ResultEls)]). + +add_arguments_details(ArgumentsFormat, Descriptions, none) -> + add_arguments_details(ArgumentsFormat, Descriptions, []); +add_arguments_details(ArgumentsFormat, none, Examples) -> + add_arguments_details(ArgumentsFormat, [], Examples); +add_arguments_details(ArgumentsFormat, Descriptions, Examples) -> + lists_zipwith3(fun({A, B}, C, D) -> {A, B, C, D} end, + ArgumentsFormat, + Descriptions, + Examples, + {pad, {none, "", ""}}). + +-ifdef(OTP_BELOW_26). + +lists_zipwith3(Combine, List1, List2, List3, {pad, {DefaultX, DefaultY, DefaultZ}}) -> + lists_zipwith3(Combine, List1, List2, List3, DefaultX, DefaultY, DefaultZ, []). + +lists_zipwith3(_Combine, [], [], [], _DefaultX, _DefaultY, _DefaultZ, Res) -> + lists:reverse(Res); +lists_zipwith3(Combine, + [E1 | List1], + [E2 | List2], + [E3 | List3], + DefX, + DefY, + DefZ, + Res) -> + E123 = Combine(E1, E2, E3), + lists_zipwith3(Combine, List1, List2, List3, DefX, DefY, DefZ, [E123 | Res]); +lists_zipwith3(Combine, [E1 | List1], [], [], DefX, DefY, DefZ, Res) -> + E123 = Combine(E1, DefY, DefZ), + lists_zipwith3(Combine, List1, [], [], DefX, DefY, DefZ, [E123 | Res]); +lists_zipwith3(Combine, [E1 | List1], [], [E3 | List3], DefX, DefY, DefZ, Res) -> + E123 = Combine(E1, DefY, E3), + lists_zipwith3(Combine, List1, [], List3, DefX, DefY, DefZ, [E123 | Res]); +lists_zipwith3(Combine, [E1 | List1], [E2 | List2], [], DefX, DefY, DefZ, Res) -> + E123 = Combine(E1, E2, DefZ), + lists_zipwith3(Combine, List1, List2, [], DefX, DefY, DefZ, [E123 | Res]). + +-endif. + +-ifndef(OTP_BELOW_26). + +lists_zipwith3(Combine, List1, List2, List3, How) -> + lists:zipwith3(Combine, List1, List2, List3, How). + +-endif. + +%%%================================== +%%%% make_command: presentation + +make_command_presentation(Name, Tags) -> + NameBin = misc:atom_to_binary(Name), + NiceNameBin = nice_this(Name), + Text = ejabberd_ctl:get_usage_command(atom_to_list(Name), 100, false, 1000000), + AnchorLink = [?ANCHORL(NameBin)], + MaybeDocsLink = + case lists:member(internal, Tags) of + true -> + []; + false -> + [?GL(<<"developer/ejabberd-api/admin-api/#", NameBin/binary>>, NameBin)] + end, + [?XE(<<"details">>, + [?XAE(<<"summary">>, [{<<"id">>, NameBin}], [?XC(<<"strong">>, NiceNameBin)])] + ++ MaybeDocsLink + ++ AnchorLink + ++ [?XC(<<"pre">>, list_to_binary(Text))])]. + +nice_this(This, integer) -> + {nice_this(This), right}; +nice_this(This, _Format) -> + nice_this(This). + +-spec nice_this(This :: atom() | string() | [byte()]) -> NiceThis :: binary(). +nice_this(This) when is_atom(This) -> + nice_this(atom_to_list(This)); +nice_this(This) when is_binary(This) -> + nice_this(binary_to_list(This)); +nice_this(This) when is_list(This) -> + list_to_binary(lists:flatten([string:titlecase(Word) + || Word <- string:replace(This, "_", " ", all)])). + +-spec long_this(These :: [This :: atom()]) -> Long :: binary(). +long_this(These) -> + list_to_binary(lists:join($/, [atom_to_list(This) || This <- These])). + +%%%================================== +%%%% make_command: arguments + +make_command_arguments(Name, + Query, + Only, + Method, + Style, + ArgumentsFormat, + BaseArguments, + InputNameAppend) -> + ArgumentsFormat2 = remove_base_arguments(ArgumentsFormat, BaseArguments), + ArgumentsFields = make_arguments_fields(Name, Query, ArgumentsFormat2), + Button = make_button_element(Name, Method, Style, InputNameAppend), + ButtonElement = + ?XE(<<"tr">>, + [?X(<<"td">>), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [Button])]), + case {(ArgumentsFields /= []) or (Method == manual), Only} of + {false, _} -> + []; + {true, button} -> + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [Button])]; + {true, _} -> + [?XAE(<<"form">>, + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [?XE(<<"table">>, ArgumentsFields ++ [ButtonElement])])] + end. + +remove_base_arguments(ArgumentsFormat, BaseArguments) -> + lists:filter(fun({ArgName, _ArgFormat, _ArgDesc, _ArgExample}) -> + not + lists:keymember( + misc:atom_to_binary(ArgName), 1, BaseArguments) + end, + ArgumentsFormat). + +make_button_element(Name, _, Style, InputNameAppend) -> + Id = term_to_id(InputNameAppend), + NameBin = <<(misc:atom_to_binary(Name))/binary, Id/binary>>, + NiceNameBin = nice_this(Name), + case Style of + danger -> + ?INPUTD(<<"submit">>, NameBin, NiceNameBin); + _ -> + ?INPUT(<<"submit">>, NameBin, NiceNameBin) + end. + +make_arguments_fields(Name, Query, ArgumentsFormat) -> + lists:map(fun({ArgName, ArgFormat, _ArgDescription, ArgExample}) -> + ArgExampleBin = format_result(ArgExample, {ArgName, ArgFormat}), + ArgNiceNameBin = nice_this(ArgName), + ArgLongNameBin = long_this([Name, ArgName]), + ArgValue = + case lists:keysearch(ArgLongNameBin, 1, Query) of + {value, {ArgLongNameBin, V}} -> + V; + _ -> + <<"">> + end, + ?XE(<<"tr">>, + [?XC(<<"td">>, <>), + ?XE(<<"td">>, + [?INPUTPH(<<"text">>, ArgLongNameBin, ArgValue, ArgExampleBin)])]) + end, + ArgumentsFormat). + +%%%================================== +%%%% make_command: execute + +execute_command(Name, + Query, + BaseArguments, + Method, + ArgumentsFormat, + CallerInfo, + InputNameAppend) -> + try Args = prepare_arguments(Name, BaseArguments ++ Query, ArgumentsFormat), + {Args, + execute_command2(Name, Query, Args, Method, ArgumentsFormat, CallerInfo, InputNameAppend)} + of + R -> + R + catch + A:E -> + {error, {A, E}} + end. + +execute_command2(Name, + Query, + Arguments, + Method, + ArgumentsFormat, + CallerInfo, + InputNameAppend) -> + AllArgumentsProvided = length(Arguments) == length(ArgumentsFormat), + PressedExecuteButton = is_this_to_execute(Name, Query, Arguments, InputNameAppend), + LetsExecute = + case {Method, PressedExecuteButton, AllArgumentsProvided} of + {auto, _, true} -> + true; + {manual, true, true} -> + true; + _ -> + false + end, + case LetsExecute of + true -> + catch ejabberd_commands:execute_command2(Name, Arguments, CallerInfo); + false -> + not_executed + end. + +is_this_to_execute(Name, Query, Arguments, InputNameAppend) -> + NiceNameBin = nice_this(Name), + NameBin = misc:atom_to_binary(Name), + AppendBin = term_to_id(lists:sublist(Arguments, length(InputNameAppend))), + ArgumentsId = <>, + {value, {ArgumentsId, NiceNameBin}} == lists:keysearch(ArgumentsId, 1, Query). + +prepare_arguments(ComName, Args, ArgsFormat) -> + lists:foldl(fun({ArgName, ArgFormat}, FinalArguments) -> + %% Give priority to the value enforced in our code + %% Otherwise use the value provided by the user + case {lists:keyfind( + misc:atom_to_binary(ArgName), 1, Args), + lists:keyfind(long_this([ComName, ArgName]), 1, Args)} + of + %% Value enforced in our code + {{_, Value}, _} -> + [format_arg(Value, ArgFormat) | FinalArguments]; + %% User didn't provide value in the field + {_, {_, <<>>}} -> + FinalArguments; + %% Value provided by the user in the form field + {_, {_, Value}} -> + [format_arg(Value, ArgFormat) | FinalArguments]; + {false, false} -> + FinalArguments + end + end, + [], + lists:reverse(ArgsFormat)). + +format_arg(Value, any) -> + Value; +format_arg(Value, atom) when is_atom(Value) -> + Value; +format_arg(Value, binary) when is_binary(Value) -> + Value; +format_arg(Value, ArgFormat) -> + ejabberd_ctl:format_arg(binary_to_list(Value), ArgFormat). + +%%%================================== +%%%% make_command: result + +make_command_result(not_executed, _, _, _, _, _, _) -> + []; +make_command_result({error, ErrorElement}, _, _, _, _, _, _) -> + [?DIVRES([?C(<<"Error: ">>), + ?XC(<<"code">>, list_to_binary(io_lib:format("~p", [ErrorElement])))])]; +make_command_result(Value, + ArgumentsUsed, + {ResName, _ResFormat} = ResultFormatApi, + Automated, + Resultnamed, + ResultLinks, + TO) -> + ResNameBin = nice_this(ResName), + ResultValueEl = + make_command_result_element(ArgumentsUsed, Value, ResultFormatApi, ResultLinks, TO), + ResultEls = + case Resultnamed of + true -> + [?C(<>), ResultValueEl]; + false -> + [ResultValueEl] + end, + case Automated of + true -> + ResultEls; + false -> + [?DIVRES(ResultEls)] + end. + +make_command_result_element(ArgumentsUsed, + ListOfTuples, + {_ArgName, {list, {_ListElementsName, {tuple, TupleElements}}}}, + ResultLinks, + {PageSize, RPath}) -> + HeadElements = + [nice_this(ElementName, ElementFormat) || {ElementName, ElementFormat} <- TupleElements], + ContentElements = + [list_to_tuple([make_result(format_result(V, {ElementName, ElementFormat}), + ElementName, + ArgumentsUsed, + ResultLinks) + || {V, {ElementName, ElementFormat}} + <- lists:zip(tuple_to_list(Tuple), TupleElements)]) + || Tuple <- ListOfTuples], + make_table(PageSize, RPath, HeadElements, ContentElements); +make_command_result_element(_ArgumentsUsed, + Values, + {_ArgName, {tuple, TupleElements}}, + _ResultLinks, + _TO) -> + ?XE(<<"table">>, + [?XE(<<"thead">>, + [?XE(<<"tr">>, + [?XC(<<"td">>, nice_this(ElementName)) + || {ElementName, _ElementFormat} <- TupleElements])]), + ?XE(<<"tbody">>, + [?XE(<<"tr">>, + [?XC(<<"td">>, format_result(V, {ElementName, ElementFormat})) + || {V, {ElementName, ElementFormat}} + <- lists:zip(tuple_to_list(Values), TupleElements)])])]); +make_command_result_element(ArgumentsUsed, + Value, + {_ArgName, {list, {ElementsName, ElementsFormat}}}, + ResultLinks, + {PageSize, RPath}) -> + HeadElements = [nice_this(ElementsName)], + ContentElements = + [{make_result(format_result(V, {ElementsName, ElementsFormat}), + ElementsName, + ArgumentsUsed, + ResultLinks)} + || V <- Value], + make_table(PageSize, RPath, HeadElements, ContentElements); +make_command_result_element(ArgumentsUsed, Value, ResultFormatApi, ResultLinks, _TO) -> + Res = make_result(format_result(Value, ResultFormatApi), + unknown_element_name, + ArgumentsUsed, + ResultLinks), + Res2 = + case Res of + [{xmlel, _, _, _} | _] = X -> + X; + Z -> + [Z] + end, + ?XE(<<"code">>, Res2). + +make_result(Binary, ElementName, ArgumentsUsed, [{ResultName, arg_host, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + {_, Host} = lists:keyfind(host, 1, ArgumentsUsed), + UrlBinary = + replace_url_elements([<<"server/">>, host, <<"/">>, Append], [{host, Host}], Level), + ?AC(UrlBinary, Binary); +make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, host, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + UrlBinary = + replace_url_elements([<<"server/">>, host, <<"/">>, Append], [{host, Binary}], Level), + ?AC(UrlBinary, Binary); +make_result(Binary, + ElementName, + _ArgumentsUsed, + [{ResultName, mnesia_table, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + Node = misc:atom_to_binary(node()), + UrlBinary = + replace_url_elements([<<"node/">>, node, <<"/db/table/">>, tablename, <<"/">>, Append], + [{node, Node}, {tablename, Binary}], + Level), + ?AC(UrlBinary, Binary); +make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, node, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + UrlBinary = + replace_url_elements([<<"node/">>, node, <<"/">>, Append], [{node, Binary}], Level), + ?AC(UrlBinary, Binary); +make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, user, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + Jid = try jid:decode(Binary) of + #jid{} = J -> + J + catch + _:{bad_jid, _} -> + %% TODO: Find a method to be able to link to this user to delete it + ?INFO_MSG("Error parsing Binary that is not a valid JID:~n ~p", [Binary]), + jid:decode(<<"unknown-username@localhost">>) + end, + {User, Host, _R} = jid:split(Jid), + case lists:member(Host, ejabberd_config:get_option(hosts)) of + true -> + UrlBinary = + replace_url_elements([<<"server/">>, host, <<"/user/">>, user, <<"/">>, Append], + [{user, misc:url_encode(User)}, {host, Host}], + Level), + ?AC(UrlBinary, Binary); + false -> + ?C(Binary) + end; +make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, room, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + Jid = jid:decode(Binary), + {Roomname, Service, _} = jid:split(Jid), + Host = ejabberd_router:host_of_route(Service), + case lists:member(Host, ejabberd_config:get_option(hosts)) of + true -> + UrlBinary = + replace_url_elements([<<"server/">>, + host, + <<"/muc/rooms/room/">>, + room, + <<"/">>, + Append], + [{room, misc:url_encode(Roomname)}, {host, Host}], + Level), + ?AC(UrlBinary, Binary); + false -> + ?C(Binary) + end; +make_result(Binary, + ElementName, + ArgumentsUsed, + [{ResultName, shared_roster, Level, Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + First = proplists:get_value(first, ArgumentsUsed), + Second = proplists:get_value(second, ArgumentsUsed), + FirstUrlencoded = + list_to_binary(string:replace( + misc:url_encode(First), "%40", "@")), + {GroupId, Host} = + case jid:decode(FirstUrlencoded) of + #jid{luser = <<"">>, server = G} -> + {G, Second}; + #jid{user = G, lserver = H} -> + {G, H} + end, + UrlBinary = + replace_url_elements([<<"server/">>, + host, + <<"/shared-roster/group/">>, + srg, + <<"/">>, + Append], + [{host, Host}, {srg, GroupId}], + Level), + ?AC(UrlBinary, Binary); +make_result([{xmlcdata, _, _, _} | _] = Any, + _ElementName, + _ArgumentsUsed, + _ResultLinks) -> + Any; +make_result([{xmlel, _, _, _} | _] = Any, _ElementName, _ArgumentsUsed, _ResultLinks) -> + Any; +make_result(Binary, + ElementName, + _ArgumentsUsed, + [{ResultName, paragraph, _Level, _Append}]) + when (ElementName == ResultName) or (ElementName == unknown_element_name) -> + ?XC(<<"pre">>, Binary); +make_result(Binary, _ElementName, _ArgumentsUsed, _ResultLinks) -> + ?C(Binary). + +replace_url_elements(UrlComponents, Replacements, Level) -> + Base = get_base_path_sum(0, 0, Level), + Binary2 = + lists:foldl(fun (El, Acc) when is_binary(El) -> + [El | Acc]; + (El, Acc) when is_atom(El) -> + {El, Value} = lists:keyfind(El, 1, Replacements), + [Value | Acc] + end, + [], + UrlComponents), + Binary3 = + binary:list_to_bin( + lists:reverse(Binary2)), + <>. + +format_result(Value, {_ResultName, integer}) when is_integer(Value) -> + integer_to_binary(Value); +format_result(Value, {_ResultName, string}) when is_list(Value) -> + Value; +format_result(Value, {_ResultName, string}) when is_binary(Value) -> + Value; +format_result(Value, {_ResultName, atom}) when is_atom(Value) -> + misc:atom_to_binary(Value); +format_result(Value, {_ResultName, any}) -> + Value; +format_result({ok, String}, {_ResultName, restuple}) when is_list(String) -> + list_to_binary(String); +format_result({error, Type, Code, Desc}, {_ResultName, restuple}) -> + <<"Error: ", + (misc:atom_to_binary(Type))/binary, + " ", + (integer_to_binary(Code))/binary, + ": ", + (list_to_binary(Desc))/binary>>; +format_result([], {_Name, {list, _ElementsDef}}) -> + ""; +format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> + Separator = ",", + Head = format_result(FirstElement, ElementsDef), + Tail = + lists:map(fun(Element) -> [Separator | format_result(Element, ElementsDef)] end, + Elements), + [Head | Tail]; +format_result([], {_Name, {tuple, _ElementsDef}}) -> + ""; +format_result(Value, {_Name, {tuple, [FirstDef | ElementsDef]}}) -> + [FirstElement | Elements] = tuple_to_list(Value), + Separator = ":", + Head = format_result(FirstElement, FirstDef), + Tail = + lists:map(fun(Element) -> [Separator | format_result(Element, ElementsDef)] end, + Elements), + [Head | Tail]; +format_result(Value, _ResultFormat) when is_atom(Value) -> + misc:atom_to_binary(Value); +format_result(Value, _ResultFormat) when is_list(Value) -> + list_to_binary(Value); +format_result(Value, _ResultFormat) when is_binary(Value) -> + Value; +format_result(Value, _ResultFormat) -> + io_lib:format("~p", [Value]). + +%%%================================== +%%%% make_table + +-spec make_table(PageSize :: integer(), + RemainingPath :: [binary()], + NameOptionList :: [Name :: binary() | {Name :: binary(), left | right}], + Values :: [tuple()]) -> + xmlel(). +make_table(PageSize, RPath, NameOptionList, Values1) -> + Values = + case lists:member(<<"sort">>, RPath) of + true -> + Values1; + false -> + GetXmlValue = + fun ({xmlcdata, _} = X) -> + X; + ({xmlel, _, _, _} = X) -> + X; + ({raw_and_value, _V, X}) -> + X + end, + ConvertTupleToTuple = + fun(Row1) -> list_to_tuple(lists:map(GetXmlValue, tuple_to_list(Row1))) end, + lists:map(ConvertTupleToTuple, Values1) + end, + make_table1(PageSize, RPath, <<"">>, <<"">>, 1, NameOptionList, Values). + +make_table1(PageSize, + [<<"page">>, PageNumber | RPath], + PageUrlBase, + SortUrlBase, + _Start, + NameOptionList, + Values1) -> + make_table1(PageSize, + RPath, + <>, + <>, + 1 + PageSize * binary_to_integer(PageNumber), + NameOptionList, + Values1); +make_table1(PageSize, + [<<"sort">>, SortType | RPath], + PageUrlBase, + SortUrlBase, + Start, + NameOptionList, + Rows1) -> + ColumnToSort = + length(lists:takewhile(fun (A) when A == SortType -> + false; + ({A, _}) when A == SortType -> + false; + (_) -> + true + end, + NameOptionList)) + + 1, + Direction = + case lists:nth(ColumnToSort, NameOptionList) of + {_, right} -> + descending; + {_, left} -> + ascending; + _ -> + ascending + end, + ColumnToSort = ColumnToSort, + GetRawValue = + fun ({xmlcdata, _} = X) -> + X; + ({xmlel, _, _, _} = X) -> + X; + ({raw_and_value, R, _X}) -> + R + end, + GetXmlValue = + fun ({xmlcdata, _} = X) -> + X; + ({xmlel, _, _, _} = X) -> + X; + ({raw_and_value, _R, X}) -> + X + end, + SortTwo = + fun(A1, B1) -> + A2 = GetRawValue(element(ColumnToSort, A1)), + B2 = GetRawValue(element(ColumnToSort, B1)), + case Direction of + ascending -> + A2 < B2; + descending -> + A2 > B2 + end + end, + Rows1Sorted = lists:sort(SortTwo, Rows1), + ConvertTupleToTuple = + fun(Row1) -> list_to_tuple(lists:map(GetXmlValue, tuple_to_list(Row1))) end, + Rows = lists:map(ConvertTupleToTuple, Rows1Sorted), + make_table1(PageSize, + RPath, + PageUrlBase, + <>, + Start, + NameOptionList, + Rows); +make_table1(PageSize, [], PageUrlBase, SortUrlBase, Start, NameOptionList, Values1) -> + Values = lists:sublist(Values1, Start, PageSize), + Table = make_table(NameOptionList, Values), + Size = length(Values1), + Remaining = + case Size rem PageSize of + 0 -> + 0; + _ -> + 1 + end, + NumPages = max(0, Size div PageSize + Remaining - 1), + PLinks1 = + lists:foldl(fun(N, Acc) -> + NBin = integer_to_binary(N), + Acc + ++ [?C(<<", ">>), + ?AC(<>, NBin)] + end, + [], + lists:seq(1, NumPages)), + PLinks = + case PLinks1 of + [] -> + []; + _ -> + [?XE(<<"p">>, [?C(<<"Page: ">>), ?AC(<>, <<"0">>) | PLinks1])] + end, + + Names = + lists:map(fun ({Name, _}) -> + Name; + (Name) -> + Name + end, + NameOptionList), + [_ | SLinks1] = + lists:foldl(fun(N, Acc) -> + [?C(<<", ">>), ?AC(<>, N) | Acc] + end, + [], + lists:reverse(Names)), + SLinks = + case {PLinks, SLinks1} of + {_, []} -> + []; + {[], _} -> + []; + {_, [_]} -> + []; + {_, SLinks2} -> + [?XE(<<"p">>, [?C(<<"Sort all pages by: ">>) | SLinks2])] + end, + + ?XE(<<"div">>, [Table | PLinks ++ SLinks]). + +-spec make_table(NameOptionList :: [Name :: binary() | {Name :: binary(), left | right}], + Values :: [tuple()]) -> + xmlel(). +make_table(NameOptionList, Values) -> + NamesAndAttributes = [make_column_attributes(NameOption) || NameOption <- NameOptionList], + {Names, ColumnsAttributes} = lists:unzip(NamesAndAttributes), + make_table(Names, ColumnsAttributes, Values). + +make_table(Names, ColumnsAttributes, Values) -> + ?XAE(<<"table">>, + [{<<"class">>, <<"sortable">>}], + [?XE(<<"thead">>, + [?XE(<<"tr">>, [?XC(<<"th">>, nice_this(HeadElement)) || HeadElement <- Names])]), + ?XE(<<"tbody">>, + [?XE(<<"tr">>, + [?XAE(<<"td">>, CAs, [V]) + || {CAs, V} <- lists:zip(ColumnsAttributes, tuple_to_list(ValueTuple))]) + || ValueTuple <- Values])]). + +make_column_attributes({Name, Option}) -> + {Name, [make_column_attribute(Option)]}; +make_column_attributes(Name) -> + {Name, []}. + +make_column_attribute(left) -> + {<<"class">>, <<"alignleft">>}; +make_column_attribute(right) -> + {<<"class">>, <<"alignright">>}. + +%%%================================== %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl index 966242331..dbcf8e2e0 100644 --- a/src/ejabberd_websocket.erl +++ b/src/ejabberd_websocket.erl @@ -33,11 +33,12 @@ %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE %%% POSSIBILITY OF SUCH DAMAGE. %%% ========================================================================================================== -%%% ejabberd, Copyright (C) 2002-2022 ProcessOne +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne %%%---------------------------------------------------------------------- -module(ejabberd_websocket). -protocol({rfc, 6455}). +-protocol({rfc, 7395}). -author('ecestari@process-one.net'). @@ -61,6 +62,11 @@ ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). -define(HEADER, [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). +-ifndef(OTP_BELOW_28). +-dialyzer([no_opaque_union]). +-endif. + + is_valid_websocket_upgrade(_Path, Headers) -> HeadersToValidate = [{'Upgrade', <<"websocket">>}, {'Connection', ignore}, @@ -154,7 +160,7 @@ connect(#ws{socket = Socket, sockmod = SockMod} = Ws, WsLoop) -> _ -> SockMod:setopts(Socket, [{packet, 0}, {active, true}]) end, - ws_loop(none, Socket, WsHandleLoopPid, SockMod, none). + ws_loop(ejabberd_websocket_codec:new_server(), Socket, WsHandleLoopPid, SockMod, none). handshake(#ws{headers = Headers} = State) -> {_, Key} = lists:keyfind(<<"Sec-Websocket-Key">>, 1, @@ -187,17 +193,20 @@ find_subprotocol(Headers) -> end. -ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper) -> +ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper) -> receive {DataType, _Socket, Data} when DataType =:= tcp orelse DataType =:= raw -> - case handle_data(DataType, FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper) of - {error, Error} -> + case handle_data(DataType, Codec, Data, Socket, WsHandleLoopPid, SockMod, Shaper) of + {error, tls, Error} -> ?DEBUG("TLS decode error ~p", [Error]), - websocket_close(Socket, WsHandleLoopPid, SockMod, 1002); % protocol error - {NewFrameInfo, ToSend, NewShaper} -> + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, 1002); % protocol error + {error, protocol, Error} -> + ?DEBUG("Websocket decode error ~p", [Error]), + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, 1002); % protocol error + {NewCodec, ToSend, NewShaper} -> lists:foreach(fun(Pkt) -> SockMod:send(Socket, Pkt) end, ToSend), - ws_loop(NewFrameInfo, Socket, WsHandleLoopPid, SockMod, NewShaper) + ws_loop(NewCodec, Socket, WsHandleLoopPid, SockMod, NewShaper) end; {new_shaper, NewShaper} -> NewShaper = case NewShaper of @@ -206,13 +215,13 @@ ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper) -> _ -> NewShaper end, - ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, NewShaper); + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, NewShaper); {tcp_closed, _Socket} -> ?DEBUG("TCP connection was closed, exit", []), - websocket_close(Socket, WsHandleLoopPid, SockMod, 0); + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, 0); {tcp_error, Socket, Reason} -> ?DEBUG("TCP connection error: ~ts", [inet:format_error(Reason)]), - websocket_close(Socket, WsHandleLoopPid, SockMod, 0); + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, 0); {'DOWN', Ref, process, WsHandleLoopPid, Reason} -> Code = case Reason of normal -> @@ -224,224 +233,95 @@ ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper) -> 1011 % internal error end, erlang:demonitor(Ref), - websocket_close(Socket, WsHandleLoopPid, SockMod, Code); + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, Code); {text_with_reply, Data, Sender} -> - SockMod:send(Socket, encode_frame(Data, 1)), + SockMod:send(Socket, ejabberd_websocket_codec:encode(Codec, 1, Data)), Sender ! {text_reply, self()}, - ws_loop(FrameInfo, Socket, WsHandleLoopPid, + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper); {data_with_reply, Data, Sender} -> - SockMod:send(Socket, encode_frame(Data, 2)), + SockMod:send(Socket, ejabberd_websocket_codec:encode(Codec, 2, Data)), Sender ! {data_reply, self()}, - ws_loop(FrameInfo, Socket, WsHandleLoopPid, + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper); {text, Data} -> - SockMod:send(Socket, encode_frame(Data, 1)), - ws_loop(FrameInfo, Socket, WsHandleLoopPid, + SockMod:send(Socket, ejabberd_websocket_codec:encode(Codec, 1, Data)), + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper); {data, Data} -> - SockMod:send(Socket, encode_frame(Data, 2)), - ws_loop(FrameInfo, Socket, WsHandleLoopPid, + SockMod:send(Socket, ejabberd_websocket_codec:encode(Codec, 2, Data)), + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper); {ping, Data} -> - SockMod:send(Socket, encode_frame(Data, 9)), - ws_loop(FrameInfo, Socket, WsHandleLoopPid, + SockMod:send(Socket, ejabberd_websocket_codec:encode(Codec, 9, Data)), + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper); shutdown -> ?DEBUG("Shutdown request received, closing websocket " "with pid ~p", [self()]), - websocket_close(Socket, WsHandleLoopPid, SockMod, 1001); % going away - _Ignored -> + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, 1001); % going away + Ignored -> ?WARNING_MSG("Received unexpected message, ignoring: ~p", - [_Ignored]), - ws_loop(FrameInfo, Socket, WsHandleLoopPid, + [Ignored]), + ws_loop(Codec, Socket, WsHandleLoopPid, SockMod, Shaper) end. -encode_frame(Data, Opcode) -> - case byte_size(Data) of - S1 when S1 < 126 -> - <<1:1, 0:3, Opcode:4, 0:1, S1:7, Data/binary>>; - S2 when S2 < 65536 -> - <<1:1, 0:3, Opcode:4, 0:1, 126:7, S2:16, Data/binary>>; - S3 -> - <<1:1, 0:3, Opcode:4, 0:1, 127:7, S3:64, Data/binary>> - end. - --record(frame_info, - {mask = none, offset = 0, left, final_frame = true, - opcode, unprocessed = <<>>, unmasked = <<>>, - unmasked_msg = <<>>}). - -decode_header(<>) - when Len < 126 -> - {Len, Final, Opcode, none, Data}; -decode_header(<>) -> - {Len, Final, Opcode, none, Data}; -decode_header(<>) -> - {Len, Final, Opcode, none, Data}; -decode_header(<>) - when Len < 126 -> - {Len, Final, Opcode, Mask, Data}; -decode_header(<>) -> - {Len, Final, Opcode, Mask, Data}; -decode_header(<>) -> - {Len, Final, Opcode, Mask, Data}; -decode_header(_) -> none. - -unmask_int(Offset, _, <<>>, Acc) -> - {Acc, Offset}; -unmask_int(0, <> = Mask, - <>, Acc) -> - unmask_int(0, Mask, Rest, - <>); -unmask_int(0, <> = Mask, - <>, Acc) -> - unmask_int(1, Mask, Rest, - <>); -unmask_int(1, <<_:8, M:8, _/binary>> = Mask, - <>, Acc) -> - unmask_int(2, Mask, Rest, - <>); -unmask_int(2, <<_:16, M:8, _/binary>> = Mask, - <>, Acc) -> - unmask_int(3, Mask, Rest, - <>); -unmask_int(3, <<_:24, M:8>> = Mask, - <>, Acc) -> - unmask_int(0, Mask, Rest, - <>). - -unmask(#frame_info{mask = none} = State, Data) -> - {State, Data}; -unmask(#frame_info{mask = Mask, offset = Offset} = State, Data) -> - {Unmasked, NewOffset} = unmask_int(Offset, Mask, - Data, <<>>), - {State#frame_info{offset = NewOffset}, Unmasked}. - -process_frame(none, Data) -> - process_frame(#frame_info{}, Data); -process_frame(#frame_info{left = Left} = FrameInfo, <<>>) when Left > 0 -> - {FrameInfo, [], []}; -process_frame(#frame_info{unprocessed = none, - unmasked = UnmaskedPre, left = Left} = - State, - Data) - when byte_size(Data) < Left -> - {State2, Unmasked} = unmask(State, Data), - {State2#frame_info{left = Left - byte_size(Data), - unmasked = [UnmaskedPre, Unmasked]}, - [], []}; -process_frame(#frame_info{unprocessed = none, - unmasked = UnmaskedPre, opcode = Opcode, - final_frame = Final, left = Left, - unmasked_msg = UnmaskedMsg} = - FrameInfo, - Data) -> - <> = Data, - {_, Unmasked} = unmask(FrameInfo, ToProcess), - case Final of - true -> - {FrameInfo3, Recv, Send} = process_frame(#frame_info{}, - Unprocessed), - case Opcode of - X when X < 3 -> - {FrameInfo3, - [iolist_to_binary([UnmaskedMsg, UnmaskedPre, Unmasked]) - | Recv], - Send}; - 9 -> % Ping - Frame = encode_frame(Unmasked, 10), - {FrameInfo3#frame_info{unmasked_msg = UnmaskedMsg}, [ping | Recv], - [Frame | Send]}; - 10 -> % Pong - {FrameInfo3, [pong | Recv], Send}; - 8 -> % Close - CloseCode = case Unmasked of - <> -> - ?DEBUG("WebSocket close op: ~p ~ts", - [Code, Message]), - Code; - <> -> - ?DEBUG("WebSocket close op: ~p", [Code]), - Code; - _ -> - ?DEBUG("WebSocket close op unknown: ~p", - [Unmasked]), - 1000 - end, - - Frame = encode_frame(<>, 8), - {FrameInfo3#frame_info{unmasked_msg=UnmaskedMsg}, Recv, - [Frame | Send]}; - _ -> - {FrameInfo3#frame_info{unmasked_msg = UnmaskedMsg}, Recv, - Send} - end; - _ -> - process_frame(#frame_info{unmasked_msg = - [UnmaskedMsg, UnmaskedPre, - Unmasked]}, - Unprocessed) - end; -process_frame(#frame_info{unprocessed = <<>>} = - FrameInfo, - Data) -> - case decode_header(Data) of - none -> - {FrameInfo#frame_info{unprocessed = Data}, [], []}; - {Len, Final, Opcode, Mask, Rest} -> - process_frame(FrameInfo#frame_info{mask = Mask, - final_frame = Final == 1, - left = Len, opcode = Opcode, - unprocessed = none}, - Rest) - end; -process_frame(#frame_info{unprocessed = - UnprocessedPre} = - FrameInfo, - Data) -> - process_frame(FrameInfo#frame_info{unprocessed = <<>>}, - <>). - -handle_data(tcp, FrameInfo, Data, Socket, WsHandleLoopPid, fast_tls, Shaper) -> +handle_data(tcp, Codec, Data, Socket, WsHandleLoopPid, fast_tls, Shaper) -> case fast_tls:recv_data(Socket, Data) of {ok, NewData} -> - handle_data_int(FrameInfo, NewData, Socket, WsHandleLoopPid, fast_tls, Shaper); + handle_data_int(Codec, NewData, Socket, WsHandleLoopPid, fast_tls, Shaper); {error, Error} -> - {error, Error} + {error, tls, Error} end; -handle_data(_, FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper) -> - handle_data_int(FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper). +handle_data(_, Codec, Data, Socket, WsHandleLoopPid, SockMod, Shaper) -> + handle_data_int(Codec, Data, Socket, WsHandleLoopPid, SockMod, Shaper). -handle_data_int(FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper) -> - {NewFrameInfo, Recv, Send} = process_frame(FrameInfo, Data), - lists:foreach(fun (El) -> - case El of - pong -> - WsHandleLoopPid ! pong; - ping -> - WsHandleLoopPid ! ping; - _ -> - WsHandleLoopPid ! {received, El} - end - end, - Recv), - {NewFrameInfo, Send, handle_shaping(Data, Socket, SockMod, Shaper)}. +handle_data_int(Codec, Data, Socket, WsHandleLoopPid, SockMod, Shaper) -> + {Type, NewCodec, Recv} = ejabberd_websocket_codec:decode(Codec, Data), + Send = + lists:filtermap( + fun({Op, Payload}) when Op == 1; Op == 2 -> + WsHandleLoopPid ! {received, Payload}, + false; + ({8, Payload}) -> + CloseCode = + case Payload of + <> -> + ?DEBUG("WebSocket close op: ~p ~ts", + [Code, Message]), + Code; + <> -> + ?DEBUG("WebSocket close op: ~p", [Code]), + Code; + _ -> + ?DEBUG("WebSocket close op unknown: ~p", [Payload]), + 1000 + end, + Frame = ejabberd_websocket_codec:encode(Codec, 8, <>), + {true, Frame}; + ({9, Payload}) -> + WsHandleLoopPid ! ping, + Frame = ejabberd_websocket_codec:encode(Codec, 10, Payload), + {true, Frame}; + ({10, _Payload}) -> + WsHandleLoopPid ! pong, + false + end, Recv), + case Type of + error -> + {error, protocol, NewCodec}; + _ -> + {NewCodec, Send, handle_shaping(Data, Socket, SockMod, Shaper)} + end. -websocket_close(Socket, WsHandleLoopPid, +websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, CloseCode) when CloseCode > 0 -> - Frame = encode_frame(<>, 8), + Frame = ejabberd_websocket_codec:encode(Codec, 8, <>), SockMod:send(Socket, Frame), - websocket_close(Socket, WsHandleLoopPid, SockMod, 0); -websocket_close(Socket, WsHandleLoopPid, SockMod, _CloseCode) -> + websocket_close(Codec, Socket, WsHandleLoopPid, SockMod, 0); +websocket_close(_Codec, Socket, WsHandleLoopPid, SockMod, _CloseCode) -> WsHandleLoopPid ! closed, SockMod:close(Socket). diff --git a/src/ejabberd_websocket_codec.erl b/src/ejabberd_websocket_codec.erl new file mode 100644 index 000000000..8dde29f53 --- /dev/null +++ b/src/ejabberd_websocket_codec.erl @@ -0,0 +1,175 @@ +%% +% File : ejabberd_websocket_codec.erl +% Author : Paweł Chmielowski +% Purpose : Coder/Encoder of websocket frames +% Created : 9 sty 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(ejabberd_websocket_codec). +-author("pawel@process-one.net"). + +%% API +-export([new_server/0, new_client/0, decode/2, encode/3]). + +-record(codec_state, { + our_mask = none :: none | binary(), + partial = none :: none | {non_neg_integer(), binary()}, + opcode = 0 :: non_neg_integer(), + is_fin = false :: boolean(), + mask = none :: none | binary(), + mask_offset = 0 :: non_neg_integer(), + required = -1 :: integer(), + data = <<>> :: binary() +}). + +-opaque codec_state() :: #codec_state{}. +-export_type([codec_state/0]). + +-spec new_server() -> codec_state(). +new_server() -> + #codec_state{}. + +new_client() -> + #codec_state{our_mask = p1_rand:bytes(4)}. + +-spec decode(codec_state(), binary()) -> {ok, codec_state(), [binary()]} | {error, atom(), [binary()]}. +decode(#codec_state{required = -1, data = PrevData, partial = Partial} = S, Data) -> + Data2 = <>, + case parse_header(Data2) of + none -> + {ok, S#codec_state{data = Data2}, []}; + {_, _, Opcode, _, _} when (Opcode > 2 andalso Opcode < 8) orelse (Opcode > 10) -> + {error, unknown_opcode, []}; + {_, 0, Opcode, _, _} when Opcode > 7 -> + {error, partial_control_frame, []}; + {_, _, Opcode, _, _} when Opcode > 0 andalso Opcode < 8 andalso Partial /= none -> + {error, partial_frame_non_finished, []}; + {Len, Final, Opcode, Mask, Payload} -> + decode(S#codec_state{opcode = Opcode, is_fin = Final == 1, + mask = Mask, mask_offset = 0, + required = Len, data = <<>>}, Payload) + end; +decode(#codec_state{required = Req, data = PrevData, + mask = Mask, mask_offset = Offset} = S, Data) + when byte_size(PrevData) + byte_size(Data) < Req -> + {Unmasked, NewOffset} = apply_mask(Offset, Mask, Data, PrevData), + {ok, S#codec_state{data = Unmasked, mask_offset = NewOffset}, []}; +decode(#codec_state{required = Req, data = PrevData, + mask = Mask, mask_offset = Offset, + is_fin = IsFin, opcode = Opcode, + partial = Partial} = S, Data) -> + Left = Req - byte_size(PrevData), + <> = Data, + {Unmasked, _} = apply_mask(Offset, Mask, CurrentPayload, PrevData), + {NS, Packets} = + case {IsFin, Partial} of + {false, none} -> + {S#codec_state{partial = {Opcode, Unmasked}, + data = <<>>, required = -1}, []}; + {false, {PartOp, PartData}} -> + {S#codec_state{partial = {PartOp, <>}, + data = <<>>, required = -1}, []}; + {true, none} -> + {S#codec_state{data = <<>>, required = -1}, [{Opcode, Unmasked}]}; + {true, {PartOp, PartData}} -> + {S#codec_state{partial = none, data = <<>>, required = -1}, + [{PartOp, <>}]} + end, + case NextPacketData of + <<>> -> + {ok, NS, Packets}; + _ -> + case decode(NS, NextPacketData) of + {T1, T2, Packets2} -> + {T1, T2, Packets ++ Packets2} + end + end. + +-spec encode(codec_state(), non_neg_integer(), binary()) -> binary(). +encode(#codec_state{our_mask = none}, Opcode, Data) -> + case byte_size(Data) of + S1 when S1 < 126 -> + <<1:1, 0:3, Opcode:4, 0:1, S1:7, Data/binary>>; + S2 when S2 < 65536 -> + <<1:1, 0:3, Opcode:4, 0:1, 126:7, S2:16, Data/binary>>; + S3 -> + <<1:1, 0:3, Opcode:4, 0:1, 127:7, S3:64, Data/binary>> + end; +encode(#codec_state{our_mask = Mask}, Opcode, Data) -> + {MaskedData, _} = apply_mask(0, Mask, Data, <<>>), + case byte_size(Data) of + S1 when S1 < 126 -> + <<1:1, 0:3, Opcode:4, 1:1, S1:7, Mask/binary, MaskedData/binary>>; + S2 when S2 < 65536 -> + <<1:1, 0:3, Opcode:4, 1:1, 126:7, S2:16, Mask/binary, MaskedData/binary>>; + S3 -> + <<1:1, 0:3, Opcode:4, 1:1, 127:7, S3:64, Mask/binary, MaskedData/binary>> + end. + + +-spec parse_header(binary()) -> none | {integer(), integer(), integer(), none | binary(), binary()}. +parse_header(<>) + when Len < 126 -> + {Len, Final, Opcode, none, Data}; +parse_header(<>) -> + {Len, Final, Opcode, none, Data}; +parse_header(<>) -> + {Len, Final, Opcode, none, Data}; +parse_header(<>) + when Len < 126 -> + {Len, Final, Opcode, Mask, Data}; +parse_header(<>) -> + {Len, Final, Opcode, Mask, Data}; +parse_header(<>) -> + {Len, Final, Opcode, Mask, Data}; +parse_header(_) -> + none. + +-spec apply_mask(integer(), none | binary(), binary(), binary()) -> {binary(), non_neg_integer()}. +apply_mask(_, none, Data, _) -> + {Data, 0}; +apply_mask(Offset, _, <<>>, Acc) -> + {Acc, Offset}; +apply_mask(0, <> = Mask, + <>, Acc) -> + apply_mask(0, Mask, Rest, + <>); +apply_mask(0, <> = Mask, + <>, Acc) -> + apply_mask(1, Mask, Rest, + <>); +apply_mask(1, <<_:8, M:8, _/binary>> = Mask, + <>, Acc) -> + apply_mask(2, Mask, Rest, + <>); +apply_mask(2, <<_:16, M:8, _/binary>> = Mask, + <>, Acc) -> + apply_mask(3, Mask, Rest, + <>); +apply_mask(3, <<_:24, M:8>> = Mask, + <>, Acc) -> + apply_mask(0, Mask, Rest, + <>). diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 9e587add8..d257fea02 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -5,7 +5,7 @@ %%% Created : 21 Aug 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -238,7 +238,7 @@ do_command(Auth, Command, AttrL, ArgsF, ArgsR, ArgsFormatted = format_args(rename_old_args(AttrL, ArgsR), ArgsF), Result = ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth), ResultFormatted = format_result(Result, ResultF), - {command_result, ResultFormatted}. + {command_result, {struct, [ResultFormatted]}}. rename_old_args(Args, []) -> Args; @@ -291,6 +291,14 @@ format_args(Args, ArgsFormat) -> L when is_list(L) -> exit({additional_unused_args, L}) end. +format_arg({array, Elements}, + {list, {_ElementDefName, ElementDefFormat}}) + when is_list(Elements) -> + lists:map(fun (ElementValue) -> + format_arg(ElementValue, ElementDefFormat) + end, + Elements); + format_arg({array, Elements}, {list, {ElementDefName, ElementDefFormat}}) when is_list(Elements) -> @@ -307,11 +315,18 @@ format_arg({array, [{struct, Elements}]}, format_arg(ElementValue, ElementDefFormat) end, Elements); +%% Old ejabberd 23.10 format_arg({array, [{struct, Elements}]}, {tuple, ElementsDef}) when is_list(Elements) -> FormattedList = format_args(Elements, ElementsDef), list_to_tuple(FormattedList); +%% New ejabberd 24.02 +format_arg({struct, Elements}, + {tuple, ElementsDef}) + when is_list(Elements) -> + FormattedList = format_args(Elements, ElementsDef), + list_to_tuple(FormattedList); format_arg({array, Elements}, {list, ElementsDef}) when is_list(Elements) and is_atom(ElementsDef) -> [format_arg(Element, ElementsDef) @@ -336,6 +351,10 @@ process_unicode_codepoints(Str) -> %% Result %% ----------------------------- +format_result(Code, {Name, rescode}) -> + {Name, make_status(Code)}; +format_result({_Code, Text}, {_Name, restuple}) -> + {text, io_lib:format("~s", [Text])}; format_result({error, Error}, _) when is_list(Error) -> throw({error, lists:flatten(Error)}); format_result({error, Error}, _) -> @@ -346,45 +365,36 @@ format_result({error, _Type, _Code, Error}, _) -> throw({error, Error}); format_result(String, string) -> lists:flatten(String); format_result(Atom, {Name, atom}) -> - {struct, - [{Name, iolist_to_binary(atom_to_list(Atom))}]}; + {Name, iolist_to_binary(atom_to_list(Atom))}; format_result(Int, {Name, integer}) -> - {struct, [{Name, Int}]}; + {Name, Int}; format_result([A|_]=String, {Name, string}) when is_list(String) and is_integer(A) -> - {struct, [{Name, lists:flatten(String)}]}; + {Name, lists:flatten(String)}; format_result(Binary, {Name, string}) when is_binary(Binary) -> - {struct, [{Name, binary_to_list(Binary)}]}; + {Name, binary_to_list(Binary)}; format_result(Atom, {Name, string}) when is_atom(Atom) -> - {struct, [{Name, atom_to_list(Atom)}]}; + {Name, atom_to_list(Atom)}; format_result(Integer, {Name, string}) when is_integer(Integer) -> - {struct, [{Name, integer_to_list(Integer)}]}; + {Name, integer_to_list(Integer)}; format_result(Other, {Name, string}) -> - {struct, [{Name, io_lib:format("~p", [Other])}]}; + {Name, io_lib:format("~p", [Other])}; format_result(String, {Name, binary}) when is_list(String) -> - {struct, [{Name, lists:flatten(String)}]}; + {Name, lists:flatten(String)}; format_result(Binary, {Name, binary}) when is_binary(Binary) -> - {struct, [{Name, binary_to_list(Binary)}]}; -format_result(Code, {Name, rescode}) -> - {struct, [{Name, make_status(Code)}]}; -format_result({Code, Text}, {Name, restuple}) -> - {struct, - [{Name, make_status(Code)}, - {text, io_lib:format("~s", [Text])}]}; -format_result(Elements, {Name, {list, ElementsDef}}) -> - FormattedList = lists:map(fun (Element) -> - format_result(Element, ElementsDef) - end, - Elements), - {struct, [{Name, {array, FormattedList}}]}; -format_result(ElementsTuple, - {Name, {tuple, ElementsDef}}) -> - ElementsList = tuple_to_list(ElementsTuple), - ElementsAndDef = lists:zip(ElementsList, ElementsDef), - FormattedList = lists:map(fun ({Element, ElementDef}) -> - format_result(Element, ElementDef) - end, - ElementsAndDef), - {struct, [{Name, {array, FormattedList}}]}; + {Name, binary_to_list(Binary)}; + + +format_result(Els, {Name, {list, Def}}) -> + FormattedList = [element(2, format_result(El, Def)) || El <- Els], + {Name, {array, FormattedList}}; + + +format_result(Tuple, + {Name, {tuple, Def}}) -> + Els = lists:zip(tuple_to_list(Tuple), Def), + FormattedList = [format_result(El, ElDef) || {El, ElDef} <- Els], + {Name, {struct, FormattedList}}; + format_result(404, {Name, _}) -> {struct, [{Name, make_status(not_found)}]}. @@ -400,6 +410,5 @@ listen_options() -> " method may be removed in a future ejabberd release. You are " "encouraged to define ejabberd_xmlrpc inside request_handlers " "option of ejabberd_http listen module. See the ejabberd " - "documentation for details: https://docs.ejabberd.im/admin/" - "configuration/listen/#ejabberd-xmlrpc", []), + "documentation for details: _`/admin/configuration/listen/#ejabberd-xmlrpc|ejabberd_xmlrpc listener`_.", []), []. diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl index 469457048..263e98f1a 100644 --- a/src/ejd2sql.erl +++ b/src/ejd2sql.erl @@ -5,7 +5,7 @@ %%% Created : 22 Aug 2005 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -74,7 +74,10 @@ export(Server, Output) -> close_output(Output, IO). export(Server, Output, mod_mam = M1) -> - MucServices = gen_mod:get_module_opt_hosts(Server, mod_muc), + MucServices = case gen_mod:is_loaded(Server, mod_muc) of + true -> gen_mod:get_module_opt_hosts(Server, mod_muc); + false -> [] + end, [export2(MucService, Output, M1, M1) || MucService <- MucServices], export2(Server, Output, M1, M1); export(Server, Output, mod_pubsub = M1) -> diff --git a/src/eldap.erl b/src/eldap.erl index 3a9d8974c..3676bd09a 100644 --- a/src/eldap.erl +++ b/src/eldap.erl @@ -605,10 +605,10 @@ init([Hosts, Port, Rootdn, Passwd, Opts]) -> []), CertOpts; Verify == soft -> - [{verify, verify_peer}, {fail_if_no_peer_cert, false}] ++ CertOpts ++ CacertOpts ++ DepthOpts; + [{verify, verify_peer}] ++ CertOpts ++ CacertOpts ++ DepthOpts; Verify == hard -> - [{verify, verify_peer}, {fail_if_no_peer_cert, true}] ++ CertOpts ++ CacertOpts ++ DepthOpts; - true -> [] + [{verify, verify_peer}] ++ CertOpts ++ CacertOpts ++ DepthOpts; + true -> [{verify, verify_none}] end, {ok, connecting, #eldap{hosts = Hosts, port = PortTemp, rootdn = Rootdn, diff --git a/src/eldap_filter.erl b/src/eldap_filter.erl index 4e554572d..3483e8b02 100644 --- a/src/eldap_filter.erl +++ b/src/eldap_filter.erl @@ -6,7 +6,7 @@ %%% Author: Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/eldap_pool.erl b/src/eldap_pool.erl index 148095783..78391eb1d 100644 --- a/src/eldap_pool.erl +++ b/src/eldap_pool.erl @@ -5,7 +5,7 @@ %%% Created : 12 Nov 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/eldap_utils.erl b/src/eldap_utils.erl index efbd78550..6225c5cce 100644 --- a/src/eldap_utils.erl +++ b/src/eldap_utils.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/elixir_logger_backend.erl b/src/elixir_logger_backend.erl index 4db60789d..e431dcef3 100644 --- a/src/elixir_logger_backend.erl +++ b/src/elixir_logger_backend.erl @@ -5,7 +5,7 @@ %%% Created : 9 March 2016 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/ext_mod.erl b/src/ext_mod.erl index eeb310207..9d9b438c6 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -5,7 +5,7 @@ %%% Created : 19 Feb 2015 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2006-2022 ProcessOne +%%% ejabberd, Copyright (C) 2006-2025 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,22 +33,25 @@ installed_command/0, installed/0, installed/1, install/1, uninstall/1, upgrade/0, upgrade/1, add_paths/0, add_sources/1, add_sources/2, del_sources/1, modules_dir/0, + install_contrib_modules/2, config_dir/0, get_commands_spec/0]). -export([modules_configs/0, module_ebin_dir/1]). --export([compile_erlang_file/2, compile_elixir_file/2]). --export([web_menu_node/3, web_page_node/5, get_page/3]). +-export([compile_erlang_file/2, compile_elixir_files/2]). +-export([web_menu_node/3, web_page_node/3, webadmin_node_contrib/3]). %% 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_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). +-include_lib("stdlib/include/zip.hrl"). --define(REPOS, "https://github.com/processone/ejabberd-contrib"). +-define(REPOS, "git@github.com:processone/ejabberd-contrib.git"). -record(state, {}). @@ -66,7 +69,7 @@ init([]) -> {ok, #state{}}. add_paths() -> - [code:add_patha(module_ebin_dir(Module)) + [code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)]) || {Module, _} <- installed()]. handle_call(Request, From, State) -> @@ -77,6 +80,11 @@ handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. +handle_info({'ETS-TRANSFER', Table, Process, Module}, State) -> + ?DEBUG("ejabberd now controls ETS table ~p from process ~p for module ~p", + [Table, Process, Module]), + {noreply, State}; + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. @@ -141,7 +149,8 @@ get_commands_spec() -> #ejabberd_commands{name = module_upgrade, tags = [modules], desc = "Upgrade the running code of an installed module", - longdesc = "In practice, this uninstalls and installs the module", + longdesc = "In practice, this uninstalls, cleans the compiled files, and installs the module", + note = "improved in 25.07", module = ?MODULE, function = upgrade, args_desc = ["Module name"], args_example = [<<"mod_rest">>], @@ -215,10 +224,13 @@ installed_command() -> [short_spec(Item) || Item <- installed()]. install(Module) when is_atom(Module) -> - install(misc:atom_to_binary(Module)); + install(misc:atom_to_binary(Module), undefined); install(Package) when is_binary(Package) -> + install(Package, undefined). + +install(Package, Config) when is_binary(Package) -> Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package], - case {Spec, installed(Package), is_contrib_allowed()} of + case {Spec, installed(Package), is_contrib_allowed(Config)} of {_, _, false} -> {error, not_allowed}; {[], _, _} -> @@ -227,13 +239,15 @@ install(Package) when is_binary(Package) -> {error, conflict}; {[Attrs], _, _} -> Module = misc:binary_to_atom(Package), - case compile_and_install(Module, Attrs) of + case compile_and_install(Module, Attrs, Config) of ok -> - code:add_patha(module_ebin_dir(Module)), - ejabberd_config:reload(), + code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)]), + ejabberd_config_reload(Config), + maybe_print_module_status(Module), copy_commit_json(Package, Attrs), - case erlang:function_exported(Module, post_install, 0) of - true -> Module:post_install(); + ModuleRuntime = get_runtime_module_name(Module), + case erlang:function_exported(ModuleRuntime, post_install, 0) of + true -> ModuleRuntime:post_install(); _ -> ok end; Error -> @@ -242,21 +256,38 @@ install(Package) when is_binary(Package) -> end end. +ejabberd_config_reload(Config) when is_list(Config) -> + %% Don't reload config when ejabberd is starting + %% because it will be reloaded after installing + %% all the external modules from install_contrib_modules + ok; +ejabberd_config_reload(undefined) -> + ejabberd_config:reload(). + +maybe_print_module_status(Module) -> + case get_module_status_el(Module) of + [_, {xmlcdata, String}] -> + io:format("~ts~n", [String]); + _ -> + ok + end. + uninstall(Module) when is_atom(Module) -> uninstall(misc:atom_to_binary(Module)); uninstall(Package) when is_binary(Package) -> case installed(Package) of true -> Module = misc:binary_to_atom(Package), - case erlang:function_exported(Module, pre_uninstall, 0) of - true -> Module:pre_uninstall(); + ModuleRuntime = get_runtime_module_name(Module), + case erlang:function_exported(ModuleRuntime, pre_uninstall, 0) of + true -> ModuleRuntime:pre_uninstall(); _ -> ok end, - [catch gen_mod:stop_module(Host, Module) + [catch gen_mod:stop_module(Host, ModuleRuntime) || Host <- ejabberd_option:hosts()], - code:purge(Module), - code:delete(Module), - code:del_path(module_ebin_dir(Module)), + code:purge(ModuleRuntime), + code:delete(ModuleRuntime), + [code:del_path(PathDelete) || PathDelete <- [module_ebin_dir(Module)|module_deps_dirs(Module)]], delete_path(module_lib_dir(Module)), ejabberd_config:reload(); false -> @@ -269,8 +300,19 @@ upgrade(Module) when is_atom(Module) -> upgrade(misc:atom_to_binary(Module)); upgrade(Package) when is_binary(Package) -> uninstall(Package), + clean(Package), install(Package). +clean(Package) -> + Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package], + case Spec of + [] -> + {error, not_available}; + [Attrs] -> + Path = proplists:get_value(path, Attrs), + [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/{deps,ebin}")] + end. + add_sources(Path) when is_list(Path) -> add_sources(iolist_to_binary(module_name(Path)), Path). add_sources(_, "") -> @@ -280,7 +322,7 @@ add_sources(Module, Path) when is_atom(Module), is_list(Path) -> add_sources(Package, Path) when is_binary(Package), is_list(Path) -> DestDir = sources_dir(), RepDir = filename:join(DestDir, module_name(Path)), - delete_path(RepDir), + delete_path(RepDir, binary_to_list(Package)), case filelib:ensure_dir(RepDir) of ok -> case {string:left(Path, 4), string:right(Path, 2)} of @@ -343,14 +385,14 @@ geturl(Url) -> case httpc:request(get, {Url, [UA]}, User, [{body_format, binary}], ext_mod) of {ok, {{_, 200, _}, Headers, Response}} -> {ok, Headers, Response}; + {ok, {{_, 403, Reason}, _Headers, _Response}} -> + {error, Reason}; {ok, {{_, Code, _}, _Headers, Response}} -> {error, {Code, Response}}; {error, Reason} -> {error, Reason} end. -getenv(Env) -> - getenv(Env, ""). getenv(Env, Default) -> case os:getenv(Env) of false -> Default; @@ -367,6 +409,23 @@ extract(tar, {ok, _, Body}, DestDir) -> extract(_, {error, Reason}, _) -> {error, Reason}; extract(zip, Zip, DestDir) -> + {ok, DirList} = zip:list_dir(Zip), + Offending = + lists:filter(fun (#zip_comment{}) -> + false; + (#zip_file{name = Filename}) -> + absolute == filename:pathtype(Filename) + end, + DirList), + case Offending of + [] -> + extract(zip_verified, Zip, DestDir); + _ -> + Filenames = [F#zip_file.name || F <- Offending], + ?ERROR_MSG("The zip file includes absolute file paths:~n ~p", [Filenames]), + {error, {zip_absolute_path, Filenames}} + end; +extract(zip_verified, Zip, DestDir) -> case zip:extract(Zip, [{cwd, DestDir}]) of {ok, _} -> ok; Error -> Error @@ -390,8 +449,9 @@ extract_github_master(Repos, DestDir) -> case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of ok -> RepDir = filename:join(DestDir, module_name(Repos)), - file:rename(RepDir++"-master", RepDir), - maybe_write_commit_json(Url, RepDir); + RepDirSpec = filename:join(DestDir, module_spec_name(RepDir)), + file:rename(RepDir++"-master", RepDirSpec), + maybe_write_commit_json(Url, RepDirSpec); Error -> Error end. @@ -426,8 +486,11 @@ delete_path(Path) -> file:delete(Path) end. +delete_path(Path, Package) -> + delete_path(filename:join(filename:dirname(Path), Package)). + modules_dir() -> - DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"), + DefaultDir = filename:join(misc:get_home(), ".ejabberd-modules"), getenv("CONTRIB_MODULES_PATH", DefaultDir). sources_dir() -> @@ -464,6 +527,14 @@ module_src_dir(Package) -> module_name(Id) -> filename:basename(filename:rootname(Id)). +module_spec_name(Path) -> + case filelib:wildcard(filename:join(Path++"-master", "*.spec")) of + "" -> + module_name(Path); + ModuleName -> + filename:basename(ModuleName, ".spec") + end. + module(Id) -> misc:binary_to_atom(iolist_to_binary(module_name(Id))). @@ -483,7 +554,13 @@ modules_spec(Dir, Path) -> short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) -> {Module, proplists:get_value(summary, Attrs, "")}. -is_contrib_allowed() -> +is_contrib_allowed(Config) when is_list(Config) -> + case lists:keyfind(allow_contrib_modules, 1, Config) of + false -> true; + {_, false} -> false; + {_, true} -> true + end; +is_contrib_allowed(undefined) -> ejabberd_option:allow_contrib_modules(). %% -- build functions @@ -503,7 +580,7 @@ check_sources(Module) -> true -> Acc; false -> [{missing, Name}|Acc] end - end, HaveSrc, [{is_file, "README.txt"}, + end, HaveSrc, [{is_file, "README.md"}, {is_file, "COPYING"}, {is_file, SpecFile}]), SpecCheck = case consult(SpecFile) of @@ -526,15 +603,15 @@ check_sources(Module) -> _ -> {error, Result} end. -compile_and_install(Module, Spec) -> +compile_and_install(Module, Spec, Config) -> SrcDir = module_src_dir(Module), LibDir = module_lib_dir(Module), case filelib:is_dir(SrcDir) of true -> case compile_deps(SrcDir) of ok -> - case compile(SrcDir) of - ok -> install(Module, Spec, SrcDir, LibDir); + case compile(SrcDir, filename:join(SrcDir, "deps")) of + ok -> install(Module, Spec, SrcDir, LibDir, Config); Error -> Error end; Error -> @@ -543,33 +620,47 @@ compile_and_install(Module, Spec) -> false -> Path = proplists:get_value(url, Spec, ""), case add_sources(Module, Path) of - ok -> compile_and_install(Module, Spec); + ok -> compile_and_install(Module, Spec, Config); Error -> Error end end. compile_deps(LibDir) -> - Deps = filename:join(LibDir, "deps"), - case filelib:is_dir(Deps) of + DepsDir = filename:join(LibDir, "deps"), + case filelib:is_dir(DepsDir) of true -> ok; % assume deps are included false -> fetch_rebar_deps(LibDir) end, - Rs = [compile(Dep) || Dep <- filelib:wildcard(filename:join(Deps, "*"))], + Rs = [compile(Dep, DepsDir) || Dep <- filelib:wildcard(filename:join(DepsDir, "*"))], compile_result(Rs). -compile(LibDir) -> +compile(LibDir, DepsDir) -> Bin = filename:join(LibDir, "ebin"), - Inc = filename:join(LibDir, "include"), Lib = filename:join(LibDir, "lib"), Src = filename:join(LibDir, "src"), - Options = [{outdir, Bin}, {i, Inc} | compile_options()], + Includes = [ {i, Inc} || Inc <- filelib:wildcard(DepsDir++"/**/include") ], + Options = [ {outdir, Bin}, + {i, LibDir++"/.."}, + {i, filename:join(LibDir, "include")} + | Includes ++ compile_options()], + ?DEBUG("compile options: ~p", [Options]), filelib:ensure_dir(filename:join(Bin, ".")), - [copy(App, Bin) || App <- filelib:wildcard(Src++"/*.app")], + [copy(App, filename:join(Bin, filename:basename(App, ".src"))) || App <- filelib:wildcard(Src++"/*.app*")], + compile_c_files(LibDir), + ErlFiles = filelib:wildcard(Src++"/**/*.erl"), + ?DEBUG("erl files to compile: ~p", [ErlFiles]), Er = [compile_erlang_file(Bin, File, Options) - || File <- filelib:wildcard(Src++"/*.erl")], - Ex = [compile_elixir_file(Bin, File) - || File <- filelib:wildcard(Lib ++ "/*.ex")], - compile_result(Er++Ex). + || File <- ErlFiles], + Ex = compile_elixir_files(Bin, filelib:wildcard(Lib ++ "/**/*.ex")), + compile_result(lists:flatten([Er, Ex])). + +compile_c_files(LibDir) -> + case file:read_file_info(filename:join(LibDir, "c_src/Makefile")) of + {ok, _} -> + os:cmd("cd "++LibDir++"; make -C c_src"); + {error, _} -> + ok + end. compile_result(Results) -> case lists:dropwhile( @@ -591,6 +682,8 @@ compile_options() -> ++ maybe_define_lager_macro() ++ [{i, filename:join(app_dir(App), "include")} || App <- [fast_xml, xmpp, p1_utils, ejabberd]] + ++ [{i, filename:join(app_dir(App), "include")} + || App <- [p1_xml, p1_xmpp]] % paths used in Debian packages ++ [{i, filename:join(mod_dir(Mod), "include")} || Mod <- installed()]. @@ -625,31 +718,49 @@ compile_erlang_file(Dest, File, ErlOptions) -> end. -ifdef(ELIXIR_ENABLED). -compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) -> - compile_elixir_file(list_to_binary(Dest), list_to_binary(File)); +compile_elixir_files(_, []) -> + ok; +compile_elixir_files(Dest, [File | _] = Files) when is_list(Dest) and is_list(File) -> + BinFiles = [list_to_binary(F) || F <- Files], + compile_elixir_files(list_to_binary(Dest), BinFiles); -compile_elixir_file(Dest, File) -> - try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of - [Module] -> {ok, Module} +compile_elixir_files(Dest, Files) -> + try 'Elixir.Kernel.ParallelCompiler':compile_to_path(Files, Dest, [{return_diagnostics, true}]) of + {ok, Modules, []} when is_list(Modules) -> + {ok, Modules}; + {ok, Modules, Warnings} when is_list(Modules) -> + ?WARNING_MSG("Warnings compiling module: ~n~p", [Warnings]), + {ok, Modules} catch - _ -> {error, {compilation_failed, File}} + A:B -> + ?ERROR_MSG("Problem ~p compiling Elixir files: ~p~nFiles: ~p", [A, B, Files]), + {error, {compilation_failed, Files}} end. -else. -compile_elixir_file(_, File) -> - {error, {compilation_failed, File}}. +compile_elixir_files(_, []) -> + ok; +compile_elixir_files(_, Files) -> + ErrorString = "Attempted to compile Elixir files, but Elixir support is " + "not available in ejabberd. Try compiling ejabberd using " + "'./configure --enable-elixir' or './configure --with-rebar=mix'", + ?ERROR_MSG(ErrorString, []), + io:format("Error: " ++ ErrorString ++ "~n", []), + {error, {elixir_not_available, Files}}. -endif. -install(Module, Spec, SrcDir, LibDir) -> +install(Module, Spec, SrcDir, LibDir, Config) -> {ok, CurDir} = file:get_cwd(), file:set_cwd(SrcDir), Files1 = [{File, copy(File, filename:join(LibDir, File))} || File <- filelib:wildcard("{ebin,priv,conf,include}/**")], Files2 = [{File, copy(File, filename:join(LibDir, filename:join(lists:nthtail(2,filename:split(File)))))} - || File <- filelib:wildcard("deps/*/{ebin,priv}/**")], + || File <- filelib:wildcard("deps/*/ebin/**")], + Files3 = [{File, copy(File, filename:join(LibDir, File))} + || File <- filelib:wildcard("deps/*/priv/**")], Errors = lists:dropwhile(fun({_, ok}) -> true; (_) -> false - end, Files1++Files2), - inform_module_configuration(Module, LibDir, Files1), + end, Files1++Files2++Files3), + inform_module_configuration(Module, LibDir, Files1, Config), Result = case Errors of [{F, {error, E}}|_] -> {error, {F, E}}; @@ -661,24 +772,37 @@ install(Module, Spec, SrcDir, LibDir) -> file:set_cwd(CurDir), Result. -inform_module_configuration(Module, LibDir, Files1) -> +inform_module_configuration(Module, LibDir, Files1, Config) -> Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true; (_) -> false end, Files1), - case Res of - [{ConfigPath, ok}] -> + AlreadyConfigured = lists:keymember(Module, 1, get_modules(Config)), + case {Res, AlreadyConfigured} of + {[{ConfigPath, ok}], false} -> FullConfigPath = filename:join(LibDir, ConfigPath), io:format("Module ~p has been installed and started.~n" "It's configured in the file:~n ~s~n" "Configure the module in that file, or remove it~n" "and configure in your main ejabberd.yml~n", [Module, FullConfigPath]); - [] -> + {[{ConfigPath, ok}], true} -> + FullConfigPath = filename:join(LibDir, ConfigPath), + file:rename(FullConfigPath, FullConfigPath++".example"), + io:format("Module ~p has been installed and started.~n" + "The ~p configuration in your ejabberd.yml is used.~n", + [Module, Module]); + {[], _} -> io:format("Module ~p has been installed.~n" "Now you can configure it in your ejabberd.yml~n", [Module]) end. +get_modules(Config) when is_list(Config) -> + {modules, Modules} = lists:keyfind(modules, 1, Config), + Modules; +get_modules(undefined) -> + ejabberd_config:get_option(modules). + %% -- minimalist rebar spec parser, only support git fetch_rebar_deps(SrcDir) -> @@ -690,8 +814,10 @@ fetch_rebar_deps(SrcDir) -> {ok, CurDir} = file:get_cwd(), file:set_cwd(SrcDir), filelib:ensure_dir(filename:join("deps", ".")), - lists:foreach(fun({_App, Cmd}) -> - os:cmd("cd deps; "++Cmd++"; cd ..") + lists:foreach(fun({App, Cmd}) -> + io:format("Fetching dependency ~s: ", [App]), + Result = os:cmd("cd deps; "++Cmd++"; cd .."), + io:format("~s", [Result]) end, Deps), file:set_cwd(CurDir) end. @@ -705,6 +831,19 @@ rebar_deps(Script) -> _ -> [] end. + +rebar_dep({App, Version, Git}) when Version /= ".*" -> + AppS = atom_to_list(App), + Help = os:cmd("mix hex.package"), + case string:find(Help, "mix hex.package fetch") /= nomatch of + true -> + {App, "mix hex.package fetch "++AppS++" "++Version++" --unpack --output "++AppS}; + false -> + io:format("I'll download ~p using git because I can't use Mix " + "to fetch from hex.pm:~n~s", [AppS, Help]), + rebar_dep({App, ".*", Git}) + end; + rebar_dep({App, _, {git, Url}}) -> {App, "git clone "++Url++" "++filename:basename(App)}; rebar_dep({App, _, {git, Url, {branch, Ref}}}) -> @@ -720,6 +859,14 @@ rebar_dep({App, _, {git, Url, Ref}}) -> "; (cd "++filename:basename(App)++ "; git checkout -q "++Ref++")"}. +module_deps_dirs(Module) -> + SrcDir = module_src_dir(Module), + LibDir = module_lib_dir(Module), + DepsDir = filename:join(LibDir, "deps"), + Deps = rebar_deps(filename:join(SrcDir, "rebar.config")) + ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")), + [filename:join(DepsDir, App) || {App, _Cmd} <- Deps]. + %% -- YAML spec parser consult(File) -> @@ -747,13 +894,17 @@ maybe_write_commit_json(Url, RepDir) -> write_commit_json(Url, RepDir) -> Url2 = string_replace(Url, "https://github.com", "https://api.github.com/repos"), BranchUrl = lists:flatten(Url2 ++ "/branches/master"), - {ok, _Headers, Body} = geturl(BranchUrl), - {ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]), - file:write(F, Body), - file:close(F). + case geturl(BranchUrl) of + {ok, _Headers, Body} -> + {ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]), + file:write(F, Body), + file:close(F); + {error, Reason} -> + Reason + end. find_commit_json(Attrs) -> - {_, FromPath} = lists:keyfind(path, 1, Attrs), + FromPath = get_module_path(Attrs), case {find_commit_json_path(FromPath), find_commit_json_path(filename:join(FromPath, ".."))} of @@ -814,21 +965,21 @@ get_commit_details2(Path) -> end. parse_details(Body) -> - {Contents} = jiffy:decode(Body), + Contents = misc:json_decode(Body), - {_, {Commit}} = lists:keyfind(<<"commit">>, 1, Contents), - {_, Sha} = lists:keyfind(<<"sha">>, 1, Commit), - {_, CommitHtmlUrl} = lists:keyfind(<<"html_url">>, 1, Commit), + {ok, Commit} = maps:find(<<"commit">>, Contents), + {ok, Sha} = maps:find(<<"sha">>, Commit), + {ok, CommitHtmlUrl} = maps:find(<<"html_url">>, Commit), - {_, {Commit2}} = lists:keyfind(<<"commit">>, 1, Commit), - {_, Message} = lists:keyfind(<<"message">>, 1, Commit2), - {_, {Author}} = lists:keyfind(<<"author">>, 1, Commit2), - {_, AuthorName} = lists:keyfind(<<"name">>, 1, Author), - {_, {Committer}} = lists:keyfind(<<"committer">>, 1, Commit2), - {_, Date} = lists:keyfind(<<"date">>, 1, Committer), + {ok, Commit2} = maps:find(<<"commit">>, Commit), + {ok, Message} = maps:find(<<"message">>, Commit2), + {ok, Author} = maps:find(<<"author">>, Commit2), + {ok, AuthorName} = maps:find(<<"name">>, Author), + {ok, Committer} = maps:find(<<"committer">>, Commit2), + {ok, Date} = maps:find(<<"date">>, Committer), - {_, {Links}} = lists:keyfind(<<"_links">>, 1, Contents), - {_, Html} = lists:keyfind(<<"html">>, 1, Links), + {ok, Links} = maps:find(<<"_links">>, Contents), + {ok, Html} = maps:find(<<"html">>, Links), #{sha => Sha, date => Date, @@ -854,29 +1005,159 @@ parse_details(Body) -> ) ). -web_menu_node(Acc, _Node, Lang) -> - Acc ++ [{<<"contrib">>, translate:translate(Lang, ?T("Contrib Modules"))}]. +%% @format-begin -web_page_node(_, Node, [<<"contrib">>], Query, Lang) -> - Res = rpc:call(Node, ?MODULE, get_page, [Node, Query, Lang]), - {stop, Res}; -web_page_node(Acc, _, _, _, _) -> +web_menu_node(Acc, _Node, _Lang) -> + Acc + ++ [{<<"contrib">>, <<"Contrib Modules (Detailed)">>}, + {<<"contrib-api">>, <<"Contrib Modules (API)">>}]. + +web_page_node(_, + Node, + #request{path = [<<"contrib">>], + q = Query, + lang = Lang} = + R) -> + Title = + ?H1GL(<<"Contrib Modules (Detailed)">>, + <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>, + <<"ejabberd-contrib">>), + Res = [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [webadmin_node_contrib, + R, + [{<<"node">>, Node}, {<<"query">>, Query}, {<<"lang">>, Lang}], + []])], + {stop, Title ++ Res}; +web_page_node(_, Node, #request{path = [<<"contrib-api">> | RPath]} = R) -> + Title = + ?H1GL(<<"Contrib Modules (API)">>, + <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>, + <<"ejabberd-contrib">>), + _TableInstalled = make_table_installed(Node, R, RPath), + _TableAvailable = make_table_available(Node, R, RPath), + TableInstalled = make_table_installed(Node, R, RPath), + TableAvailable = make_table_available(Node, R, RPath), + Res = [?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"specs">>}], <<"Specs">>), + ?XE(<<"blockquote">>, + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [modules_update_specs, R])]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"installed">>}], <<"Installed">>), + ?XE(<<"blockquote">>, + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [modules_installed, R, [], [{only, presentation}]]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [module_uninstall, R, [], [{only, presentation}]]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [module_upgrade, R, [], [{only, presentation}]]), + TableInstalled]), + ?X(<<"hr">>), + ?XAC(<<"h2">>, [{<<"id">>, <<"available">>}], <<"Available">>), + ?XE(<<"blockquote">>, + [ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [modules_available, R, [], [{only, presentation}]]), + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [module_install, R, [], [{only, presentation}]]), + TableAvailable, + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [module_check, R])])], + {stop, Title ++ Res}; +web_page_node(Acc, _, _) -> Acc. -get_page(Node, Query, Lang) -> +make_table_installed(Node, R, RPath) -> + Columns = [<<"Name">>, <<"Summary">>, <<"">>, <<"">>], + ModulesInstalled = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command_raw_value, + [modules_installed, R, []]), + Rows = + lists:map(fun({Name, Summary}) -> + NameBin = misc:atom_to_binary(Name), + Upgrade = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [module_upgrade, + R, + [{<<"module">>, NameBin}], + [{only, button}, {input_name_append, [NameBin]}]]), + Uninstall = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [module_uninstall, + R, + [{<<"module">>, NameBin}], + [{only, button}, + {style, danger}, + {input_name_append, [NameBin]}]]), + {?C(NameBin), ?C(list_to_binary(Summary)), Upgrade, Uninstall} + end, + ModulesInstalled), + ejabberd_web_admin:make_table(200, RPath, Columns, Rows). + +make_table_available(Node, R, RPath) -> + Columns = [<<"Name">>, <<"Summary">>, <<"">>], + ModulesAll = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command_raw_value, + [modules_available, R, []]), + ModulesInstalled = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command_raw_value, + [modules_installed, R, []]), + ModulesNotInstalled = + lists:filter(fun({Mod, _}) -> not lists:keymember(Mod, 1, ModulesInstalled) end, + ModulesAll), + Rows = + lists:map(fun({Name, Summary}) -> + NameBin = misc:atom_to_binary(Name), + Install = + ejabberd_cluster:call(Node, + ejabberd_web_admin, + make_command, + [module_install, + R, + [{<<"module">>, NameBin}], + [{only, button}, {input_name_append, [NameBin]}]]), + {?C(NameBin), ?C(list_to_binary(Summary)), Install} + end, + ModulesNotInstalled), + ejabberd_web_admin:make_table(200, RPath, Columns, Rows). + +webadmin_node_contrib(Node, Query, Lang) -> QueryRes = list_modules_parse_query(Query), - Title = ?H1GL(translate:translate(Lang, ?T("Contrib Modules")), - <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>, - <<"ejabberd-contrib">>), Contents = get_content(Node, Query, Lang), - Result = case QueryRes of - ok -> [?XREST(?T("Submitted"))]; - nothing -> [] - end, - Title ++ Result ++ Contents. + Result = + case QueryRes of + ok -> + [?XREST(?T("Submitted"))]; + nothing -> + [] + end, + Result ++ Contents. +%% @format-end get_module_home(Module, Attrs) -> - case element(2, lists:keyfind(home, 1, Attrs)) of + case get_module_information(home, Attrs) of "https://github.com/processone/ejabberd-contrib/tree/master/" = P1 -> P1 ++ atom_to_list(Module); Other -> @@ -884,17 +1165,26 @@ get_module_home(Module, Attrs) -> end. get_module_summary(Attrs) -> - element(2, lists:keyfind(summary, 1, Attrs)). + get_module_information(summary, Attrs). get_module_author(Attrs) -> - element(2, lists:keyfind(author, 1, Attrs)). + get_module_information(author, Attrs). + +get_module_path(Attrs) -> + get_module_information(path, Attrs). + +get_module_information(Attribute, Attrs) -> + case lists:keyfind(Attribute, 1, Attrs) of + false -> ""; + {_, Value} -> Value + end. get_installed_module_el({ModAtom, Attrs}, Lang) -> Mod = misc:atom_to_binary(ModAtom), Home = list_to_binary(get_module_home(ModAtom, Attrs)), Summary = list_to_binary(get_module_summary(Attrs)), Author = list_to_binary(get_module_author(Attrs)), - {_, FromPath} = lists:keyfind(path, 1, Attrs), + FromPath = get_module_path(Attrs), FromFile = case find_commit_json_path(FromPath) of {ok, FF} -> FF; {error, _} -> "dummypath" @@ -936,12 +1226,7 @@ get_installed_module_el({ModAtom, Attrs}, Lang) -> [] end, TitleEl = make_title_el(CommitDate, CommitMessage, CommitAuthorName), - Status = case lists:member({mod_status, 0}, ModAtom:module_info(exports)) of - true -> - [?C(<<" ">>), - ?C(ModAtom:mod_status())]; - false -> [] - end, + Status = get_module_status_el(ModAtom), HomeTitleEl = make_home_title_el(Summary, Author), ?XE(<<"tr">>, [?XE(<<"td">>, [?AXC(Home, [HomeTitleEl], Mod)]), @@ -954,6 +1239,66 @@ get_installed_module_el({ModAtom, Attrs}, Lang) -> ++ Status) | UpgradeEls]). +get_module_status_el(ModAtom) -> + case {get_module_status(ModAtom), + get_module_status(elixir_module_name(ModAtom))} of + {Str, unknown} when is_list(Str) -> + [?C(<<" ">>), ?C(Str)]; + {unknown, Str} when is_list(Str) -> + [?C(<<" ">>), ?C(Str)]; + {unknown, unknown} -> + [] + end. + +get_module_status(Module) -> + try Module:mod_status() of + Str when is_list(Str) -> + Str + catch + _:_ -> + unknown + end. + +%% When a module named mod_whatever in ejabberd-modules +%% is written in Elixir, its runtime name is 'Elixir.ModWhatever' +get_runtime_module_name(Module) -> + case is_elixir_module(Module) of + true -> elixir_module_name(Module); + false -> Module + end. + +is_elixir_module(Module) -> + LibDir = module_src_dir(Module), + Lib = filename:join(LibDir, "lib"), + Src = filename:join(LibDir, "src"), + case {filelib:wildcard(Lib++"/*.{ex}"), + filelib:wildcard(Src++"/*.{erl}")} of + {[_ | _], []} -> + true; + {[], _} -> + false + end. + +%% Converts mod_some_thing to Elixir.ModSomeThing +elixir_module_name(ModAtom) -> + list_to_atom("Elixir." ++ elixir_module_name("_" ++ atom_to_list(ModAtom), [])). + +elixir_module_name([], Res) -> + lists:reverse(Res); +elixir_module_name([$_, Char | Remaining], Res) -> + [Upper] = uppercase([Char]), + elixir_module_name(Remaining, [Upper | Res]); +elixir_module_name([Char | Remaining], Res) -> + elixir_module_name(Remaining, [Char | Res]). + +-ifdef(HAVE_URI_STRING). +uppercase(String) -> + string:uppercase(String). % OTP 20 or higher +-else. +uppercase(String) -> + string:to_upper(String). % OTP older than 20 +-endif. + get_available_module_el({ModAtom, Attrs}) -> Installed = installed(), Mod = misc:atom_to_binary(ModAtom), @@ -1159,3 +1504,14 @@ list_modules_parse_uninstall(Query) -> 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 64c1b9ebb..b1acd4a7e 100644 --- a/src/extauth.erl +++ b/src/extauth.erl @@ -2,7 +2,7 @@ %%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/extauth_sup.erl b/src/extauth_sup.erl index c10bbf3bf..40769cbc9 100644 --- a/src/extauth_sup.erl +++ b/src/extauth_sup.erl @@ -2,7 +2,7 @@ %%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/gen_iq_handler.erl b/src/gen_iq_handler.erl index 313061ae0..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-2022 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,7 +37,7 @@ -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). --include("ejabberd_stacktrace.hrl"). + -type component() :: ejabberd_sm | ejabberd_local. @@ -111,14 +111,14 @@ process_iq(_Host, Module, Function, IQ) -> ejabberd_router:route(ResIQ); ignore -> ok - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?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) + 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. -spec process_iq(module(), atom(), iq()) -> ignore | iq(). diff --git a/src/gen_mod.erl b/src/gen_mod.erl index 1d9d26b40..667ff5055 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -5,7 +5,7 @@ %%% Created : 24 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -author('alexey@process-one.net'). -export([init/1, start_link/0, start_child/3, start_child/4, - stop_child/1, stop_child/2, stop/0, config_reloaded/0]). + 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, @@ -44,11 +44,13 @@ -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). --include("ejabberd_stacktrace.hrl"). + +-include("ejabberd_commands.hrl"). -record(ejabberd_module, {module_host = {undefined, <<"">>} :: {atom(), binary()}, opts = [] :: opts() | '_' | '$2', + registrations = [] :: [registration()], order = 0 :: integer()}). -type opts() :: #{atom() => term()}. @@ -57,17 +59,35 @@ value => string() | binary()}. -type opt_doc() :: {atom(), opt_desc()} | {atom(), opt_desc(), [opt_doc()]}. --callback start(binary(), opts()) -> ok | {ok, pid()} | {error, term()}. +-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]). +-optional_callbacks([mod_opt_type/1, reload/3, prep_stop/1]). -export_type([opts/0]). -export_type([db_type/0]). @@ -95,6 +115,10 @@ init([]) -> {read_concurrency, true}]), {ok, {{one_for_one, 10, 1}, []}}. +-spec prep_stop() -> ok. +prep_stop() -> + prep_stop_modules(). + -spec stop() -> ok. stop() -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 60), @@ -155,20 +179,28 @@ start_module(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 ?EX_RULE(Class, Reason, Stack) -> - StackTrace = ?EX_STACK(Stack), - 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) + 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. -spec reload_modules(binary()) -> ok. @@ -218,14 +250,18 @@ reload_module(Host, Module, NewOpts, OldOpts, Order) -> {ok, Pid} when is_pid(Pid) -> {ok, Pid}; Err -> erlang:error({bad_return, Module, Err}) end - catch ?EX_RULE(Class, Reason, Stack) -> - StackTrace = ?EX_STACK(Stack), - ErrorText = format_module_error( - Module, reload, 3, - NewOpts, Class, Reason, - StackTrace), + catch + Class:Reason:StackTrace -> + ErrorText = format_module_error( + Module, + reload, + 3, + NewOpts, + Class, + Reason, + StackTrace), ?CRITICAL_MSG(ErrorText, []), - erlang:raise(Class, Reason, StackTrace) + erlang:raise(Class, Reason, StackTrace) end; false -> ?WARNING_MSG("Module ~ts doesn't support reloading " @@ -246,9 +282,21 @@ update_module(Host, Module, Opts) -> -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, order = Order}). + opts = Opts, + registrations = Registrations, + order = Order}). maybe_halt_ejabberd() -> case is_app_running(ejabberd) of @@ -266,6 +314,21 @@ is_app_running(AppName) -> lists:keymember(AppName, 1, application:which_applications(Timeout)). +-spec prep_stop_modules() -> ok. +prep_stop_modules() -> + lists:foreach( + fun(Host) -> + prep_stop_modules(Host) + end, ejabberd_option:hosts()). + +-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( @@ -281,28 +344,103 @@ stop_modules(Host) -> stop_module_keep_config(Host, Module) end, Modules). --spec stop_module(binary(), atom()) -> error | {aborted, any()} | {atomic, any()}. +-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. -spec stop_module_keep_config(binary(), atom()) -> error | ok. stop_module_keep_config(Host, Module) -> ?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 ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), + catch + Class:Reason:StackTrace -> ?ERROR_MSG("Failed to stop module ~ts at ~ts:~n** ~ts", - [Module, Host, + [Module, + Host, misc:format_exception(2, Class, Reason, StackTrace)]), - error + error end. +-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). + +-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). @@ -431,7 +569,7 @@ is_equal_opt(Opt, NewOpts, OldOpts) -> %%%=================================================================== -spec format_module_error(atom(), start | reload, non_neg_integer(), opts(), error | exit | throw, any(), - [erlang:stack_item()]) -> iolist(). + [tuple()]) -> iolist(). format_module_error(Module, Fun, Arity, Opts, Class, Reason, St) -> case {Class, Reason} of {error, {bad_return, Module, {error, _} = Err}} -> @@ -485,10 +623,10 @@ validator(Host, Module, Opts) -> lists:mapfoldl( fun({Opt, Def}, {DAcc1, VAcc1}) -> {[], {DAcc1#{Opt => Def}, - VAcc1#{Opt => get_opt_type(Module, M, Opt)}}}; + VAcc1#{Opt => get_opt_type(Host, Module, M, Opt)}}}; (Opt, {DAcc1, VAcc1}) -> {[Opt], {DAcc1, - VAcc1#{Opt => get_opt_type(Module, M, Opt)}}} + VAcc1#{Opt => get_opt_type(Host, Module, M, Opt)}}} end, {DAcc, VAcc}, DefOpts) end, {#{}, #{}}, get_defaults(Host, Module, Opts)), econf:and_then( @@ -538,11 +676,16 @@ get_defaults(Host, Module, Opts) -> false end, DefaultOpts)]. --spec get_opt_type(module(), module(), atom()) -> econf:validator(). -get_opt_type(Mod, SubMod, Opt) -> - try SubMod:mod_opt_type(Opt) +-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. + 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) -> diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl index df6c61944..48b43a05b 100644 --- a/src/gen_pubsub_node.erl +++ b/src/gen_pubsub_node.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/gen_pubsub_nodetree.erl b/src/gen_pubsub_nodetree.erl index 190051fd7..6d958ae62 100644 --- a/src/gen_pubsub_nodetree.erl +++ b/src/gen_pubsub_nodetree.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/jd2ejd.erl b/src/jd2ejd.erl index b7fd5f66c..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-2022 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/misc.erl b/src/misc.erl index fcdf61d9a..87f8b24e6 100644 --- a/src/misc.erl +++ b/src/misc.erl @@ -8,7 +8,7 @@ %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -36,14 +36,19 @@ 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, + 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]). + semver_to_xxyy/1, logical_processors/0, get_mucsub_event_type/1, + lists_uniq/1]). %% Deprecated functions -export([decode_base64/1, encode_base64/1]). @@ -54,33 +59,57 @@ -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()}. --ifdef(USE_OLD_HTTP_URI). -uri_parse(URL) when is_binary(URL) -> - uri_parse(binary_to_list(URL)); uri_parse(URL) -> - case http_uri:parse(URL) of - {ok, {Scheme, UserInfo, Host, Port, Path, Query}} -> - {ok, atom_to_list(Scheme), UserInfo, Host, Port, Path, Query}; - {error, _} = E -> - E - end. + 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_parse(URL) when is_binary(URL) -> - uri_parse(binary_to_list(URL)); -uri_parse(URL) -> - case uri_string:parse(URL) of - #{scheme := Scheme, host := Host, port := Port, path := Path} = M1 -> - {ok, Scheme, maps:get(userinfo, M1, ""), Host, Port, Path, maps:get(query, M1, "")}; - #{scheme := "https", host := Host, path := Path} = M2 -> - {ok, "https", maps:get(userinfo, M2, ""), Host, 443, Path, maps:get(query, M2, "")}; - #{scheme := "http", host := Host, path := Path} = M3 -> - {ok, "http", maps:get(userinfo, M3, ""), Host, 80, Path, maps:get(query, M3, "")}; - {error, Atom, _} -> - {error, Atom} - end. +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). @@ -91,6 +120,28 @@ 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 %%%=================================================================== @@ -154,7 +205,11 @@ unwrap_mucsub_message(_Packet) -> false. -spec is_mucsub_message(xmpp_element()) -> boolean(). -is_mucsub_message(#message{} = OuterMsg) -> +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{ @@ -166,18 +221,18 @@ is_mucsub_message(#message{} = OuterMsg) -> Node == ?NS_MUCSUB_NODES_PARTICIPANTS; Node == ?NS_MUCSUB_NODES_PRESENCE; Node == ?NS_MUCSUB_NODES_SUBSCRIBERS -> - true; + Node; _ -> false end; -is_mucsub_message(_Packet) -> +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], + IgnoreNS = [?NS_CHATSTATES, ?NS_DELAY, ?NS_EVENT, ?NS_HINTS], Stripped = [El || El <- Els, not lists:member(xmpp:get_ns(El), IgnoreNS)], Stripped == []; @@ -260,6 +315,9 @@ binary_to_atom(Bin) -> 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). @@ -404,6 +462,25 @@ get_descr(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( @@ -725,3 +802,29 @@ 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 1a2d63038..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-2022 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 @@ -author('henoch@dtek.chalmers.se'). --protocol({xep, 50, '1.2'}). +-protocol({xep, 50, '1.2', '1.1.0', "complete", ""}). -behaviour(gen_mod). @@ -42,49 +42,20 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -start(Host, _Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_COMMANDS, ?MODULE, process_local_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_COMMANDS, ?MODULE, process_sm_iq), - 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). +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}]}. -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). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -281,9 +252,11 @@ mod_options(_Host) -> mod_doc() -> #{desc => - ?T("This module implements https://xmpp.org/extensions/xep-0050.html" + [?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."), + "only needed by some of the other modules.")], opts => [{report_commands_node, #{value => "true | 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_admin_extra.erl b/src/mod_admin_extra.erl index 12e775cfb..4a7877d19 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -5,7 +5,7 @@ %%% Created : 10 Aug 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -43,13 +43,15 @@ % Sessions num_resources/2, resource_num/3, kick_session/4, status_num/2, status_num/1, - status_list/2, status_list/1, connected_users_info/0, + 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, ban_account/3, check_password/3, + 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, @@ -58,7 +60,7 @@ % Roster add_rosteritem/7, delete_rosteritem/4, - get_roster/2, push_roster/3, + 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, @@ -66,8 +68,10 @@ private_get/4, private_set/3, % Shared roster - srg_create/5, + 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 @@ -79,12 +83,21 @@ % 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"). %%% @@ -92,15 +105,20 @@ %%% start(_Host, _Opts) -> - ejabberd_commands:register_commands(?MODULE, get_commands_spec()). + {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) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -113,14 +131,14 @@ depends(_Host, _Opts) -> %%% get_commands_spec() -> - Vcard1FieldsString = "Some vcard field names in get/set_vcard are:\n\n" + 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" + 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" @@ -134,8 +152,8 @@ get_commands_spec() -> "* ORG ORGNAME - Work: Company\n" "* ORG ORGUNIT - Work: Department\n", - VcardXEP = "For a full list of vCard fields check XEP-0054: vcard-temp at " - "https://xmpp.org/extensions/xep-0054.html", + 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], @@ -145,8 +163,7 @@ get_commands_spec() -> args_example = ["/home/me/srcs/ejabberd/mod_example.erl"], args_desc = ["Filename of erlang source file to compile"], result = {res, rescode}, - result_example = ok, - result_desc = "Status code: 0 on success, 1 otherwise"}, + result_example = ok}, #ejabberd_commands{name = get_cookie, tags = [erlang], desc = "Get the Erlang cookie of this node", module = ?MODULE, function = get_cookie, @@ -163,16 +180,18 @@ get_commands_spec() -> 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"}, + " - `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", + " - deny: all\n" + "```\n", module = ?MODULE, function = delete_old_users, args = [{days, integer}], args_example = [30], @@ -183,10 +202,12 @@ get_commands_spec() -> #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", + " - allow: all\n" + "```\n", module = ?MODULE, function = delete_old_users_vhost, args = [{host, binary}, {days, integer}], args_example = [<<"myserver.com">>, 30], @@ -202,8 +223,7 @@ get_commands_spec() -> args_example = [<<"peter">>, <<"myserver.com">>], args_desc = ["User name to check", "Server to check"], result = {res, rescode}, - result_example = ok, - result_desc = "Status code: 0 on success, 1 otherwise"}, + result_example = ok}, #ejabberd_commands{name = check_password, tags = [accounts], desc = "Check if a password is correct", module = ?MODULE, function = check_password, @@ -211,11 +231,11 @@ get_commands_spec() -> args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>], args_desc = ["User name to check", "Server to check", "Password to check"], result = {res, rescode}, - result_example = ok, - result_desc = "Status code: 0 on success, 1 otherwise"}, + result_example = ok}, #ejabberd_commands{name = check_password_hash, tags = [accounts], desc = "Check if the password hash is correct", - longdesc = "Allows hash methods from crypto application", + 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}], @@ -224,8 +244,7 @@ get_commands_spec() -> args_desc = ["User name to check", "Server to check", "Password's hash value", "Name of hash method"], result = {res, rescode}, - result_example = ok, - result_desc = "Status code: 0 on success, 1 otherwise"}, + result_example = ok}, #ejabberd_commands{name = change_password, tags = [accounts], desc = "Change the password of an account", module = ?MODULE, function = set_password, @@ -234,18 +253,65 @@ get_commands_spec() -> args_desc = ["User name", "Server name", "New password for user"], result = {res, rescode}, - result_example = ok, - result_desc = "Status code: 0 on success, 1 otherwise"}, + 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, - result_desc = "Status code: 0 on success, 1 otherwise"}, + 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, @@ -273,8 +339,7 @@ get_commands_spec() -> args_desc = ["User name", "Server name", "User's resource", "Reason for closing session"], result = {res, rescode}, - result_example = ok, - result_desc = "Status code: 0 on success, 1 otherwise"}, + result_example = ok}, #ejabberd_commands{name = status_num_host, tags = [session, statistics], desc = "Number of logged users with this status in host", policy = admin, @@ -311,6 +376,21 @@ get_commands_spec() -> {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, @@ -327,6 +407,21 @@ get_commands_spec() -> {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", @@ -357,8 +452,9 @@ get_commands_spec() -> module = ?MODULE, function = connected_users_vhost, args_example = [<<"myexample.com">>], args_desc = ["Server name"], - result_example = [<<"user1@myserver.com/tka">>, <<"user2@localhost/tka">>], 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], @@ -390,14 +486,14 @@ get_commands_spec() -> "and its presence (show and status message) " "for a given user.", longdesc = - "The 'jid' value contains the user jid " - "with resource.\nThe 'show' value contains " + "The `jid` value contains the user JID " + "with resource.\n\nThe `show` value contains " "the user presence flag. It can take " - "limited values:\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 " + "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}], @@ -421,8 +517,25 @@ get_commands_spec() -> 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", + "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}}, @@ -485,7 +598,7 @@ get_commands_spec() -> #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\"", + 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}, @@ -497,11 +610,28 @@ get_commands_spec() -> 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}], @@ -511,52 +641,56 @@ get_commands_spec() -> result = {res, rescode}}, #ejabberd_commands{name = process_rosteritems, tags = [roster], desc = "List/delete rosteritems that match filter", - longdesc = "Explanation of each argument:\n" - " - action: what to do with each rosteritem that " + 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" + "* `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: \n" + "**Mnesia backend:**\n" "\n" - "Allowed values in the arguments:\n" - " ACTION = list | delete\n" - " SUBS = SUB[:SUB]* | any\n" - " SUB = none | from | to | both\n" - " ASKS = ASK[:ASK]* | any\n" - " ASK = none | out | in\n" - " USERS = JID[:JID]* | any\n" - " CONTACTS = JID[:JID]* | any\n" - " JID = characters valid in a JID, and can use the " - "globs: *, ?, ! and [...]\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 " + "`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 " + "`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*" + "user part and the server part contains the word `icq`" + ":\n `list none:from:to any *@example.org *:*@*icq*`" "\n\n" - " *** SQL:\n" + "**SQL backend:**\n" "\n" - "Allowed values in the arguments:\n" - " ACTION = list | delete\n" - " SUBS = any | none | from | to | both\n" - " ASKS = any | none | out | in\n" - " USERS = JID\n" - " CONTACTS = JID\n" - " JID = characters valid in a JID, and can use the " - "globs: _ and %\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 " + "`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%", + "`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}, @@ -569,26 +703,42 @@ get_commands_spec() -> ]}} }}}, #ejabberd_commands{name = get_roster, tags = [roster], - desc = "Get roster of a local user", + 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}, - {ask, string}, - {group, 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. Example:\n" - "[{<<\"user1\">>, <<\"localhost\">>, <<\"Workers\">>, <<\"User 1\">>},\n" - " {<<\"user2\">>, <<\"localhost\">>, <<\"Workers\">>, <<\"User 2\">>}].\n" - "When using UTF8 character encoding add /utf8 to certain string. Example:\n" - "[{<<\"user2\">>, <<\"localhost\">>, <<\"Workers\"/utf8>>, <<\"User 2\"/utf8>>}].", + "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">>], @@ -598,8 +748,8 @@ get_commands_spec() -> 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\"}].", + "`[{\"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">>], @@ -615,8 +765,10 @@ get_commands_spec() -> #ejabberd_commands{name = get_last, tags = [last], desc = "Get last activity information", - longdesc = "Timestamp is UTC and XEP-0082 format, for example: " - "2017-02-23T22:25:28.063062Z ONLINE", + 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">>], @@ -630,7 +782,7 @@ get_commands_spec() -> #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: date +%s", + "`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">>], @@ -657,11 +809,11 @@ get_commands_spec() -> 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" + "put `\\ \"` around the argument and\nseparate the " + "identifiers with `\\ \\ n`\n" "For example:\n" - " ejabberdctl srg_create group3 myserver.com " - "name desc \\\"group1\\\\ngroup2\\\"", + " `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}, @@ -672,6 +824,27 @@ get_commands_spec() -> 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, @@ -697,6 +870,48 @@ get_commands_spec() -> 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, @@ -731,10 +946,22 @@ get_commands_spec() -> 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, " + "`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, @@ -742,13 +969,13 @@ get_commands_spec() -> {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", + 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 instead.", + 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">>, @@ -773,7 +1000,9 @@ get_commands_spec() -> result = {res, rescode}}, #ejabberd_commands{name = stats, tags = [statistics], - desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds processes", + 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}], @@ -783,7 +1012,8 @@ get_commands_spec() -> result_desc = "Integer statistic value", result = {stat, integer}}, #ejabberd_commands{name = stats_host, tags = [statistics], - desc = "Get statistical value for this host: registeredusers onlineusers", + 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}], @@ -848,7 +1078,7 @@ set_password(User, Host, Password) -> check_password(User, Host, Password) -> ejabberd_auth:check_password(User, <<>>, Host, Password). -%% Copied some code from ejabberd_commands.erl +%% 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, @@ -921,7 +1151,7 @@ delete_or_not(LUser, LServer, TimeStamp_oldest) -> end. %% -%% Ban account +%% Ban account v0 ban_account(User, Host, ReasonText) -> Reason = prepare_reason(ReasonText), @@ -930,6 +1160,7 @@ ban_account(User, Host, ReasonText) -> 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) @@ -957,6 +1188,112 @@ prepare_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 %%% @@ -992,6 +1329,14 @@ status_list(Host, Status) -> 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 @@ -1072,14 +1417,10 @@ get_presence(U, S) -> {FullJID, Show, Status} end. -set_presence(User, Host, Resource, Type, Show, Status, Priority) - when is_integer(Priority) -> - BPriority = integer_to_binary(Priority), - set_presence(User, Host, Resource, Type, Show, Status, BPriority); -set_presence(User, Host, Resource, Type, Show, Status, Priority0) -> - Priority = if is_integer(Priority0) -> Priority0; - true -> binary_to_integer(Priority0) - 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), @@ -1088,8 +1429,10 @@ set_presence(User, Host, Resource, Type, Show, Status, Priority0) -> show = misc:binary_to_atom(Show), priority = Priority, sub_els = []}, - Ref = ejabberd_sm:get_session_pid(User, Host, Resource), - ejabberd_c2s:set_presence(Ref, Pres). + 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) -> @@ -1277,14 +1620,16 @@ update_vcard_els(Data, ContentList, Els1) -> %%% Roster %%% -add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) -> +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, Group}), + 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 @@ -1329,24 +1674,24 @@ get_roster(User, Server) -> make_roster_xmlrpc(Items) end. -%% Note: if a contact is in several groups, the contact is returned -%% several times, each one in a different group. make_roster_xmlrpc(Roster) -> - lists:foldl( - fun(#roster_item{jid = JID, name = Nick, subscription = Sub, ask = Ask} = Item, Res) -> + 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), - Groups = case Item#roster_item.groups of - [] -> [<<>>]; - Gs -> Gs - end, - ItemsX = [{JIDS, Nick, Subs, Asks, Group} || Group <- Groups], - ItemsX ++ Res + {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 @@ -1408,6 +1753,11 @@ push_roster_item(LU, LS, R, U, S, Action) -> 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), @@ -1464,11 +1814,20 @@ set_last(User, Server, Timestamp, Status) -> %% Cluth private_get(Username, Host, Element, Ns) -> - ElementXml = #xmlel{name = Element, attrs = [{<<"xmlns">>, Ns}]}, - Els = mod_private:get_data(jid:nodeprep(Username), jid:nameprep(Host), - [{Ns, ElementXml}]), + 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} -> @@ -1488,16 +1847,40 @@ private_set2(Username, Host, Xml) -> %%% Shared Roster Groups %%% -srg_create(Group, Host, Label, Description, Display) -> +srg_create(Group, Host, Label, Description, Display) when is_binary(Display) -> DisplayList = case Display of - <<>> -> []; - _ -> ejabberd_regexp:split(Display, <<"\\\\n">>) + <<>> -> []; + _ -> 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}], - {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts), - ok. + 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), @@ -1514,9 +1897,109 @@ srg_get_info(Group, Host) -> [{misc:atom_to_binary(Title), to_list(Value)} || {Title, Value} <- Opts]. to_list([]) -> []; -to_list([H|T]) -> [to_list(H)|to_list(T)]; +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) -> binary_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), @@ -1660,28 +2143,213 @@ num_prio(Priority) when is_integer(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-acount':"), + ?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("- 'pushroster': (and 'pushroster-all')"), + "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 " + "`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':"), - ?T("If you want to put a group Name with blankspaces, use the " - "characters \"\' and \'\" to define when the Name starts and " + "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 " @@ -1696,14 +2364,14 @@ mod_doc() -> " mod_admin_extra: {}", " mod_vcard:", " access_set: vcard_set"]}, - {?T("Content of roster file for 'pushroster' command:"), + {?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 " + "'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 using double-quotes and single-quotes:"), - ["ejabberdctl srg-create g1 example.org \"\'Group number 1\'\" this_is_g1 g1"]}]}. + ["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 index 3f17deefc..105828f7d 100644 --- a/src/mod_admin_update_sql.erl +++ b/src/mod_admin_update_sql.erl @@ -5,7 +5,7 @@ %%% Created : 9 Aug 2017 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +34,8 @@ % Commands API -export([update_sql/0]). +% For testing +-export([update_sql/1]). -include("logger.hrl"). -include("ejabberd_commands.hrl"). @@ -46,10 +48,10 @@ %%% start(_Host, _Opts) -> - ejabberd_commands:register_commands(?MODULE, get_commands_spec()). + {ok, [{commands, get_commands_spec()}]}. stop(_Host) -> - ejabberd_commands:unregister_commands(get_commands_spec()). + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -63,14 +65,14 @@ depends(_Host, _Opts) -> get_commands_spec() -> [#ejabberd_commands{name = update_sql, tags = [sql], - desc = "Convert PostgreSQL DB to the new format", + 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, - result_desc = "Status code: 0 on success, 1 otherwise"} + result_example = ok} ]. update_sql() -> @@ -93,6 +95,8 @@ update_sql(Host) -> DBType = ejabberd_option:sql_type(LHost), IsSupported = case DBType of + mssql -> true; + mysql -> true; pgsql -> true; _ -> false end, @@ -110,189 +114,318 @@ update_sql(Host) -> State = #state{host = LHost, dbtype = DBType, escape = Escape}, - update_tables(State) + 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) -> - add_sh_column(State, "users"), - drop_pkey(State, "users"), - add_pkey(State, "users", ["server_host", "username"]), - drop_sh_default(State, "users"), + 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, - add_sh_column(State, "last"), - drop_pkey(State, "last"), - add_pkey(State, "last", ["server_host", "username"]), - drop_sh_default(State, "last"), + 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, - add_sh_column(State, "rosterusers"), - drop_index(State, "i_rosteru_user_jid"), - drop_index(State, "i_rosteru_username"), - drop_index(State, "i_rosteru_jid"), - create_unique_index(State, "rosterusers", "i_rosteru_sh_user_jid", ["server_host", "username", "jid"]), - create_index(State, "rosterusers", "i_rosteru_sh_username", ["server_host", "username"]), - create_index(State, "rosterusers", "i_rosteru_sh_jid", ["server_host", "jid"]), - drop_sh_default(State, "rosterusers"), + 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, - add_sh_column(State, "rostergroups"), - drop_index(State, "pk_rosterg_user_jid"), - create_index(State, "rostergroups", "i_rosterg_sh_user_jid", ["server_host", "username", "jid"]), - drop_sh_default(State, "rostergroups"), + 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, - add_sh_column(State, "sr_group"), - add_pkey(State, "sr_group", ["server_host", "name"]), - create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]), - drop_sh_default(State, "sr_group"), + 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, - add_sh_column(State, "sr_user"), - drop_index(State, "i_sr_user_jid_grp"), - drop_index(State, "i_sr_user_jid"), - drop_index(State, "i_sr_user_grp"), - add_pkey(State, "sr_user", ["server_host", "jid", "grp"]), - create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]), - create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]), - create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]), - drop_sh_default(State, "sr_user"), + 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, - add_sh_column(State, "spool"), - drop_index(State, "i_despool"), - create_index(State, "spool", "i_spool_sh_username", ["server_host", "username"]), - drop_sh_default(State, "spool"), + 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, - add_sh_column(State, "archive"), - drop_index(State, "i_username"), - drop_index(State, "i_username_timestamp"), - drop_index(State, "i_timestamp"), - drop_index(State, "i_peer"), - drop_index(State, "i_bare_peer"), - drop_index(State, "i_username_peer"), - drop_index(State, "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"), + 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, - add_sh_column(State, "archive_prefs"), - drop_pkey(State, "archive_prefs"), - add_pkey(State, "archive_prefs", ["server_host", "username"]), - drop_sh_default(State, "archive_prefs"), + 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, - add_sh_column(State, "vcard"), - drop_pkey(State, "vcard"), - add_pkey(State, "vcard", ["server_host", "username"]), - drop_sh_default(State, "vcard"), + 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, - add_sh_column(State, "vcard_search"), - drop_pkey(State, "vcard_search"), - drop_index(State, "i_vcard_search_lfn"), - drop_index(State, "i_vcard_search_lfamily"), - drop_index(State, "i_vcard_search_lgiven"), - drop_index(State, "i_vcard_search_lmiddle"), - drop_index(State, "i_vcard_search_lnickname"), - drop_index(State, "i_vcard_search_lbday"), - drop_index(State, "i_vcard_search_lctry"), - drop_index(State, "i_vcard_search_llocality"), - drop_index(State, "i_vcard_search_lemail"), - drop_index(State, "i_vcard_search_lorgname"), - drop_index(State, "i_vcard_search_lorgunit"), - add_pkey(State, "vcard_search", ["server_host", "username"]), - 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"), + 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, - add_sh_column(State, "privacy_default_list"), - drop_pkey(State, "privacy_default_list"), - add_pkey(State, "privacy_default_list", ["server_host", "username"]), - drop_sh_default(State, "privacy_default_list"), + 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, - add_sh_column(State, "privacy_list"), - drop_index(State, "i_privacy_list_username"), - drop_index(State, "i_privacy_list_username_name"), - create_index(State, "privacy_list", "i_privacy_list_sh_username", ["server_host", "username"]), - create_unique_index(State, "privacy_list", "i_privacy_list_sh_username_name", ["server_host", "username", "name"]), - drop_sh_default(State, "privacy_list"), + 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, - add_sh_column(State, "private_storage"), - drop_index(State, "i_private_storage_username"), - drop_index(State, "i_private_storage_username_namespace"), - add_pkey(State, "private_storage", ["server_host", "username", "namespace"]), - create_index(State, "private_storage", "i_private_storage_sh_username", ["server_host", "username"]), - drop_sh_default(State, "private_storage"), + 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, - add_sh_column(State, "roster_version"), - drop_pkey(State, "roster_version"), - add_pkey(State, "roster_version", ["server_host", "username"]), - drop_sh_default(State, "roster_version"), + 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, - add_sh_column(State, "muc_room"), - drop_sh_default(State, "muc_room"), + case add_sh_column(State, "muc_room") of + true -> + drop_sh_default(State, "muc_room"); + false -> + ok + end, - add_sh_column(State, "muc_registered"), - drop_sh_default(State, "muc_registered"), + case add_sh_column(State, "muc_registered") of + true -> + drop_sh_default(State, "muc_registered"); + false -> + ok + end, - add_sh_column(State, "muc_online_room"), - drop_sh_default(State, "muc_online_room"), + case add_sh_column(State, "muc_online_room") of + true -> + drop_sh_default(State, "muc_online_room"); + false -> + ok + end, - add_sh_column(State, "muc_online_users"), - drop_sh_default(State, "muc_online_users"), + case add_sh_column(State, "muc_online_users") of + true -> + drop_sh_default(State, "muc_online_users"); + false -> + ok + end, - add_sh_column(State, "motd"), - drop_pkey(State, "motd"), - add_pkey(State, "motd", ["server_host", "username"]), - drop_sh_default(State, "motd"), + 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, - add_sh_column(State, "sm"), - drop_index(State, "i_sm_sid"), - drop_index(State, "i_sm_username"), - add_pkey(State, "sm", ["usec", "pid"]), - create_index(State, "sm", "i_sm_sh_username", ["server_host", "username"]), - drop_sh_default(State, "sm"), + 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, - add_sh_column(State, "push_session"), - drop_index(State, "i_push_usn"), - drop_index(State, "i_push_ut"), - add_pkey(State, "push_session", ["server_host", "username", "timestamp"]), - create_unique_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]), - drop_sh_default(State, "push_session"), + 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, - add_sh_column(State, "mix_pam"), - drop_index(State, "i_mix_pam"), - drop_index(State, "i_mix_pam_us"), - create_unique_index(State, "mix_pam", "i_mix_pam", ["username", "server_host", "channel", "service"]), - create_index(State, "mix_pam", "i_mix_pam_us", ["username", "server_host"]), - drop_sh_default(State, "mix_pam"), + 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, - add_sh_column(State, "route"), - drop_index(State, "i_route"), - create_unique_index(State, "route", "i_route", ["domain", "server_host", "node", "pid"]), - drop_sh_default(State, "route"), - - add_sh_column(State, "mqtt_pub"), - drop_index(State, "i_mqtt_topic"), - create_unique_index(State, "mqtt_pub", "i_mqtt_topic_server", ["topic", "server_host"]), - drop_sh_default(State, "mqtt_pub"), + 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. -add_sh_column(#state{dbtype = pgsql} = State, Table) -> +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), "';"]); -add_sh_column(#state{dbtype = mysql} = State, Table) -> +do_add_sh_column(#state{dbtype = mssql} = State, Table) -> sql_query( State#state.host, - ["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '", + ["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), "';"]). @@ -300,18 +433,31 @@ 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) -> - SCols = string:join(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) -> - SCols = string:join(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, ");"]). @@ -320,19 +466,56 @@ 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;"]). -drop_index(#state{dbtype = pgsql} = State, Index) -> +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, ";"]); -drop_index(#state{dbtype = mysql} = State, Index) -> +do_drop_index(#state{dbtype = mssql} = State, Table, Index) -> sql_query( State#state.host, - ["DROP INDEX ", Index, ";"]). + ["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, ", "), @@ -340,8 +523,17 @@ create_unique_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> 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 ++ "(75)" || C <- Cols], + Cols2 = [C ++ mysql_keylen(Index, C) || C <- Cols], SCols = string:join(Cols2, ", "), sql_query( State#state.host, @@ -349,25 +541,104 @@ create_unique_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> 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 ", Index, " ON ", Table, " USING btree (", + ["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) -> - Cols2 = [C ++ "(75)" || C <- 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 ", Index, " ON ", Table, "(", + ["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) -> - io:format("executing \"~ts\" on ~ts~n", [Query, Host]), + 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]), - ok; + false; _ -> ok end. @@ -378,6 +649,6 @@ mod_doc() -> #{desc => ?T("This module can be used to update existing SQL database " "from the default to the new schema. Check the section " - "http://../database/#default-and-new-schemas[Default and New Schemas] for details. " - "Please note that only PostgreSQL is supported. " + "_`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 8698a8a3b..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-2022 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 @@ -494,9 +494,6 @@ announce_commands(From, To, {error, xmpp:err_bad_request(Txt, Lang)} end. --define(TVFIELD(Type, Var, Val), - #xdata_field{type = Type, var = Var, values = vvaluel(Val)}). - vvaluel(Val) -> case Val of <<>> -> []; @@ -923,38 +920,43 @@ mod_doc() -> [?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 " + "client either using Ad-Hoc Commands or sending messages " "to specific JIDs."), "", - ?T("Note that this module can be resource intensive on large " + ?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("The Ad-hoc Commands are listed in the Server Discovery. " - "For this feature to work, _`mod_adhoc`_ must be enabled."), "", - ?T("The specific JIDs where messages can be sent are listed below. " - "The first JID in each entry will apply only to the specified " - "virtual host example.org, while the JID between brackets " - "will apply to all virtual hosts in ejabberd:"), "", - "- example.org/announce/all (example.org/announce/all-hosts/all)::", - ?T("The message is sent to all registered users. If the user is " + ?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 will be " + "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 (example.org/announce/all-hosts/online)::", - ?T("The message is sent to all connected users. If the user is " + "- '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 (example.org/announce/all-hosts/motd)::", - ?T("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 announce/online)."), - "- example.org/announce/motd/update (example.org/announce/all-hosts/motd/update)::", - ?T("The message is set as message of the day (MOTD) and is sent to users " - "when they login. The message is not sent to any currently connected user."), - "- example.org/announce/motd/delete (example.org/announce/all-hosts/motd/delete)::", - ?T("Any message sent to this JID removes the existing message of the day (MOTD).")], + "- '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"), diff --git a/src/mod_announce_mnesia.erl b/src/mod_announce_mnesia.erl index d56d036ad..6e578e001 100644 --- a/src/mod_announce_mnesia.erl +++ b/src/mod_announce_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_announce_sql.erl b/src/mod_announce_sql.erl index 5ef3a7900..2a2692d37 100644 --- a/src/mod_announce_sql.erl +++ b/src/mod_announce_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +31,7 @@ -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"). @@ -40,9 +41,26 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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( 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 index 0db65c253..e5fc3503b 100644 --- a/src/mod_avatar.erl +++ b/src/mod_avatar.erl @@ -3,7 +3,7 @@ %%% Created : 13 Sep 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +22,8 @@ %%%------------------------------------------------------------------- -module(mod_avatar). -behaviour(gen_mod). --protocol({xep, 398, '0.2.0'}). + +-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]). @@ -43,23 +44,14 @@ %%%=================================================================== %%% API %%%=================================================================== -start(Host, _Opts) -> - ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, - pubsub_publish_item, 50), - ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, - vcard_iq_convert, 30), - ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, - vcard_iq_publish, 100), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 50). +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) -> - ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE, - pubsub_publish_item, 50), - ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_convert, 30), - ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_publish, 100), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 50). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_block_strangers.erl b/src/mod_block_strangers.erl index 437df2307..fd4e9974d 100644 --- a/src/mod_block_strangers.erl +++ b/src/mod_block_strangers.erl @@ -5,7 +5,7 @@ %%% Created : 25 Dec 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -32,7 +32,8 @@ -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]). +-export([filter_packet/1, filter_offline_msg/1, filter_subscription/2, + get_sm_features/5]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). @@ -40,30 +41,36 @@ -define(SETS, gb_sets). +-define(NS_BLOCK_STRANGERS, <<"urn:ejabberd:block-strangers">>). + -type c2s_state() :: ejabberd_c2s:state(). %%%=================================================================== %%% Callbacks and hooks %%%=================================================================== -start(Host, _Opts) -> - ejabberd_hooks:add(user_receive_packet, Host, - ?MODULE, filter_packet, 25), - ejabberd_hooks:add(roster_in_subscription, Host, - ?MODULE, filter_subscription, 25), - ejabberd_hooks:add(offline_message_hook, Host, - ?MODULE, filter_offline_msg, 25). +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) -> - ejabberd_hooks:delete(user_receive_packet, Host, - ?MODULE, filter_packet, 25), - ejabberd_hooks:delete(roster_in_subscription, Host, - ?MODULE, filter_subscription, 25), - ejabberd_hooks:delete(offline_message_hook, Host, - ?MODULE, 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) -> @@ -264,7 +271,7 @@ mod_options(_) -> mod_doc() -> #{desc => - ?T("This module allows to block/log messages coming from an " + ?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. " @@ -274,9 +281,9 @@ mod_doc() -> #{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 ACL where " - "'deny' means the message will be rejected (or a CAPTCHA " - "would be generated for a presence, if configured), and " + "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.")}}, @@ -307,8 +314,8 @@ mod_doc() -> {captcha, #{value => "true | false", desc => - ?T("Whether to generate CAPTCHA or not in response to " - "messages from strangers. See also section " - "https://docs.ejabberd.im/admin/configuration/#captcha" - "[CAPTCHA] of the Configuration Guide. " + ?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_blocking.erl b/src/mod_blocking.erl index 06aa4b195..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-2022 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 @@ -behaviour(gen_mod). --protocol({xep, 191, '1.2'}). +-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]). @@ -37,14 +37,12 @@ -include("mod_privacy.hrl"). -include("translate.hrl"). -start(Host, _Opts) -> - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_BLOCKING, ?MODULE, process_iq). +start(_Host, _Opts) -> + {ok, [{hook, disco_local_features, disco_features, 50}, + {iq_handler, ejabberd_sm, ?NS_BLOCKING, process_iq}]}. -stop(Host) -> - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_BLOCKING). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -271,5 +269,5 @@ mod_doc() -> [?T("The module implements " "https://xmpp.org/extensions/xep-0191.html" "[XEP-0191: Blocking Command]."), "", - ?T("This module depends on 'mod_privacy' where " + ?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 index 8f2690d60..5f70a4ea9 100644 --- a/src/mod_bosh.erl +++ b/src/mod_bosh.erl @@ -7,7 +7,7 @@ %%% Created : 20 Jul 2011 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -74,8 +74,8 @@ 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]), +process(_Path, Request) -> + ?DEBUG("Bad Request: ~p", [Request]), {400, ?HEADER(?CT_XML), #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"400 Bad Request">>}]}}. @@ -154,14 +154,22 @@ get_type(Hdrs) -> depends(_Host, _Opts) -> []. -mod_opt_type(json) -> +-ifdef(OTP_BELOW_27). +mod_opt_type_json() -> econf:and_then( econf:bool(), fun(false) -> false; (true) -> ejabberd:start_app(jiffy), true - end); + 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) -> diff --git a/src/mod_bosh_mnesia.erl b/src/mod_bosh_mnesia.erl index 01a64d67e..7ac19df01 100644 --- a/src/mod_bosh_mnesia.erl +++ b/src/mod_bosh_mnesia.erl @@ -2,7 +2,7 @@ %%% Created : 12 Jan 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_bosh_redis.erl b/src/mod_bosh_redis.erl index 12c0a925d..efd05a7ba 100644 --- a/src/mod_bosh_redis.erl +++ b/src/mod_bosh_redis.erl @@ -5,7 +5,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 diff --git a/src/mod_bosh_sql.erl b/src/mod_bosh_sql.erl index 80369facc..29549ed49 100644 --- a/src/mod_bosh_sql.erl +++ b/src/mod_bosh_sql.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_bosh_sql.erl %%% Author : Evgeny Khramtsov -%%% Purpose : +%%% Purpose : %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 @@ -29,6 +29,7 @@ %% 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"). @@ -37,6 +38,8 @@ %%% 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( @@ -48,6 +51,20 @@ init() -> 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), diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 3c8b16b0b..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-2022 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,7 +29,7 @@ -author('henoch@dtek.chalmers.se'). --protocol({xep, 115, '1.5'}). +-protocol({xep, 115, '1.5', '2.1.4', "complete", ""}). -behaviour(gen_server). diff --git a/src/mod_caps_mnesia.erl b/src/mod_caps_mnesia.erl index db3ac8351..a0dc48c4f 100644 --- a/src/mod_caps_mnesia.erl +++ b/src/mod_caps_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_caps_sql.erl b/src/mod_caps_sql.erl index 7ab39b489..9d96697e7 100644 --- a/src/mod_caps_sql.erl +++ b/src/mod_caps_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +29,7 @@ %% 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"). @@ -37,9 +38,25 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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, diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index 3013ab749..d040d948f 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -7,7 +7,7 @@ %%% {mod_carboncopy, []} %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -module (mod_carboncopy). -author ('ecestari@process-one.net'). --protocol({xep, 280, '0.13.2'}). +-protocol({xep, 280, '1.0.1', '13.06', "complete", ""}). -behaviour(gen_mod). @@ -37,7 +37,8 @@ -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]). +-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]). @@ -48,25 +49,20 @@ -type direction() :: sent | received. -type c2s_state() :: ejabberd_c2s:state(). -start(Host, _Opts) -> - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), - %% 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), - 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_session_opened, Host, ?MODULE, c2s_session_opened, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2, ?MODULE, iq_handler). +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}]}. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2), - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), - %% 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(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_session_opened, Host, ?MODULE, c2s_session_opened, 50). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -149,6 +145,23 @@ c2s_session_resumed(State) -> c2s_session_opened(State) -> maps:remove(carboncopy, State). +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. + +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. + % 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 diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index 14a49fdba..67f973784 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -5,7 +5,7 @@ %%% Created : 11 Sep 2014 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2014-2022 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,8 +25,8 @@ -module(mod_client_state). -author('holger@zedat.fu-berlin.de'). --protocol({xep, 85, '2.1'}). --protocol({xep, 352, '0.1'}). +-protocol({xep, 85, '2.1', '2.1.0', "complete", ""}). +-protocol({xep, 352, '0.1', '14.12', "complete", ""}). -behaviour(gen_mod). diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 385c6d657..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-2022 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 @@ -author('alexey@process-one.net'). --protocol({xep, 133, '1.1'}). +-protocol({xep, 133, '1.3.0', '13.10', "partial", ""}). -behaviour(gen_mod). @@ -36,6 +36,7 @@ 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, mod_options/1, + mod_opt_type/1, depends/2, mod_doc/0]). -include("logger.hrl"). @@ -44,50 +45,20 @@ -include("translate.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -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), - ok. +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}]}. -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). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -122,6 +93,10 @@ depends(_Host, _Opts) -> -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 @@ -163,8 +138,6 @@ get_local_identity(Acc, _From, _To, Node, Lang) -> ?INFO_COMMAND(?T("Delete User"), Lang); ?NS_ADMINL(<<"end-user-session">>) -> ?INFO_COMMAND(?T("End User Session"), Lang); - ?NS_ADMINL(<<"get-user-password">>) -> - ?INFO_COMMAND(?T("Get User Password"), Lang); ?NS_ADMINL(<<"change-user-password">>) -> ?INFO_COMMAND(?T("Change User Password"), Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> @@ -199,7 +172,7 @@ get_sm_features(Acc, From, 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], Lang); _ -> Acc @@ -214,7 +187,7 @@ get_local_features(Acc, From, 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, [], Lang); [<<"user">>] -> ?INFO_RESULT(Allow, [], Lang); @@ -249,8 +222,6 @@ get_local_features(Acc, From, ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"end-user-session">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); - ?NS_ADMINL(<<"get-user-password">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"change-user-password">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> @@ -274,7 +245,7 @@ get_local_features(Acc, From, 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; @@ -300,7 +271,7 @@ 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, ?T("Configuration"), <<"config">>), @@ -329,13 +300,13 @@ get_user_resources(User, Server) -> 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( @@ -382,9 +353,10 @@ recursively_get_local_items(PermLev, LServer, Node, end, Items)). --spec get_permission_level(jid()) -> global | vhost. -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. @@ -395,7 +367,7 @@ 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, jid:encode(To), Lang) of @@ -415,11 +387,11 @@ 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}, [], jid:encode(To), Lang) of @@ -434,7 +406,7 @@ 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">>] -> @@ -477,8 +449,6 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"end-user-session">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); - ?NS_ADMINL(<<"get-user-password">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"change-user-password">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-lastlogin">>) -> @@ -520,8 +490,6 @@ get_local_items(_Host, [<<"user">>], Server, Lang) -> (?NS_ADMINX(<<"delete-user">>))), ?NODE(?T("End User Session"), (?NS_ADMINX(<<"end-user-session">>))), - ?NODE(?T("Get User Password"), - (?NS_ADMINX(<<"get-user-password">>))), ?NODE(?T("Change User Password"), (?NS_ADMINX(<<"change-user-password">>))), ?NODE(?T("Get User Last Login Time"), @@ -728,7 +696,7 @@ get_stopped_nodes(_Lang) -> -define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), - case acl:match_rule(LServerOrGlobal, configure, From) of + 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). @@ -1039,16 +1007,6 @@ get_form(_Host, ?NS_ADMINL(<<"end-user-session">>), label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}]}}; -get_form(_Host, ?NS_ADMINL(<<"get-user-password">>), - Lang) -> - {result, - #xdata{title = tr(Lang, ?T("Get User Password")), - type = form, - fields = [?HFIELD(), - #xdata_field{type = 'jid-single', - label = tr(Lang, ?T("Jabber ID")), - var = <<"accountjid">>, - required = true}]}}; get_form(_Host, ?NS_ADMINL(<<"change-user-password">>), Lang) -> {result, @@ -1316,7 +1274,7 @@ set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, Server = AccountJID#jid.lserver, true = lists:member(Server, ejabberd_option:hosts()), true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, case ejabberd_auth:try_register(User, Server, Password) of ok -> {result, undefined}; {error, exists} -> {error, xmpp:err_conflict()}; @@ -1332,7 +1290,7 @@ set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, true = ejabberd_auth:user_exists(User, Server), {User, Server} end, @@ -1346,7 +1304,7 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), JID = jid:decode(AccountString), LServer = JID#jid.lserver, true = LServer == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, case JID#jid.lresource of <<>> -> ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver); @@ -1354,23 +1312,6 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver, R) end, {result, undefined}; -set_form(From, Host, - ?NS_ADMINL(<<"get-user-password">>), Lang, XData) -> - AccountString = get_value(<<"accountjid">>, XData), - JID = jid:decode(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, - #xdata{type = form, - fields = [?HFIELD(), - ?XFIELD('jid-single', ?T("Jabber ID"), - <<"accountjid">>, AccountString), - ?XFIELD('text-single', ?T("Password"), - <<"password">>, Password)]}}; set_form(From, Host, ?NS_ADMINL(<<"change-user-password">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), @@ -1379,7 +1320,7 @@ set_form(From, Host, User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, true = ejabberd_auth:user_exists(User, Server), ejabberd_auth:set_password(User, Server, Password), {result, undefined}; @@ -1390,7 +1331,7 @@ set_form(From, Host, 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 @@ -1422,7 +1363,7 @@ set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang, 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) @@ -1513,7 +1454,7 @@ adhoc_sm_commands(_Acc, From, #jid{user = User, server = Server, lserver = LServer}, #adhoc_command{lang = Lang, node = <<"config">>, action = Action, xdata = XData} = Request) -> - case acl:match_rule(LServer, configure, From) of + case acl_match_rule(LServer, From) of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> @@ -1595,11 +1536,76 @@ set_sm_form(_User, _Server, _Node, _Request) -> tr(Lang, Text) -> translate:translate(Lang, Text). -mod_options(_) -> []. +-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 functionality via " - "https://xmpp.org/extensions/xep-0050.html" - "[XEP-0050: Ad-Hoc Commands]. This module requires " - "_`mod_adhoc`_ to be loaded.")}. + [?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_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 index 61cec2322..c28151076 100644 --- a/src/mod_conversejs.erl +++ b/src/mod_conversejs.erl @@ -5,7 +5,7 @@ %%% Created : 8 Nov 2021 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +31,7 @@ -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"). @@ -39,7 +40,7 @@ -include("ejabberd_web_admin.hrl"). start(_Host, _Opts) -> - ok. + {ok, [{hook, webadmin_menu_system_post, web_menu_system, 50, global}]}. stop(_Host) -> ok. @@ -50,24 +51,25 @@ reload(_Host, _NewOpts, _OldOpts) -> depends(_Host, _Opts) -> []. -process([], #request{method = 'GET', host = Host, raw_path = RawPath}) -> +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), - DomainRaw = gen_mod:get_module_opt(Host, ?MODULE, default_domain), - Domain = misc:expand_keyword(<<"@HOST@">>, DomainRaw, 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">>, RawPath}, - {<<"i18n">>, ejabberd_option:language(Host)}, + {<<"assets_path">>, <>}, {<<"view_mode">>, <<"fullscreen">>} | ExtraOptions], Init2 = @@ -80,6 +82,7 @@ process([], #request{method = 'GET', host = Host, raw_path = RawPath}) -> undefined -> Init2; BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2] end, + Init4 = maps:from_list(Init3), {200, [html], [<<"">>, <<"">>, @@ -87,11 +90,12 @@ process([], #request{method = 'GET', host = Host, raw_path = RawPath}) -> <<"">>, <<"">>, - <<"">>, + <<"">> + ] ++ PluginsHtml ++ [ <<"">>, <<"">>, <<"">>, <<"">>, <<"">>]}; @@ -113,11 +117,14 @@ 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 -> ejabberd_web:error(not_found); + undefined -> + Path = str:join(LocalPath, <<"/">>), + {303, [{<<"Location">>, <<"https://cdn.conversejs.org/dist/", Path/binary>>}], <<>>}; MainPath -> serve2(LocalPath, MainPath) end. @@ -220,6 +227,90 @@ get_auto_file_url(Host, Filename, 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 + %%---------------------------------------------------------------------- %% %%---------------------------------------------------------------------- @@ -236,33 +327,35 @@ 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:binary(). + econf:host(). -mod_options(_) -> +mod_options(Host) -> [{bosh_service_url, auto}, {websocket_url, auto}, - {default_domain, <<"@HOST@">>}, + {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("This module is available since ejabberd 21.12."), - ?T("Several options were improved in ejabberd 22.05."), "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " - "http://../listen-options/#request-handlers[request_handlers]."), "", - ?T("Make sure either 'mod_bosh' or 'ejabberd_http_ws' " - "http://../listen-options/#request-handlers[request_handlers] " - "are enabled."), "", + "_`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.") + "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:", @@ -277,6 +370,7 @@ mod_doc() -> "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:", @@ -290,7 +384,9 @@ mod_doc() -> "", "modules:", " mod_conversejs:", - " conversejs_resources: \"/home/ejabberd/conversejs-9.0.0/package/dist\""]}, + " 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:", @@ -308,7 +404,7 @@ mod_doc() -> #{value => ?T("auto | WebSocketURL"), desc => ?T("A WebSocket URL to which Converse can connect to. " - "The keyword '@HOST@' is replaced with the real virtual " + "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. " @@ -342,6 +438,15 @@ mod_doc() -> "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 => diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl index c8132bfab..37deac7ef 100644 --- a/src/mod_conversejs_opt.erl +++ b/src/mod_conversejs_opt.erl @@ -6,6 +6,7 @@ -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]). @@ -29,6 +30,12 @@ conversejs_options(Opts) when is_map(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); diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl index 19a1adcc6..f6879ea7f 100644 --- a/src/mod_delegation.erl +++ b/src/mod_delegation.erl @@ -4,7 +4,7 @@ %%% Purpose : XEP-0355: Namespace Delegation %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +25,7 @@ -author('amuhar3@gmail.com'). --protocol({xep, 0355, '0.4.1'}). +-protocol({xep, 355, '0.4.1', '16.09', "complete", ""}). -behaviour(gen_server). -behaviour(gen_mod). @@ -117,7 +117,7 @@ mod_doc() -> [{?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."), + "correctly disable the _`mod_pubsub`_ module."), ["access_rules:", " external_pubsub:", " allow: external_component", @@ -129,7 +129,6 @@ mod_doc() -> " server: sat-pubsub.example.org", "", "modules:", - " ...", " mod_delegation:", " namespaces:", " urn:xmpp:mam:1:", diff --git a/src/mod_disco.erl b/src/mod_disco.erl index 8a74d85df..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-2022 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,8 +27,8 @@ -author('alexey@process-one.net'). --protocol({xep, 30, '2.4'}). --protocol({xep, 157, '1.0'}). +-protocol({xep, 30, '2.4', '0.1.0', "complete", ""}). +-protocol({xep, 157, '1.0', '2.1.0', "complete", ""}). -behaviour(gen_mod). @@ -51,16 +51,6 @@ -export_type([features_acc/0, items_acc/0]). start(Host, Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_ITEMS, ?MODULE, - process_local_iq_items), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_INFO, ?MODULE, - process_local_iq_info), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_ITEMS, ?MODULE, process_sm_iq_items), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_INFO, ?MODULE, process_sm_iq_info), catch ets:new(disco_extra_domains, [named_table, ordered_set, public, {heir, erlang:group_leader(), none}]), @@ -69,45 +59,19 @@ start(Host, Opts) -> register_extra_domain(Host, Domain) end, ExtraDomains), - 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_extra_domains, {{'_', Host}}), ok. diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index d7b2963f4..2dbd8575c 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -5,7 +5,7 @@ %%% Created : 15 Aug 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2022 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 @@ -107,16 +107,11 @@ c2s_stream_started(#{ip := {Addr, _}} = State, _) -> start(Host, Opts) -> catch ets:new(failed_auth, [named_table, public, {heir, erlang:group_leader(), none}]), - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end, + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()), gen_mod:stop_child(?MODULE, Host). reload(_Host, _NewOpts, _OldOpts) -> @@ -139,8 +134,8 @@ 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]), +handle_cast(Msg, State) -> + ?WARNING_MSG("Unexpected cast = ~p", [Msg]), {noreply, State}. handle_info(clean, State) -> @@ -151,8 +146,8 @@ handle_info(clean, State) -> ets:fun2ms(fun({_, _, UnbanTS, _}) -> UnbanTS =< Now end)), erlang:send_after(?CLEAN_INTERVAL, self(), clean), {noreply, State}; -handle_info(_Info, State) -> - ?WARNING_MSG("Unexpected info = ~p", [_Info]), +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info = ~p", [Info]), {noreply, State}. terminate(_Reason, #state{host = Host}) -> diff --git a/src/mod_host_meta.erl b/src/mod_host_meta.erl index abbca332a..ae7d7d697 100644 --- a/src/mod_host_meta.erl +++ b/src/mod_host_meta.erl @@ -27,14 +27,14 @@ -author('badlop@process-one.net'). --protocol({xep, 156, '1.4.0'}). +-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]). +-export([get_url/4, get_auto_url/2]). -include("logger.hrl"). @@ -125,7 +125,7 @@ file_json(Host) -> {200, [html, {<<"Content-Type">>, <<"application/json">>}, {<<"Access-Control-Allow-Origin">>, <<"*">>}], - [jiffy:encode(#{links => BoshList ++ WsList})]}. + [misc:json_encode(#{links => BoshList ++ WsList})]}. get_url(M, bosh, Tls, Host) -> get_url(M, Tls, Host, bosh_service_url, mod_bosh); @@ -151,10 +151,10 @@ get_auto_url(Tls, Module) -> [] -> undefined; [{ThisTls, Port, Path} | _] -> Protocol = case {ThisTls, Module} of - {false, mod_bosh} -> <<"http">>; - {true, mod_bosh} -> <<"https">>; {false, ejabberd_http_ws} -> <<"ws">>; - {true, ejabberd_http_ws} -> <<"wss">> + {true, ejabberd_http_ws} -> <<"wss">>; + {false, _} -> <<"http">>; + {true, _} -> <<"https">> end, < fun({{Port, _, _}, ejabberd_http, #{tls := ThisTls, request_handlers := Handlers}}) - when (Tls == any) or (Tls == ThisTls) -> + 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}} @@ -211,11 +211,12 @@ mod_doc() -> [?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("This module is available since ejabberd 22.05."), "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " - "http://../listen-options/#request-handlers[request_handlers]."), "", - ?T("Notice it only works if ejabberd_http has tls enabled.")], + "_`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:", " -", diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 8bd9522ce..087be0e72 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -5,7 +5,7 @@ %%% Created : 15 Sep 2014 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,16 +30,16 @@ -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, - format_arg/2, - mod_options/1, mod_doc/0]). + 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("ejabberd_stacktrace.hrl"). + -include("translate.hrl"). --define(DEFAULT_API_VERSION, 0). +-define(DEFAULT_API_VERSION, 1000000). -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). @@ -135,7 +135,7 @@ extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) -> 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) -> +process([Call | _], #request{method = 'POST', data = Data, ip = IPPort} = Req) -> Version = get_api_version(Req), try Args = extract_args(Data), @@ -145,15 +145,14 @@ process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) -> %% 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]), + _:{error,{_,invalid_json}} = Err -> + ?DEBUG("Bad Request: ~p", [Err]), badrequest_response(<<"Invalid JSON input">>); - ?EX_RULE(_Class, _Error, Stack) -> - StackTrace = ?EX_STACK(Stack), - ?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]), + _Class:Error:StackTrace -> + ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]), badrequest_response() end; -process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> +process([Call | _], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> Version = get_api_version(Req), try Args = case Data of @@ -166,9 +165,8 @@ process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> json_format({404, 44, <<"Command not found.">>}); - ?EX_RULE(_, _Error, Stack) -> - StackTrace = ?EX_STACK(Stack), - ?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]), + _:Error:StackTrace -> + ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]), badrequest_response() end; process([_Call], #request{method = 'OPTIONS', data = <<>>}) -> @@ -197,26 +195,29 @@ perform_call(Command, Args, Req, Version) -> %% Be tolerant to make API more easily usable from command-line pipe. extract_args(<<"\n">>) -> []; extract_args(Data) -> - case jiffy:decode(Data) of - List when is_list(List) -> List; - {List} when is_list(List) -> List; - Other -> [Other] - end. + 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}) -> - get_api_version(lists:reverse(Path)); -get_api_version([<<"v", String/binary>> | Tail]) -> +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) + get_api_version(Tail, Host) end; -get_api_version([_Head | Tail]) -> - get_api_version(Tail); -get_api_version([]) -> - ?DEFAULT_API_VERSION. +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 @@ -254,13 +255,15 @@ handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> {400, misc:atom_to_binary(Error)}; throw:Msg when is_list(Msg); is_binary(Msg) -> {400, iolist_to_binary(Msg)}; - ?EX_RULE(Class, Error, Stack) -> - StackTrace = ?EX_STACK(Stack), - ?ERROR_MSG("REST API Error: " - "~ts(~p) -> ~p:~p ~p", - [Call, hide_sensitive_args(Args), - Class, Error, StackTrace]), - {500, <<"internal_error">>} + 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) -> @@ -335,21 +338,43 @@ format_arg({Elements}, ({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) -> @@ -366,11 +391,14 @@ format_arg({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; @@ -412,7 +440,15 @@ format_command_result(Cmd, Auth, Result, Version) -> {_, T} = format_result(Result, ResultFormat), {200, T}; _ -> - {200, {[format_result(Result, ResultFormat)]}} + OtherResult1 = format_result(Result, ResultFormat), + OtherResult2 = case Version of + 0 -> + {[OtherResult1]}; + _ -> + {_, Other3} = OtherResult1, + Other3 + end, + {200, OtherResult2} end. format_result(Atom, {Name, atom}) -> @@ -428,6 +464,9 @@ format_result([String | _] = StringList, {Name, string}) when is_list(String) -> 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}; @@ -441,13 +480,17 @@ format_result(Code, {Name, restuple}) -> {[{<<"res">>, Code == true orelse Code == ok}, {<<"text">>, <<"">>}]}}; -format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> +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(Els, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) -> +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]}}; -format_result(Els, {Name, {list, Def}}) -> +%% 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]}}) -> @@ -460,9 +503,11 @@ format_result(Tuple, {_Name, {tuple, [{name, string}, {value, _} = ValFmt]}}) -> {_, 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), - {misc:atom_to_binary(Name), {[format_result(El, ElDef) || {El, ElDef} <- Els]}}; + Els2 = [format_result(El, ElDef) || {El, ElDef} <- Els], + {misc:atom_to_binary(Name), maps:from_list(Els2)}; format_result(404, {_Name, _}) -> "not_found". @@ -487,10 +532,10 @@ invalid_token_response() -> badrequest_response() -> badrequest_response(<<"400 Bad Request">>). badrequest_response(Body) -> - json_response(400, jiffy:encode(Body)). + json_response(400, misc:json_encode(Body)). json_format({Code, Result}) -> - json_response(Code, jiffy:encode(Result)); + json_response(Code, misc:json_encode(Result)); json_format({HTMLCode, JSONErrorCode, Message}) -> json_error(HTMLCode, JSONErrorCode, Message). @@ -501,9 +546,9 @@ json_response(Code, Body) when is_integer(Code) -> %% message is binary json_error(HTTPCode, JSONCode, Message) -> {HTTPCode, ?HEADER(?CT_JSON), - jiffy:encode({[{<<"status">>, <<"error">>}, - {<<"code">>, JSONCode}, - {<<"message">>, Message}]}) + misc:json_encode(#{<<"status">> => <<"error">>, + <<"code">> => JSONCode, + <<"message">> => Message}) }. log(Call, Args, {Addr, Port}) -> @@ -513,29 +558,57 @@ 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)}; + 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 " - "https://docs.ejabberd.im/developer/ejabberd-api[ejabberd API] " + "_`../../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' -> " - "http://../listen-options/#request-handlers[request_handlers]."), "", + "_`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'"), "", + "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/'")], + "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:", " -", @@ -545,4 +618,5 @@ mod_doc() -> " /api: mod_http_api", "", "modules:", - " mod_http_api: {}"]}. + " mod_http_api:", + " default_version: 2"]}. diff --git a/src/mod_http_api_opt.erl b/src/mod_http_api_opt.erl index 3d928fc1b..326c53e02 100644 --- a/src/mod_http_api_opt.erl +++ b/src/mod_http_api_opt.erl @@ -3,11 +3,11 @@ -module(mod_http_api_opt). --export([admin_ip_access/1]). +-export([default_version/1]). --spec admin_ip_access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). -admin_ip_access(Opts) when is_map(Opts) -> - gen_mod:get_opt(admin_ip_access, Opts); -admin_ip_access(Host) -> - gen_mod:get_module_opt(Host, mod_http_api, admin_ip_access). +-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_fileserver.erl b/src/mod_http_fileserver.erl index 9177552a3..3f8db94f5 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -5,7 +5,7 @@ %%% Created : %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -557,22 +557,17 @@ mod_doc() -> example => [{?T("This example configuration will serve the files from the " "local directory '/var/www' in the address " - "'http://example.org:5280/pub/archive/'. In this example a new " + "'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/archive: mod_http_fileserver", - " ...", - " ...", + " /pub/content: mod_http_fileserver", "", "modules:", - " ...", " mod_http_fileserver:", " docroot: /var/www", " accesslog: /var/log/ejabberd/access.log", @@ -585,5 +580,4 @@ mod_doc() -> " content_types:", " .ogg: audio/ogg", " .png: image/png", - " default_content_type: text/html", - " ..."]}]}. + " default_content_type: text/html"]}]}. diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index bda66253e..faa085811 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -5,7 +5,7 @@ %%% Created : 20 Aug 2015 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2015-2022 ProcessOne +%%% 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 @@ -27,7 +27,7 @@ -author('holger@zedat.fu-berlin.de'). -behaviour(gen_server). -behaviour(gen_mod). --protocol({xep, 363, '0.1'}). +-protocol({xep, 363, '0.3.0', '15.10', "complete", ""}). -define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. -define(CALL_TIMEOUT, 60000). % 1 minute. @@ -234,7 +234,7 @@ mod_doc() -> "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' -> " - "http://../listen-options/#request-handlers[request_handlers].")], + "_`listen-options.md#request_handlers|request_handlers`_.")], opts => [{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, @@ -243,14 +243,14 @@ mod_doc() -> 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.\". " + "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. " - "This will only be displayed by special XMPP clients. " - "The default value is \"HTTP File Upload\".")}}, + "The default value is '\"HTTP File Upload\"'. " + "Please note this will only be displayed by some XMPP clients.")}}, {access, #{value => ?T("AccessName"), desc => @@ -270,7 +270,7 @@ mod_doc() -> 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, " + "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, @@ -293,8 +293,8 @@ mod_doc() -> #{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 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, @@ -302,8 +302,8 @@ mod_doc() -> 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 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, @@ -311,26 +311,28 @@ mod_doc() -> 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\".")}}, + "'@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. NOTE: different virtual " - "hosts cannot use the same PUT URL. " - "The default value is \"https://@HOST@:5443/upload\".")}}, + "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 " + "to the same value as 'put_url'. The keyword '@HOST@' is " "replaced with the virtual host name. NOTE: if GET requests " - "are handled by 'mod_http_upload', the 'get_url' must match the " + "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.")}}, @@ -349,9 +351,9 @@ mod_doc() -> "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" - "[Prosody's mod_http_upload_external] in the " - "'Implementation' section. There is no default value.")}}, + "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 => @@ -367,40 +369,35 @@ mod_doc() -> "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => - [{?T("For example, the following XML representation of vCard:"), - ["", - " Conferences", - " ", - " ", - " Elm Street", - " ", - ""]}, - {?T("will be translated to:"), - ["vcard:", - " fn: Conferences", - " adr:", - " -", - " work: true", - " street: Elm Street"]}]}}], + ["# 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\"", - " ..."]}. + " put_url: \"https://@HOST@:5443/upload\""]}. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> @@ -536,7 +533,8 @@ process(LocalPath, #request{method = Method, host = Host, ip = IP}) [Method, encode_addr(IP), Host]), http_response(404); process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, - length = Length} = Request) -> + 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} -> @@ -576,9 +574,10 @@ process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, [encode_addr(IP), Host, Error]), http_response(500) end; -process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request) +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} -> @@ -722,7 +721,7 @@ get_proc_name(ServerHost, ModuleName, PutURL) -> -spec expand_home(binary()) -> binary(). expand_home(Input) -> - {ok, [[Home]]} = init:get_argument(home), + Home = misc:get_home(), misc:expand_keyword(<<"@HOME@">>, Input, Home). -spec expand_host(binary(), binary()) -> binary(). @@ -912,8 +911,8 @@ mk_slot(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS, Query) -> GetURL = str:join([GetPrefix | Slot], <<$/>>), mk_slot(PutURL, GetURL, XMLNS, Query); mk_slot(PutURL, GetURL, XMLNS, Query) -> - PutURL1 = <<(misc:url_encode(PutURL))/binary, Query/binary>>, - GetURL1 = misc:url_encode(GetURL), + 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}; @@ -921,6 +920,18 @@ mk_slot(PutURL, GetURL, XMLNS, Query) -> #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(<>); diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl index 2d8a0b0de..76b99ca05 100644 --- a/src/mod_http_upload_quota.erl +++ b/src/mod_http_upload_quota.erl @@ -5,7 +5,7 @@ %%% Created : 15 Oct 2015 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2015-2022 ProcessOne +%%% 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 @@ -96,7 +96,7 @@ mod_options(_) -> mod_doc() -> #{desc => [?T("This module adds quota support for mod_http_upload."), "", - ?T("This module depends on 'mod_http_upload'.")], + ?T("This module depends on _`mod_http_upload`_.")], opts => [{max_days, #{value => ?T("Days"), @@ -126,27 +126,23 @@ mod_doc() -> "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 'access_soft_quota'). " + "the specified soft quota (see also option 'access_soft_quota'). " "The default value is 'hard_upload_quota'.")}}], example => - [{?T("Please note that it's not necessary to specify the " + [{?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", - " ..."]}]}. + " max_days: 100"]}]}. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> diff --git a/src/mod_jidprep.erl b/src/mod_jidprep.erl index 605ca53b1..3de051156 100644 --- a/src/mod_jidprep.erl +++ b/src/mod_jidprep.erl @@ -5,7 +5,7 @@ %%% Created : 11 Sep 2019 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2019-2022 ProcessOne +%%% 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 @@ -25,7 +25,7 @@ -module(mod_jidprep). -author('holger@zedat.fu-berlin.de'). --protocol({xep, 328, '0.1'}). +-protocol({xep, 328, '0.1', '19.09', "complete", ""}). -behaviour(gen_mod). @@ -46,15 +46,14 @@ %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- --spec start(binary(), gen_mod:opts()) -> ok. -start(Host, _Opts) -> - register_iq_handlers(Host), - register_hooks(Host). +-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) -> - unregister_hooks(Host), - unregister_iq_handlers(Host). +stop(_Host) -> + ok. -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(_Host, _NewOpts, _OldOpts) -> @@ -88,19 +87,6 @@ mod_doc() -> "be used to control who is allowed to use this " "service. The default value is 'local'.")}}]}. -%%-------------------------------------------------------------------- -%% Register/unregister hooks. -%%-------------------------------------------------------------------- --spec register_hooks(binary()) -> ok. -register_hooks(Host) -> - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - disco_local_features, 50). - --spec unregister_hooks(binary()) -> ok. -unregister_hooks(Host) -> - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, - disco_local_features, 50). - %%-------------------------------------------------------------------- %% Service discovery. %%-------------------------------------------------------------------- @@ -123,15 +109,6 @@ disco_local_features(Acc, _From, _To, _Node, _Lang) -> %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- --spec register_iq_handlers(binary()) -> ok. -register_iq_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_JIDPREP_0, ?MODULE, process_iq). - --spec unregister_iq_handlers(binary()) -> ok. -unregister_iq_handlers(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_JIDPREP_0). - -spec process_iq(iq()) -> iq(). process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), diff --git a/src/mod_last.erl b/src/mod_last.erl index c13e4d22f..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-2022 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 @@ -author('alexey@process-one.net'). --protocol({xep, 12, '2.0'}). +-protocol({xep, 12, '2.0', '0.5.0', "complete", ""}). -behaviour(gen_mod). @@ -62,32 +62,15 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_LAST, ?MODULE, process_local_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_LAST, ?MODULE, process_sm_iq), - ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, - privacy_check_packet, 30), - ejabberd_hooks:add(register_user, Host, ?MODULE, - register_user, 50), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, - on_presence_update, 50). + {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(register_user, Host, ?MODULE, - register_user, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(unset_presence_hook, Host, - ?MODULE, on_presence_update, 50), - ejabberd_hooks:delete(privacy_check_packet, Host, ?MODULE, - privacy_check_packet, 30), - 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), diff --git a/src/mod_last_mnesia.erl b/src/mod_last_mnesia.erl index c081ba039..f108101c9 100644 --- a/src/mod_last_mnesia.erl +++ b/src/mod_last_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_last_sql.erl b/src/mod_last_sql.erl index 121c96045..b61300fd2 100644 --- a/src/mod_last_sql.erl +++ b/src/mod_last_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +30,7 @@ %% 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"). @@ -38,9 +39,25 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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, diff --git a/src/mod_legacy_auth.erl b/src/mod_legacy_auth.erl index 0053d88a6..1fb772d2c 100644 --- a/src/mod_legacy_auth.erl +++ b/src/mod_legacy_auth.erl @@ -2,7 +2,7 @@ %%% Created : 11 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +22,7 @@ -module(mod_legacy_auth). -behaviour(gen_mod). --protocol({xep, 78, '2.5'}). +-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]). @@ -37,17 +37,12 @@ %%%=================================================================== %%% API %%%=================================================================== -start(Host, _Opts) -> - ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, - c2s_unauthenticated_packet, 50), - ejabberd_hooks:add(c2s_pre_auth_features, Host, ?MODULE, - c2s_stream_features, 50). +start(_Host, _Opts) -> + {ok, [{hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}, + {hook, c2s_pre_auth_features, c2s_stream_features, 50}]}. -stop(Host) -> - ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, - c2s_unauthenticated_packet, 50), - ejabberd_hooks:delete(c2s_pre_auth_features, Host, ?MODULE, - c2s_stream_features, 50). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 7d9e308f5..c3d5ae435 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -5,7 +5,7 @@ %%% Created : 4 Jul 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2022 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 @@ -25,10 +25,13 @@ -module(mod_mam). --protocol({xep, 313, '0.6.1'}). --protocol({xep, 334, '0.2'}). --protocol({xep, 359, '0.5.0'}). --protocol({xep, 441, '0.2.0'}). +-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). @@ -37,6 +40,7 @@ -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, @@ -44,12 +48,22 @@ 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, - delete_old_messages_batch/5, delete_old_messages_status/1, delete_old_messages_abort/1]). + 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"). @@ -65,9 +79,10 @@ -callback delete_old_messages(binary() | global, erlang:timestamp(), all | chat | groupchat) -> any(). --callback extended_fields() -> [mam_query:property() | #xdata_field{}]. +-callback extended_fields(binary()) -> [mam_query:property() | #xdata_field{}]. -callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat, - jid(), binary(), recv | send, integer()) -> ok | any(). + 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(), @@ -136,6 +151,8 @@ start(Host, Opts) -> 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, @@ -146,6 +163,12 @@ start(Host, Opts) -> 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, @@ -161,7 +184,7 @@ start(Host, Opts) -> ejabberd_hooks:add(check_create_room, Host, ?MODULE, check_create_room, 50) end, - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), ok; Err -> Err @@ -209,6 +232,8 @@ stop(Host) -> 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, @@ -219,6 +244,12 @@ stop(Host) -> 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, @@ -234,12 +265,7 @@ stop(Host) -> ejabberd_hooks:delete(check_create_room, Host, ?MODULE, check_create_room, 50) end, - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -352,6 +378,23 @@ remove_mam_for_user_with_peer(User, Server, Peer) -> {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} -> @@ -446,6 +489,8 @@ offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) -> -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) -> @@ -491,6 +536,17 @@ set_stanza_id(Pkt, JID, 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)), @@ -550,8 +606,18 @@ parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) -> #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [?NS_MAM_1]}, Query#mam_query.xdata), - try mam_query:decode(X#xdata.fields) of - Form -> {ok, Form} + {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)} @@ -559,12 +625,27 @@ parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) -> 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 | + {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. @@ -580,6 +661,52 @@ message_is_archived(false, #{lserver := LServer}, Pkt) -> 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">> -> @@ -702,7 +829,7 @@ process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) -> CommonFields = [{with, undefined}, {start, undefined}, {'end', undefined}], - ExtendedFields = Mod:extended_fields(), + 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]}, @@ -787,6 +914,13 @@ process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang, -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) -> @@ -974,6 +1108,31 @@ may_enter_room(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} -> @@ -1017,9 +1176,16 @@ 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), + Mod:store(El, LServer, US, Type, Peer, Nick, Dir, ID, OriginID, Retract), Pkt. write_prefs(LUser, LServer, Host, Default, Always, Never) -> @@ -1316,8 +1482,9 @@ msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick, 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 = [Pkt3], delay = Delay}} + {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", @@ -1444,25 +1611,60 @@ 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 = delete_old_mam_messages, tags = [purge], + [ + #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\".", + "`chat`, `groupchat`, `all`.", module = ?MODULE, function = delete_old_messages, - args_desc = ["Type of messages to delete (chat, groupchat, all)", + 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 = [purge], + #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\".", + "`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)", + "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"], @@ -1471,7 +1673,7 @@ get_commands_spec() -> 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 = [purge], + #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, @@ -1481,7 +1683,7 @@ get_commands_spec() -> result = {status, string}, result_desc = "Status test", result_example = "Operation in progress, delete 5000 messages"}, - #ejabberd_commands{name = abort_delete_old_mam_messages, tags = [purge], + #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, @@ -1513,6 +1715,49 @@ get_commands_spec() -> 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) -> @@ -1525,6 +1770,8 @@ 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) -> @@ -1546,6 +1793,7 @@ mod_options(Host) -> {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)}, @@ -1554,11 +1802,17 @@ mod_options(Host) -> mod_doc() -> #{desc => - ?T("This module implements " + [?T("This module implements " "https://xmpp.org/extensions/xep-0313.html" - "[XEP-0313: Message Archive Management]. " + "[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."), + "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"), @@ -1634,7 +1888,19 @@ mod_doc() -> #{value => "true | false", desc => ?T("When this option is disabled, for each individual " - "subscriber a separa mucsub message is stored. With this " + "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'.")}}]}. + "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 index 8b8a4b91e..4f59fa1fc 100644 --- a/src/mod_mam_mnesia.erl +++ b/src/mod_mam_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,8 +28,10 @@ %% API -export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, - extended_fields/0, store/8, 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]). + 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"). @@ -75,14 +77,14 @@ remove_user(LUser, LServer) -> remove_room(_LServer, LName, LHost) -> remove_user(LName, LHost). -remove_from_archive(LUser, LServer, none) -> - US = {LUser, LServer}, +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(LUser, LServer, WithJid) -> - US = {LUser, LServer}, +remove_from_archive(US, _LServer, #jid{} = WithJid) -> Peer = jid:remove_resource(jid:split(WithJid)), F = fun () -> Msgs = mnesia:select( @@ -96,6 +98,21 @@ remove_from_archive(LUser, LServer, WithJid) -> 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) -> @@ -168,10 +185,31 @@ delete_old_messages_batch(LServer, TimeStamp, Type, Batch, LastUS) -> {error, Err} end. -extended_fields() -> +extended_fields(_) -> []. -store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS) -> +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 -> @@ -189,7 +227,8 @@ store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS) -> bare_peer = {PUser, PServer, <<>>}, type = Type, nick = Nick, - packet = Pkt}) + packet = Pkt, + origin_id = OriginID}) end, case mnesia:transaction(F) of {atomic, ok} -> @@ -314,3 +353,18 @@ 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 index d8d970a13..940ddd183 100644 --- a/src/mod_mam_opt.erl +++ b/src/mod_mam_opt.erl @@ -4,6 +4,7 @@ -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]). @@ -22,6 +23,12 @@ access_preferences(Opts) when is_map(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); diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl index 09175d83f..8a1d8e02f 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,9 +29,10 @@ %% API -export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, - extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/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"). @@ -43,9 +44,113 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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, @@ -58,18 +163,26 @@ 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, WithJid) -> +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) -> @@ -100,21 +213,47 @@ count_messages_to_delete(ServerHost, TimeStamp, Type) -> delete_old_messages_batch(ServerHost, TimeStamp, Type, Batch) -> TS = misc:now_to_usec(TimeStamp), Res = - case Type of - all -> - ejabberd_sql:sql_query( - ServerHost, - ?SQL("delete from archive" - " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d")); - _ -> - 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 limit %(Batch)d")) - end, + 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}; @@ -141,10 +280,20 @@ delete_old_messages(ServerHost, TimeStamp, Type) -> end, ok. -extended_fields() -> - [{withtext, <<"">>}]. +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) -> +store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS, + OriginID, Retract) -> SUser = case Type of chat -> LUser; groupchat -> jid:encode({LUser, LHost, <<>>}) @@ -167,8 +316,19 @@ store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS) -> _ -> fxml:element_to_binary(Pkt) end, - case SqlType of - mssql -> case ejabberd_sql:sql_query( + 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", @@ -180,13 +340,14 @@ store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS) -> "xml=N%(XML)s", "txt=N%(Body)s", "kind=%(SType)s", - "nick=%(Nick)s"])) of + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])) of {updated, _} -> ok; Err -> Err end; - _ -> case ejabberd_sql:sql_query( + _ -> case ejabberd_sql:sql_query( LServer, ?SQL_INSERT( "archive", @@ -198,13 +359,14 @@ store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS) -> "xml=%(XML)s", "txt=%(Body)s", "kind=%(SType)s", - "nick=%(Nick)s"])) of + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])) of {updated, _} -> ok; Err -> Err end - end. + end. write_prefs(LUser, _LServer, #archive_prefs{default = Default, never = Never, @@ -364,7 +526,7 @@ export(_Server) -> {archive_msg, fun([Host | HostTail], #archive_msg{us ={LUser, LServer}, id = _ID, timestamp = TS, peer = Peer, - type = Type, nick = Nick, packet = Pkt}) + 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 @@ -388,7 +550,8 @@ export(_Server) -> "xml=N%(XML)s", "txt=N%(Body)s", "kind=%(SType)s", - "nick=%(Nick)s"])]; + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])]; _ -> [?SQL_INSERT( "archive", ["username=%(SUser)s", @@ -399,7 +562,8 @@ export(_Server) -> "xml=%(XML)s", "txt=%(Body)s", "kind=%(SType)s", - "nick=%(Nick)s"])] + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])] end; (_Host, _R) -> [] @@ -441,6 +605,11 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> true -> [] end, + SubOrderClause = if LimitClause /= []; TopClause /= [] -> + <<" ORDER BY timestamp DESC ">>; + true -> + [] + end, WithTextClause = if is_binary(WithText), WithText /= <<>> -> [<<" and match (txt) against (">>, ToString(WithText), <<")">>]; @@ -528,7 +697,7 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> % XEP-0059: Result Set Management % 2.5 Requesting the Last Page in a Result Set [<<"SELECT">>, UserSel, <<" timestamp, xml, peer, kind, nick FROM (">>, - Query, <<" ORDER BY timestamp DESC ">>, + Query, SubOrderClause, LimitClause, <<") AS t ORDER BY timestamp ASC;">>]; _ -> [Query, <<" ORDER BY timestamp ASC ">>, 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 index d1f24c700..77b690867 100644 --- a/src/mod_metrics.erl +++ b/src/mod_metrics.erl @@ -5,7 +5,7 @@ %%% Created : 22 Oct 2015 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -49,27 +49,19 @@ %% API %%==================================================================== -start(Host, _Opts) -> - ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, offline_message_hook, 20), - ejabberd_hooks:add(sm_register_connection_hook, Host, ?MODULE, sm_register_connection_hook, 20), - ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, sm_remove_connection_hook, 20), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 20), - ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet, 20), - ejabberd_hooks:add(s2s_send_packet, Host, ?MODULE, s2s_send_packet, 20), - ejabberd_hooks:add(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 20), - ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 20), - ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 20). +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) -> - ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, offline_message_hook, 20), - ejabberd_hooks:delete(sm_register_connection_hook, Host, ?MODULE, sm_register_connection_hook, 20), - ejabberd_hooks:delete(sm_remove_connection_hook, Host, ?MODULE, sm_remove_connection_hook, 20), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 20), - ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet, 20), - ejabberd_hooks:delete(s2s_send_packet, Host, ?MODULE, s2s_send_packet, 20), - ejabberd_hooks:delete(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 20), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 20), - ejabberd_hooks:delete(register_user, Host, ?MODULE, register_user, 20). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_mix.erl b/src/mod_mix.erl index 315c7b80d..25216b6fc 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -24,7 +24,7 @@ -module(mod_mix). -behaviour(gen_mod). -behaviour(gen_server). --protocol({xep, 369, '0.14.1'}). +-protocol({xep, 369, '0.14.1', '16.03', "complete", ""}). %% API -export([route/1]). @@ -33,7 +33,7 @@ -export([mod_doc/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3, format_status/2]). + terminate/2, code_change/3]). %% Hooks -export([process_disco_info/1, process_disco_items/1, @@ -44,7 +44,8 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). --include("ejabberd_stacktrace.hrl"). + + -callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. -callback set_channel(binary(), binary(), binary(), @@ -102,16 +103,13 @@ mod_doc() -> [?T("This module is an experimental implementation of " "https://xmpp.org/extensions/xep-0369.html" "[XEP-0369: Mediated Information eXchange (MIX)]. " - "MIX support was added in ejabberd 16.03 as an " - "experimental feature, updated in 19.02, and is not " - "yet ready to use in production. It's asserted that " + "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: https://docs.ejabberd.im/tutorials/mix-010/" - "[Getting started with XEP-0369: Mediated Information " - "eXchange (MIX) v0.1]."), "", + "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"), @@ -125,7 +123,7 @@ mod_doc() -> 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.\". " + "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"), @@ -322,11 +320,11 @@ handle_cast(Request, State) -> handle_info({route, Packet}, State) -> try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + 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) -> @@ -343,9 +341,6 @@ terminate(_Reason, State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -format_status(_Opt, Status) -> - Status. - %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -632,7 +627,7 @@ notify_participant_joined(Mod, LServer, To, From, ID, Nick) -> notify_participant_left(Mod, LServer, To, ID) -> {Chan, Host, _} = jid:tolower(To), Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, - retract = ID}, + retract = [ID]}, Event = #ps_event{items = Items}, Msg = #message{from = jid:remove_resource(To), id = p1_rand:get_string(), diff --git a/src/mod_mix_mnesia.erl b/src/mod_mix_mnesia.erl index 2ffd32bee..0d3a4d20c 100644 --- a/src/mod_mix_mnesia.erl +++ b/src/mod_mix_mnesia.erl @@ -128,6 +128,7 @@ set_participant(_LServer, Channel, Service, JID, ID, Nick) -> 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 diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl index 7bd6f2a71..bae6133fb 100644 --- a/src/mod_mix_pam.erl +++ b/src/mod_mix_pam.erl @@ -22,7 +22,7 @@ %%%---------------------------------------------------------------------- -module(mod_mix_pam). -behaviour(gen_mod). --protocol({xep, 405, '0.3.0'}). +-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]). @@ -34,7 +34,7 @@ process_iq/1, get_mix_roster_items/2, webadmin_user/4, - webadmin_page/3]). + webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). @@ -64,27 +64,22 @@ start(Host, Opts) -> case Mod:init(Host, Opts) of ok -> init_cache(Mod, Host, Opts), - ejabberd_hooks:add(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 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(roster_get, Host, ?MODULE, get_mix_roster_items, 50), - ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50), - ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0, ?MODULE, process_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2, ?MODULE, process_iq); + {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) -> - ejabberd_hooks:delete(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 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(roster_get, Host, ?MODULE, get_mix_roster_items, 50), - ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50), - ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2). +stop(_Host) -> + ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -126,7 +121,7 @@ mod_doc() -> "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 " + ?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 => @@ -225,7 +220,7 @@ get_mix_roster_items(Acc, {LUser, LServer}) -> name = <<>>, subscription = both, ask = undefined, - groups = [<<"Channels">>], + groups = [], mix_channel = #mix_roster_channel{participant_id = Id} } end, Channels); @@ -469,7 +464,7 @@ delete_cache(Mod, JID, Channel) -> %%%=================================================================== %%% Webadmin interface %%%=================================================================== -webadmin_user(Acc, User, Server, Lang) -> +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 @@ -482,12 +477,13 @@ webadmin_user(Acc, User, Server, Lang) -> ?C(<<" | ">>), FQueueView]. -webadmin_page(_, Host, - #request{us = _US, path = [<<"user">>, U, <<"mix_channels">>], - lang = Lang} = _Request) -> +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(Acc, _, _) -> Acc. +webadmin_page_hostuser(Acc, _, _, _) -> Acc. web_mix_channels(User, Server, Lang) -> LUser = jid:nodeprep(User), @@ -512,7 +508,7 @@ web_mix_channels(User, Server, Lang) -> [?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">>)) + (?H1GL(PageTitle, <<"modules/#mod_mix_pam">>, <<"mod_mix_pam">>)) ++ FItems. us_to_list({User, Server}) -> diff --git a/src/mod_mix_pam_sql.erl b/src/mod_mix_pam_sql.erl index 7fc26b9e6..af22c74f4 100644 --- a/src/mod_mix_pam_sql.erl +++ b/src/mod_mix_pam_sql.erl @@ -26,6 +26,7 @@ %% 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"). @@ -33,10 +34,29 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> - %% TODO +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), diff --git a/src/mod_mix_sql.erl b/src/mod_mix_sql.erl index 753fd9fd0..be3b28124 100644 --- a/src/mod_mix_sql.erl +++ b/src/mod_mix_sql.erl @@ -27,6 +27,7 @@ -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"). @@ -34,10 +35,65 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> - %% TODO +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)), @@ -111,6 +167,7 @@ set_participant(LServer, Channel, Service, JID, ID, Nick) -> _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( diff --git a/src/mod_mqtt.erl b/src/mod_mqtt.erl index 01950d64a..e38c7aae6 100644 --- a/src/mod_mqtt.erl +++ b/src/mod_mqtt.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. @@ -128,6 +128,7 @@ publish({_, S, _} = USR, Pkt, ExpiryTime) -> 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 -> @@ -146,6 +147,7 @@ subscribe({_, S, _} = USR, TopicFilter, SubOpts, ID) -> 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} @@ -157,6 +159,7 @@ subscribe({_, S, _} = USR, TopicFilter, SubOpts, ID) -> -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()) -> @@ -279,7 +282,7 @@ listen_options() -> mod_doc() -> #{desc => ?T("This module adds " - "https://docs.ejabberd.im/admin/guide/mqtt/[support for the MQTT] " + "_`../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 => @@ -603,7 +606,7 @@ match([H|T1], [<<"%c">>|T2], U, S, R) -> end; match([H|T1], [<<"%g">>|T2], U, S, R) -> case jid:resourceprep(H) of - H -> + H -> case acl:loaded_shared_roster_module(S) of undefined -> false; Mod -> 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 index 1d8e7aa5b..5c2902d2b 100644 --- a/src/mod_mqtt_mnesia.erl +++ b/src/mod_mqtt_mnesia.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl index 9b8c6ed44..c6d0338d9 100644 --- a/src/mod_mqtt_session.erl +++ b/src/mod_mqtt_session.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. @@ -1214,7 +1214,13 @@ authenticate(#connect{password = Pass, properties = Props} = Pkt, State) -> true -> {ok, JID, pkix}; false -> - {error, 'not-authorized'} + {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( @@ -1228,9 +1234,9 @@ authenticate(#connect{password = Pass, properties = Props} = Pkt, State) -> Err end. --spec tls_auth(jid:jid(), state()) -> boolean(). +-spec tls_auth(jid:jid(), state()) -> boolean() | no_cert. tls_auth(_JID, #state{tls_verify = false}) -> - false; + no_cert; tls_auth(JID, State) -> case State#state.socket of {fast_tls, Sock} -> @@ -1251,10 +1257,10 @@ tls_auth(JID, State) -> false end; error -> - false + no_cert end; _ -> - false + no_cert end. get_cert_jid(Cert) -> diff --git a/src/mod_mqtt_sql.erl b/src/mod_mqtt_sql.erl index dd40771f5..0f2b05b35 100644 --- a/src/mod_mqtt_sql.erl +++ b/src/mod_mqtt_sql.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. @@ -25,6 +25,7 @@ -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"). @@ -36,9 +37,33 @@ init() -> ?ERROR_MSG("Backend 'sql' is only supported for db_type", []), {error, db_failure}. -init(_Host, _Opts) -> +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, <<"">>), diff --git a/src/mod_mqtt_ws.erl b/src/mod_mqtt_ws.erl index 1c9c8de7a..fd1e7d871 100644 --- a/src/mod_mqtt_ws.erl +++ b/src/mod_mqtt_ws.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. @@ -27,7 +27,7 @@ -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, format_status/2]). + terminate/2, code_change/3]). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). @@ -132,9 +132,6 @@ terminate(_Reason, State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -format_status(_Opt, Status) -> - Status. - %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/src/mod_muc.erl b/src/mod_muc.erl index c1c1c7f2a..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-2022 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,7 +24,9 @@ %%%---------------------------------------------------------------------- -module(mod_muc). -author('alexey@process-one.net'). --protocol({xep, 45, '1.25'}). +-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. @@ -50,6 +52,7 @@ 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, @@ -82,7 +85,7 @@ -include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -include("translate.hrl"). --include("ejabberd_stacktrace.hrl"). + -type state() :: #{hosts := [binary()], server_host := binary(), @@ -94,7 +97,7 @@ -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. +-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{}]. @@ -316,6 +319,15 @@ create_room(Host, Name, Opts) -> store_room(ServerHost, Host, Name, Opts) -> store_room(ServerHost, Host, Name, Opts, undefined). +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), @@ -416,6 +428,7 @@ handle_call({create, Room, Host, Opts}, _From, 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 -> @@ -431,6 +444,7 @@ handle_call({create, Room, Host, From, Nick, Opts}, _From, 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 -> @@ -440,11 +454,11 @@ handle_call({create, Room, Host, From, Nick, Opts}, _From, -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({route_to_room, Packet}, #{server_host := ServerHost} = State) -> try route_to_room(Packet, ServerHost) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), + catch + Class:Reason:StackTrace -> ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_cast({room_destroyed, {Room, Host}, Pid}, @@ -469,11 +483,11 @@ handle_info({route, Packet}, #{server_host := ServerHost} = State) -> %% where mod_muc is not loaded. Such configuration %% is *highly* discouraged try route(Packet, ServerHost) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), + catch + Class:Reason:StackTrace -> ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info({room_destroyed, {Room, Host}, Pid}, State) -> @@ -577,20 +591,17 @@ extract_password(#iq{} = IQ) -> false end. --spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | error. +-spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | {error, notfound | db_failure | term()}. unhibernate_room(ServerHost, Host, Room) -> unhibernate_room(ServerHost, Host, Room, true). --spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | error. +-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}), - case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000) of - {ok, _} = R -> R; - _ -> error - end; + ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000); {ok, _} = R2 -> R2 end. @@ -663,29 +674,33 @@ process_vcard(#iq{lang = Lang} = IQ) -> xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_register(iq()) -> iq(). -process_register(#iq{type = Type, from = From, to = To, lang = Lang, - sub_els = [El = #register{}]} = 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 -> - xmpp:make_iq_result( - IQ, iq_get_register_info(ServerHost, Host, From, Lang)); + {result, iq_get_register_info(ServerHost, RegisterDestination, From, Lang)}; set -> - case process_iq_register_set(ServerHost, Host, From, El, Lang) of - {result, Result} -> - xmpp:make_iq_result(IQ, Result); - {error, Err} -> - xmpp:make_error(IQ, Err) - end + process_iq_register_set(ServerHost, RegisterDestination, From, El, Lang) end; deny -> ErrText = ?T("Access denied by service policy"), Err = xmpp:err_forbidden(ErrText, Lang), - xmpp:make_error(IQ, Err) + {error, Err} end. -spec process_disco_info(iq()) -> iq(). @@ -703,6 +718,10 @@ process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, 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 -> [] @@ -713,7 +732,7 @@ process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, end, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE - | RegisterFeatures ++ RSMFeatures ++ MAMFeatures], + | RegisterFeatures ++ RSMFeatures ++ MAMFeatures ++ OccupantIdFeatures], Name = mod_muc_opt:name(ServerHost), Identity = #identity{category = <<"conference">>, type = <<"text">>, @@ -866,6 +885,8 @@ load_room(RMod, Host, ServerHost, Room, ResetHibernationTime) -> case restore_room(ServerHost, Host, Room) of error -> {error, notfound}; + {error, _} = Err -> + Err; Opts0 -> Mod = gen_mod:db_mod(ServerHost, mod_muc), case proplists:get_bool(persistent, Opts0) of @@ -974,15 +995,38 @@ iq_disco_items(ServerHost, Host, From, Lang, MaxRoomsDiscoItems, Node, RSM) #rsm_set{max = Max} -> Max end, - {Items, HitMax} = lists:foldr( - fun(_, {Acc, _}) when length(Acc) >= MaxItems -> - {Acc, true}; - (R, {Acc, _}) -> - case get_room_disco_item(R, Query) of - {ok, Item} -> {[Item | Acc], false}; - {error, _} -> {Acc, false} + 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, {[], false}, get_online_rooms(ServerHost, Host, RSM)), + 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), @@ -1166,19 +1210,28 @@ remove_user(User, Server) -> 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)}; + [{password, iolist_to_binary(Pass)}]; ({subject, [C|_] = Subj}) when is_integer(C), C >= 0, C =< 255 -> - {subject, iolist_to_binary(Subj)}; - ({subject_author, Author}) -> - {subject_author, iolist_to_binary(Author)}; + [{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( + [{affiliations, lists:map( fun({{U, S, R}, Aff}) -> NewAff = case Aff of @@ -1191,16 +1244,38 @@ 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). export(LServer) -> @@ -1273,7 +1348,8 @@ mod_opt_type(cleanup_affiliations_on_start) -> mod_opt_type(default_room_options) -> econf:options( #{allow_change_subj => econf:bool(), - allow_private_messages => econf:bool(), + allowpm => + econf:enum([anyone, participants, moderators, none]), allow_private_messages_from_visitors => econf:enum([anyone, moderators, nobody]), allow_query_users => econf:bool(), @@ -1357,7 +1433,7 @@ mod_options(Host) -> {cleanup_affiliations_on_start, false}, {default_room_options, [{allow_change_subj,true}, - {allow_private_messages,true}, + {allowpm,anyone}, {allow_query_users,true}, {allow_user_invites,false}, {allow_visitor_nickchange,true}, @@ -1390,6 +1466,11 @@ mod_doc() -> "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 " @@ -1435,11 +1516,12 @@ mod_doc() -> "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. The default is 'all' for " + "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.")}}, + "to register any free nick in the MUC service and in the rooms.")}}, {db_type, #{value => "mnesia | sql", desc => @@ -1461,12 +1543,12 @@ mod_doc() -> ?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 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. " + "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.")}}, + "and have _`mod_mam`_ module loaded.")}}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), @@ -1545,7 +1627,7 @@ mod_doc() -> desc => ?T("This option defines after how many users in the room, " "it is considered overcrowded. When a MUC room is considered " - "overcrowed, presence broadcasts are limited to reduce load, " + "overcrowded, presence broadcasts are limited to reduce load, " "traffic and excessive presence \"storm\" received by participants. " "The default value is '1000'.")}}, {min_message_interval, @@ -1557,7 +1639,7 @@ mod_doc() -> "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. " + "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.")}}, @@ -1574,7 +1656,7 @@ mod_doc() -> "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.")}}, + "silently discarded. A good value for this option is '4' seconds.")}}, {queue_type, #{value => "ram | file", desc => @@ -1617,21 +1699,22 @@ mod_doc() -> "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => - [{?T("For example, the following XML representation of vCard:"), - ["", - " Conferences", - " ", - " ", - " Elm Street", - " ", - ""]}, - {?T("will be translated to:"), - ["vcard:", - " fn: Conferences", - " adr:", - " -", - " work: true", - " street: Elm Street"]}]}}, + ["# 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", @@ -1642,7 +1725,7 @@ mod_doc() -> #{value => ?T("Options"), note => "improved in 22.05", desc => - ?T("This option allows to define the desired " + ?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:")}, @@ -1651,11 +1734,11 @@ mod_doc() -> desc => ?T("Allow occupants to change the subject. " "The default value is 'true'.")}}, - {allow_private_messages, - #{value => "true | false", + {allowpm, + #{value => "anyone | participants | moderators | none", desc => - ?T("Occupants can send private messages to other occupants. " - "The default value is 'true'.")}}, + ?T("Who can send private messages. " + "The default value is 'anyone'.")}}, {allow_query_users, #{value => "true | false", desc => @@ -1695,7 +1778,7 @@ mod_doc() -> ?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 " - "https://docs.ejabberd.im/admin/configuration/#captcha[CAPTCHA] " + "_`basic.md#captcha|CAPTCHA`_ " "in order to accept their join in the room. " "The default value is 'false'.")}}, {description, @@ -1705,8 +1788,10 @@ mod_doc() -> "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"), @@ -1773,6 +1858,11 @@ mod_doc() -> 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 => @@ -1787,8 +1877,7 @@ mod_doc() -> #{value => "true | false", desc => ?T("Allow users to subscribe to room events as described in " - "https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/" - "[Multi-User Chat Subscriptions]. " + "_`../../developer/xmpp-clients-bots/extensions/muc-sub.md|Multi-User Chat Subscriptions`_. " "The default value is 'false'.")}}, {title, #{value => ?T("Room Title"), @@ -1807,7 +1896,7 @@ mod_doc() -> ?T("Maximum number of occupants in the room. " "The default value is '200'.")}}, {presence_broadcast, - #{value => "[moderator | participant | visitor, ...]", + #{value => "[Role]", desc => ?T("List of roles for which presence is broadcasted. " "The list can contain one or several of: 'moderator', " diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 5f96801c6..9a8ab60b1 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_muc_admin.erl -%%% Author : Badlop +%%% Author : Badlop %%% Purpose : Tools for additional MUC administration -%%% Created : 8 Sep 2007 by Badlop +%%% Created : 8 Sep 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,29 +24,36 @@ %%%---------------------------------------------------------------------- -module(mod_muc_admin). --author('badlop@ono.com'). +-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_unregister_nick/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_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, get_room_affiliations/2, get_room_affiliation/3, - web_menu_main/2, web_page_main/2, web_menu_host/3, - subscribe_room/4, subscribe_room_many/3, - unsubscribe_room/2, get_subscribers/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_page_host/3, + 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_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"). @@ -61,24 +68,18 @@ %% gen_mod %%---------------------------- -start(Host, _Opts) -> - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), - ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), - ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), - ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), - ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). +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) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end, - ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), - ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), - ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), - ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -93,26 +94,28 @@ depends(_Host, _Opts) -> get_commands_spec() -> [ #ejabberd_commands{name = muc_online_rooms, tags = [muc], - desc = "List existing rooms ('global' to get all vhosts)", + 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 = ["muc.example.com"], - result_desc = "List of rooms", - result_example = ["room1@muc.example.com", "room2@muc.example.com"], + 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 ('global' to get all vhosts) by regex", + 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 = ["muc.example.com", "^prefix"], + 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@muc.example.com", "true", 10}, - {"room2@muc.example.com", "false", 10}], + 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, @@ -124,48 +127,83 @@ get_commands_spec() -> 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">>, <<"muc.example.org">>], + 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">>, <<"muc.example.org">>], + 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", "muc.example.com", "example.com"], - args = [{name, binary}, {service, binary}, + 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", "muc.example.com"], - args = [{name, binary}, {service, binary}], + 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], + #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", "muc.example.com", "localhost", [{"members_only","true"}]], - args = [{name, binary}, {service, binary}, + 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, @@ -173,6 +211,7 @@ get_commands_spec() -> {value, binary} ]}} }}], + args_rename = [{name, room}], result = {res, rescode}}, #ejabberd_commands{name = destroy_rooms_file, tags = [muc], desc = "Destroy the rooms indicated in file", @@ -186,12 +225,12 @@ get_commands_spec() -> 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.", + " 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 = ["muc.example.com", 31], + 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@muc.example.com", "room2@muc.example.com"], + result_example = ["room1@conference.example.com", "room2@conference.example.com"], args = [{service, binary}, {days, integer}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, @@ -199,54 +238,67 @@ get_commands_spec() -> 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.", + " 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 = ["muc.example.com", 31], + 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@muc.example.com", "room2@muc.example.com"], + 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.", + 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 = ["muc.example.com"], + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], result_desc = "List of empty rooms", - result_example = ["room1@muc.example.com", "room2@muc.example.com"], + 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.", + 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 = ["muc.example.com"], + 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@muc.example.com", "room2@muc.example.com"], + 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@muc.example.com", "room2@muc.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], + #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@muc.example.com", "Tommy", ["mucsub:config"]}], + result_example = [{"room1@conference.example.com", "Tommy", ["mucsub:config"]}], args = [{user, binary}, {host, binary}], result = {rooms, {list, @@ -262,10 +314,11 @@ get_commands_spec() -> 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", "muc.example.com"], + 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 = [{name, binary}, {service, binary}], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], result = {occupants, {list, {occupant, {tuple, [{jid, string}, @@ -278,10 +331,11 @@ get_commands_spec() -> 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", "muc.example.com"], + args_example = ["room1", "conference.example.com"], result_desc = "Number of room occupants", result_example = 7, - args = [{name, binary}, {service, binary}], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], result = {occupants, integer}}, #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], @@ -289,45 +343,66 @@ get_commands_spec() -> 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 : ", + "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">>, <<"muc.example.com">>, + 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 = [{name, binary}, {service, binary}, {password, binary}, + 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", "muc.example.com", "members_only", "true"], - args = [{name, binary}, {service, binary}, + 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", "muc.example.com"], + args_example = ["room1", "conference.example.com"], result_desc = "List of room options tuples with name and value", result_example = [{"members_only", "true"}], - args = [{name, binary}, {service, binary}], + 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], + #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: ,"], + "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", @@ -336,14 +411,46 @@ get_commands_spec() -> args = [{user, binary}, {nick, binary}, {room, binary}, {nodes, binary}], result = {nodes, {list, {node, string}}}}, - #ejabberd_commands{name = subscribe_room_many, tags = [muc_room], + #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 accept up to 50 users at once (this is configurable with `subscribe_room_many_max_users` option)", + 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: ,"], + "nodes separated by commas: `,`"], args_example = [[{"tom@localhost", "Tom"}, {"jerry@localhost", "Jerry"}], "room1@conference.localhost", @@ -357,35 +464,108 @@ get_commands_spec() -> {room, binary}, {nodes, binary}], result = {res, rescode}}, - #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], + #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 = get_subscribers, tags = [muc_room], + #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", "muc.example.com"], + 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 = [{name, binary}, {service, binary}], + 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", "muc.example.com", "user2@example.com", "member"], + 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", "muc.example.com"], + 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}], @@ -397,15 +577,53 @@ get_commands_spec() -> {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", "muc.example.com", "user1@example.com"], + args_example = ["room1", "conference.example.com", "user1@example.com"], result_desc = "Affiliation of the user", result_example = member, - args = [{name, binary}, {service, binary}, {jid, binary}], - result = {affiliation, atom}} + 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}} ]. @@ -414,7 +632,7 @@ get_commands_spec() -> %%% muc_online_rooms(ServiceArg) -> - Hosts = find_services(ServiceArg), + Hosts = find_services_validate(ServiceArg, <<"serverhost">>), lists:flatmap( fun(Host) -> [<> @@ -423,7 +641,7 @@ muc_online_rooms(ServiceArg) -> muc_online_rooms_by_regex(ServiceArg, Regex) -> {_, P} = re:compile(Regex), - Hosts = find_services(ServiceArg), + Hosts = find_services_validate(ServiceArg, <<"serverhost">>), lists:flatmap( fun(Host) -> [build_summary_room(Name, RoomHost, Pid) @@ -447,6 +665,9 @@ build_summary_room(Name, Host, Pid) -> 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} -> @@ -460,15 +681,18 @@ muc_register_nick(Nick, FromBinary, Service) -> end catch error:{invalid_domain, _} -> - throw({error, "Invalid 'service'"}); + throw({error, "Invalid value of 'service'"}); error:{unregistered_route, _} -> - throw({error, "Invalid 'service'"}); + 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). @@ -487,8 +711,10 @@ get_user_rooms(User, Server) -> 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(jid:nodeprep(User), jid:nodeprep(Server)), + UserJid = jid:make(User2, Server2), lists:flatmap( fun(ServerHost) -> {ok, Rooms} = mod_muc:get_subscribed_rooms(ServerHost, UserJid), @@ -505,6 +731,8 @@ get_user_subscriptions(User, Server) -> %% Web Admin %%---------------------------- +%% @format-begin + %%--------------- %% Web Admin Menu @@ -514,112 +742,402 @@ web_menu_main(Acc, Lang) -> web_menu_host(Acc, _Host, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. - %%--------------- %% Web Admin Page --define(TDTD(L, N), - ?XE(<<"tr">>, [?XCT(<<"td">>, L), - ?XC(<<"td">>, integer_to_binary(N)) - ])). - -web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) -> - OnlineRoomsNumber = lists:foldl( - fun(Host, Acc) -> - Acc + mod_muc:count_online_rooms(Host) - end, 0, find_hosts(global)), +web_page_main(_, #request{path = [<<"muc">>], lang = Lang} = R) -> PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), - Res = ?H1GL(PageTitle, <<"modules/#mod-muc">>, <<"mod_muc">>) ++ - [?XCT(<<"h3">>, ?T("Statistics")), - ?XAE(<<"table">>, [], - [?XE(<<"tbody">>, [?TDTD(?T("Total rooms"), OnlineRoomsNumber) - ]) - ]), - ?XE(<<"ul">>, [?LI([?ACT(<<"rooms/">>, ?T("List of rooms"))])]) - ], + 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. -web_page_main(_, #request{path=[<<"muc">>, <<"rooms">>], q = Q, lang = Lang} = _Request) -> - Sort_query = get_sort_query(Q), - Res = make_rooms_page(global, Lang, Sort_query), - {stop, Res}; +%%--------------- +%% WebAdmin MUC Host Page -web_page_main(Acc, _) -> Acc. +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) -> + []. -web_page_host(_, Host, - #request{path = [<<"muc">>], - q = Q, - lang = Lang} = _Request) -> - Sort_query = get_sort_query(Q), - Res = make_rooms_page(Host, Lang, Sort_query), - {stop, Res}; -web_page_host(Acc, _, _) -> Acc. +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} + {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)}} + true -> + {ok, {normal, Integer}}; + false -> + {ok, {reverse, abs(Integer)}} end. -make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) -> +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(Room) -> - ?XE(<<"tr">>, [?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")], + 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), - PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), - ?H1GL(PageTitle, <<"modules/#mod-muc">>, <<"mod_muc">>) ++ + 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) - ] - ) - ]. + [?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) + normal -> + Rooms2; + reverse -> + lists:reverse(Rooms2) end. build_info_rooms(Rooms) -> @@ -637,16 +1155,16 @@ build_info_room({Name, Host, _ServerHost, Pid}) -> Num_participants = maps:size(S#state.users), Node = node(Pid), - History = (S#state.history)#lqueue.queue, + 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, + 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, @@ -664,6 +1182,7 @@ get_queue_last(Queue) -> prepare_rooms_infos(Rooms) -> [prepare_room_info(Room) || Room <- Rooms]. + prepare_room_info(Room_info) -> {NameHost, Num_participants, @@ -673,7 +1192,8 @@ prepare_room_info(Room_info) -> Logging, Just_created, Title, - Node} = Room_info, + Node} = + Room_info, [NameHost, integer_to_binary(Num_participants), Ts_last_message, @@ -688,10 +1208,61 @@ 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]); + [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 %%---------------------------- @@ -702,43 +1273,26 @@ create_room(Name1, Host1, ServerHost) -> create_room_with_opts(Name1, Host1, ServerHost, []). create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) -> - case {jid:nodeprep(Name1), jid:nodeprep(Host1), jid:nodeprep(ServerHost1)} of - {error, _, _} -> - throw({error, "Invalid 'name'"}); - {_, error, _} -> - throw({error, "Invalid 'host'"}); - {_, _, error} -> - throw({error, "Invalid 'serverhost'"}); - {Name, Host, ServerHost} -> - case get_room_pid(Name, Host) of - room_not_found -> - %% 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 -> - maybe_store_room(ServerHost, Host, Name, RoomOpts); - {error, _} -> - throw({error, "Unable to start room"}) - end; - invalid_service -> - throw({error, "Invalid 'service'"}); - _ -> - throw({error, "Room already exists"}) - end - end. - -maybe_store_room(ServerHost, Host, Name, RoomOpts) -> - case proplists:get_bool(persistent, RoomOpts) of - true -> - {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts), - ok; - false -> - ok + 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. @@ -752,21 +1306,14 @@ muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> %% 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 {jid:nodeprep(Name1), jid:nodeprep(Service1)} of - {error, _} -> - throw({error, "Invalid 'name'"}); - {_, error} -> - throw({error, "Invalid 'service'"}); - {Name, Service} -> - case get_room_pid(Name, Service) of - room_not_found -> - throw({error, "Room doesn't exists"}); - invalid_service -> - throw({error, "Invalid 'service'"}); - Pid -> - mod_muc_room:destroy(Pid), - ok - end + 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}) -> @@ -828,11 +1375,31 @@ create_rooms_file(Filename) -> RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), - %% Read the default room options defined for the first virtual host - DefRoomOpts = mod_muc_opt:default_room_options(ejabberd_config:get_myname()), - [muc_create_room(ejabberd_config:get_myname(), A, DefRoomOpts) || A <- Rooms], + 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 @@ -851,6 +1418,10 @@ rooms_empty_list(Service) -> 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), @@ -1033,8 +1604,8 @@ act_on_room(_Method, list, _) -> %%---------------------------- get_room_occupants(Room, Host) -> - case get_room_pid(Room, Host) of - Pid when is_pid(Pid) -> get_room_occupants(Pid); + case get_room_pid_validate(Room, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> get_room_occupants(Pid); _ -> throw({error, room_not_found}) end. @@ -1049,8 +1620,8 @@ get_room_occupants(Pid) -> maps:to_list(S#state.users)). get_room_occupants_number(Room, Host) -> - case get_room_pid(Room, Host) of - Pid when is_pid(Pid )-> + 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; _ -> @@ -1062,24 +1633,28 @@ get_room_occupants_number(Room, Host) -> %%---------------------------- %% http://xmpp.org/extensions/xep-0249.html -send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) -> +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, UsersString), + Users = get_users_to_invite(RoomJid, UsersStrings), [send_direct_invitation(RoomJid, UserJid, XmlEl) || UserJid <- Users], ok end. -get_users_to_invite(RoomJid, UsersString) -> - UsersStrings = binary:split(UsersString, <<":">>, [global]), +get_users_to_invite(RoomJid, UsersStrings) -> OccupantsTuples = get_room_occupants(RoomJid#jid.luser, RoomJid#jid.lserver), - OccupantsJids = [jid:decode(JidString) - || {JidString, _Nick, _} <- OccupantsTuples], + 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), @@ -1124,12 +1699,12 @@ send_direct_invitation(FromJid, UserJid, Msg) -> %% For example: %% `change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)' change_room_option(Name, Service, OptionString, ValueString) -> - case get_room_pid(Name, Service) of - room_not_found -> + case get_room_pid_validate(Name, Service, <<"service">>) of + {room_not_found, _, _} -> throw({error, "Room not found"}); - invalid_service -> - throw({error, "Invalid 'service'"}); - Pid -> + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + {Pid, _, _} -> {Option, Value} = format_room_option(OptionString, ValueString), change_room_option(Pid, Option, Value) end. @@ -1154,27 +1729,129 @@ format_room_option(OptionString, ValueString) -> password -> ValueString; subject ->ValueString; subject_author ->ValueString; - presence_broadcast ->misc:expr_to_term(ValueString); - max_users -> binary_to_integer(ValueString); - voice_request_min_interval -> binary_to_integer(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; - _ -> misc:binary_to_atom(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 | invalid_service. +-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 -> + {error, notfound} -> room_not_found; + {error, db_failure} -> + db_failure; {ok, Pid} -> Pid end @@ -1182,7 +1859,7 @@ get_room_pid(Name, Service) -> error:{invalid_domain, _} -> invalid_service; error:{unregistered_route, _} -> - invalid_service + unknown_service end. room_diagnostics(Name, Service) -> @@ -1204,7 +1881,7 @@ room_diagnostics(Name, Service) -> error:{invalid_domain, _} -> invalid_service; error:{unregistered_route, _} -> - invalid_service + unknown_service end. %% It is required to put explicitly all the options because @@ -1213,7 +1890,7 @@ room_diagnostics(Name, Service) -> change_option(Option, Value, Config) -> case Option of allow_change_subj -> Config#config{allow_change_subj = Value}; - allow_private_messages -> Config#config{allow_private_messages = 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}; @@ -1224,6 +1901,7 @@ change_option(Option, Value, Config) -> 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}; @@ -1249,8 +1927,8 @@ change_option(Option, Value, Config) -> %%---------------------------- get_room_options(Name, Service) -> - case get_room_pid(Name, Service) of - Pid when is_pid(Pid) -> get_room_options(Pid); + case get_room_pid_validate(Name, Service, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> get_room_options(Pid); _ -> [] end. @@ -1272,11 +1950,11 @@ get_options(Config) -> %%---------------------------- %% @spec(Name::binary(), Service::binary()) -> -%% [{JID::string(), Domain::string(), Role::string(), Reason::string()}] +%% [{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(Name, Service) of - Pid when is_pid(Pid) -> + 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), @@ -1286,6 +1964,51 @@ get_room_affiliations(Name, Service) -> ({{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. @@ -1299,20 +2022,25 @@ get_room_affiliations(Name, Service) -> %% @doc Get affiliation of a user in the room Name@Service. get_room_affiliation(Name, Service, JID) -> - case get_room_pid(Name, 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); - _ -> - throw({error, "The room does not exist."}) - end. + 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() @@ -1331,8 +2059,8 @@ set_room_affiliation(Name, Service, JID, AffiliationString) -> _ -> throw({error, "Invalid affiliation"}) end, - case get_room_pid(Name, Service) of - Pid when is_pid(Pid) -> + 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, _} -> @@ -1342,27 +2070,33 @@ set_room_affiliation(Name, Service, JID, AffiliationString) -> {error, _} -> throw({error, "Unable to perform change"}) end; - room_not_found -> - throw({error, "Room doesn't exists"}); - invalid_service -> - throw({error, "Invalid 'service'"}) + {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) -> +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(Name, Host) of - Pid when is_pid(Pid) -> + 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} -> @@ -1370,6 +2104,8 @@ subscribe_room(User, Nick, Room, Nodes) -> {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 @@ -1382,6 +2118,10 @@ subscribe_room(User, Nick, Room, Nodes) -> 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 @@ -1394,19 +2134,25 @@ subscribe_room_many(Users, 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(Name, Host) of - Pid when is_pid(Pid) -> + 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 @@ -1420,10 +2166,12 @@ unsubscribe_room(User, Room) -> end. get_subscribers(Name, Host) -> - case get_room_pid(Name, Host) of - Pid when is_pid(Pid) -> + 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. @@ -1432,11 +2180,85 @@ get_subscribers(Name, Host) -> %% 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( @@ -1494,5 +2316,5 @@ mod_doc() -> note => "added in 22.05", desc => ?T("How many users can be subscribed to a room at once using " - "the 'subscribe_room_many' command. " + "the _`subscribe_room_many`_ API. " "The default value is '50'.")}}]}. diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 0ee493b94..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-2022 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,7 +25,7 @@ -module(mod_muc_log). --protocol({xep, 334, '0.2'}). +-protocol({xep, 334, '0.2', '15.09', "complete", ""}). -author('badlop@process-one.net'). @@ -34,8 +34,8 @@ -behaviour(gen_mod). %% API --export([start/2, stop/1, reload/3, get_url/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]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, @@ -82,7 +82,9 @@ 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 @@ -90,16 +92,18 @@ check_access_log(Host, From) -> Res -> Res end. --spec get_url(#state{}) -> {ok, binary()} | error. -get_url(#state{room = Room, host = Host, server_host = ServerHost}) -> +-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, <>}; + {ok, <>}; room_name -> - {ok, <>} + {ok, <>} end catch error:{module_not_loaded, _, _} -> @@ -115,6 +119,9 @@ depends(_Host, _Opts) -> 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)}. handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> @@ -137,7 +144,11 @@ handle_cast(Msg, State) -> handle_info(_Info, State) -> {noreply, State}. -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. code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -443,11 +454,13 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, {_, _, Microsecs} = Now, STimeUnique = io_lib:format("~ts.~w", [STime, Microsecs]), + maybe_print_jl(open, F, Message, FileFormat), fw(F, io_lib:format("[~ts] ", [STimeUnique, STimeUnique, STimeUnique, STime]) ++ Text, FileFormat), + maybe_print_jl(close, F, Message, FileFormat), file:close(F), ok. @@ -583,7 +596,7 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, "class=\"nav\" href=\"~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; @@ -598,6 +611,7 @@ 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]) @@ -663,6 +677,35 @@ put_room_occupants(F, RoomOccupants, Lang, [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) -> @@ -783,6 +826,12 @@ roomconfig_to_string(Options, Lang, FileFormat) -> (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, "\"">> end end, @@ -811,8 +860,8 @@ 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(allow_private_messages, Lang) -> - tr(Lang, ?T("Allow users to send private messages")); +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) -> @@ -898,6 +947,11 @@ get_room_occupants(RoomJIDString) -> [] end. +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) -> @@ -1043,7 +1097,7 @@ mod_doc() -> {dirname, #{value => "room_jid | room_name", desc => - ?T("Allows to configure the name of the room directory. " + ?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 " diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl index 18b1e74ef..02ecb3ce8 100644 --- a/src/mod_muc_mnesia.erl +++ b/src/mod_muc_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -57,6 +57,10 @@ init(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. @@ -72,9 +76,11 @@ store_room(_LServer, Host, Name, Opts, _) -> mnesia:transaction(F). restore_room(_LServer, Host, Name) -> - case catch mnesia:dirty_read(muc_room, {Name, Host}) of + 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) -> @@ -82,13 +88,19 @@ forget_room(_LServer, Host, Name) -> end, mnesia:transaction(F). -can_use_nick(_LServer, Host, JID, Nick) -> +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, _ = '_'}, - [{'==', {element, 2, '$1'}, Host}], + MatchSpec, ['$_']}]) of {'EXIT', _Reason} -> true; @@ -110,31 +122,46 @@ get_nick(_LServer, Host, From) -> [#muc_registered{nick = Nick}] -> Nick end. -set_nick(_LServer, Host, From, Nick) -> +set_nick(_LServer, ServiceOrRoom, From, Nick) -> {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, F = fun () -> case Nick of <<"">> -> - mnesia:delete({muc_registered, {LUS, Host}}), + 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, - _ = '_'}, - [{'==', {element, 2, '$1'}, - Host}], + [{#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, Host}, + us_host = {LUS, ServiceOrRoom}, nick = Nick}), ok; true -> @@ -401,6 +428,19 @@ 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", []), @@ -408,9 +448,48 @@ need_transform({muc_registered, {{U, S}, H}, Nick}) need_transform(_) -> false. -transform(#muc_room{name_host = {N, H}, opts = Opts} = R) -> +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)}, 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 index c22a9594d..d4550da1a 100644 --- a/src/mod_muc_opt.erl +++ b/src/mod_muc_opt.erl @@ -86,7 +86,7 @@ db_type(Opts) when is_map(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' | 'true' | 'undefined' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer() | tuple()}]. +-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) -> @@ -212,7 +212,7 @@ ram_db_type(Opts) when is_map(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()) -> <<>> | re:mp(). +-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) -> diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 79d7da928..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-2022 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,8 @@ -author('alexey@process-one.net'). --protocol({xep, 317, '0.1'}). +-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). @@ -73,14 +74,19 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -include("mod_muc_room.hrl"). --include("ejabberd_stacktrace.hrl"). + -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). --define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>). --define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>). --define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>). +-define(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). @@ -115,6 +121,10 @@ -callback search_affiliation(binary(), binary(), binary(), affiliation()) -> {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}. +-ifndef(OTP_BELOW_28). +-dialyzer([no_opaque_union]). +-endif. + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- @@ -291,17 +301,18 @@ get_disco_item(Pid, Filter, JID, Lang) -> init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts, QueueType]) -> process_flag(trap_exit, true), + misc:set_proc_label({?MODULE, Room, Host}), Shaper = ejabberd_shaper:new(RoomShaper), RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), - State = set_affiliation(Creator, owner, - #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_opts(DefRoomOpts, State), + 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)]), @@ -313,22 +324,43 @@ init([Host, ServerHost, Access, Room, HistorySize, {ok, normal_state, reset_hibernate_timer(State1)}; init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) -> process_flag(trap_exit, true), + 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, QueueType), - jid = jid:make(Room, Host), + jid = Jid, room_queue = RoomQueue, room_shaper = Shaper}), add_to_log(room_existence, started, 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(State1)}. + {ok, normal_state, reset_hibernate_timer(State2)}. normal_state({route, <<"">>, #message{from = From, type = Type, lang = Lang} = Packet}, @@ -453,6 +485,8 @@ normal_state({route, <<"">>, [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}; @@ -478,6 +512,19 @@ normal_state({route, <<"">>, 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"), @@ -502,7 +549,7 @@ normal_state({route, <<"">>, case NewStateData of stop -> Conf = StateData#state.config, - {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; _ when NewStateData#state.just_created -> close_room_if_temporary_and_empty(NewStateData); _ -> @@ -561,7 +608,7 @@ normal_state({route, ToNick, forget_message -> {next_state, normal_state, StateData}; continue_delivery -> - case {(StateData#state.config)#config.allow_private_messages, + 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 @@ -590,15 +637,22 @@ normal_state({route, ToNick, jid:replace_resource(StateData#state.jid, FromNick), X = #muc_user{}, - PrivMsg = xmpp:set_from( - xmpp:set_subtag(Packet, X), - FromNickJID), - lists:foreach( - fun(ToJID) -> - ejabberd_router:route(xmpp:set_to(PrivMsg, ToJID)) - end, ToJIDs); + 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 = ?T("It is not allowed to send private messages"), + ErrText = ?T("You are not allowed to send private messages"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Packet, Err) end @@ -609,7 +663,7 @@ normal_state({route, ToNick, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(Packet, Err); {false, _} -> - ErrText = ?T("It is not allowed to send private messages"), + ErrText = ?T("You are not allowed to send private messages"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Packet, Err) end, @@ -632,6 +686,10 @@ normal_state({route, ToNick, 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)); @@ -687,7 +745,7 @@ handle_event({destroy, Reason}, _StateName, [jid:encode(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), Conf = StateData#state.config, - {stop, shutdown, StateData#state{config = Conf#config{persistent = false}}}; + {stop, shutdown, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; handle_event(destroy, StateName, StateData) -> ?INFO_MSG("Destroyed MUC room ~ts", [jid:encode(StateData#state.jid)]), @@ -778,20 +836,24 @@ handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, 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}, + 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}, + 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}, + password_protected = PasswordProtected, + members_only = MembersOnly}, {reply, {error, ?T("Request is ignored")}, NewState#state{config = NewConfig}}; {error, Err} -> @@ -803,7 +865,7 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName, from = From, sub_els = [#muc_unsubscribe{}]}, case process_iq_mucsub(From, IQ, StateData) of {result, _, stop} -> - {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; {result, _, NewState} -> {reply, ok, StateName, NewState}; {ignore, NewState} -> @@ -962,16 +1024,18 @@ terminate(Reason, _StateName, _ -> 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 -> + 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 ?EX_RULE(E, R, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Got exception on room termination:~n** ~ts", - [misc:format_exception(2, E, R, StackTrace)]) + catch + E:R:StackTrace -> + ?ERROR_MSG("Got exception on room termination:~n** ~ts", + [misc:format_exception(2, E, R, StackTrace)]) end. %%%---------------------------------------------------------------------- @@ -997,12 +1061,8 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData not Moderated, IsSubscriber} of {true, _} -> true; {_, true} -> - case get_default_role(get_affiliation(From, StateData), - StateData) of - moderator -> true; - participant -> true; - _ -> false - end; + % We assume all subscribers are at least members + true; _ -> false end, @@ -1021,7 +1081,7 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData true -> NSD = StateData#state{subject = Subject, - subject_author = FromNick}, + subject_author = {FromNick, From}}, store_room(NSD), {NSD, true}; _ -> {StateData, false} @@ -1038,24 +1098,26 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData drop -> {next_state, normal_state, StateData}; NewPacket1 -> - NewPacket = xmpp:put_meta(xmpp:remove_subtag(NewPacket1, #nick{}), + 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, NewStateData1), - NewStateData2 = case has_body_or_subject(NewPacket) of + NewPacket, Node, NewStateData2), + NewStateData3 = case has_body_or_subject(NewPacket) of true -> add_message_to_history(FromNick, From, NewPacket, - NewStateData1); + NewStateData2); false -> - NewStateData1 + NewStateData2 end, - {next_state, normal_state, NewStateData2} + {next_state, normal_state, NewStateData3} end; _ -> Err = case (StateData#state.config)#config.allow_change_subj of @@ -1087,6 +1149,58 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData {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( @@ -1211,12 +1325,13 @@ process_voice_approval(From, Pkt, VoiceApproval, StateData) -> StateData end. --spec direct_iq_type(iq()) -> vcard | ping | request | response | stanza_error(). +-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; @@ -1243,6 +1358,24 @@ is_user_allowed_message_nonparticipant(JID, _ -> false end. +-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. + %% @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()}. @@ -1290,7 +1423,7 @@ do_process_presence(Nick, #presence{from = From, type = available, lang = Lang} true -> case {nick_collision(From, Nick, StateData), mod_muc:can_use_nick(StateData#state.server_host, - StateData#state.host, + jid:encode(StateData#state.jid), From, Nick), {(StateData#state.config)#config.allow_visitor_nickchange, is_visitor(From, StateData)}} of @@ -1609,13 +1742,23 @@ set_affiliations_fallback(Affiliations, StateData) -> -spec get_affiliation(ljid() | jid(), state()) -> affiliation(). get_affiliation(#jid{} = JID, StateData) -> case get_service_affiliation(JID, StateData) of - owner -> - owner; - none -> - case do_get_affiliation(JID, StateData) of - {Affiliation, _Reason} -> Affiliation; - Affiliation -> Affiliation - end + 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). @@ -1730,11 +1873,13 @@ set_role(JID, Role, StateData) -> 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 - none -> - StateData#state.roles; + %% 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, @@ -2096,11 +2241,22 @@ get_priority_from_presence(#presence{priority = Prio}) -> _ -> Prio end. --spec find_nick_by_jid(jid(), state()) -> binary(). +-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), - #user{nick = Nick} = maps:get(LJID, StateData#state.users), - Nick. + 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) -> @@ -2151,7 +2307,7 @@ add_new_user(From, Nick, Packet, StateData) -> andalso NConferences < MaxConferences), Collision, mod_muc:can_use_nick(StateData#state.server_host, - StateData#state.host, From, Nick), + jid:encode(StateData#state.jid), From, Nick), get_occupant_initial_role(From, Affiliation, StateData)} of {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> @@ -2317,6 +2473,10 @@ 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, 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 @@ -2830,6 +2990,37 @@ add_message_to_history(FromNick, FromJID, Packet, StateData) -> StateData#state{just_created = erlang:system_time(microsecond)} end. +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( @@ -2842,14 +3033,24 @@ send_history(JID, History, StateData) -> end, History). -spec send_subject(jid(), state()) -> ok. -send_subject(JID, #state{subject_author = Nick} = StateData) -> +send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) -> Subject = case StateData#state.subject of [] -> [#text{}]; [_|_] = S -> S end, - Packet = #message{from = jid:replace_resource(StateData#state.jid, Nick), + Packet = #message{from = AuthorJID, to = JID, type = groupchat, subject = Subject}, - ejabberd_router:route(Packet). + 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. -spec check_subject(message()) -> [text()]. check_subject(#message{subject = [_|_] = Subj, body = [], @@ -3029,6 +3230,7 @@ process_item_change(Item, SD, UJID) -> 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); _ -> @@ -3045,11 +3247,12 @@ process_item_change(Item, SD, UJID) -> {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_affiliation(JID, outcast, set_role(JID, none, SD2), Reason); + 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), @@ -3072,19 +3275,44 @@ process_item_change(Item, SD, UJID) -> maybe_send_affiliation(JID, A, SD1), SD1 end - catch ?EX_RULE(E, R, St) -> - StackTrace = ?EX_STACK(St), - 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()} + 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(), @@ -3379,7 +3607,7 @@ send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users), - ActorNick = get_actor_nick(MJID, StateData), + ActorNick = find_nick_by_jid(MJID, StateData), %% TODO: optimize further UserMap = maps:merge( @@ -3422,15 +3650,6 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, end end, ok, UserMap). --spec get_actor_nick(undefined | jid(), state()) -> binary(). -get_actor_nick(undefined, _StateData) -> - <<"">>; -get_actor_nick(MJID, StateData) -> - try maps:get(jid:tolower(MJID), StateData#state.users) of - #user{nick = ActorNick} -> ActorNick - catch _:{badkey, _} -> <<"">> - end. - -spec convert_legacy_fields([xdata_field()]) -> [xdata_field()]. convert_legacy_fields(Fs) -> lists:map( @@ -3541,8 +3760,10 @@ is_allowed_log_change(Options, StateData, From) -> false -> true; true -> allow == - mod_muc_log:check_access_log(StateData#state.server_host, - From) + ejabberd_hooks:run_fold(muc_log_check_access_log, + StateData#state.server_host, + deny, + [StateData#state.server_host, From]) end. -spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean(). @@ -3657,7 +3878,7 @@ get_config(Lang, StateData, From) -> {moderatedroom, Config#config.moderated}, {members_by_default, Config#config.members_by_default}, {changesubject, Config#config.allow_change_subj}, - {allow_private_messages, Config#config.allow_private_messages}, + {allowpm, Config#config.allowpm}, {allow_private_messages_from_visitors, Config#config.allow_private_messages_from_visitors}, {allow_query_users, Config#config.allow_query_users}, @@ -3681,7 +3902,10 @@ get_config(Lang, StateData, From) -> [] end ++ - case mod_muc_log:check_access_log(StateData#state.server_host, From) of + 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, @@ -3728,8 +3952,8 @@ set_config(Opts, Config, ServerHost, Lang) -> ({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}; - ({allow_private_messages, V}, C) -> - C#config{allow_private_messages = 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}; @@ -3861,14 +4085,23 @@ remove_nonmembers(StateData) -> end, StateData, get_users_and_subscribers(StateData)). -spec set_opts([{atom(), any()}], state()) -> state(). -set_opts([], StateData) -> +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_opts([{vcard, Val} | Opts], 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_opts([{vcard, ValRaw} | Opts], StateData); -set_opts([{Opt, Val} | Opts], StateData) -> + set_opts2([{vcard, ValRaw} | Opts], StateData); +set_opts2([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> StateData#state{config = @@ -3886,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 = @@ -4017,7 +4250,7 @@ set_opts([{Opt, Val} | Opts], StateData) -> end, muc_subscribers_new(), Val), StateData#state{muc_subscribers = MUCSubscribers}; affiliations -> - StateData#state{affiliations = maps:from_list(Val)}; + set_affiliations(maps:from_list(Val), StateData); roles -> StateData#state{roles = maps:from_list(Val)}; subject -> @@ -4026,17 +4259,20 @@ set_opts([{Opt, Val} | Opts], StateData) -> is_list(Val) -> Val end, StateData#state{subject = Subj}; - subject_author -> StateData#state{subject_author = Val}; + 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 -> - Hats = maps:from_list( - lists:map(fun({U, H}) -> {U, maps:from_list(H)} end, - Val)), - StateData#state{hats_users = Hats}; + 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). -spec set_vcard_xupdate(state()) -> state(). set_vcard_xupdate(#state{config = @@ -4080,7 +4316,7 @@ make_opts(StateData, Hibernation) -> [?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.allow_private_messages), + ?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), @@ -4111,9 +4347,8 @@ make_opts(StateData, Hibernation) -> {roles, maps:to_list(StateData#state.roles)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author}, - {hats_users, - lists:map(fun({U, H}) -> {U, maps:to_list(H)} end, - maps:to_list(StateData#state.hats_users))}, + {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}]. @@ -4135,7 +4370,7 @@ expand_opts(CompactOpts) -> {Pos+1, [{Field, Val}|Opts]} end end, {2, []}, Fields), - SubjectAuthor = proplists:get_value(subject_author, CompactOpts, <<"">>), + 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), @@ -4201,10 +4436,14 @@ maybe_forget_room(StateData) -> end). -spec make_disco_info(jid(), state()) -> disco_info(). -make_disco_info(_From, StateData) -> +make_disco_info(From, StateData) -> Config = StateData#state.config, + 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), @@ -4217,10 +4456,24 @@ make_disco_info(_From, StateData) -> <<"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} -> @@ -4244,6 +4497,7 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang, DiscoInfo = make_disco_info(From, StateData), Extras = iq_disco_info_extras(Lang, StateData, false), {result, DiscoInfo#disco_info{xdata = [Extras]}}; + process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = ?NS_COMMANDS}]}, StateData) -> @@ -4261,9 +4515,25 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang, Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; + process_iq_disco_info(From, #iq{type = get, lang = Lang, - sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]}, - StateData) -> + 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, + case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of @@ -4273,48 +4543,13 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang, identities = [#identity{category = <<"automation">>, type = <<"command-node">>, name = translate:translate( - Lang, ?T("Add a hat to a user"))}], - features = [?NS_COMMANDS]}}; - false -> - Txt = ?T("Node not found"), - {error, xmpp:err_item_not_found(Txt, Lang)} - end; -process_iq_disco_info(From, #iq{type = get, lang = Lang, - sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]}, - StateData) -> - case (StateData#state.config)#config.enable_hats andalso - is_admin(From, StateData) - of - true -> - {result, - #disco_info{ - identities = [#identity{category = <<"automation">>, - type = <<"command-node">>, - name = translate:translate( - Lang, ?T("Remove a hat from a user"))}], - features = [?NS_COMMANDS]}}; - false -> - Txt = ?T("Node not found"), - {error, xmpp:err_item_not_found(Txt, Lang)} - end; -process_iq_disco_info(From, #iq{type = get, lang = Lang, - sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]}, - StateData) -> - case (StateData#state.config)#config.enable_hats andalso - is_admin(From, StateData) - of - true -> - {result, - #disco_info{ - identities = [#identity{category = <<"automation">>, - type = <<"command-node">>, - name = translate:translate( - Lang, ?T("List users with hats"))}], + 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) -> @@ -4334,20 +4569,12 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang, -spec iq_disco_info_extras(binary(), state(), boolean()) -> xdata(). iq_disco_info_extras(Lang, StateData, Static) -> Config = StateData#state.config, - AllowPM = case Config#config.allow_private_messages of - false -> none; - true -> - case Config#config.allow_private_messages_from_visitors of - nobody -> participants; - _ -> anyone - end - end, 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, AllowPM}, + {allowpm, Config#config.allowpm}, {lang, Config#config.lang}], Fs2 = case Config#config.pubsub of Node when is_binary(Node), Node /= <<"">> -> @@ -4363,7 +4590,10 @@ iq_disco_info_extras(Lang, StateData, Static) -> end, Fs4 = case Config#config.logging of true -> - case mod_muc_log:get_url(StateData) of + case ejabberd_hooks:run_fold(muc_log_get_url, + StateData#state.server_host, + error, + [StateData]) of {ok, URL} -> [{logs, URL}|Fs3]; error -> @@ -4372,8 +4602,25 @@ iq_disco_info_extras(Lang, StateData, Static) -> 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(Fs4, Lang)}. + fields = muc_roominfo:encode(Fs7, Lang)}. -spec process_iq_disco_items(jid(), iq(), state()) -> {error, stanza_error()} | {result, disco_items()}. @@ -4406,15 +4653,27 @@ process_iq_disco_items(From, #iq{type = get, lang = Lang, {result, #disco_items{ items = [#disco_item{jid = StateData#state.jid, - node = ?MUC_HAT_ADD_CMD, + node = ?MUC_HAT_CREATE_CMD, name = translate:translate( - Lang, ?T("Add a hat to a user"))}, + Lang, ?T("Create a hat"))}, #disco_item{jid = StateData#state.jid, - node = ?MUC_HAT_REMOVE_CMD, + 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_LIST_CMD, + node = ?MUC_HAT_LISTUSERS_CMD, name = translate:translate( Lang, ?T("List users with hats"))}]}}; false -> @@ -4424,9 +4683,12 @@ process_iq_disco_items(From, #iq{type = get, lang = Lang, process_iq_disco_items(From, #iq{type = get, lang = Lang, sub_els = [#disco_items{node = Node}]}, StateData) - when Node == ?MUC_HAT_ADD_CMD; - Node == ?MUC_HAT_REMOVE_CMD; - Node == ?MUC_HAT_LIST_CMD -> + 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 @@ -4521,7 +4783,7 @@ process_iq_mucsub(From, {error, xmpp:err_conflict(ErrText, Lang)}; false -> case mod_muc:can_use_nick(StateData#state.server_host, - StateData#state.host, + jid:encode(StateData#state.jid), From, Nick) of false -> Err = case Nick of @@ -4693,270 +4955,490 @@ get_mucroom_disco_items(StateData) -> 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()}. + {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) -> +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 + 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, + #adhoc_command{lang = Lang2, + node = Node, + action = Action, + xdata = XData} = + Request, + Lang = + case Lang2 of + <<"">> -> + Lang1; + _ -> + Lang2 + end, case {Node, Action} of {_, cancel} -> {result, - xmpp_util:make_adhoc_response( - Request, - #adhoc_command{status = canceled, lang = Lang, - node = Node})}; - {?MUC_HAT_ADD_CMD, execute} -> - Form = - #xdata{ - title = translate:translate( - Lang, ?T("Add a hat to a user")), - type = form, - fields = - [#xdata_field{ - type = 'jid-single', - label = translate:translate(Lang, ?T("Jabber ID")), - required = true, - var = <<"jid">>}, - #xdata_field{ - type = 'text-single', - label = translate:translate(Lang, ?T("Hat title")), - var = <<"hat_title">>}, - #xdata_field{ - type = 'text-single', - label = translate:translate(Lang, ?T("Hat URI")), - required = true, - var = <<"hat_uri">>} - ]}, + 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 = executing, - xdata = Form})}; - {?MUC_HAT_ADD_CMD, complete} when XData /= undefined -> - JID = try - jid:decode(hd(xmpp_util:get_xdata_values( - <<"jid">>, XData))) - catch _:_ -> error - end, - URI = try - hd(xmpp_util:get_xdata_values( - <<"hat_uri">>, XData)) - catch _:_ -> error - end, - Title = case xmpp_util:get_xdata_values( - <<"hat_title">>, XData) of - [] -> <<"">>; - [T] -> T - end, - if - (JID /= error) and (URI /= error) -> - case add_hat(JID, URI, Title, StateData) of - {ok, NewStateData} -> - store_room(NewStateData), - send_update_presence( - JID, NewStateData, StateData), - {result, - xmpp_util:make_adhoc_response( - Request, - #adhoc_command{status = completed}), - NewStateData}; - {error, size_limit} -> - Txt = ?T("Hats limit exceeded"), - {error, xmpp:err_not_allowed(Txt, Lang)} - end; - true -> - {error, xmpp:err_bad_request()} - end; - {?MUC_HAT_ADD_CMD, complete} -> - {error, xmpp:err_bad_request()}; - {?MUC_HAT_ADD_CMD, _} -> - Txt = ?T("Incorrect value of 'action' attribute"), - {error, xmpp:err_bad_request(Txt, Lang)}; - {?MUC_HAT_REMOVE_CMD, execute} -> - Form = - #xdata{ - title = translate:translate( - Lang, ?T("Remove a hat from a user")), - type = form, - fields = - [#xdata_field{ - type = 'jid-single', - label = translate:translate(Lang, ?T("Jabber ID")), - required = true, - var = <<"jid">>}, - #xdata_field{ - type = 'text-single', - label = translate:translate(Lang, ?T("Hat URI")), - required = true, - var = <<"hat_uri">>} - ]}, - {result, - xmpp_util:make_adhoc_response( - Request, - #adhoc_command{ - status = executing, - xdata = Form})}; - {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined -> - JID = try - jid:decode(hd(xmpp_util:get_xdata_values( - <<"jid">>, XData))) - catch _:_ -> error - end, - URI = try - hd(xmpp_util:get_xdata_values( - <<"hat_uri">>, XData)) - catch _:_ -> error - end, - if - (JID /= error) and (URI /= error) -> - NewStateData = del_hat(JID, URI, StateData), - store_room(NewStateData), - send_update_presence( - JID, NewStateData, StateData), + 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}), + xmpp_util:make_adhoc_response(Request, + #adhoc_command{status = completed}), NewStateData}; - true -> + {error, XmlElement} -> + {error, XmlElement}; + error -> {error, xmpp:err_bad_request()} end; - {?MUC_HAT_REMOVE_CMD, complete} -> + {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()}; - {?MUC_HAT_REMOVE_CMD, _} -> - Txt = ?T("Incorrect value of 'action' attribute"), - {error, xmpp:err_bad_request(Txt, Lang)}; - {?MUC_HAT_LIST_CMD, execute} -> - Hats = get_all_hats(StateData), - Items = - lists:map( - fun({JID, URI, Title}) -> - [#xdata_field{ - var = <<"jid">>, - values = [jid:encode(JID)]}, - #xdata_field{ - var = <<"hat_title">>, - values = [URI]}, - #xdata_field{ - var = <<"hat_uri">>, - values = [Title]}] - end, Hats), - Form = - #xdata{ - title = translate:translate( - Lang, ?T("List of users with hats")), - type = result, - reported = - [#xdata_field{ - label = translate:translate(Lang, ?T("Jabber ID")), - var = <<"jid">>}, - #xdata_field{ - label = translate:translate(Lang, ?T("Hat title")), - var = <<"hat_title">>}, - #xdata_field{ - label = translate:translate(Lang, ?T("Hat URI")), - var = <<"hat_uri">>}], - items = Items}, - {result, - xmpp_util:make_adhoc_response( - Request, - #adhoc_command{ - status = completed, - xdata = Form})}; - {?MUC_HAT_LIST_CMD, _} -> + {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()} + _ -> + {error, xmpp:err_forbidden()} end. --spec add_hat(jid(), binary(), binary(), state()) -> - {ok, state()} | {error, size_limit}. -add_hat(JID, URI, Title, StateData) -> - Hats = StateData#state.hats_users, - LJID = jid:remove_resource(jid:tolower(JID)), - UserHats = maps:get(LJID, Hats, #{}), - UserHats2 = maps:put(URI, Title, UserHats), - USize = maps:size(UserHats2), - if - USize =< ?MAX_HATS_PER_USER -> - Hats2 = maps:put(LJID, UserHats2, Hats), - Size = maps:size(Hats2), - if - Size =< ?MAX_HATS_USERS -> - {ok, StateData#state{hats_users = Hats2}}; - true -> - {error, size_limit} - end; - true -> - {error, size_limit} - end. +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}. --spec del_hat(jid(), binary(), state()) -> state(). -del_hat(JID, URI, StateData) -> - Hats = StateData#state.hats_users, - LJID = jid:remove_resource(jid:tolower(JID)), - UserHats = maps:get(LJID, Hats, #{}), - UserHats2 = maps:remove(URI, UserHats), - Hats2 = - case maps:size(UserHats2) of - 0 -> - maps:remove(LJID, Hats); - _ -> - maps:put(LJID, UserHats2, Hats) +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, - StateData#state{hats_users = Hats2}. + 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. --spec get_all_hats(state()) -> list({jid(), binary(), binary()}). -get_all_hats(StateData) -> - lists:flatmap( - fun({LJID, H}) -> - JID = jid:make(LJID), - lists:map(fun({URI, Title}) -> {JID, URI, Title} end, - maps:to_list(H)) - end, - maps:to_list(StateData#state.hats_users)). +%% 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 + case StateData#state.config#config.enable_hats of true -> Hats = StateData#state.hats_users, - LJID = jid:remove_resource(jid:tolower(JID)), - UserHats = maps:get(LJID, Hats, #{}), - case maps:size(UserHats) of - 0 -> Pres; + LJID = + jid:remove_resource( + jid:tolower(JID)), + UserHats = maps:get(LJID, Hats, []), + case length(UserHats) of + 0 -> + Pres; _ -> Items = - lists:map(fun({URI, Title}) -> - #muc_hat{uri = URI, title = Title} + lists:map(fun(URI) -> + {URI, Title, Hue} = get_hat_details(URI, StateData), + #muc_hat{uri = URI, + title = Title, + hue = Hue} end, - maps:to_list(UserHats)), - xmpp:set_subtag(Pres, - #muc_hats{hats = Items}) + 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 @@ -5090,15 +5572,23 @@ handle_roommessage_from_nonparticipant(Packet, StateData, From) -> 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, false)); + 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, false)); + 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. @@ -5275,29 +5765,34 @@ wrap(From, To, Packet, Node, Id) -> -spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok. send_wrapped_multiple(From, Users, Packet, Node, State) -> - {Dir, Wra} = - maps:fold( - fun(_, #user{jid = To, last_presence = LP}, {Direct, Wrapped} = Res) -> - IsOffline = LP == undefined, - if IsOffline -> - LBareTo = jid:tolower(jid:remove_resource(To)), - case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of - {ok, #subscriber{nodes = Nodes}} -> - case lists:member(Node, Nodes) of - true -> - {Direct, [To | Wrapped]}; - _ -> - %% TODO: check that this branch is never called - Res - end; - _ -> - Res - end; - true -> - {[To | Direct], Wrapped} - end - end, {[],[]}, Users), - case Dir of + {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 @@ -5310,7 +5805,9 @@ send_wrapped_multiple(From, Users, Packet, Node, State) -> not lists:member(303, Codes)) of true -> ejabberd_router_multicast:route_multicast( - From, State#state.server_host, Dir, + From, + State#state.server_host, + DirAll, #presence{id = p1_rand:get_string(), type = unavailable}, false); false -> @@ -5322,8 +5819,27 @@ send_wrapped_multiple(From, Users, Packet, Node, State) -> _ -> ok end, - ejabberd_router_multicast:route_multicast(From, State#state.server_host, - Dir, Packet, false) + 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; 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 index 3fed0bf2b..31c8703c1 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -42,6 +42,7 @@ 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"). @@ -52,6 +53,7 @@ %%% 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); @@ -59,6 +61,82 @@ init(Host, Opts) -> 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}; @@ -142,10 +220,12 @@ restore_room(LServer, Host, Name) -> Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}), mod_muc:opts_to_binary(Opts2); _ -> - error + {error, db_failure} end; + {selected, _} -> + error; _ -> - error + {error, db_failure} end. forget_room(LServer, Host, Name) -> @@ -159,13 +239,19 @@ forget_room(LServer, Host, Name) -> end, ejabberd_sql:sql_transaction(LServer, F). -can_use_nick(LServer, Host, JID, Nick) -> +can_use_nick(LServer, ServiceOrRoom, JID, Nick) -> SJID = jid:encode(jid:tolower(jid:remove_resource(JID))), - case catch ejabberd_sql:sql_query( - LServer, - ?SQL("select @(jid)s from muc_registered " - "where nick=%(Nick)s" - " and host=%(Host)s")) of + 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. @@ -258,28 +344,57 @@ get_nick(LServer, Host, From) -> _ -> error end. -set_nick(LServer, Host, From, Nick) -> +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=%(Host)s")), + " jid=%(JID)s and host=%(ServiceOrRoom)s")), ok; _ -> - Allow = case ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s from muc_registered" - " where nick=%(Nick)s" - " and host=%(Host)s")) of - {selected, [{J}]} -> J == JID; - _ -> true + 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=%(Host)s", + "!host=%(ServiceOrRoom)s", "server_host=%(LServer)s", "nick=%(Nick)s"]), ok; diff --git a/src/mod_muc_sup.erl b/src/mod_muc_sup.erl index 2c2d62313..744e20c45 100644 --- a/src/mod_muc_sup.erl +++ b/src/mod_muc_sup.erl @@ -2,7 +2,7 @@ %%% Created : 4 Jul 2019 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_multicast.erl b/src/mod_multicast.erl index a702f0f74..369d5f92d 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -5,7 +5,7 @@ %%% Created : 29 May 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -author('badlop@process-one.net'). --protocol({xep, 33, '1.1'}). +-protocol({xep, 33, '1.1', '15.04', "complete", ""}). -behaviour(gen_server). @@ -407,7 +407,7 @@ route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) -> route_single -> route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet); {route_multicast, Service, Limits} -> - route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) + route_multicast(From, jid:make(Service), CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) end end, ok, Groups). diff --git a/src/mod_offline.erl b/src/mod_offline.erl index a66ffb20d..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-2022 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,11 +27,12 @@ -author('alexey@process-one.net'). --protocol({xep, 13, '1.2'}). --protocol({xep, 22, '1.4'}). --protocol({xep, 23, '1.3'}). --protocol({xep, 160, '1.0'}). --protocol({xep, 334, '0.2'}). +-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). @@ -59,12 +60,17 @@ find_x_expire/2, c2s_handle_info/2, c2s_copy_session/2, - webadmin_page/3, + 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]). -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("logger.hrl"). @@ -120,51 +126,25 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, - store_packet, 50), - ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), - ejabberd_hooks:add(remove_user, 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(disco_sm_identity, Host, - ?MODULE, get_sm_identity, 50), - ejabberd_hooks:add(disco_sm_items, Host, - ?MODULE, get_sm_items, 50), - ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 50), - ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), - ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 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), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE, - ?MODULE, handle_offline_query). + {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) -> - ejabberd_hooks:delete(offline_message_hook, Host, - ?MODULE, store_packet, 50), - ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), - ejabberd_hooks:delete(remove_user, 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(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), - ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 50), - ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50), - ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), - ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 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), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE). +stop(_Host) -> + ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -325,6 +305,15 @@ c2s_copy_session(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}, @@ -473,14 +462,17 @@ need_to_store(LServer, #message{type = Type} = Packet) -> _ -> true end, - case {Store, mod_offline_opt:store_empty_body(LServer)} of - {false, _} -> + case {misc:get_mucsub_event_type(Packet), Store, + mod_offline_opt:store_empty_body(LServer)} of + {?NS_MUCSUB_NODES_PRESENCE, _, _} -> false; - {_, true} -> + {_, false, _} -> + false; + {_, _, true} -> true; - {_, false} -> + {_, _, false} -> Packet#message.body /= []; - {_, unless_chat_state} -> + {_, _, unless_chat_state} -> not misc:is_standalone_chat_state(Packet) end end @@ -748,12 +740,39 @@ discard_warn_sender(Packet, Reason) -> ok 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. +%%% +%%% 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) -> [Packet || {_Seq, Packet} <- read_messages(LUser, LServer)]. @@ -957,8 +976,7 @@ count_mam_messages(LUser, LServer, ReadMsgs) -> format_user_queue(Hdrs) -> lists:map( - fun({Seq, From, To, TS, El}) -> - ID = integer_to_binary(Seq), + fun({_Seq, From, To, TS, El}) -> FPacket = ejabberd_web_admin:pretty_print_xml(El), SFrom = jid:encode(From), STo = jid:encode(To), @@ -974,14 +992,7 @@ format_user_queue(Hdrs) -> {_, _, _} = Now -> format_time(Now) end, - ?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)])]) + {Time, SFrom, STo, FPacket} end, Hdrs). format_time(Now) -> @@ -989,111 +1000,20 @@ format_time(Now) -> str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second]). -user_queue(User, Server, Query, Lang) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), - US = {LUser, LServer}, - Mod = gen_mod:db_mod(LServer, ?MODULE), - user_queue_parse_query(LUser, LServer, Query), - HdrsAll = case Mod:read_message_headers(LUser, LServer) of - error -> []; - L -> L - end, - Hdrs = get_messages_subset(User, Server, HdrsAll), - FMsgs = format_user_queue(Hdrs), - PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]), - (?H1GL(PageTitle, <<"modules/#mod-offline">>, <<"mod_offline">>)) - ++ [?XREST(?T("Submitted"))] ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XE(<<"table">>, - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?X(<<"td">>), ?XCT(<<"td">>, ?T("Time")), - ?XCT(<<"td">>, ?T("From")), - ?XCT(<<"td">>, ?T("To")), - ?XCT(<<"td">>, ?T("Packet"))])]), - ?XE(<<"tbody">>, - if FMsgs == [] -> - [?XE(<<"tr">>, - [?XAC(<<"td">>, [{<<"colspan">>, <<"4">>}], - <<" ">>)])]; - true -> FMsgs - end)]), - ?BR, - ?INPUTTD(<<"submit">>, <<"delete">>, - ?T("Delete Selected"))])]. - -user_queue_parse_query(LUser, LServer, Query) -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - case user_queue_parse_query(LUser, LServer, Query, Mod, false) of - true -> - flush_cache(Mod, LUser, LServer); - false -> - ok - end; - _ -> - ok - end. - -user_queue_parse_query(LUser, LServer, Query, Mod, Acc) -> - case lists:keytake(<<"selected">>, 1, Query) of - {value, {_, Seq}, Query2} -> - NewAcc = case catch binary_to_integer(Seq) of - I when is_integer(I), I>=0 -> - Mod:remove_message(LUser, LServer, I), - true; - _ -> - Acc - end, - user_queue_parse_query(LUser, LServer, Query2, Mod, NewAcc); - false -> - Acc - end. - us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). get_queue_length(LUser, LServer) -> count_offline_messages(LUser, LServer). -get_messages_subset(User, Host, MsgsAll) -> - MaxOfflineMsgs = case get_max_user_messages(User, Host) of - Number when is_integer(Number) -> Number; - _ -> 100 - end, - Length = length(MsgsAll), - get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll). +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_subset2(Max, Length, MsgsAll) when Length =< Max * 2 -> - MsgsAll; -get_messages_subset2(Max, Length, MsgsAll) -> - FirstN = Max, - {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), - MsgsLastN = lists:nthtail(Length - FirstN - FirstN, - Msgs2), - NoJID = jid:make(<<"...">>, <<"...">>), - Seq = <<"0">>, - IntermediateMsg = #xmlel{name = <<"...">>, attrs = [], - children = []}, - MsgsFirstN ++ [{Seq, NoJID, NoJID, IntermediateMsg}] ++ MsgsLastN. - -webadmin_user(Acc, User, Server, Lang) -> - QueueLen = count_offline_messages(jid:nodeprep(User), - jid:nameprep(Server)), - FQueueLen = ?C(integer_to_binary(QueueLen)), - FQueueView = ?AC(<<"queue/">>, - ?T("View Queue")), - Acc ++ - [?XCT(<<"h3">>, ?T("Offline Messages:")), - FQueueLen, - ?C(<<" | ">>), - FQueueView, - ?C(<<" | ">>), - ?INPUTTD(<<"submit">>, <<"removealloffline">>, - ?T("Remove All Offline Messages"))]. +%%% +%%% +%%% -spec delete_all_msgs(binary(), binary()) -> {atomic, any()}. delete_all_msgs(User, Server) -> @@ -1264,10 +1184,8 @@ mod_doc() -> "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("NOTE: 'ejabberdctl' has a command to " - "delete expired messages (see chapter " - "https://docs.ejabberd.im/admin/guide/managing" - "[Managing an ejabberd server] in online documentation.")], + ?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"), @@ -1276,16 +1194,16 @@ mod_doc() -> "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 " + "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 " + ?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 " + "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].")}}, @@ -1297,8 +1215,8 @@ mod_doc() -> {use_mam_for_storage, #{value => "true | false", desc => - ?T("This is an experimental option. Enabling this option, " - "'mod_offline' uses the 'mod_mam' archive table instead " + ?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 " @@ -1312,7 +1230,7 @@ mod_doc() -> {bounce_groupchat, #{value => "true | false", desc => - ?T("This option is use the disable an optimisation that " + ?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. " @@ -1322,7 +1240,7 @@ mod_doc() -> "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 optimisation is enabled.")}}, + "the optimization is enabled.")}}, {db_type, #{value => "mnesia | sql", desc => diff --git a/src/mod_offline_mnesia.erl b/src/mod_offline_mnesia.erl index 28a105dcf..24406c5ac 100644 --- a/src/mod_offline_mnesia.erl +++ b/src/mod_offline_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -217,7 +217,7 @@ need_transform(_) -> 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}; + 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)}, diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index 07978befd..9078b082c 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +31,7 @@ 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"). @@ -40,9 +41,28 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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, @@ -107,8 +127,8 @@ remove_old_messages(Days, LServer) -> of {updated, N} -> ?INFO_MSG("~p message(s) deleted from offline spool", [N]); - _Error -> - ?ERROR_MSG("Cannot delete message in offline spool: ~p", [_Error]) + Error -> + ?ERROR_MSG("Cannot delete message in offline spool: ~p", [Error]) end, {atomic, ok}. diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 825acf2cd..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-2022 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 @@ -author('bjc@kublai.com'). --protocol({xep, 199, '2.0'}). +-protocol({xep, 199, '2.0', '2.1.0', "complete", ""}). -behaviour(gen_mod). @@ -49,14 +49,13 @@ -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]). --export([iq_ping/1, user_online/3, user_offline/3, mod_doc/0, - user_send/1, mod_opt_type/1, mod_options/1, depends/2]). +-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 :: binary(), send_pings :: boolean(), ping_interval :: pos_integer(), - ping_ack_timeout :: undefined | non_neg_integer(), timeout_action :: none | kill, timers :: timers()}). @@ -167,13 +166,8 @@ handle_info({timeout, _TRef, {ping, JID}}, State) -> JID#jid.lresource) of none -> del_timer(JID, State#state.timers); - _ -> - Host = State#state.host, - From = jid:make(Host), - IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, - ejabberd_router:route_iq(IQ, JID, - gen_mod:get_module_proc(Host, ?MODULE), - State#state.ping_ack_timeout), + Pid -> + ejabberd_c2s:cast(Pid, send_ping), add_timer(JID, State#state.ping_interval, State#state.timers) end, @@ -214,19 +208,29 @@ 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), - PingAckTimeout = mod_ping_opt:ping_ack_timeout(Opts), TimeoutAction = mod_ping_opt:timeout_action(Opts), #state{host = Host, send_pings = SendPings, ping_interval = PingInterval, timeout_action = TimeoutAction, - ping_ack_timeout = PingAckTimeout, timers = #{}}. register_hooks(Host) -> @@ -235,7 +239,9 @@ register_hooks(Host) -> ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, user_offline, 100), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send, 100). + 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, @@ -243,7 +249,9 @@ unregister_hooks(Host) -> ejabberd_hooks:delete(sm_register_connection_hook, Host, ?MODULE, user_online, 100), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send, 100). + 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, @@ -315,7 +323,10 @@ mod_doc() -> #{value => "timeout()", desc => ?T("How long to wait before deeming that a client " - "has not answered a given server ping request. " + "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", @@ -341,9 +352,7 @@ mod_doc() -> "The default value is 'none'.")}}], example => ["modules:", - " ...", " mod_ping:", " send_pings: true", " ping_interval: 4 min", - " timeout_action: kill", - " ..."]}. + " timeout_action: kill"]}. diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index 80b8fb85f..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-2022 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,10 @@ -record(pres_counter, {dir, start, count, logged = false}). -start(Host, _Opts) -> - ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, - check_packet, 25), - ok. +start(_Host, _Opts) -> + {ok, [{hook, privacy_check_packet, check_packet, 25}]}. -stop(Host) -> - ejabberd_hooks:delete(privacy_check_packet, Host, - ?MODULE, check_packet, 25), +stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> @@ -154,8 +150,6 @@ mod_doc() -> ?T("The time interval. The default value is '1' minute.")}}], example => ["modules:", - " ...", " mod_pres_counter:", " count: 5", - " interval: 30 secs", - " ..."]}. + " interval: 30 secs"]}. diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index 4a4ebd5c5..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-2022 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 @@ -author('alexey@process-one.net'). --protocol({xep, 16, '1.6'}). +-protocol({xep, 16, '1.6', '0.5.0', "complete", ""}). -behaviour(gen_mod). @@ -40,8 +40,14 @@ import_start/2, import_stop/2, import/5, import_info/0, mod_opt_type/1, mod_options/1, depends/2]). +-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). + +-import(ejabberd_web_admin, [make_command/4, make_command/2]). + -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). -include("mod_privacy.hrl"). -include("translate.hrl"). @@ -73,32 +79,17 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - disco_features, 50), - ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, - c2s_copy_session, 50), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send_packet, 50), - ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, - check_packet, 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). + {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) -> - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, - disco_features, 50), - ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, - c2s_copy_session, 50), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send_packet, 50), - ejabberd_hooks:delete(privacy_check_packet, Host, - ?MODULE, check_packet, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_PRIVACY). +stop(_Host) -> + ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -857,6 +848,24 @@ 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) -> []. @@ -886,8 +895,8 @@ mod_doc() -> ?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' module. However, you still need " - "'mod_privacy' loaded in order for _`mod_blocking`_ to work.")], + "_`mod_blocking`_. However, you still need " + "'mod_privacy' loaded in order for 'mod_blocking' to work.")], opts => [{db_type, #{value => "mnesia | sql", diff --git a/src/mod_privacy_mnesia.erl b/src/mod_privacy_mnesia.erl index f2c6879f1..b8657e719 100644 --- a/src/mod_privacy_mnesia.erl +++ b/src/mod_privacy_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_privacy_sql.erl b/src/mod_privacy_sql.erl index 07c97ca7f..e0dc4476b 100644 --- a/src/mod_privacy_sql.erl +++ b/src/mod_privacy_sql.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +34,8 @@ -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"). @@ -42,9 +44,57 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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 -> @@ -107,16 +157,21 @@ set_lists(#privacy{us = {LUser, LServer}, set_list(LUser, LServer, Name, List) -> RItems = lists:map(fun item_to_raw/1, List), - F = fun () -> - ID = 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; - {selected, [{I}]} -> I - end, - set_privacy_list(ID, RItems) + 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). @@ -402,22 +457,59 @@ add_privacy_list(LUser, LServer, Name) -> "server_host=%(LServer)s", "name=%(Name)s"])). -set_privacy_list(ID, RItems) -> - ejabberd_sql:sql_query_t( - ?SQL("delete from privacy_list_data where id=%(ID)d")), +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). + 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( diff --git a/src/mod_private.erl b/src/mod_private.erl index f6cebbcda..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-2022 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,23 +27,32 @@ -author('alexey@process-one.net'). --protocol({xep, 49, '1.2'}). --protocol({xep, 411, '0.2.0'}). +-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, 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]). + 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("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"). -define(PRIVATE_CACHE, private_cache). @@ -62,23 +71,18 @@ start(Host, Opts) -> 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), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), - ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE, ?MODULE, process_sm_iq), - ejabberd_commands:register_commands(?MODULE, get_commands_spec()). + {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) -> - ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), - ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE), - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. +stop(_Host) -> + ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -123,7 +127,10 @@ mod_doc() -> "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]).")], + "[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", @@ -154,7 +161,7 @@ get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> get_sm_features(Acc, _From, To, <<"">>, _Lang) -> case gen_mod:is_loaded(To#jid.lserver, mod_pubsub) of true -> - {result, [?NS_BOOKMARKS_CONVERSION_0 | + {result, [?NS_BOOKMARKS_CONVERSION_0, ?NS_PEP_BOOKMARKS_COMPAT, ?NS_PEP_BOOKMARKS_COMPAT_PEP | case Acc of {result, Features} -> Features; empty -> [] @@ -211,17 +218,21 @@ filter_xmlels(Els) -> -spec set_data(jid(), [{binary(), xmlel()}]) -> ok | {error, _}. set_data(JID, Data) -> - set_data(JID, Data, true). + set_data(JID, Data, true, true). --spec set_data(jid(), [{binary(), xmlel()}], boolean()) -> ok | {error, _}. -set_data(JID, Data, Publish) -> +-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 Publish of - true -> publish_data(JID, 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 -> @@ -282,37 +293,218 @@ remove_user(User, Server) -> %%%=================================================================== %%% Pubsub %%%=================================================================== --spec publish_data(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. -publish_data(JID, Data) -> +-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} -> - 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 + 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. +-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. + +err_ret({error, _} = E, _) -> + E; +err_ret(ok, {error, _} = E) -> + E; +err_ret(_, _) -> + ok. + -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); + 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. +-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 %%%=================================================================== @@ -348,16 +540,37 @@ bookmarks_to_pep(User, Server) -> case Res of {ok, El} -> Data = [{?NS_STORAGE_BOOKMARKS, El}], - case publish_data(jid:make(User, Server), Data) of + case publish_pep_storage_bookmarks(jid:make(User, Server), Data) of ok -> - {ok, <<"Bookmarks exported to PEP node">>}; + 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 %%%=================================================================== diff --git a/src/mod_private_mnesia.erl b/src/mod_private_mnesia.erl index 5c789f0a7..42bab447f 100644 --- a/src/mod_private_mnesia.erl +++ b/src/mod_private_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_private_sql.erl b/src/mod_private_sql.erl index d2efbe98d..d493b0587 100644 --- a/src/mod_private_sql.erl +++ b/src/mod_private_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +28,7 @@ %% 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"). @@ -37,9 +38,28 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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( diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl index 9c8c9462e..d614c8a35 100644 --- a/src/mod_privilege.erl +++ b/src/mod_privilege.erl @@ -4,7 +4,7 @@ %%% Purpose : XEP-0356: Privileged Entity %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +25,7 @@ -author('amuhar3@gmail.com'). --protocol({xep, 0356, '0.2.1'}). +-protocol({xep, 356, '0.4.1', '24.10', "complete", ""}). -behaviour(gen_server). -behaviour(gen_mod). @@ -37,6 +37,7 @@ -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]). @@ -45,14 +46,17 @@ -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_permissions()} | - {presence, presence_permissions()} | - {message, message_permissions()}]. +-type access() :: [{roster, roster_permission()} | + {iq, [privilege_namespace()]} | + {presence, presence_permission()} | + {message, message_permission()}]. -type permissions() :: #{binary() => access()}. -record(state, {server_host = <<"">> :: binary()}). @@ -71,6 +75,10 @@ reload(_Host, _NewOpts, _OldOpts) -> 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()}); @@ -80,6 +88,7 @@ mod_opt_type(presence) -> mod_options(_) -> [{roster, [{both, none}, {get, none}, {set, none}]}, + {iq, []}, {presence, [{managed_entity, none}, {roster, none}]}, {message, [{outgoing,none}]}]. @@ -108,6 +117,7 @@ mod_doc() -> "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"), @@ -130,6 +140,27 @@ mod_doc() -> 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 => @@ -163,15 +194,16 @@ mod_doc() -> "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", - " ..."]}. + " outgoing: all"]}. depends(_, _) -> []. @@ -192,6 +224,10 @@ component_disconnected(Host, _Reason) -> 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, @@ -214,22 +250,99 @@ process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From, %% Component is disconnected ok end; + process_message(_Stanza) -> ok. --spec roster_access(boolean(), iq()) -> boolean(). -roster_access(true, _) -> - true; -roster_access(false, #iq{from = From, to = To, type = Type}) -> +%% +%% 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), - (Permission == both) - orelse (Permission == get andalso Type == get) - orelse (Permission == set andalso Type == set); + 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 @@ -306,6 +419,8 @@ init([Host|_]) -> 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) -> @@ -317,22 +432,26 @@ handle_cast({component_connected, Host}, State) -> 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; PresencePerm /= none; MessagePerm /= none -> + 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", - [Host, RosterPerm, PresencePerm, MessagePerm]), + "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)), @@ -363,6 +482,8 @@ 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, @@ -388,10 +509,15 @@ code_change(_OldVsn, State, _Extra) -> %%%=================================================================== -spec get_permissions(binary()) -> permissions(). get_permissions(ServerHost) -> - try ets:lookup_element(?MODULE, ServerHost, 2) - catch _:badarg -> #{} + 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, @@ -403,6 +529,9 @@ forward_message(#message{to = To} = Msg) -> #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), @@ -429,6 +558,44 @@ forward_message(#message{to = To} = Msg) -> 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), @@ -445,6 +612,26 @@ get_roster_permission(ServerHost, Host) -> 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), @@ -466,7 +653,12 @@ get_presence_permission(ServerHost, Host) -> 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) -> diff --git a/src/mod_privilege_opt.erl b/src/mod_privilege_opt.erl index 64198b387..36bf54efa 100644 --- a/src/mod_privilege_opt.erl +++ b/src/mod_privilege_opt.erl @@ -3,10 +3,17 @@ -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); 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 1ccb31978..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-2022 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 @@ -author('xram@jabber.ru'). --protocol({xep, 65, '1.8'}). +-protocol({xep, 65, '1.8', '2.0.0', "complete", ""}). -behaviour(gen_mod). @@ -237,23 +237,7 @@ mod_doc() -> "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 => - [{?T("For example, the following XML representation of vCard:"), - ["", - " Conferences", - " ", - " ", - " Elm Street", - " ", - ""]}, - {?T("will be translated to:"), - ["vcard:", - " fn: Conferences", - " adr:", - " -", - " work: true", - " street: Elm Street"]}]}}], + "the mapping is straightforward.")}}], example => ["acl:", " admin:", @@ -274,7 +258,6 @@ mod_doc() -> " proxyrate: 10240", "", "modules:", - " ...", " mod_proxy65:", " host: proxy1.example.org", " name: \"File Transfer Proxy\"", @@ -284,5 +267,4 @@ mod_doc() -> " access: proxy65_access", " shaper: proxy65_shaper", " recbuf: 10240", - " sndbuf: 10240", - " ..."]}. + " sndbuf: 10240"]}. diff --git a/src/mod_proxy65_lib.erl b/src/mod_proxy65_lib.erl index 6298d7bf0..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-2022 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 index 73f248d4c..3661b62c0 100644 --- a/src/mod_proxy65_mnesia.erl +++ b/src/mod_proxy65_mnesia.erl @@ -2,7 +2,7 @@ %%% Created : 16 Jan 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_redis.erl b/src/mod_proxy65_redis.erl index e93dce36a..588bd55f3 100644 --- a/src/mod_proxy65_redis.erl +++ b/src/mod_proxy65_redis.erl @@ -3,7 +3,7 @@ %%% Created : 31 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_service.erl b/src/mod_proxy65_service.erl index c6995482d..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-2022 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 @@ -40,7 +40,7 @@ -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). --include("ejabberd_stacktrace.hrl"). + -define(PROCNAME, ejabberd_mod_proxy65_service). @@ -86,8 +86,8 @@ terminate(_Reason, #state{myhosts = MyHosts}) -> handle_info({route, Packet}, State) -> try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), + catch + Class:Reason:StackTrace -> ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) diff --git a/src/mod_proxy65_sql.erl b/src/mod_proxy65_sql.erl index 6e69e5a0f..c05f055b7 100644 --- a/src/mod_proxy65_sql.erl +++ b/src/mod_proxy65_sql.erl @@ -3,7 +3,7 @@ %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +26,7 @@ %% 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"). @@ -34,6 +35,8 @@ %%% 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( @@ -47,6 +50,25 @@ init() -> 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), diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index 4e1eaf3fe..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-2022 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_pubsub.erl b/src/mod_pubsub.erl index 7b01c4af0..bd0aff20f 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -35,16 +35,18 @@ -behaviour(gen_mod). -behaviour(gen_server). -author('christophe.romain@process-one.net'). --protocol({xep, 60, '1.14'}). --protocol({xep, 163, '1.2'}). --protocol({xep, 248, '0.2'}). +-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("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("pubsub.hrl"). -include("mod_roster.hrl"). -include("translate.hrl"). --include("ejabberd_stacktrace.hrl"). + -include("ejabberd_commands.hrl"). -define(STDTREE, <<"tree">>). @@ -265,10 +267,7 @@ init([ServerHost|_]) -> ejabberd_router:register_route( Host, ServerHost, {apply, ?MODULE, route}), {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), - DefaultModule = plugin(Host, hd(Plugins)), - DefaultNodeCfg = merge_config( - [mod_pubsub_opt:default_node_config(Opts), - DefaultModule:options()]), + DefaultNodeCfg = mod_pubsub_opt:default_node_config(Opts), lists:foreach( fun(H) -> T = gen_mod:get_module_proc(H, config), @@ -341,7 +340,7 @@ init([ServerHost|_]) -> false -> ok end, - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(ServerHost, ?MODULE, get_commands_spec()), NodeTree = config(ServerHost, nodetree), Plugins = config(ServerHost, plugins), PepMapping = config(ServerHost, pep_mapping), @@ -599,7 +598,7 @@ on_self_presence(Acc) -> -spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state(). on_user_offline(#{jid := JID} = C2SState, _Reason) -> - purge_offline(jid:tolower(JID)), + purge_offline(JID), C2SState; on_user_offline(C2SState, _Reason) -> C2SState. @@ -749,11 +748,11 @@ handle_cast(Msg, State) -> handle_info({route, Packet}, State) -> try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + 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) -> @@ -812,12 +811,7 @@ terminate(_Reason, terminate_plugins(Host, ServerHost, Plugins, TreePlugin), ejabberd_router:unregister_route(Host) end, Hosts), - case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(ServerHost, ?MODULE, get_commands_spec()). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -969,6 +963,7 @@ node_disco_info(Host, Node, _From, _Identity, _Features) -> _ -> [] 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]}, @@ -1899,14 +1894,14 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed), + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), set_cached_item(Host, Nidx, ItemId, Publisher, Payload), {result, Reply}; {result, {TNode, {Result, Removed}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed), + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), set_cached_item(Host, Nidx, ItemId, Publisher, Payload), {result, Result}; {result, {_, default}} -> @@ -1974,7 +1969,10 @@ delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Node, Nidx, Type, Options, [ItemId], ForceNotify), + 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 @@ -2833,16 +2831,16 @@ broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payloa {result, false} end. --spec broadcast_retract_items(host(), binary(), nodeIdx(), binary(), +-spec broadcast_retract_items(host(), jid(), binary(), nodeIdx(), binary(), nodeOptions(), [itemId()]) -> {result, boolean()}. -broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds) -> - broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, false). +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(), binary(), nodeIdx(), binary(), +-spec broadcast_retract_items(host(), jid(), binary(), nodeIdx(), binary(), nodeOptions(), [itemId()], boolean()) -> {result, boolean()}. -broadcast_retract_items(_Host, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) -> +broadcast_retract_items(_Host, _Publisher, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) -> {result, false}; -broadcast_retract_items(Host, Node, Nidx, 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 @@ -2853,7 +2851,7 @@ broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, ForceNotif items = #ps_items{ node = Node, retract = ItemIds}}]}, - broadcast_stanza(Host, Node, Nidx, Type, + broadcast_stanza(Host, Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, items, Stanza, true), {result, true}; _ -> @@ -3025,7 +3023,8 @@ broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType end, lists:foreach(fun(To) -> ejabberd_router:route( - xmpp:set_to(StanzaToSend, jid:make(To))) + xmpp:set_to(xmpp:put_meta(StanzaToSend, ignore_sm_bounce, true), + jid:make(To))) end, LJIDs) end, SubIDsByJID). @@ -3047,8 +3046,7 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeO 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}), - ejabberd_router:route(xmpp:set_to(Stanza, jid:make(LUser, LServer))); + {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). @@ -3365,21 +3363,19 @@ get_option(Options, Var, Def) -> -spec node_options(host(), binary()) -> [{atom(), any()}]. node_options(Host, Type) -> - DefaultOpts = node_plugin_options(Host, Type), - case config(Host, plugins) of - [Type|_] -> config(Host, default_node_config, DefaultOpts); - _ -> DefaultOpts - end. + 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 catch Module:options() of - {'EXIT', {undef, _}} -> + case {lists:member(Type, config(Host, plugins)), catch Module:options()} of + {true, Opts} when is_list(Opts) -> + Opts; + {_, _} -> DefaultModule = plugin(Host, ?STDNODE), - DefaultModule:options(); - Result -> - Result + DefaultModule:options() end. -spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. @@ -3455,6 +3451,10 @@ get_configure_xfields(_Type, Options, Lang, Groups) -> {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). @@ -3797,7 +3797,9 @@ tree_call({_User, Server, _Resource}, Function, Args) -> tree_call(Host, Function, Args) -> Tree = tree(Host), ?DEBUG("Tree_call apply(~ts, ~ts, ~p) @ ~ts", [Tree, Function, Args, Host]), - case apply(Tree, Function, Args) of + 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 -> @@ -3817,9 +3819,9 @@ tree_action(Host, Function, Args) -> DBType = mod_pubsub_opt:db_type(ServerHost), Fun = fun () -> try tree_call(Host, Function, Args) - catch ?EX_RULE(Class, Reason, St) when DBType == sql -> - StackTrace = ?EX_STACK(St), - ejabberd_sql:abort({exception, Class, Reason, StackTrace}) + catch + Class:Reason:StackTrace when DBType == sql -> + ejabberd_sql:abort({exception, Class, Reason, StackTrace}) end end, Ret = case DBType of @@ -3917,15 +3919,15 @@ transaction(Host, Fun, Trans) -> do_transaction(ServerHost, Fun, Trans, DBType) -> F = fun() -> try Fun() - catch ?EX_RULE(Class, Reason, St) when (DBType == mnesia andalso - Trans == transaction) orelse - DBType == sql -> - StackTrace = ?EX_STACK(St), - Ex = {exception, Class, Reason, StackTrace}, - case DBType of - mnesia -> mnesia:abort(Ex); - sql -> ejabberd_sql:abort(Ex) - end + 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 @@ -4095,9 +4097,8 @@ subid_shim(SubIds) -> extended_headers(Jids) -> [#address{type = replyto, jid = Jid} || Jid <- Jids]. --spec purge_offline(ljid()) -> ok. -purge_offline(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}) -> @@ -4112,7 +4113,7 @@ purge_offline(LJID) -> andalso lists:member(<<"persistent-items">>, Features), if Items -> case node_action(Host, Type, - get_entity_affiliations, [Host, LJID]) of + get_entity_affiliations, [Host, JID]) of {result, Affs} -> {Status, [Affs | Acc]}; {error, _} = Err -> @@ -4133,7 +4134,7 @@ purge_offline(LJID) -> Purge = (get_option(Options, purge_offline) andalso get_option(Options, persist_items)), if (Publisher or Open) and Purge -> - purge_offline(Host, LJID, Node); + purge_offline(Host, JID, Node); true -> ok end @@ -4142,8 +4143,8 @@ purge_offline(LJID) -> ok end. --spec purge_offline(host(), ljid(), #pubsub_node{}) -> ok | {error, stanza_error()}. -purge_offline(Host, LJID, Node) -> +-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, @@ -4151,7 +4152,6 @@ purge_offline(Host, LJID, Node) -> {result, {[], _}} -> ok; {result, {Items, _}} -> - {User, Server, Resource} = LJID, PublishModel = get_option(Options, publish_model), ForceNotify = get_option(Options, notify_retract), {_, NodeId} = Node#pubsub_node.nodeid, @@ -4160,7 +4160,7 @@ purge_offline(Host, LJID, Node) -> 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, NodeId, Nidx, Type, Options, [ItemId], ForceNotify), + 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 @@ -4189,7 +4189,7 @@ delete_old_items(N) -> fun(#pubsub_node{id = Nidx, type = Type}) -> case node_action(Host, Type, remove_extra_items, - [Nidx , N]) of + [Nidx, N]) of {result, _} -> ok; {error, _} -> @@ -4403,7 +4403,7 @@ mod_doc() -> "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 to raise user connection rate. The cost " + "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", @@ -4458,7 +4458,7 @@ mod_doc() -> {pep_mapping, #{value => "List of Key:Value", desc => - ?T("This allows to define a list of key-value to choose " + ?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:"), @@ -4483,7 +4483,7 @@ mod_doc() -> "follows standard XEP-0060 implementation."), ?T("- 'pep' plugin adds extension to handle Personal " "Eventing Protocol (XEP-0163) to the PubSub engine. " - "Adding pep allows to handle PEP automatically.")]}}, + "When enabled, PEP is handled automatically.")]}}, {vcard, #{value => ?T("vCard"), desc => @@ -4493,27 +4493,27 @@ mod_doc() -> "representation of vCard. Since the representation has " "no attributes, the mapping is straightforward."), example => - [{?T("The following XML representation of vCard:"), - ["", - " PubSub Service", - " ", - " ", - " Elm Street", - " ", - ""]}, - {?T("will be translated to:"), - ["vcard:", - " fn: PubSub Service", - " adr:", - " -", - " work: true", - " street: Elm Street"]}]}} + ["# 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", @@ -4523,14 +4523,12 @@ mod_doc() -> " max_items: 4", " plugins:", " - flat", - " - pep", - " ..."]}, + " - 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", @@ -4538,6 +4536,5 @@ mod_doc() -> " last_item_cache: false", " plugins:", " - flat", - " - pep", - " ..."]} + " - pep"]} ]}. diff --git a/src/mod_pubsub_mnesia.erl b/src/mod_pubsub_mnesia.erl index 3d2d1e46b..a1dbc2ff3 100644 --- a/src/mod_pubsub_mnesia.erl +++ b/src/mod_pubsub_mnesia.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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_pubsub_opt.erl b/src/mod_pubsub_opt.erl index cb3c014b9..612abf35b 100644 --- a/src/mod_pubsub_opt.erl +++ b/src/mod_pubsub_opt.erl @@ -39,7 +39,7 @@ default_node_config(Opts) when is_map(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()) -> [{re:mp(),[{atom(),atom() | integer()}]}]. +-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) -> 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 index 41b7eed70..59b22c110 100644 --- a/src/mod_pubsub_sql.erl +++ b/src/mod_pubsub_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2022 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,13 +20,98 @@ %% API -export([init/3]). +-export([sql_schemas/0]). + +-include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _ServerHost, _Opts) -> +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 index 5389e3dfb..fb5ba1be4 100644 --- a/src/mod_push.erl +++ b/src/mod_push.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 @@ -25,7 +25,7 @@ -module(mod_push). -author('holger@zedat.fu-berlin.de'). --protocol({xep, 357, '0.2'}). +-protocol({xep, 357, '0.2', '17.08', "complete", ""}). -behaviour(gen_mod). @@ -91,25 +91,27 @@ %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- --spec start(binary(), gen_mod:opts()) -> ok. +-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), - register_iq_handlers(Host), - register_hooks(Host), - ejabberd_commands:register_commands(?MODULE, get_commands_spec()). + {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) -> - unregister_hooks(Host), - unregister_iq_handlers(Host), - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. +stop(_Host) -> + ok. -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> @@ -128,6 +130,8 @@ 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) -> @@ -147,7 +151,8 @@ mod_opt_type(cache_life_time) -> -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(Host) -> - [{include_sender, false}, + [{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)}, @@ -166,9 +171,19 @@ mod_doc() -> "\"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-dependant backend services such as FCM or APNS."), + "platform-dependent backend services such as FCM or APNS."), opts => - [{include_sender, + [{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 " @@ -248,51 +263,6 @@ delete_old_sessions(Days) -> Reason end. -%%-------------------------------------------------------------------- -%% Register/unregister hooks. -%%-------------------------------------------------------------------- --spec register_hooks(binary()) -> ok. -register_hooks(Host) -> - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - disco_sm_features, 50), - ejabberd_hooks:add(c2s_session_pending, Host, ?MODULE, - c2s_session_pending, 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_handle_cast, Host, ?MODULE, - c2s_handle_cast, 50), - ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, - c2s_stanza, 50), - ejabberd_hooks:add(store_mam_message, Host, ?MODULE, - mam_message, 50), - ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, - offline_message, 55), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50). - --spec unregister_hooks(binary()) -> ok. -unregister_hooks(Host) -> - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - disco_sm_features, 50), - ejabberd_hooks:delete(c2s_session_pending, Host, ?MODULE, - c2s_session_pending, 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_handle_cast, Host, ?MODULE, - c2s_handle_cast, 50), - ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, - c2s_stanza, 50), - ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, - mam_message, 50), - ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, - offline_message, 55), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50). - %%-------------------------------------------------------------------- %% Service discovery. %%-------------------------------------------------------------------- @@ -311,15 +281,6 @@ disco_sm_features(Acc, _From, _To, _Node, _Lang) -> %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- --spec register_iq_handlers(binary()) -> ok. -register_iq_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUSH_0, - ?MODULE, process_iq). - --spec unregister_iq_handlers(binary()) -> ok. -unregister_iq_handlers(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUSH_0). - -spec process_iq(iq()) -> iq(). process_iq(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), @@ -492,7 +453,7 @@ 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_disable, State), + State1 = maps:remove(push_enabled, State), State2 = maps:remove(push_session_id, State1), {stop, State2}; c2s_handle_cast(State, _Msg) -> @@ -557,16 +518,21 @@ notify(LUser, LServer, Clients, Pkt, Dir) -> notify(LServer, PushLJID, Node, XData, Pkt0, Dir, HandleResponse) -> Pkt = unwrap_message(Pkt0), From = jid:make(LServer), - Summary = make_summary(LServer, Pkt, Dir), - 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). + 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. @@ -712,7 +678,7 @@ drop_online_sessions(LUser, LServer, Clients) -> -spec make_summary(binary(), xmpp_element() | xmlel() | none, direction()) -> xdata() | undefined. -make_summary(Host, #message{from = From} = Pkt, recv) -> +make_summary(Host, #message{from = From0} = Pkt, recv) -> case {mod_push_opt:include_sender(Host), mod_push_opt:include_body(Host)} of {false, false} -> @@ -732,6 +698,7 @@ make_summary(Host, #message{from = From} = Pkt, recv) -> end, Fields2 = case IncludeSender of true -> + From = jid:remove_resource(From0), [{'last-message-sender', From} | Fields1]; false -> Fields1 diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl index 8a816aef1..33bd2b53e 100644 --- a/src/mod_push_keepalive.erl +++ b/src/mod_push_keepalive.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 @@ -32,8 +32,9 @@ -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. --export([c2s_session_pending/1, c2s_session_resumed/1, c2s_copy_session/2, - c2s_handle_cast/2, c2s_handle_info/2, c2s_stanza/3]). +-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"). @@ -46,29 +47,26 @@ %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- --spec start(binary(), gen_mod:opts()) -> ok. -start(Host, Opts) -> - case mod_push_keepalive_opt:wake_on_start(Opts) of - true -> - wake_all(Host); - false -> - ok - end, - register_hooks(Host). +-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) -> - unregister_hooks(Host). +stop(_Host) -> + ok. -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. -reload(Host, NewOpts, OldOpts) -> - case {mod_push_keepalive_opt:wake_on_start(NewOpts), - mod_push_keepalive_opt:wake_on_start(OldOpts)} of - {true, false} -> - wake_all(Host); - _ -> - ok - end. +reload(_Host, _NewOpts, _OldOpts) -> + ok. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> @@ -130,39 +128,6 @@ mod_doc() -> "out as per the 'resume_timeout' option. " "The default value is 'true'.")}}]}. -%%-------------------------------------------------------------------- -%% Register/unregister hooks. -%%-------------------------------------------------------------------- --spec register_hooks(binary()) -> ok. -register_hooks(Host) -> - ejabberd_hooks:add(c2s_session_pending, Host, ?MODULE, - c2s_session_pending, 50), - ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, - c2s_session_resumed, 50), - ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, - c2s_copy_session, 50), - ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, - c2s_handle_cast, 40), - ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, - c2s_handle_info, 50), - ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, - c2s_stanza, 50). - --spec unregister_hooks(binary()) -> ok. -unregister_hooks(Host) -> - ejabberd_hooks:delete(c2s_session_pending, Host, ?MODULE, - c2s_session_pending, 50), - ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, - c2s_session_resumed, 50), - ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, - c2s_copy_session, 50), - ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, - c2s_handle_cast, 40), - ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, - c2s_handle_info, 50), - ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, - c2s_stanza, 50). - %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- @@ -233,6 +198,15 @@ c2s_handle_info(#{push_enabled := true, mgmt_state := pending, 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. %%-------------------------------------------------------------------- diff --git a/src/mod_push_mnesia.erl b/src/mod_push_mnesia.erl index e265678e7..6a5f068b9 100644 --- a/src/mod_push_mnesia.erl +++ b/src/mod_push_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 diff --git a/src/mod_push_opt.erl b/src/mod_push_opt.erl index 6ab94b9c7..db6c55389 100644 --- a/src/mod_push_opt.erl +++ b/src/mod_push_opt.erl @@ -9,6 +9,7 @@ -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(). @@ -47,6 +48,12 @@ include_sender(Opts) when is_map(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); diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl index ae6774a51..a36e50f8e 100644 --- a/src/mod_push_sql.erl +++ b/src/mod_push_sql.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_push_sql.erl %%% Author : Evgeniy Khramtsov -%%% Purpose : +%%% Purpose : %%% Created : 26 Oct 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2017-2022 ProcessOne +%%% 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 @@ -30,6 +30,7 @@ -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"). @@ -39,9 +40,32 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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), diff --git a/src/mod_register.erl b/src/mod_register.erl index 5f3d7de56..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-2022 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 @@ -author('alexey@process-one.net'). --protocol({xep, 77, '2.4'}). +-protocol({xep, 77, '2.4', '0.1.0', "complete", ""}). -behaviour(gen_mod). @@ -43,29 +43,17 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -start(Host, _Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_REGISTER, ?MODULE, process_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_REGISTER, ?MODULE, process_iq), - ejabberd_hooks:add(c2s_pre_auth_features, Host, ?MODULE, - stream_feature_register, 50), - ejabberd_hooks:add(c2s_unauthenticated_packet, Host, - ?MODULE, c2s_unauthenticated_packet, 50), +start(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, mod_register_ip, [{ram_copies, [node()]}, {local_content, true}, {attributes, [key, value]}]), - ok. + {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) -> - ejabberd_hooks:delete(c2s_pre_auth_features, Host, - ?MODULE, stream_feature_register, 50), - ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, - ?MODULE, c2s_unauthenticated_packet, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_REGISTER), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_REGISTER). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -99,7 +87,7 @@ c2s_unauthenticated_packet(#{ip := IP, server := Server} = 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)), + Err = make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)), {stop, ejabberd_c2s:send(State, Err)} end; c2s_unauthenticated_packet(State, _) -> @@ -128,7 +116,7 @@ process_iq(#iq{type = set, lang = Lang, sub_els = [#register{remove = true}]} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove = false) -> Txt = ?T("Access denied by service policy"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); + 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, @@ -153,12 +141,12 @@ process_iq(#iq{type = set, lang = Lang, to = To, from = From, ignore; false -> Txt = ?T("Incorrect password"), - xmpp:make_error( + make_stripped_error( IQ, xmpp:err_forbidden(Txt, Lang)) end; true -> Txt = ?T("No 'password' found in this query"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) end end; true -> @@ -170,7 +158,7 @@ process_iq(#iq{type = set, lang = Lang, to = To, from = From, ignore; _ -> Txt = ?T("The query is only allowed from local users"), - xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)) end end; process_iq(#iq{type = set, to = To, @@ -198,17 +186,17 @@ process_iq(#iq{type = set, to = To, User, Server, Password, IQ, Source, true); _ -> Txt = ?T("Incorrect data form"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; {error, malformed} -> Txt = ?T("Incorrect CAPTCHA submit"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)); _ -> ErrText = ?T("The CAPTCHA verification has failed"), - xmpp:make_error(IQ, xmpp:err_not_allowed(ErrText, Lang)) + make_stripped_error(IQ, xmpp:err_not_allowed(ErrText, Lang)) end; process_iq(#iq{type = set} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove) -> - xmpp:make_error(IQ, xmpp:err_bad_request()); + 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, @@ -260,11 +248,11 @@ process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ, sub_els = [Xdata | CaptchaEls2]}); {error, limit} -> ErrText = ?T("Too many CAPTCHA requests"), - xmpp:make_error( + make_stripped_error( IQ, xmpp:err_resource_constraint(ErrText, Lang)); _Err -> ErrText = ?T("Unable to generate a CAPTCHA"), - xmpp:make_error( + make_stripped_error( IQ, xmpp:err_internal_server_error(ErrText, Lang)) end; true -> @@ -279,8 +267,11 @@ process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ, try_register_or_set_password(User, Server, Password, #iq{from = From, lang = Lang} = IQ, Source, CaptchaSucceed) -> - case From of - #jid{user = User, lserver = Server} -> + 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 @@ -289,14 +280,14 @@ try_register_or_set_password(User, Server, Password, ok -> xmpp:make_iq_result(IQ); {error, Error} -> - xmpp:make_error(IQ, Error) + make_stripped_error(IQ, Error) end; deny -> Txt = ?T("Access denied by service policy"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; _ -> - xmpp:make_error(IQ, xmpp:err_not_allowed()) + make_stripped_error(IQ, xmpp:err_not_allowed()) end. try_set_password(User, Server, Password) -> @@ -319,15 +310,15 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) -> xmpp:make_iq_result(IQ); {error, not_allowed} -> Txt = ?T("Changing password is not allowed"), - xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); + make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)); {error, invalid_jid = Why} -> - xmpp:make_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang)); + make_stripped_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang)); {error, invalid_password = Why} -> - xmpp:make_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang)); + make_stripped_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang)); {error, weak_password = Why} -> - xmpp:make_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang)); + make_stripped_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang)); {error, db_failure = Why} -> - xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang)) + make_stripped_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang)) end. try_register(User, Server, Password, SourceRaw, Module) -> @@ -427,6 +418,7 @@ send_welcome_message(JID) -> ejabberd_router:route( #message{from = jid:make(Host), to = JID, + type = chat, subject = xmpp:mk_text(Subj), body = xmpp:mk_text(Body)}) end. @@ -568,6 +560,9 @@ is_strong_password2(Server, Password) -> ejabberd_auth:entropy(Password) >= Entropy end. +make_stripped_error(IQ, Err) -> + xmpp:make_error(xmpp:remove_subtag(IQ, #register{}), Err). + %%% %%% ip_access management %%% @@ -579,24 +574,24 @@ may_remove_resource(From) -> From. get_ip_access(Host) -> 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(Source, IPAccess); + allow -> check_ip_access(Server, Source, IPAccess); deny -> deny end. @@ -660,11 +655,14 @@ mod_doc() -> ?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.")}}, + "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 to register new accounts " + ?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.")}}, @@ -683,7 +681,7 @@ mod_doc() -> {captcha_protected, #{value => "true | false", desc => - ?T("Protect registrations with http://../basic/#captcha[CAPTCHA]. " + ?T("Protect registrations with _`basic.md#captcha|CAPTCHA`_. " "The default is 'false'.")}}, {ip_access, #{value => ?T("AccessName"), @@ -714,4 +712,13 @@ mod_doc() -> #{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'.")}}]}. + "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_web.erl b/src/mod_register_web.erl index 59bc855bc..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-2022 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 @@ -62,12 +62,26 @@ depends(_Host, _Opts) -> %%% 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([Section], +process2([Section], #request{method = 'GET', lang = Lang, host = Host, ip = {Addr, _Port}}) -> Host2 = case ejabberd_router:is_my_host(Host) of @@ -82,7 +96,7 @@ process([Section], <<"change_password">> -> form_changepass_get(Host2, Lang); _ -> {404, [], "Not Found"} end; -process([<<"new">>], +process2([<<"new">>], #request{method = 'POST', q = Q, ip = {Ip, _Port}, lang = Lang, host = _HTTPHost}) -> case form_new_post(Q, Ip) of @@ -97,7 +111,7 @@ process([<<"new">>], translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; -process([<<"delete">>], +process2([<<"delete">>], #request{method = 'POST', q = Q, lang = Lang, host = _HTTPHost}) -> case form_del_post(Q) of @@ -112,7 +126,7 @@ process([<<"delete">>], 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 = _HTTPHost}) -> case form_changepass_post(Q) of @@ -126,7 +140,7 @@ process([<<"change_password">>], {404, [], ErrorText} end; -process(_Path, _Request) -> +process2(_Path, _Request) -> {404, [], "Not Found"}. %%%---------------------------------------------------------------------- @@ -601,7 +615,7 @@ mod_doc() -> ?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 http://../basic/#captcha[CAPTCHA] " + ?T("This module supports _`basic.md#captcha|CAPTCHA`_ " "to register a new account. " "To enable this feature, configure the " "top-level _`captcha_cmd`_ and " @@ -611,7 +625,7 @@ mod_doc() -> "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' -> " - "http://../listen-options/#request-handlers[request_handlers], " + "_`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.")], diff --git a/src/mod_roster.erl b/src/mod_roster.erl index b6cf771f0..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-2022 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,7 +34,7 @@ -module(mod_roster). --protocol({xep, 237, '1.3'}). +-protocol({xep, 237, '1.3', '2.1.0', "complete", ""}). -author('alexey@process-one.net'). @@ -46,19 +46,22 @@ 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, webadmin_page/3, - webadmin_user/4, get_versioning_feature/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("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). --include("ejabberd_stacktrace.hrl"). + -include("translate.hrl"). -define(ROSTER_CACHE, roster_cache). @@ -91,48 +94,20 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster_items, 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_jid_info, Host, ?MODULE, - get_jid_info, 50), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, - c2s_self_presence, 50), - ejabberd_hooks:add(c2s_post_auth_features, 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). + {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) -> - ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster_items, 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_jid_info, Host, - ?MODULE, get_jid_info, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, - c2s_self_presence, 50), - ejabberd_hooks:delete(c2s_post_auth_features, - 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). +stop(_Host) -> + ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -157,8 +132,8 @@ process_iq(#iq{lang = Lang, to = To} = IQ) -> false -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); - true -> - process_local_iq(IQ) + {true, IQ1} -> + process_local_iq(IQ1) end. -spec process_local_iq(iq()) -> iq(). @@ -176,7 +151,13 @@ process_local_iq(#iq{type = set, from = From, lang = Lang, Txt = ?T("Duplicated groups are not allowed by RFC6121"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); false -> - #jid{lserver = LServer} = From, + 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 -> @@ -567,24 +548,25 @@ transaction(LUser, LServer, LJIDs, F) -> -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). -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, <<"">>). + process_subscription(out, User, Server, JID, Type, <<"">>, []). -spec process_subscription(in | out, binary(), binary(), jid(), subscribe | subscribed | unsubscribe | unsubscribed, - binary()) -> boolean(). + binary(), [fxml:xmlel()]) -> boolean(). process_subscription(Direction, User, Server, JID1, - Type, Reason) -> + Type, Reason, SubEls) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LJID = jid:tolower(jid:remove_resource(JID1)), @@ -618,6 +600,8 @@ process_subscription(Direction, User, Server, JID1, {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 @@ -655,6 +639,12 @@ process_subscription(Direction, User, Server, JID1, 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 %% NewState = none | {NewSubscription, NewPending} -ifdef(ROSTER_GATEWAY_WORKAROUND). @@ -983,6 +973,7 @@ resend_pending_subscriptions(#{jid := JID} = State) -> 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) -> @@ -1029,205 +1020,88 @@ process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -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. +%%% @format-begin -user_roster(User, Server, Query, Lang) -> - LUser = jid:nodeprep(User), - LServer = jid: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(?T("None"))]; - _ -> - [?XE(<<"table">>, - [?XE(<<"thead">>, - [?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Jabber ID")), - ?XCT(<<"td">>, ?T("Nickname")), - ?XCT(<<"td">>, ?T("Subscription")), - ?XCT(<<"td">>, ?T("Pending")), - ?XCT(<<"td">>, ?T("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>>, - ?T("Validate"))]); - true -> ?X(<<"td">>) - end, - ?XAE(<<"td">>, - [{<<"class">>, - <<"valign">>}], - [?INPUTTD(<<"submit">>, - <<"remove", - (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>, - ?T("Remove"))])]) - end, - SItems)))])] - end, - PageTitle = str:translate_and_format(Lang, ?T("Roster of ~ts"), [us_to_list(US)]), - (?H1GL(PageTitle, <<"modules/#mod-roster">>, <<"mod_roster">>)) - ++ - case Res of - ok -> [?XREST(?T("Submitted"))]; - error -> [?XREST(?T("Bad format"))]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - (FItems ++ - [?P, ?INPUT(<<"text">>, <<"newjid">>, <<"">>), - ?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"addjid">>, - ?T("Add Jabber ID"))]))]. +webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> + Acc ++ [{<<"roster">>, <<"Roster">>}]. -build_contact_jid_td(RosterJID) -> - ContactJID = jid:make(RosterJID), - JIDURI = case {ContactJID#jid.luser, - ContactJID#jid.lserver} - of - {<<"">>, _} -> <<"">>; - {CUser, CServer} -> - case lists:member(CServer, ejabberd_option:hosts()) of - false -> <<"">>; - true -> - <<"../../../../../server/", CServer/binary, "/user/", - CUser/binary, "/">> - end - end, - case JIDURI of - <<>> -> - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], - (jid:encode(RosterJID))); - URI when is_binary(URI) -> - ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?AC(JIDURI, (jid:encode(RosterJID)))]) - end. +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. -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}} -> - try jid:decode(SJID) of - JID -> - user_roster_subscribe_jid(User, Server, JID), ok - catch _:{bad_jid, _} -> - 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. +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]). -user_roster_subscribe_jid(User, Server, JID) -> - UJID = jid:make(User, Server), - Presence = #presence{from = UJID, to = JID, type = subscribe}, - out_subscription(Presence), - ejabberd_router:route(Presence). - -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 = jid:make(JID), - UJID = jid:make(User, Server), - Pres = #presence{from = UJID, to = JID1, - type = subscribed}, - out_subscription(Pres), - ejabberd_router:route(Pres), - throw(submitted); - false -> - case lists:keysearch(<<"remove", - (ejabberd_web_admin:term_to_id(JID))/binary>>, - 1, Query) - of - {value, _} -> - UJID = jid:make(User, Server), - RosterItem = #roster_item{ - jid = jid:make(JID), - subscription = remove}, - process_iq_set( - #iq{type = set, - from = UJID, - to = UJID, - id = p1_rand:get_string(), - sub_els = [#roster_query{ - items = [RosterItem]}]}), - throw(submitted); - false -> ok - end - end - end, - Items), - nothing. - -us_to_list({User, Server}) -> - jid:encode({User, Server, <<"">>}). - -webadmin_user(Acc, User, Server, Lang) -> - QueueLen = length(get_roster(jid:nodeprep(User), jid:nameprep(Server))), - FQueueLen = ?C(integer_to_binary(QueueLen)), - FQueueView = ?AC(<<"roster/">>, ?T("View Roster")), - Acc ++ - [?XCT(<<"h3">>, ?T("Roster:")), - FQueueLen, - ?C(<<" | ">>), - FQueueView]. +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(). @@ -1423,8 +1297,6 @@ mod_doc() -> ?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", - " ..."]}. + " store_current_id: false"]}. diff --git a/src/mod_roster_mnesia.erl b/src/mod_roster_mnesia.erl index 19f602f1e..d8d4bd1c9 100644 --- a/src/mod_roster_mnesia.erl +++ b/src/mod_roster_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_roster_sql.erl b/src/mod_roster_sql.erl index 1a8d812b6..44d507e5e 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +34,7 @@ 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"). @@ -43,9 +44,55 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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, @@ -293,16 +340,16 @@ update_roster_sql({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}, raw_to_record(LServer, [User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType]) -> + SServer, SSubscribe, SType]) -> raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType}); + SServer, SSubscribe, SType}); raw_to_record(LServer, {User, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType}) -> + SServer, SSubscribe, SType}) -> raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType}); + SServer, SSubscribe, SType}); raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType}) -> 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 index 5e966967d..f6128b573 100644 --- a/src/mod_s2s_dialback.erl +++ b/src/mod_s2s_dialback.erl @@ -2,7 +2,7 @@ %%% Created : 16 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,8 +21,8 @@ %%%------------------------------------------------------------------- -module(mod_s2s_dialback). -behaviour(gen_mod). --protocol({xep, 220, '1.1.1'}). --protocol({xep, 185, '1.0'}). +-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]). @@ -40,49 +40,21 @@ %%%=================================================================== %%% API %%%=================================================================== -start(Host, _Opts) -> - ejabberd_hooks:add(s2s_out_init, Host, ?MODULE, s2s_out_init, 50), - ejabberd_hooks:add(s2s_out_closed, Host, ?MODULE, s2s_out_closed, 50), - ejabberd_hooks:add(s2s_in_pre_auth_features, Host, ?MODULE, - s2s_in_features, 50), - ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE, - s2s_in_features, 50), - ejabberd_hooks:add(s2s_in_handle_recv, Host, ?MODULE, - s2s_in_recv, 50), - ejabberd_hooks:add(s2s_in_unauthenticated_packet, Host, ?MODULE, - s2s_in_packet, 50), - ejabberd_hooks:add(s2s_in_authenticated_packet, Host, ?MODULE, - s2s_in_packet, 50), - ejabberd_hooks:add(s2s_out_packet, Host, ?MODULE, - s2s_out_packet, 50), - ejabberd_hooks:add(s2s_out_downgraded, Host, ?MODULE, - s2s_out_downgraded, 50), - ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, - s2s_out_auth_result, 50), - ejabberd_hooks:add(s2s_out_tls_verify, Host, ?MODULE, - s2s_out_tls_verify, 50). +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) -> - ejabberd_hooks:delete(s2s_out_init, Host, ?MODULE, s2s_out_init, 50), - ejabberd_hooks:delete(s2s_out_closed, Host, ?MODULE, s2s_out_closed, 50), - ejabberd_hooks:delete(s2s_in_pre_auth_features, Host, ?MODULE, - s2s_in_features, 50), - ejabberd_hooks:delete(s2s_in_post_auth_features, Host, ?MODULE, - s2s_in_features, 50), - ejabberd_hooks:delete(s2s_in_handle_recv, Host, ?MODULE, - s2s_in_recv, 50), - ejabberd_hooks:delete(s2s_in_unauthenticated_packet, Host, ?MODULE, - s2s_in_packet, 50), - ejabberd_hooks:delete(s2s_in_authenticated_packet, Host, ?MODULE, - s2s_in_packet, 50), - ejabberd_hooks:delete(s2s_out_packet, Host, ?MODULE, - s2s_out_packet, 50), - ejabberd_hooks:delete(s2s_out_downgraded, Host, ?MODULE, - s2s_out_downgraded, 50), - ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, - s2s_out_auth_result, 50), - ejabberd_hooks:delete(s2s_out_tls_verify, Host, ?MODULE, - s2s_out_tls_verify, 50). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -121,14 +93,12 @@ mod_doc() -> "is 'all'.")}}], example => ["modules:", - " ...", " mod_s2s_dialback:", " access:", " allow:", " server: legacy.domain.tld", " server: invalid-cert.example.org", - " deny: all", - " ..."]}. + " deny: all"]}. s2s_in_features(Acc, _) -> [#db_feature{errors = true}|Acc]. 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 2f962a13f..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-2022 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 @@ -36,18 +36,11 @@ -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -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), - ok. +start(_Host, _Opts) -> + {ok, [{hook, user_send_packet, log_user_send, 50}, + {hook, user_receive_packet, log_user_receive, 50}]}. -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), +stop(_Host) -> ok. depends(_Host, _Opts) -> @@ -99,9 +92,7 @@ mod_doc() -> "to which stanzas will be forwarded.")}}], example => ["modules:", - " ...", " mod_service_log:", " loggers:", " - xmpp-server.tld", - " - component.domain.tld", - " ..."]}. + " - component.domain.tld"]}. diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 69bbc520d..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-2022 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 @@ -41,6 +41,8 @@ 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("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). @@ -85,53 +87,20 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - 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_jid_info, Host, ?MODULE, - get_jid_info, 70), - ejabberd_hooks:add(roster_process_item, Host, ?MODULE, - process_item, 50), - ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, - c2s_self_presence, 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(remove_user, Host, ?MODULE, - remove_user, 50). + {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) -> - 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_jid_info, Host, - ?MODULE, get_jid_info, 70), - ejabberd_hooks:delete(roster_process_item, Host, - ?MODULE, process_item, 50), - ejabberd_hooks:delete(c2s_self_presence, Host, - ?MODULE, c2s_self_presence, 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(remove_user, Host, ?MODULE, - remove_user, - 50). +stop(_Host) -> + ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -412,6 +381,19 @@ create_group(Host, Group) -> create_group(Host, Group, []). create_group(Host, Group, Opts) -> + 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. + +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 @@ -457,8 +439,7 @@ get_group_opts(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), get_group_opts_int(Host, Group). -get_group_opts_int(Host1, Group1) -> - {Host, Group} = split_grouphost(Host1, Group1), +get_group_opts_int(Host, Group) -> Mod = gen_mod:db_mod(Host, ?MODULE), Res = case use_cache(Mod, Host) of true -> @@ -525,7 +506,8 @@ get_online_users(Host) -> lists:usort([{U, S} || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host)]). -get_group_users_cached(Host, Group, Cache) -> +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). @@ -894,286 +876,402 @@ unset_presence(User, Server, Resource, Status) -> end. %%--------------------- -%% Web Admin +%% Web Admin: Page Frontend %%--------------------- +%% @format-begin + webadmin_menu(Acc, _Host, Lang) -> - [{<<"shared-roster">>, translate:translate(Lang, ?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 = list_groups(Host), - FGroups = (?XAE(<<"table">>, [], - [?XE(<<"tbody">>, - [?XE(<<"tr">>, - [?X(<<"td">>), - ?XE(<<"td">>, [?CT(?T("Name:"))]) - ])]++ - (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">>, - <<"">>), - ?C(<<" ">>), - ?INPUTT(<<"submit">>, <<"addnew">>, - ?T("Add New"))])])]))])), - (?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))), - <<"modules/#mod-shared-roster">>, <<"mod_shared_roster">>)) - ++ - case Res of - ok -> [?XREST(?T("Submitted"))]; - error -> [?XREST(?T("Bad format"))]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [FGroups, ?BR, - ?INPUTTD(<<"submit">>, <<"delete">>, - ?T("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 /= <<"">> -> - 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 = list_groups(Host), - lists:foreach(fun (Group) -> - case lists:member({<<"selected">>, Group}, Query) of - true -> 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 = get_group_opts(Host, Group), - Label = get_opt(GroupOpts, label, <<"">>), %%++ - 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 = 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">>, ?T("Name:")), - ?XE(<<"td">>, [?C(Group)]), - ?XE(<<"td">>, [?C(<<"">>)])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Label:")), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"label">>, Label)]), - ?XE(<<"td">>, [?CT(?T("Name in the rosters where this group will be displayed"))])]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Description:")), - ?XE(<<"td">>, - [?TEXTAREA(<<"description">>, - integer_to_binary(lists:max([3, - DescNL])), - <<"20">>, Description)]), - ?XE(<<"td">>, [?CT(?T("Only admins can see this"))]) -]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Members:")), - ?XE(<<"td">>, - [?TEXTAREA(<<"members">>, - integer_to_binary(lists:max([3, - length(Members)+3])), - <<"20">>, FMembers)]), - ?XE(<<"td">>, [?C(<<"JIDs, @all@, @online@">>)]) -]), - ?XE(<<"tr">>, - [?XCT(<<"td">>, ?T("Displayed:")), - ?XE(<<"td">>, - [?TEXTAREA(<<"dispgroups">>, - integer_to_binary(lists:max([3, length(FDisplayedGroups)])), - <<"20">>, - list_to_binary(FDisplayedGroups))]), - ?XE(<<"td">>, [?CT(?T("Groups that will be displayed to the members"))]) -])])])), - (?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))), - <<"modules/#mod-shared-roster">>, <<"mod_shared_roster">>)) - ++ - [?XC(<<"h2">>, translate:translate(Lang, ?T("Group")))] ++ - case Res of - ok -> [?XREST(?T("Submitted"))]; - {error_elements, NonAddedList1, NG1} -> - make_error_el(Lang, - ?T("Members not added (inexistent vhost!): "), - [jid:encode({U,S,<<>>}) || {U,S} <- NonAddedList1]) - ++ make_error_el(Lang, ?T("'Displayed groups' not added (they do not exist!): "), NG1); - error -> [?XREST(?T("Bad format"))]; - nothing -> [] - end - ++ - [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [FGroup, ?BR, - ?INPUTT(<<"submit">>, <<"submit">>, ?T("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). -make_error_el(_, _, []) -> - []; -make_error_el(Lang, Message, BinList) -> - NG2 = str:join(BinList, <<", ">>), - NG3 = translate:translate(Lang, Message), - NG4 = str:concat(NG3, NG2), - [?XRES(NG4)]. +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]). -shared_roster_group_parse_query(Host, Group, Query) -> - case lists:keysearch(<<"submit">>, 1, Query) of - {value, _} -> - {value, {_, Label}} = lists:keysearch(<<"label">>, 1, - Query), %++ - {value, {_, Description}} = - lists:keysearch(<<"description">>, 1, Query), - {value, {_, SMembers}} = lists:keysearch(<<"members">>, - 1, Query), - {value, {_, SDispGroups}} = - lists:keysearch(<<"dispgroups">>, 1, Query), - LabelOpt = if Label == <<"">> -> []; - true -> [{label, Label}] %++ - end, - DescriptionOpt = if Description == <<"">> -> []; - true -> [{description, Description}] - end, - DispGroups1 = str:tokens(SDispGroups, <<"\r\n">>), - {DispGroups, WrongDispGroups} = filter_groups_existence(Host, DispGroups1), - DispGroupsOpt = if DispGroups == [] -> []; - true -> [{displayed_groups, DispGroups}] - end, - OldMembers = 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; - _ -> - try jid:decode(SJID) of - JID -> - [{JID#jid.luser, - JID#jid.lserver} - | USs] - catch _:{bad_jid, _} -> - 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), - set_group_opts(Host, Group, - LabelOpt ++ - DispGroupsOpt ++ - DescriptionOpt ++ - AllUsersOpt ++ OnlineUsersOpt), - if NewMembers == error -> error; - true -> - AddedMembers = NewMembers -- OldMembers, - RemovedMembers = OldMembers -- NewMembers, - lists:foreach( - fun(US) -> - remove_user_from_group(Host, - US, - Group) - end, - RemovedMembers), - NonAddedMembers = lists:filter( - fun(US) -> - error == add_user_to_group(Host, US, - Group) - end, - AddedMembers), - case (NonAddedMembers /= []) or (WrongDispGroups /= []) of - true -> {error_elements, NonAddedMembers, WrongDispGroups}; - false -> ok - end - end; - _ -> nothing - 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]). -get_opt(Opts, Opt, Default) -> - case lists:keysearch(Opt, 1, Opts) of - {value, {_, Val}} -> Val; - false -> Default - end. - -us_to_list({User, Server}) -> - jid:encode({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 @@ -1181,17 +1279,6 @@ split_grouphost(Host, Group) -> [_] -> {Host, Group} end. -filter_groups_existence(Host, Groups) -> - lists:partition( - fun(Group) -> error /= get_group_opts(Host, Group) end, - Groups). - -displayed_groups_update(Members, DisplayedGroups, Subscription) -> - lists:foreach( - fun({U, S}) -> - push_displayed_to_user(U, S, S, Subscription, DisplayedGroups) - end, Members). - opts_to_binary(Opts) -> lists:map( fun({label, Label}) -> @@ -1255,7 +1342,7 @@ mod_doc() -> "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_*'. " + "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."), "", diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index ebcb77ab3..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-2022 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 @@ -52,7 +52,6 @@ -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 --define(INVALID_SETTING_MSG, "~ts is not properly set! ~ts will not function."). -record(state, {host = <<"">> :: binary(), @@ -72,7 +71,7 @@ user_desc = <<"">> :: binary(), user_uid = <<"">> :: binary(), uid_format = <<"">> :: binary(), - uid_format_re :: undefined | re:mp(), + uid_format_re :: undefined | misc:re_mp(), filter = <<"">> :: binary(), ufilter = <<"">> :: binary(), rfilter = <<"">> :: binary(), @@ -395,14 +394,14 @@ get_member_jid(#state{user_jid_attr = UserJIDAttr, user_uid = UIDAttr} = State, [{<<"%u">>, UID}])], [UserJIDAttr]), case Entries of - [] -> - {error, error}; [#eldap_entry{attributes = [{UserJIDAttr, [MemberJID | _]}]} | _] -> try jid:decode(MemberJID) of #jid{luser = U, lserver = S} -> {U, S} catch error:{bad_jid, _} -> {error, Host} - end + end; + _ -> + {error, error} end. extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {DescAcc, JIDsAcc}) -> @@ -678,10 +677,10 @@ mod_doc() -> ?T("- Connection parameters: The module also accepts the " "connection parameters, all of which default to the top-level " "parameter of the same name, if unspecified. " - "See http://../ldap/#ldap-connection[LDAP Connection] " + "See _`ldap.md#ldap-connection|LDAP Connection`_ " "section for more information about them."), "", - ?T("Check also the http://../ldap/#ldap-examples" - "[Configuration examples] section to get details about " + ?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 => @@ -710,13 +709,13 @@ mod_doc() -> "name of roster entries (usually full names of people in " "the roster). See also the parameters 'ldap_userdesc' and " "'ldap_useruid'. For more information check the LDAP " - "http://../ldap/#filters[Filters] section.")}}, + "_`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 " - "http://../ldap/#filters[Filters] section.")}}, + "_`ldap.md#filters|Filters`_ section.")}}, %% Attributes: {ldap_groupattr, #{desc => @@ -774,8 +773,7 @@ mod_doc() -> #{desc => ?T("A regex for extracting user ID from the value of the " "attribute named by 'ldap_memberattr'. Check the LDAP " - "http://../ldap/#control-parameters" - "[Control Parameters] section.")}}, + "_`ldap.md#control-parameters|Control Parameters`_ section.")}}, {ldap_auth_check, #{value => "true | false", desc => diff --git a/src/mod_shared_roster_ldap_opt.erl b/src/mod_shared_roster_ldap_opt.erl index 3833f24f2..d4657222e 100644 --- a/src/mod_shared_roster_ldap_opt.erl +++ b/src/mod_shared_roster_ldap_opt.erl @@ -118,7 +118,7 @@ ldap_memberattr_format(Opts) when is_map(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' | re:mp(). +-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) -> diff --git a/src/mod_shared_roster_mnesia.erl b/src/mod_shared_roster_mnesia.erl index 0504184ba..028584459 100644 --- a/src/mod_shared_roster_mnesia.erl +++ b/src/mod_shared_roster_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl index ca25314fd..4d582a1bf 100644 --- a/src/mod_shared_roster_sql.erl +++ b/src/mod_shared_roster_sql.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +34,7 @@ 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"). @@ -43,9 +44,40 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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, diff --git a/src/mod_sic.erl b/src/mod_sic.erl index c21482d7f..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-2022 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,7 +25,7 @@ -module(mod_sic). --protocol({xep, 279, '0.2'}). +-protocol({xep, 279, '0.2', '2.1.3', "complete", ""}). -author('karim.gemayel@process-one.net'). @@ -38,21 +38,14 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -start(Host, _Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_SIC_0, - ?MODULE, process_local_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_SIC_0, - ?MODULE, process_sm_iq), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_SIC_1, - ?MODULE, process_local_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_SIC_1, - ?MODULE, process_sm_iq). +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}]}. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_SIC_0), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_SIC_0), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_SIC_1), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_SIC_1). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_sip.erl b/src/mod_sip.erl index 96e344569..aa98be2cc 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -5,7 +5,7 @@ %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2022 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 @@ -383,14 +383,14 @@ mod_doc() -> ?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 " - "http://../listen/#ejabberd-sip[ejabberd_sip] listen module " + "_`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. This approach allows to bypass " + "SIP messages. With this approach it is possible to bypass " "NATs/firewalls a bit more easily. " "The default value is 'true'.")}}, {flow_timeout_tcp, @@ -437,7 +437,6 @@ mod_doc() -> "cannot omit \"port\" or \"scheme\").")}}], example => ["modules:", - " ...", " mod_sip:", " always_record_route: false", " record_route: \"sip:example.com;lr\"", @@ -449,7 +448,6 @@ mod_doc() -> " via:", " - tls://sip-tls.example.com:5061", " - tcp://sip-tcp.example.com:5060", - " - udp://sip-udp.example.com:5060", - " ..."]}. + " - udp://sip-udp.example.com:5060"]}. -endif. diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index 8534766c4..8c5d8348c 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -5,7 +5,7 @@ %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2022 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 diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl index 7c6ffef9e..ade4c0be0 100644 --- a/src/mod_sip_registrar.erl +++ b/src/mod_sip_registrar.erl @@ -5,7 +5,7 @@ %%% Created : 23 Apr 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2022 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 diff --git a/src/mod_stats.erl b/src/mod_stats.erl index 52798ce49..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-2022 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 @@ -author('alexey@process-one.net'). --protocol({xep, 39, '0.6.0'}). +-protocol({xep, 39, '0.6.0', '0.1.0', "complete", ""}). -behaviour(gen_mod). @@ -38,15 +38,14 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -start(Host, _Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_STATS, - ?MODULE, process_iq). +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. -reload(Host, NewOpts, _OldOpts) -> - start(Host, NewOpts). +reload(_Host, _NewOpts, _OldOpts) -> + ok. depends(_Host, _Opts) -> []. diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl index 67ab815d1..f3a641a7a 100644 --- a/src/mod_stream_mgmt.erl +++ b/src/mod_stream_mgmt.erl @@ -3,7 +3,7 @@ %%% Created : 25 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +23,7 @@ -module(mod_stream_mgmt). -behaviour(gen_mod). -author('holger@zedat.fu-berlin.de'). --protocol({xep, 198, '1.5.2'}). +-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]). @@ -32,11 +32,16 @@ -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_call/3, - c2s_handle_recv/3]). + 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"). @@ -61,42 +66,27 @@ %%%=================================================================== %%% API %%%=================================================================== -start(Host, Opts) -> +start(_Host, Opts) -> init_cache(Opts), - ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, - c2s_stream_started, 50), - ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, - c2s_stream_features, 50), - ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, - c2s_unauthenticated_packet, 50), - ejabberd_hooks:add(c2s_unbinded_packet, Host, ?MODULE, - c2s_unbinded_packet, 50), - ejabberd_hooks:add(c2s_authenticated_packet, Host, ?MODULE, - c2s_authenticated_packet, 50), - ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, c2s_handle_send, 50), - ejabberd_hooks:add(c2s_handle_recv, Host, ?MODULE, c2s_handle_recv, 50), - ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), - ejabberd_hooks:add(c2s_handle_call, Host, ?MODULE, c2s_handle_call, 50), - ejabberd_hooks:add(c2s_closed, Host, ?MODULE, c2s_closed, 50), - ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, c2s_terminated, 50). + {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) -> - ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, - c2s_stream_started, 50), - ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, - c2s_stream_features, 50), - ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, - c2s_unauthenticated_packet, 50), - ejabberd_hooks:delete(c2s_unbinded_packet, Host, ?MODULE, - c2s_unbinded_packet, 50), - ejabberd_hooks:delete(c2s_authenticated_packet, Host, ?MODULE, - c2s_authenticated_packet, 50), - ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, c2s_handle_send, 50), - ejabberd_hooks:delete(c2s_handle_recv, Host, ?MODULE, c2s_handle_recv, 50), - ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), - ejabberd_hooks:delete(c2s_handle_call, Host, ?MODULE, c2s_handle_call, 50), - ejabberd_hooks:delete(c2s_closed, Host, ?MODULE, c2s_closed, 50), - ejabberd_hooks:delete(c2s_terminated, Host, ?MODULE, c2s_terminated, 50). +stop(_Host) -> + ok. reload(_Host, NewOpts, _OldOpts) -> init_cache(NewOpts), @@ -132,6 +122,47 @@ c2s_stream_features(Acc, Host) -> 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 @@ -229,6 +260,13 @@ c2s_handle_send(#{mgmt_state := MgmtState, mod := Mod, 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)}, @@ -372,28 +410,28 @@ perform_stream_mgmt(Pkt, #{mgmt_xmlns := Xmlns, lang := Lang} = State) -> xmlns = Xmlns}) end. --spec handle_enable(state(), sm_enable()) -> state(). -handle_enable(#{mgmt_timeout := DefaultTimeout, - mgmt_queue_type := QueueType, - mgmt_max_timeout := MaxTimeout, - mgmt_xmlns := Xmlns, jid := JID} = State, - #sm_enable{resume = Resume, max = Max}) -> +-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 -> + 0; + Max /= undefined, Max > 0, Max*1000 =< MaxTimeout -> Max*1000; - true -> + 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 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} @@ -401,6 +439,11 @@ handle_enable(#{mgmt_timeout := DefaultTimeout, 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(). @@ -414,39 +457,48 @@ handle_a(State, #sm_a{h = H}) -> resend_rack(State1). -spec handle_resume(state(), sm_resume()) -> {ok, state()} | {error, state()}. -handle_resume(#{user := User, lserver := LServer, - lang := Lang, socket := Socket} = State, - #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) -> - R = case inherit_session_state(State, PrevID) of - {ok, InheritedState} -> - {ok, InheritedState, H}; - {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, - case R of - {ok, #{jid := JID} = ResumedState, NumHandled} -> - State1 = check_h_attribute(ResumedState, NumHandled), - #{mgmt_xmlns := AttrXmlns, mgmt_stanzas_in := AttrH} = State1, - State2 = send(State1, #sm_resumed{xmlns = AttrXmlns, - h = AttrH, - previd = PrevID}), - State3 = resend_unacked_stanzas(State2), - 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)]), - {ok, State5}; +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) -> @@ -899,7 +951,7 @@ mod_doc() -> "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 acknowledgements " + "entities, including features for stanza acknowledgments " "and stream resumption."), opts => [{max_ack_queue, @@ -941,7 +993,7 @@ mod_doc() -> {ack_timeout, #{value => "timeout()", desc => - ?T("A time to wait for stanza acknowledgements. " + ?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, diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl index 26a2646cc..c210868e7 100644 --- a/src/mod_stun_disco.erl +++ b/src/mod_stun_disco.erl @@ -5,7 +5,7 @@ %%% Created : 18 Apr 2020 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2020-2022 ProcessOne +%%% 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 @@ -25,7 +25,7 @@ -module(mod_stun_disco). -author('holger@zedat.fu-berlin.de'). --protocol({xep, 215, '0.7'}). +-protocol({xep, 215, '0.7', '20.04', "complete", ""}). -behaviour(gen_server). -behaviour(gen_mod). @@ -159,8 +159,8 @@ mod_doc() -> ?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]. " - "This module is included in ejabberd since version 20.04."), + "[XEP-0215: External Service Discovery]."), + note => "added in 20.04", opts => [{access, #{value => ?T("AccessName"), @@ -484,13 +484,15 @@ process_iq(#iq{lang = Lang} = IQ) -> -spec process_iq_get(iq(), request()) -> iq(). process_iq_get(#iq{from = From, to = #jid{lserver = Host}, lang = Lang} = IQ, - Request) -> + #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. diff --git a/src/mod_time.erl b/src/mod_time.erl index 9530967ec..fbebf03a7 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -6,7 +6,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +28,7 @@ -author('alexey@process-one.net'). --protocol({xep, 202, '2.0'}). +-protocol({xep, 202, '2.0', '2.1.0', "complete", ""}). -behaviour(gen_mod). @@ -39,13 +39,11 @@ -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -start(Host, _Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_TIME, ?MODULE, process_local_iq). +start(_Host, _Opts) -> + {ok, [{iq_handler, ejabberd_local, ?NS_TIME, process_local_iq}]}. -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_TIME). +stop(_Host) -> + ok. reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 3ac97b30e..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-2022 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,8 +27,9 @@ -author('alexey@process-one.net'). --protocol({xep, 54, '1.2'}). --protocol({xep, 55, '1.3'}). +-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). @@ -42,12 +43,17 @@ -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("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_vcard.hrl"). -include("translate.hrl"). --include("ejabberd_stacktrace.hrl"). + +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). -define(VCARD_CACHE, vcard_cache). @@ -97,6 +103,8 @@ init([Host|_]) -> 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 -> @@ -143,11 +151,11 @@ handle_cast(Cast, State) -> handle_info({route, Packet}, State) -> try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + 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) -> @@ -160,6 +168,8 @@ terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> 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( @@ -390,6 +400,12 @@ make_vcard_search(User, LUser, LServer, VCARD) -> 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 @@ -397,13 +413,17 @@ vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> %% 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|binary()} | ok. +-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 -> @@ -549,6 +569,61 @@ 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) -> []. @@ -666,18 +741,21 @@ mod_doc() -> "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => - [{?T("For example, the following XML representation of vCard:"), - ["", - " Conferences", - " ", - " ", - " Elm Street", - " ", - ""]}, - {?T("will be translated to:"), - ["vcard:", - " fn: Conferences", - " adr:", - " -", - " work: true", - " street: Elm Street"]}]}}]}. + ["# 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 f8e9a1d93..3ecf39ba1 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -4,7 +4,7 @@ %%% Created : 29 Jul 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl index 694333d65..e70b13fc0 100644 --- a/src/mod_vcard_mnesia.erl +++ b/src/mod_vcard_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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_vcard_sql.erl b/src/mod_vcard_sql.erl index 842d68c80..18456f402 100644 --- a/src/mod_vcard_sql.erl +++ b/src/mod_vcard_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +31,7 @@ -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"). @@ -41,9 +42,79 @@ %%%=================================================================== %%% API %%%=================================================================== -init(_Host, _Opts) -> +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. @@ -192,7 +263,7 @@ remove_user(LUser, LServer) -> " where lusername=%(LUser)s and %(LServer)H")) end). -export(_Server) -> +export(_Server) -> [{vcard, fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) when LServer == Host -> diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 61d582437..70ed707b2 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -5,7 +5,7 @@ %%% Created : 9 Mar 2007 by Igor Goryachev %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,8 +26,6 @@ -module(mod_vcard_xupdate). -behaviour(gen_mod). --protocol({xep, 398, '0.2.0'}). - %% gen_mod callbacks -export([start/2, stop/1, reload/3]). @@ -48,22 +46,13 @@ start(Host, Opts) -> init_cache(Host, Opts), - ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, - update_presence, 100), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send_packet, 50), - ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_set, - 90), - ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). + {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) -> - ejabberd_hooks:delete(c2s_self_presence, Host, - ?MODULE, update_presence, 100), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send_packet, 50), - ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, - vcard_set, 90), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). +stop(_Host) -> + ok. reload(Host, NewOpts, _OldOpts) -> init_cache(Host, NewOpts). @@ -107,8 +96,8 @@ user_send_packet(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_sm:force_update_presence({LUser, LServer}), + 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. @@ -117,7 +106,7 @@ vcard_set(Acc) -> remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}). + ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}, ejabberd_cluster:get_nodes()). %%==================================================================== %% Storage diff --git a/src/mod_version.erl b/src/mod_version.erl index b842dcfd0..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-2022 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 @@ -author('alexey@process-one.net'). --protocol({xep, 92, '1.1'}). +-protocol({xep, 92, '1.1', '0.1.0', "complete", ""}). -behaviour(gen_mod). diff --git a/src/mqtt_codec.erl b/src/mqtt_codec.erl index e09391ddf..1ef474dd5 100644 --- a/src/mqtt_codec.erl +++ b/src/mqtt_codec.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. +%%% @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. diff --git a/src/node_flat.erl b/src/node_flat.erl index b829395cc..7093d4beb 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -385,6 +385,13 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, 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()}; _ -> diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index a4142cb10..acfdf3331 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -257,6 +257,13 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, 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()}; _ -> diff --git a/src/node_pep.erl b/src/node_pep.erl index c52db1b3e..3d208c73b 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +30,8 @@ -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). +-protocol({xep, 384, '0.8.3', '21.12', "complete", ""}). + -include("pubsub.hrl"). -export([init/3, terminate/2, options/0, features/0, @@ -82,9 +84,11 @@ features() -> <<"auto-create">>, <<"auto-subscribe">>, <<"config-node">>, + <<"config-node-max">>, <<"delete-nodes">>, <<"delete-items">>, <<"filtered-notifications">>, + <<"item-ids">>, <<"modify-affiliations">>, <<"multi-items">>, <<"outcast-affiliation">>, diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index 1d1a632f2..5d35c7bfe 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/nodetree_tree.erl b/src/nodetree_tree.erl index df6e9cb60..facb4fd74 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -73,13 +73,13 @@ get_node(Host, Node, _From) -> get_node(Host, Node) -> case mnesia:read({pubsub_node, {Host, Node}}) of - [Record] when is_record(Record, pubsub_node) -> Record; + [#pubsub_node{} = Record] -> fixup_node(Record); _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. get_node(Nidx) -> case mnesia:index_read(pubsub_node, Nidx, #pubsub_node.id) of - [Record] when is_record(Record, pubsub_node) -> Record; + [#pubsub_node{} = Record] -> fixup_node(Record); _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. @@ -87,7 +87,8 @@ get_nodes(Host) -> get_nodes(Host, infinity). get_nodes(Host, infinity) -> - mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}); + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}), + [fixup_node(N) || N <- Nodes]; get_nodes(Host, Limit) -> case mnesia:select( pubsub_node, @@ -96,16 +97,18 @@ get_nodes(Host, Limit) -> Node end), Limit, read) of '$end_of_table' -> []; - {Nodes, _} -> Nodes + {Nodes, _} -> [fixup_node(N) || N <- Nodes] end. get_all_nodes({_U, _S, _R} = Owner) -> Host = jid:tolower(jid:remove_resource(Owner)), - mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}); + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}), + [fixup_node(N) || N <- Nodes]; get_all_nodes(Host) -> - mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'}, - _ = '_'}). + _ = '_'}), + [fixup_node(N) || N <- Nodes]. get_parentnodes(Host, Node, _From) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of @@ -119,7 +122,8 @@ 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 - [Record] when is_record(Record, pubsub_node) -> + [#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); @@ -130,7 +134,8 @@ get_parentnodes_tree(Host, Node, Level, Acc) -> end. get_subnodes(Host, <<>>, infinity) -> - mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, parents = [], _ = '_'}); + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, parents = [], _ = '_'}), + [fixup_node(N) || N <- Nodes]; get_subnodes(Host, <<>>, Limit) -> case mnesia:select( pubsub_node, @@ -139,10 +144,10 @@ get_subnodes(Host, <<>>, Limit) -> Node end), Limit, read) of '$end_of_table' -> []; - {Nodes, _} -> Nodes + {Nodes, _} -> [fixup_node(N) || N <- Nodes] end; get_subnodes(Host, Node, infinity) -> - Q = qlc:q([N + Q = qlc:q([fixup_node(N) || #pubsub_node{nodeid = {NHost, _}, parents = Parents} = N @@ -158,9 +163,12 @@ get_subnodes(Host, Node, Limit) -> end), Limit, read) of '$end_of_table' -> []; {Nodes, _} -> - lists:filter( - fun(#pubsub_node{parents = Parents}) -> - lists:member(Node, Parents) + lists:filtermap( + fun(#pubsub_node{parents = Parents} = N2) -> + case lists:member(Node, Parents) of + true -> {true, fixup_node(N2)}; + _ -> false + end end, Nodes) end. @@ -236,3 +244,16 @@ delete_node(Host, Node) -> 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_sql.erl b/src/nodetree_tree_sql.erl index 6c3419948..09959099e 100644 --- a/src/nodetree_tree_sql.erl +++ b/src/nodetree_tree_sql.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -82,17 +82,22 @@ set_node(Record) when is_record(Record, pubsub_node) -> " 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; - _ -> none % this should not happen - end + {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())}; @@ -328,13 +333,16 @@ raw_to_node(Host, {Node, Parent, Type, Nidx}) -> "where nodeid=%(Nidx)d")) of {selected, ROptions} -> - DbOpts = lists:map(fun ({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), + 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) -> diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl index 988c0334c..18eb9ed30 100644 --- a/src/nodetree_virtual.erl +++ b/src/nodetree_virtual.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index df7dedc9b..045abdf90 100644 --- a/src/prosody2ejabberd.erl +++ b/src/prosody2ejabberd.erl @@ -4,7 +4,7 @@ %%% Created : 20 Jan 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -87,8 +87,8 @@ convert_dir(Path, Host, Type) -> case eval_file(FilePath) of {ok, Data} -> Name = iolist_to_binary(filename:rootname(File)), - convert_data(url_decode(Host), Type, - url_decode(Name), Data); + convert_data(misc:uri_decode(Host), Type, + misc:uri_decode(Name), Data); Err -> Err end @@ -322,8 +322,13 @@ convert_roster_item(LUser, LServer, JIDstring, LuaList) -> [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, _} -> [] @@ -405,16 +410,6 @@ convert_privacy_item({_, Item}) -> match_presence_in = MatchPresIn, match_presence_out = MatchPresOut}. -url_decode(Encoded) -> - url_decode(Encoded, <<>>). -url_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) -> - Hex = list_to_integer([Hi, Lo], 16), - url_decode(Tail, <>); -url_decode(<>, Acc) -> - url_decode(Tail, <>); -url_decode(<<>>, Acc) -> - Acc. - decode_pubsub_host(Host) -> try jid:decode(Host) of #jid{luser = <<>>, lserver = LServer} -> LServer; diff --git a/src/proxy_protocol.erl b/src/proxy_protocol.erl index 5716fe2e6..4ce0d31b4 100644 --- a/src/proxy_protocol.erl +++ b/src/proxy_protocol.erl @@ -5,7 +5,7 @@ %%% Created : 27 Nov 2018 by Paweł Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/pubsub_db_sql.erl b/src/pubsub_db_sql.erl index 99c758d43..7a789e9ea 100644 --- a/src/pubsub_db_sql.erl +++ b/src/pubsub_db_sql.erl @@ -5,7 +5,7 @@ %%% Created : 7 Aug 2009 by Pablo Polvorin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -56,7 +56,7 @@ delete_subscription(SubID) -> "where subid = %(SubID)s")), ok. --spec update_subscription(#pubsub_subscription{}) -> ok . +-spec update_subscription(#pubsub_subscription{}) -> ok. update_subscription(#pubsub_subscription{subid = SubId} = Sub) -> delete_subscription(SubId), add_subscription(Sub). diff --git a/src/pubsub_index.erl b/src/pubsub_index.erl index 370fa1967..0c34ea63b 100644 --- a/src/pubsub_index.erl +++ b/src/pubsub_index.erl @@ -5,7 +5,7 @@ %%% Created : 30 Apr 2009 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/pubsub_migrate.erl b/src/pubsub_migrate.erl index 9436ca133..8d9fc6198 100644 --- a/src/pubsub_migrate.erl +++ b/src/pubsub_migrate.erl @@ -5,7 +5,7 @@ %%% Created : 26 Jul 2014 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/pubsub_subscription.erl b/src/pubsub_subscription.erl index 0b212d21c..6db643af6 100644 --- a/src/pubsub_subscription.erl +++ b/src/pubsub_subscription.erl @@ -5,7 +5,7 @@ %%% Created : 29 May 2009 by Brian Cully %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/pubsub_subscription_sql.erl b/src/pubsub_subscription_sql.erl index 1b5257891..8f1361b47 100644 --- a/src/pubsub_subscription_sql.erl +++ b/src/pubsub_subscription_sql.erl @@ -6,7 +6,7 @@ %%% Created : 7 Aug 2009 by Pablo Polvorin %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/rest.erl b/src/rest.erl index 58e424700..b456fdaac 100644 --- a/src/rest.erl +++ b/src/rest.erl @@ -5,7 +5,7 @@ %%% Created : 16 Oct 2014 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +38,14 @@ start(Host) -> application:start(inets), Size = ejabberd_option:ext_api_http_pool_size(Host), - httpc:set_options([{max_sessions, Size}]). + 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. @@ -87,8 +94,15 @@ request(Server, Method, Path, Params, Mime, Data) -> _ -> {Params, []} end, URI = to_list(url(Server, Path, Query)), - HttpOpts = [{connect_timeout, ?CONNECT_TIMEOUT}, - {timeout, ?HTTP_TIMEOUT}], + 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"}] @@ -147,7 +161,7 @@ to_list(V) when is_list(V) -> V. encode_json(Content) -> - case catch jiffy:encode(Content) of + case catch misc:json_encode(Content) of {'EXIT', Reason} -> {error, {invalid_payload, Content, Reason}}; Encoded -> @@ -157,7 +171,7 @@ encode_json(Content) -> decode_json(<<>>) -> []; decode_json(<<" ">>) -> []; decode_json(<<"\r\n">>) -> []; -decode_json(Data) -> jiffy:decode(Data). +decode_json(Data) -> misc:json_decode(Data). custom_headers(Server) -> case ejabberd_option:ext_api_headers(Server) of diff --git a/src/str.erl b/src/str.erl index cc7957f97..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-2022 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/translate.erl b/src/translate.erl index b0034588a..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-2022 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/win32_dns.erl b/src/win32_dns.erl index bd65cdc36..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-2022 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 @@ -39,9 +39,8 @@ get_nameservers() -> is_good_ns(Addr) -> element(1, inet_res:nnslookup("a.root-servers.net", in, a, [{Addr,53}], - timer:seconds(5) - ) - ) =:= ok. + timer:seconds(5))) + =:= ok. reg() -> {ok, R} = win32reg:open([read]), diff --git a/src/xml_compress.erl b/src/xml_compress.erl index a85ec56b2..21b9044a0 100644 --- a/src/xml_compress.erl +++ b/src/xml_compress.erl @@ -480,8 +480,11 @@ encode(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) -> - {El, _} = decode(Rest, <<"jabber:client">>, J1, J2), - El. + 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 @@ -489,35 +492,37 @@ decode_string(Data) -> {Str, Rest}; <<1:2, L1:6, 0:2, L2:6, Rest/binary>> -> L = L2*64 + L1, - <> = Rest, + <> = Rest, {Str, Rest2}; <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> -> L = (L3*64 + L2)*64 + L1, - <> = Rest, + <> = Rest, {Str, Rest2} end. -decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2) -> +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) -> +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) -> +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) -> +decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2, _) -> {stop, Rest}; -decode_child(Other, PNs, J1, J2) -> - decode(Other, PNs, J1, J2). +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) end, Data). + prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data). decode_attr(<<1:8, Rest/binary>>) -> {Name, Rest2} = decode_string(Rest), @@ -545,7 +550,7 @@ add_ns(Ns, Ns, Attrs) -> add_ns(_, Ns, Attrs) -> [{<<"xmlns">>, Ns} | Attrs]. -decode(<<5:8, Rest/binary>>, PNs, J1, J2) -> +decode(<<5:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -563,12 +568,12 @@ decode(<<5:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +decode(<<13:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -581,17 +586,17 @@ decode(<<13:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +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) -> +decode(<<6:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"jabber:client">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -636,7 +641,7 @@ decode(<<6:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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>>) -> @@ -650,25 +655,25 @@ decode(<<8:8, Rest/binary>>, PNs, J1, J2) -> 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) + 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) -> +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) -> +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) -> +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) -> +decode(<<10:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:sid:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -681,7 +686,7 @@ decode(<<10:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<22:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:sid:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -697,12 +702,12 @@ decode(<<22:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +decode(<<20:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:chat-markers:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -724,7 +729,7 @@ decode(<<20:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<24:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:chat-markers:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -737,7 +742,7 @@ decode(<<24:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<16:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:eme:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -757,7 +762,7 @@ decode(<<16:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<17:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:delay">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -775,7 +780,7 @@ decode(<<17:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<18:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"http://jabber.org/protocol/address">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -796,12 +801,12 @@ decode(<<18:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +decode(<<21:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:mam:tmp">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -817,12 +822,12 @@ decode(<<21:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +decode(<<25:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:receipts">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -835,17 +840,17 @@ decode(<<25:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +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) -> +decode(<<27:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"http://jabber.org/protocol/muc#user">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -861,17 +866,17 @@ decode(<<27:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +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) -> +decode(<<30:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"jabber:x:conference">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -886,12 +891,12 @@ decode(<<30:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +decode(<<34:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"http://jabber.org/protocol/pubsub#event">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -904,7 +909,7 @@ decode(<<34:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<35:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"http://jabber.org/protocol/pubsub#event">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -919,7 +924,7 @@ decode(<<35:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +decode(<<36:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"p1:push:custom">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -935,12 +940,12 @@ decode(<<36:8, Rest/binary>>, PNs, J1, J2) -> 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) -> +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) -> +decode(<<38:8, Rest/binary>>, PNs, J1, J2, _) -> Ns = <<"urn:xmpp:message-correct:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> @@ -953,6 +958,6 @@ decode(<<38:8, Rest/binary>>, PNs, J1, J2) -> end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"replace">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; -decode(Other, PNs, J1, J2) -> - decode_child(Other, PNs, J1, J2). +decode(Other, PNs, J1, J2, Loop) -> + decode_child(Other, PNs, J1, J2, Loop). diff --git a/test/README b/test/README index 68ff183dc..8254230d2 100644 --- a/test/README +++ b/test/README @@ -17,7 +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; -$ psql ejabberd_test -f sql/pg.sql +# If you disabled the update_sql_schema option, create the schema manually: +# $ psql ejabberd_test -f sql/pg.sql ------------------- MySQL @@ -26,7 +27,8 @@ $ 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'; -$ mysql ejabberd_test < sql/mysql.sql +# If you disabled the update_sql_schema option, create the schema manually: +# $ mysql ejabberd_test < sql/mysql.sql ------------------- MS SQL Server diff --git a/test/announce_tests.erl b/test/announce_tests.erl index e1258629b..724baba27 100644 --- a/test/announce_tests.erl +++ b/test/announce_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/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 index e1717be90..eabd3af2a 100644 --- a/test/carbons_tests.erl +++ b/test/carbons_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/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 index f1f39b49d..f2b61abff 100644 --- a/test/csi_tests.erl +++ b/test/csi_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/docker/README.md b/test/docker/README.md index b19321215..7caaa4bb7 100644 --- a/test/docker/README.md +++ b/test/docker/README.md @@ -10,7 +10,7 @@ attached to it. ``` mkdir test/docker/db/mysql/data mkdir test/docker/db/postgres/data -(cd test/docker; docker-compose up) +(cd test/docker; docker compose up) ``` You can stop all the databases with CTRL-C. @@ -20,8 +20,8 @@ You can stop all the databases with CTRL-C. 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-tools/bin/sqlcmd -U SA -P ejabberd_Test1 -S localhost -i /initdb_mssql.sql -docker exec ejabberd-mssql /opt/mssql-tools/bin/sqlcmd -U SA -P ejabberd_Test1 -S localhost -i /mssql.sql +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 @@ -44,7 +44,7 @@ make test You can fully clean up the environment with: ``` -(cd test/docker; docker-compose down) +(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: diff --git a/test/docker/db/mssql/initdb/initdb_mssql.sql b/test/docker/db/mssql/initdb/initdb_mssql.sql index a9ec5a5a8..8c7acc708 100644 --- a/test/docker/db/mssql/initdb/initdb_mssql.sql +++ b/test/docker/db/mssql/initdb/initdb_mssql.sql @@ -1,8 +1,16 @@ -USE [master] +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 - set noexec on -- prevent creation when already exists +BEGIN +SET NOEXEC ON; +END CREATE DATABASE ejabberd_test; GO @@ -10,7 +18,7 @@ GO USE ejabberd_test; GO -CREATE LOGIN ejabberd_test WITH PASSWORD = 'ejabberd_Test1'; +CREATE LOGIN ejabberd_test WITH PASSWORD = 'ejabberd_Test1', CHECK_POLICY = OFF; GO CREATE USER ejabberd_test FOR LOGIN ejabberd_test; diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml index 7ce610eab..bdc470a63 100644 --- a/test/docker/docker-compose.yml +++ b/test/docker/docker-compose.yml @@ -7,7 +7,6 @@ services: volumes: - mysqldata:/var/lib/mysql - ../../sql/mysql.sql:/docker-entrypoint-initdb.d/mysql.sql:ro - command: --default-authentication-plugin=mysql_native_password restart: always ports: - 3306:3306 @@ -24,6 +23,7 @@ services: - 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 diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index 10b6aff69..ca465689d 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -3,7 +3,7 @@ %%% Created : 2 Jun 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -60,6 +60,11 @@ init_per_suite(Config) -> NewConfig. 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). end_per_suite(_Config) -> @@ -99,7 +104,8 @@ do_init_per_group(mysql, Config) -> case catch ejabberd_sql:sql_query(?MYSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?MYSQL_VHOST), - clear_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}} @@ -108,7 +114,8 @@ do_init_per_group(mssql, Config) -> case catch ejabberd_sql:sql_query(?MSSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?MSSQL_VHOST), - clear_sql_tables(mssql, ?config(base_dir, Config)), + update_sql(?MSSQL_VHOST, Config), + stop_temporary_modules(?MSSQL_VHOST), set_opt(server, ?MSSQL_VHOST, Config); Err -> {skip, {mssql_not_available, Err}} @@ -117,7 +124,8 @@ do_init_per_group(pgsql, Config) -> case catch ejabberd_sql:sql_query(?PGSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?PGSQL_VHOST), - clear_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}} @@ -161,15 +169,44 @@ do_init_per_group(GroupName, Config) -> _ -> 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(redis, _Config) -> ok; -end_per_group(mysql, _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) -> +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) -> +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; @@ -302,6 +339,10 @@ init_per_testcase(TestCase, OrigConfig) -> 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), @@ -365,6 +406,8 @@ no_db_tests() -> 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(), @@ -386,6 +429,7 @@ db_tests(DB) when DB == mnesia; DB == redis -> auth_md5, presence_broadcast, last, + antispam_tests:single_cases(), webadmin_tests:single_cases(), roster_tests:single_cases(), private_tests:single_cases(), @@ -397,6 +441,7 @@ db_tests(DB) when DB == mnesia; DB == redis -> mam_tests:single_cases(), csi_tests:single_cases(), push_tests:single_cases(), + test_pass_change, test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), @@ -426,6 +471,7 @@ db_tests(DB) -> offline_tests:single_cases(), mam_tests:single_cases(), push_tests:single_cases(), + test_pass_change, test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), @@ -644,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 -> @@ -894,7 +959,8 @@ presence_broadcast(Config) -> IQ = #iq{type = get, from = JID, sub_els = [#disco_info{node = Node}]} = recv_iq(Config), - #message{type = normal} = recv_message(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 = JID, sub_els = [Info]}), @@ -1011,37 +1077,35 @@ bookmark_conference() -> '$handle_undefined_function'(_, _) -> erlang:error(undef). + %%%=================================================================== %%% SQL stuff %%%=================================================================== -clear_sql_tables(sqlite, _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, BaseDir) -> +clear_sql_tables(Type, Config) -> + BaseDir = ?config(base_dir, Config), {VHost, File} = case Type of - mysql -> - Path = case ejabberd_sql:use_new_schema() of - true -> - "mysql.new.sql"; - false -> - "mysql.sql" - end, - {?MYSQL_VHOST, Path}; - mssql -> - Path = case ejabberd_sql:use_new_schema() of - true -> - "mssql.new.sql"; - false -> - "mssql.sql" - end, - {?MSSQL_VHOST, Path}; - pgsql -> - Path = case ejabberd_sql:use_new_schema() of - true -> - "pg.new.sql"; - false -> - "pg.sql" - end, - {?PGSQL_VHOST, Path} + 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), @@ -1067,7 +1131,17 @@ clear_table_queries(Queries) -> fun(Query, Acc) -> case split(str:to_lower(Query)) of [<<"create">>, <<"table">>, Table|_] -> - [<<"DELETE FROM ", 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 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 f071ffe8b..000000000 --- a/test/ejabberd_SUITE_data/ejabberd.cfg +++ /dev/null @@ -1,181 +0,0 @@ -{loglevel, 4}. -{hosts, ["localhost", - "mnesia.localhost", - "mysql.localhost", - "mssql.localhost", - "pgsql.localhost", - "sqlite.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, [{db_type, 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, "mssql.localhost", - [{auth_method, odbc}, - {odbc_pool_size, 1}, - {odbc_server, {mssql, "localhost", "ejabberd_test", - "ejabberd_test", "ejabberd_Test1"}}, - {{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, [{db_type, 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, [{db_type, 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, "sqlite.localhost", - [{auth_method, odbc}, - {odbc_pool_size, 1}, - {odbc_server, {sqlite, "/tmp/ejabberd_test.db"}}, - {{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, [{db_type, 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 index 660ddccd6..11a67d2cc 100644 --- a/test/ejabberd_SUITE_data/ejabberd.extauth.yml +++ b/test/ejabberd_SUITE_data/ejabberd.extauth.yml @@ -1,5 +1,5 @@ define_macro: EXTAUTH_CONFIG: queue_type: ram - extauth_program: "python extauth.py" + extauth_program: "python3 extauth.py" auth_method: external diff --git a/test/ejabberd_SUITE_data/ejabberd.mnesia.yml b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml index 14bb2bff2..56fdf5e6e 100644 --- a/test/ejabberd_SUITE_data/ejabberd.mnesia.yml +++ b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml @@ -6,6 +6,14 @@ define_macro: 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 @@ -14,6 +22,7 @@ define_macro: mod_muc: db_type: internal vcard: VCARD + mod_muc_occupantid: [] mod_offline: db_type: internal mod_privacy: diff --git a/test/ejabberd_SUITE_data/ejabberd.mssql.yml b/test/ejabberd_SUITE_data/ejabberd.mssql.yml index 1ecf77ba8..1458cafa4 100644 --- a/test/ejabberd_SUITE_data/ejabberd.mssql.yml +++ b/test/ejabberd_SUITE_data/ejabberd.mssql.yml @@ -22,6 +22,7 @@ define_macro: db_type: sql ram_db_type: sql vcard: VCARD + mod_muc_occupantid: [] mod_offline: use_cache: true db_type: sql @@ -69,3 +70,11 @@ 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 index 411901976..91705ee68 100644 --- a/test/ejabberd_SUITE_data/ejabberd.mysql.yml +++ b/test/ejabberd_SUITE_data/ejabberd.mysql.yml @@ -22,6 +22,7 @@ define_macro: db_type: sql ram_db_type: sql vcard: VCARD + mod_muc_occupantid: [] mod_offline: use_cache: true db_type: sql @@ -70,3 +71,11 @@ 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 index c0cd0b0d6..16d8b1d27 100644 --- a/test/ejabberd_SUITE_data/ejabberd.pgsql.yml +++ b/test/ejabberd_SUITE_data/ejabberd.pgsql.yml @@ -22,6 +22,7 @@ define_macro: db_type: sql ram_db_type: sql vcard: VCARD + mod_muc_occupantid: [] mod_offline: use_cache: true db_type: sql @@ -70,3 +71,11 @@ 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 index 7065f0ffd..fb1ba435f 100644 --- a/test/ejabberd_SUITE_data/ejabberd.redis.yml +++ b/test/ejabberd_SUITE_data/ejabberd.redis.yml @@ -7,6 +7,14 @@ define_macro: 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 @@ -15,6 +23,7 @@ define_macro: mod_muc: db_type: internal vcard: VCARD + mod_muc_occupantid: [] mod_offline: db_type: internal mod_privacy: diff --git a/test/ejabberd_SUITE_data/ejabberd.sqlite.yml b/test/ejabberd_SUITE_data/ejabberd.sqlite.yml index 3e22f6a2d..11420ef6c 100644 --- a/test/ejabberd_SUITE_data/ejabberd.sqlite.yml +++ b/test/ejabberd_SUITE_data/ejabberd.sqlite.yml @@ -1,5 +1,8 @@ define_macro: SQLITE_CONFIG: + auth_stored_password_types: + - plain + - scram_sha256 sql_type: sqlite sql_pool_size: 1 auth_method: sql @@ -17,6 +20,7 @@ define_macro: db_type: sql ram_db_type: sql vcard: VCARD + mod_muc_occupantid: [] mod_offline: db_type: sql mod_privacy: diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml index 195917b68..812bea841 100644 --- a/test/ejabberd_SUITE_data/ejabberd.yml +++ b/test/ejabberd_SUITE_data/ejabberd.yml @@ -1,5 +1,6 @@ include_config_file: - macros.yml + - configtest.yml - ejabberd.extauth.yml - ejabberd.ldap.yml - ejabberd.mnesia.yml @@ -10,6 +11,7 @@ include_config_file: - ejabberd.sqlite.yml host_config: + configtest.localhost: CONFIGTEST_CONFIG pgsql.localhost: PGSQL_CONFIG sqlite.localhost: SQLITE_CONFIG mysql.localhost: MYSQL_CONFIG @@ -25,6 +27,7 @@ host_config: hosts: - localhost + - configtest.localhost - mnesia.localhost - redis.localhost - mysql.localhost @@ -108,6 +111,9 @@ max_fsm_queue: 1000 queue_type: file modules: mod_adhoc: [] + mod_adhoc_api: [] + mod_admin_extra: [] + mod_admin_update_sql: [] mod_announce: [] mod_configure: [] mod_disco: [] @@ -117,6 +123,7 @@ modules: vcard: VCARD mod_muc: vcard: VCARD + mod_muc_occupantid: [] mod_muc_admin: [] mod_carboncopy: [] mod_jidprep: [] @@ -160,6 +167,8 @@ certfiles: new_sql_schema: NEW_SCHEMA +update_sql_schema: UPDATE_SQL_SCHEMA + api_permissions: "public commands": who: all diff --git a/test/ejabberd_SUITE_data/extauth.py b/test/ejabberd_SUITE_data/extauth.py index 394c2126a..e34208ed7 100755 --- a/test/ejabberd_SUITE_data/extauth.py +++ b/test/ejabberd_SUITE_data/extauth.py @@ -1,51 +1,56 @@ +"""extauth dummy script for ejabberd testing.""" + import sys import struct -def read_from_stdin(bytes): - if hasattr(sys.stdin, 'buffer'): - return sys.stdin.buffer.read(bytes) - else: - return sys.stdin.read(bytes) +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(): + """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': - u, s, p = pkt.split(':', 3)[1:] - if u == "wrong": + user, _, _ = pkt.split(':', 3)[1:] + if user == "wrong": write(False) else: write(True) elif cmd == 'isuser': - u, s = pkt.split(':', 2)[1:] - if u == "wrong": + user, _ = pkt.split(':', 2)[1:] + if user == "wrong": write(False) else: write(True) elif cmd == 'setpass': - u, s, p = pkt.split(':', 3)[1:] + user, _, _ = pkt.split(':', 3)[1:] write(True) elif cmd == 'tryregister': - u, s, p = pkt.split(':', 3)[1:] + user, _, _ = pkt.split(':', 3)[1:] write(True) elif cmd == 'removeuser': - u, s = pkt.split(':', 2)[1:] + user, _ = pkt.split(':', 2)[1:] write(True) elif cmd == 'removeuser3': - u, s, p = pkt.split(':', 3)[1:] + 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/macros.yml b/test/ejabberd_SUITE_data/macros.yml index e4270d4c1..5391562ba 100644 --- a/test/ejabberd_SUITE_data/macros.yml +++ b/test/ejabberd_SUITE_data/macros.yml @@ -14,6 +14,7 @@ define_macro: 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@@ 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/example_tests.erl b/test/example_tests.erl index dab5836d7..5fd0a86ff 100644 --- a/test/example_tests.erl +++ b/test/example_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/jidprep_tests.erl b/test/jidprep_tests.erl index efe7f711c..f18e150fb 100644 --- a/test/jidprep_tests.erl +++ b/test/jidprep_tests.erl @@ -3,7 +3,7 @@ %%% Created : 11 Sep 2019 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2019-2022 ProcessOne +%%% 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 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 63d91f6c7..2d0cea988 100644 --- a/test/ldap_srv.erl +++ b/test/ldap_srv.erl @@ -3,7 +3,7 @@ %%% Created : 21 Jun 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/mam_tests.erl b/test/mam_tests.erl index ed7bbe97d..27988bf5e 100644 --- a/test/mam_tests.erl +++ b/test/mam_tests.erl @@ -3,7 +3,7 @@ %%% Created : 14 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -431,12 +431,21 @@ set_default(Config, Default) -> send_messages(Config, Range) -> Peer = ?config(peer, Config), + send_message_extra(Config, 0, <<"to-retract-1">>, []), lists:foreach( - fun(N) -> + 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( @@ -448,7 +457,7 @@ recv_messages(Config, Range) -> xmpp:get_subtag(Msg, #mam_archived{}), #stanza_id{by = BareMyJID} = xmpp:get_subtag(Msg, #stanza_id{}) - end, Range). + end, [0 | Range]). recv_archived_messages(Config, From, To, QID, Range) -> MyJID = my_jid(Config), @@ -483,7 +492,7 @@ send_query(Config, #mam_query{xmlns = NS} = 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 -> +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, @@ -642,7 +651,8 @@ query_rsm_after(Config, From, To, NS) -> query_rsm_before(Config, From, To) -> lists:foreach( fun(NS) -> - query_rsm_before(Config, From, To, NS) + query_rsm_before(Config, From, To, NS), + query_last_message(Config, From, To, NS) end, ?VERSIONS). query_rsm_before(Config, From, To, NS) -> @@ -661,6 +671,16 @@ query_rsm_before(Config, From, To, NS) -> 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; 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 index f050e72ca..ae249691d 100644 --- a/test/muc_tests.erl +++ b/test/muc_tests.erl @@ -3,7 +3,7 @@ %%% Created : 15 Oct 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -259,7 +259,9 @@ set_room_affiliation(Config) -> RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", Headers = [{"X-Admin", "true"}], ContentType = "application/json", - Body = jiffy:encode(#{name => RoomName, service => RoomService, jid => jid:encode(PeerJID), affiliation => member}), + 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 = [ @@ -302,7 +304,68 @@ master_slave_cases() -> 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(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), @@ -1242,7 +1305,7 @@ config_private_messages_master(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}, - {allow_private_messages, false}]), + {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), @@ -1626,7 +1689,7 @@ join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) -> join(Config, Role, #muc{} = SubEl) when is_atom(Role) -> join(Config, Role, none, SubEl). -join(Config, Role, Aff, 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), @@ -1634,7 +1697,7 @@ join(Config, Role, Aff, SubEl) -> MyNickJID = jid:replace_resource(Room, MyNick), PeerNick = ?config(peer_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), - send(Config, #presence{to = MyNickJID, sub_els = [SubEl]}), + 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{}); @@ -1665,7 +1728,9 @@ join(Config, Role, Aff, SubEl) -> {History, Subj} = recv_history_and_subject(Config), {empty, History, Subj, Codes} end - end. + end; +join(Config, Role, Aff, SubEl) -> + join(Config, Role, Aff, [SubEl]). leave(Config) -> leave(Config, muc_room_jid(Config)). diff --git a/test/offline_tests.erl b/test/offline_tests.erl index 5ffe50def..d859da622 100644 --- a/test/offline_tests.erl +++ b/test/offline_tests.erl @@ -3,7 +3,7 @@ %%% Created : 7 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -142,7 +142,7 @@ unsupported_iq(Config) -> %%%=================================================================== %%% Master-slave tests %%%=================================================================== -master_slave_cases(DB) -> +master_slave_cases(_DB) -> {offline_master_slave, [sequence], [master_slave_test(flex), master_slave_test(send_all), @@ -233,8 +233,6 @@ mucsub_mam_slave(Config) -> gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), Room = suite:muc_room_jid(Config), - MyJID = my_jid(Config), - MyJIDBare = jid:remove_resource(MyJID), ok = mam_tests:set_default(Config, always), #presence{} = send_recv(Config, #presence{}), send(Config, #presence{type = unavailable}), diff --git a/test/privacy_tests.erl b/test/privacy_tests.erl index 39526e5b1..51782dcf4 100644 --- a/test/privacy_tests.erl +++ b/test/privacy_tests.erl @@ -3,7 +3,7 @@ %%% Created : 18 Oct 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/private_tests.erl b/test/private_tests.erl index fd961b8b8..e7077f4ba 100644 --- a/test/private_tests.erl +++ b/test/private_tests.erl @@ -3,7 +3,7 @@ %%% Created : 23 Nov 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -95,7 +95,11 @@ test_published(Config) -> #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, - sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}); + 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, diff --git a/test/proxy65_tests.erl b/test/proxy65_tests.erl index 0a75f820e..612a926fb 100644 --- a/test/proxy65_tests.erl +++ b/test/proxy65_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/pubsub_tests.erl b/test/pubsub_tests.erl index 043545422..1cb02f020 100644 --- a/test/pubsub_tests.erl +++ b/test/pubsub_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/push_tests.erl b/test/push_tests.erl index 553f90968..9e400cccc 100644 --- a/test/push_tests.erl +++ b/test/push_tests.erl @@ -3,7 +3,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/replaced_tests.erl b/test/replaced_tests.erl index 5f3224aea..37e22b3ac 100644 --- a/test/replaced_tests.erl +++ b/test/replaced_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/roster_tests.erl b/test/roster_tests.erl index f8d1999e6..8d096eea3 100644 --- a/test/roster_tests.erl +++ b/test/roster_tests.erl @@ -3,7 +3,7 @@ %%% Created : 22 Oct 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/sm_tests.erl b/test/sm_tests.erl index 8e87c642a..a55957856 100644 --- a/test/sm_tests.erl +++ b/test/sm_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/stundisco_tests.erl b/test/stundisco_tests.erl index 65569ead8..ca941983f 100644 --- a/test/stundisco_tests.erl +++ b/test/stundisco_tests.erl @@ -3,7 +3,7 @@ %%% Created : 22 Apr 2020 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2020-2022 ProcessOne +%%% 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 @@ -132,18 +132,17 @@ turn_credentials(Config) -> port = Port, 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}]}]} = + 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), @@ -159,18 +158,17 @@ turns_credentials(Config) -> port = Port, 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}]}]} = + 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), diff --git a/test/suite.erl b/test/suite.erl index 7669e4fe4..5fbd70463 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -3,7 +3,7 @@ %%% Created : 27 Jun 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -51,6 +51,11 @@ init_config(Config) -> {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(), @@ -59,6 +64,7 @@ init_config(Config) -> [{c2s_port, 5222}, {loglevel, 4}, {new_schema, false}, + {update_sql_schema, true}, {s2s_port, 5269}, {stun_port, 3478}, {component_port, 5270}, @@ -83,6 +89,7 @@ init_config(Config) -> {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 @@ -131,15 +138,42 @@ init_config(Config) -> {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) -> - io:format("copying ~p", [Src]), + ct:pal("copying ~p", [Src]), File = filename:basename(Src), case string:tokens(File, ".") of ["ejabberd", SBackend, "yml"] -> @@ -546,10 +580,11 @@ decode_stream_element(NS, El) -> decode(El, NS, []). format_element(El) -> - case erlang:function_exported(ct, log, 5) of + 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. + end, + binary:replace(Bin, <<"<">>, <<"<">>, [global]). decode(El, NS, Opts) -> try @@ -881,6 +916,21 @@ receiver(NS, Owner, Socket, MRef) -> 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. %%%=================================================================== diff --git a/test/suite.hrl b/test/suite.hrl index d8ea3e23b..00b341cb1 100644 --- a/test/suite.hrl +++ b/test/suite.hrl @@ -9,7 +9,7 @@ -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() -> @@ -89,6 +89,8 @@ -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">>). diff --git a/test/upload_tests.erl b/test/upload_tests.erl index 3a5885111..7e2d89958 100644 --- a/test/upload_tests.erl +++ b/test/upload_tests.erl @@ -3,7 +3,7 @@ %%% Created : 17 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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/test/vcard_tests.erl b/test/vcard_tests.erl index 791a2265c..fc3adb611 100644 --- a/test/vcard_tests.erl +++ b/test/vcard_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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,6 +31,7 @@ recv_presence/1, recv/1]). -include("suite.hrl"). +-include_lib("stdlib/include/assert.hrl"). %%%=================================================================== %%% API @@ -83,9 +84,9 @@ get_set(Config) -> "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]} = + #iq{type = result, sub_els = [VCard1]} = send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), + ?assertEqual(VCard, VCard1), disconnect(Config). service_vcard(Config) -> @@ -118,7 +119,7 @@ xupdate_master(Config) -> 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, + {_, _} = ?recv2(#presence{from = MyJID, type = available, sub_els = [#vcard_xupdate{hash = undefined}]}, #presence{from = Peer, type = unavailable}), disconnect(Config). diff --git a/test/webadmin_tests.erl b/test/webadmin_tests.erl index 34e2c1b86..fa12fd6f8 100644 --- a/test/webadmin_tests.erl +++ b/test/webadmin_tests.erl @@ -3,7 +3,7 @@ %%% Created : 23 Mar 2020 by Pawel Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2022 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 @@ -76,10 +76,11 @@ adduser(Config) -> Body = make_query( Config, "server/" ++ binary_to_list(Server) ++ "/users/", - <<"newusername=", (mue(User))/binary, "&newuserpassword=", - (mue(Password))/binary, "&addnewuser=Add+User">>), - Password = ejabberd_auth:get_password(User, Server), - ?match({_, _}, binary:match(Body, <<"Submitted

">>)). + <<"change_password/newpass=", (mue(Password))/binary, + "&change_password=Change+Password">>), + ?match(Password, ejabberd_auth:get_password_s(User, Server)), + ?match({_, _}, binary:match(Body, <<"
ok
">>)). removeuser(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, @@ -101,7 +102,7 @@ removeuser(Config) -> Config, "server/" ++ binary_to_list(Server) ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", - <<"password=&removeuser=Remove+User">>), + <<"&unregister=Unregister">>), false = ejabberd_auth:user_exists(User, Server), ?match(nomatch, binary:match(Body, <<"

Last Activity

20">>)). diff --git a/tools/check_xep_versions.sh b/tools/check_xep_versions.sh index c22781aaa..60c5fa529 100755 --- a/tools/check_xep_versions.sh +++ b/tools/check_xep_versions.sh @@ -7,7 +7,7 @@ check_xep() [ -f $BASE/doc/$xep ] || curl -s -o $BASE/doc/$xep https://xmpp.org/extensions/$xep.html title=$(sed '//!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.-]*\)'.*/\1 \2/") + 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/ /;}" } diff --git a/tools/ejabberdctl.bc b/tools/ejabberdctl.bc index 2ebae4a3b..b0c88fa15 100644 --- a/tools/ejabberdctl.bc +++ b/tools/ejabberdctl.bc @@ -1,18 +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 tags >$COMMANDCACHE.tags - ejabberdctl $CTLARGS >$COMMANDCACHE - if [[ $? == 2 ]] ; then - ISRUNNING=1 - runningcommands=`cat $COMMANDCACHE | grep "^ [a-z]" | awk '{print $1}' | xargs` - runningtags=`cat $COMMANDCACHE.tags | 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 - rm $COMMANDCACHE.tags } _ejabberdctl() 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/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 index 00aa044a7..3a3271b50 100755 --- a/tools/hook_deps.sh +++ b/tools/hook_deps.sh @@ -169,7 +169,7 @@ check_iq_handlers_export({HookedFuns, _}, Exports) -> case is_exported(Mod, Fun, 1, Exports) of true -> ok; false -> - err("~s:~B: Error: " + err("~s:~p: Error: " "iq handler is registered on unexported function: " "~s:~s/1~n", [File, FileNo, Mod, Fun]) end @@ -184,7 +184,7 @@ analyze_iq_handlers({Add, Del}) -> case maps:is_key(Handler, Del) of true -> ok; false -> - err("~s:~B: Error: " + err("~s:~p: Error: " "iq handler is added but not removed~n", [File, FileNo]) end @@ -197,7 +197,7 @@ analyze_iq_handlers({Add, Del}) -> case maps:is_key(Handler, Add) of true -> ok; false -> - err("~s:~B: Error: " + err("~s:~p: Error: " "iq handler is removed but not added~n", [File, FileNo]) end @@ -224,8 +224,8 @@ analyze_hooks({Add, Del}) -> case maps:is_key(Key, Del1) of true -> ok; false -> - err("~s:~B: Error: " - "hook ~s->~s->~s is added but was never removed~n", + 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)), @@ -234,8 +234,8 @@ analyze_hooks({Add, Del}) -> case maps:is_key(Key, Add1) of true -> ok; false -> - err("~s:~B: Error: " - "hook ~s->~s->~s is removed but was never added~n", + 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)). @@ -305,6 +305,8 @@ 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, diff --git a/tools/make-binaries b/tools/make-binaries index 20749da11..22ad3e98c 100755 --- a/tools/make-binaries +++ b/tools/make-binaries @@ -65,21 +65,21 @@ 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.25.0' +crosstool_vsn='1.28.0' termcap_vsn='1.3.1' -expat_vsn='2.4.9' -zlib_vsn='1.2.12' +expat_vsn='2.7.2' +zlib_vsn='1.3.1' yaml_vsn='0.2.5' -ssl_vsn='1.1.1q' -otp_vsn='24.3.4.5' -elixir_vsn='1.14.0' -pam_vsn='1.5.2' -png_vsn='1.6.38' -jpeg_vsn='9e' -webp_vsn='1.2.4' +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.11' -sqlite_vsn='3390300' +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" @@ -117,7 +117,7 @@ odbc_tar="$odbc_dir.tar.gz" rel_tar="$rel_name-$mix_vsn.tar.gz" ct_jobs=$(nproc) src_dir="$root_dir/src" -platform='x86_64-pc-linux-gnu' +platform=$(gcc -dumpmachine) targets='x86_64-linux-gnu aarch64-linux-gnu' build_start=$(date '+%F %T') have_current_deps='false' @@ -179,8 +179,8 @@ check_vsn() check_configured_dep_vsns() { check_vsn 'OpenSSL' "$ssl_vsn" \ - 'https://www.openssl.org/source/' \ - 'openssl-\(1\.[0-9][0-9a-z.]*\)\.tar\.gz' + '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' @@ -189,7 +189,7 @@ check_configured_dep_vsns() 'zlib-\([1-9][0-9.]*\)\.tar\.gz' check_vsn 'Expat' "$expat_vsn" \ 'https://github.com/libexpat/libexpat/releases' \ - '[0-9]\]\([1-9][0-9.]*\)' + '\([1-9]\.[0-9]*\.[0-9]*\)' check_vsn 'Termcap' "$termcap_vsn" \ 'https://ftp.gnu.org/gnu/termcap/' \ 'termcap-\([1-9][0-9.]*\)\.tar\.gz' @@ -199,9 +199,13 @@ check_configured_dep_vsns() check_vsn 'ODBC' "$odbc_vsn" \ 'http://www.unixodbc.org/download.html' \ 'unixODBC-\([1-9][0-9.]*\)\.tar\.gz' - check_vsn 'Linux-PAM' "$pam_vsn" \ - 'https://github.com/linux-pam/linux-pam/releases' \ - '[0-9]\]Linux-PAM \([1-9][0-9.]*\)' + # + # 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' @@ -258,8 +262,34 @@ create_common_config() CT_ARCH_64=y CT_KERNEL_LINUX=y CT_LINUX_V_3_16=y - CT_GLIBC_V_2_17=y - CT_GLIBC_KERNEL_VERSION_NONE=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 } #. @@ -268,8 +298,9 @@ create_common_config() create_x64_config() { local file="$1" + local libc="$2" - create_common_config "$file" + create_${libc}_config "$file" cat >>"$file" <<-'EOF' CT_ARCH_X86=y @@ -281,8 +312,9 @@ create_x64_config() create_arm64_config() { local file="$1" + local libc="$2" - create_common_config "$file" + create_${libc}_config "$file" cat >>"$file" <<-'EOF' CT_ARCH_ARM=y @@ -318,6 +350,13 @@ add_otp_path() 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" } @@ -332,7 +371,7 @@ create_data_dir() mkdir "$data_dir" "$data_dir/database" "$data_dir/logs" mv "$code_dir/conf" "$data_dir" chmod 'o-rwx' "$data_dir/"* - curl -o "$data_dir/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' + 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\ @@ -374,9 +413,9 @@ edit_ejabberdctl() sed -i \ -e "2iexport TERM='internal'" \ -e '/ERL_OPTIONS=/d' \ - -e 's|^ERLANG_NODE=ejabberd$|ERLANG_NODE=ejabberd@localhost|' \ -e 's|_DIR:=".*}/|_DIR:="/opt/ejabberd/|' \ -e 's|/database|/database/$ERLANG_NODE|' \ + -e 's|#vt100 ||' \ "$code_dir/bin/${rel_name}ctl" } #. @@ -437,6 +476,7 @@ build_toolchain() local target="$1" local prefix="$2" local arch=$(arch_name "$target") + local libc="${target##*-}" if [ -d "$prefix" ] then @@ -452,14 +492,14 @@ build_toolchain() info "Building Crosstool-NG $crosstool_vsn ..." cd "$src_dir/$crosstool_dir" ./configure --prefix="$bootstrap_dir" - make + make V=0 make install cd "$OLDPWD" fi - info "Building toolchain for $arch ..." + info "Building toolchain for $arch-$libc ..." cd "$root_dir" - create_${arch}_config 'defconfig' + 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' @@ -475,6 +515,7 @@ build_deps() local target="$2" local prefix="$3" local arch="$(arch_name "$target")" + local libc="${target##*-}" local target_src_dir="$prefix/src" local saved_path="$PATH" @@ -504,8 +545,9 @@ build_deps() tar -xJf "$src_dir/$pam_tar" cd "$OLDPWD" - info "Building Termcap $termcap_vsn for $arch ..." + 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 @@ -532,24 +574,26 @@ build_deps() make install cd "$OLDPWD" - info "Building zlib $zlib_vsn for $arch ..." + 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 ..." + info "Building OpenSSL $ssl_vsn for $arch-$libc ..." cd "$target_src_dir/$ssl_dir" - CFLAGS="$CFLAGS -O3 -fPIC" ./Configure no-shared no-ui-console \ + CFLAGS="$CFLAGS -O3 -fPIC" \ + ./Configure no-shared no-module no-ui-console \ --prefix="$prefix" \ --openssldir="$prefix" \ - "linux-${target%-linux-gnu}" + --libdir='lib' \ + "linux-${target%-linux-*}" make build_libs make install_dev cd "$OLDPWD" - info "Building Expat $expat_vsn for $arch ..." + info "Building Expat $expat_vsn for $arch-$libc ..." cd "$target_src_dir/$expat_dir" $configure --prefix="$prefix" --enable-static --disable-shared \ --without-docbook \ @@ -558,7 +602,7 @@ build_deps() make install cd "$OLDPWD" - info "Building LibYAML $yaml_vsn for $arch ..." + 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" @@ -566,7 +610,7 @@ build_deps() make install cd "$OLDPWD" - info "Building SQLite $sqlite_vsn for $arch ..." + 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" @@ -574,7 +618,7 @@ build_deps() make install cd "$OLDPWD" - info "Building ODBC $odbc_vsn for $arch ..." + 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" @@ -585,13 +629,14 @@ build_deps() 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-doc --enable-db=no \ - CFLAGS="$CFLAGS -O3 -fPIC" + --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 ..." + 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" @@ -599,7 +644,7 @@ build_deps() make install cd "$OLDPWD" - info "Building JPEG $jpeg_vsn for $arch ..." + 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" @@ -607,7 +652,7 @@ build_deps() make install cd "$OLDPWD" - info "Building WebP $webp_vsn for $arch ..." + 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" @@ -615,7 +660,7 @@ build_deps() make install cd "$OLDPWD" - info "Building LibGD $gd_vsn for $arch ..." + info "Building LibGD $gd_vsn for $arch-$libc ..." cd "$target_src_dir/$gd_dir" $configure --prefix="$prefix" --enable-static --disable-shared \ --with-zlib="$prefix" \ @@ -637,16 +682,16 @@ build_deps() make install cd "$OLDPWD" - info "Building Erlang/OTP $otp_vsn for $arch ..." + 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" - # Don't link against libnsl: https://github.com/erlang/otp/pull/5558 - sed -i -e '/LIBS="-lnsl/d' -e '/LIBS="-lsocket/d' \ - 'lib/erl_interface/configure' + # 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 \ @@ -666,7 +711,7 @@ build_deps() fi cd "$OLDPWD" - info "Building Elixir $elixir_vsn for $arch ..." + info "Building Elixir $elixir_vsn for $arch-$libc ..." cd "$target_src_dir/$elixir_dir" make install PREFIX="$prefix" cd "$OLDPWD" @@ -682,22 +727,14 @@ build_rel() 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-$arch.tar.gz" + local target_dst_tar="$rel_name-$rel_vsn-linux-$libc-$arch.tar.gz" local saved_path="$PATH" - # - # The "$ct_prefix_dir/$target/$target/bin" directory contains cross - # compilation tools without "$target-" prefix. We add it to the PATH, - # just in case tools are called without prefix somewhere. However, we - # try to use the prefixed tools everywhere, so it should be possible to - # omit this directory from the path if desired. See also: - # - # https://stackoverflow.com/a/24243789 - # - export PATH="$ct_prefix_dir/$target/bin:$ct_prefix_dir/$target/$target/bin:$PATH" + export PATH="$ct_prefix_dir/$target/bin:$PATH" export CC="$target-gcc" export CXX="$target-g++" export CPP="$target-cpp" @@ -712,7 +749,7 @@ build_rel() 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='[deterministic, no_debug_info]' + export ERL_COMPILER_OPTIONS='[no_debug_info]' # Building 25.x fails with 'deterministic'. if [ "$mode" = 'cross' ] then configure="./configure --host=$target --build=$platform" @@ -735,7 +772,7 @@ build_rel() info "Removing old $rel_name builds" rm -rf '_build' 'deps' - info "Building $rel_name $rel_vsn for $arch ..." + info "Building $rel_name $rel_vsn for $arch-$libc ..." ./autogen.sh eimp_cflags='-fcommon' eimp_libs='-lwebp -ljpeg -lpng -lz -lm' @@ -779,7 +816,7 @@ build_rel() unset host_alias build_alias fi - info "Putting together $rel_name $rel_vsn archive for $arch ..." + 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" @@ -841,23 +878,23 @@ else info 'Downloading dependencies ...' cd "$src_dir" - curl -LO "http://crosstool-ng.org/download/crosstool-ng/$crosstool_tar" - curl -LO "https://ftp.gnu.org/gnu/termcap/$termcap_tar" - curl -LO "https://github.com/libexpat/libexpat/releases/download/R_$(printf '%s' "$expat_vsn" | sed 's/\./_/g')/$expat_tar" - curl -LO "https://zlib.net/$zlib_tar" - curl -LO "https://pyyaml.org/download/libyaml/$yaml_tar" - curl -LO "https://www.openssl.org/source/$ssl_tar" - curl -LO "https://github.com/erlang/otp/releases/download/OTP-$otp_vsn/$otp_tar" - curl -LO "https://github.com/elixir-lang/elixir/archive/v$elixir_vsn.tar.gz" - curl -LO "https://github.com/linux-pam/linux-pam/releases/download/v$pam_vsn/$pam_tar" - curl -LO "https://download.sourceforge.net/libpng/$png_tar" - curl -LO "https://www.ijg.org/files/$jpeg_tar" - curl -LO "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/$webp_tar" - curl -LO "https://github.com/libgd/libgd/releases/download/gd-$gd_vsn/$gd_tar" - curl -LO "http://www.unixodbc.org/$odbc_tar" - curl -LO "https://www.sqlite.org/$(date '+%Y')/$sqlite_tar" \ - || curl -LO "https://www.sqlite.org/$(date -d '1 year ago' '+%Y')/$sqlite_tar" \ - || curl -LO "https://www.sqlite.org/$(date -d '2 years ago' '+%Y')/$sqlite_tar" + 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 @@ -867,10 +904,10 @@ export LC_ALL='C.UTF-8' # Elixir insists on a UTF-8 environment. for target in $targets do - prefix="$build_dir/$(arch_name "$target")" + prefix="$build_dir/$target" toolchain_dir="$ct_prefix_dir/$target" - if [ "$(uname -m)-linux-gnu" = "$target" ] + if [ "$platform" = "$target" ] then mode='native' else mode='cross' fi diff --git a/tools/make-installers b/tools/make-installers index d54a90bd8..9439fc597 100755 --- a/tools/make-installers +++ b/tools/make-installers @@ -67,7 +67,7 @@ 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/installation/#administration-account" +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") @@ -80,7 +80,7 @@ create_help_file() local file="$1" cat >"$file" <<-EOF - This is the $rel_name $rel_vsn-$iteration installer for linux-$arch + This is the $rel_name $rel_vsn-$iteration installer for linux-gnu-$arch Visit: $home_url @@ -247,7 +247,6 @@ create_setup_script() if [ "\$code_dir" != '$default_code_dir' ] then sed -i "s|$default_code_dir|\$code_dir|g" \ - "\$code_dir/bin/${rel_name}ctl" \ "\$code_dir/bin/$rel_name.init" \ "\$code_dir/bin/$rel_name.service" fi @@ -255,8 +254,6 @@ create_setup_script() then sed -i "s|$default_data_dir|\$data_dir|g" \ "\$code_dir/bin/${rel_name}ctl" \ - "\$code_dir/bin/$rel_name.init" \ - "\$code_dir/bin/$rel_name.service" \ "\$data_dir/conf/$rel_name.yml" \ "\$data_dir/conf/${rel_name}ctl.cfg" fi @@ -354,7 +351,7 @@ create_setup_script() for arch in $architectures do - tar_name="$rel_name-$rel_vsn-linux-$arch.tar.gz" + 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 diff --git a/tools/make-packages b/tools/make-packages index 3b3388ae8..8e3585f48 100755 --- a/tools/make-packages +++ b/tools/make-packages @@ -203,7 +203,7 @@ make_package() for arch in $architectures do - tar_name="$rel_name-$rel_vsn-linux-$arch.tar.gz" + 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" diff --git a/tools/opt_types.sh b/tools/opt_types.sh index 71d69a5d4..bf8f99da6 100755 --- a/tools/opt_types.sh +++ b/tools/opt_types.sh @@ -11,6 +11,7 @@ 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) @@ -320,9 +321,9 @@ spec(ip_mask, 0, _, _) -> spec(port, 0, _, _) -> erl_types:t_from_range(1, 65535); spec(re, A, _, _) when A == 0; A == 1 -> - t_remote(re, mp); + t_remote(misc, re_mp); spec(glob, A, _, _) when A == 0; A == 1 -> - t_remote(re, mp); + t_remote(misc, re_mp); spec(path, 0, _, _) -> erl_types:t_binary(); spec(binary_sep, 1, _, _) -> @@ -453,6 +454,17 @@ t_from_form(Spec) -> 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), diff --git a/tools/prepare-tr.sh b/tools/prepare-tr.sh index 1eeaaa64c..3c5596189 100755 --- a/tools/prepare-tr.sh +++ b/tools/prepare-tr.sh @@ -32,7 +32,7 @@ extract_lang_po2msg () MSGSTR_PATH=$PO_PATH.msgstr MSGS_PATH=$LANG_CODE.msg - cd $PO_DIR + cd $PO_DIR || exit # Check PO has correct ~ # Let's convert to C format so we can use msgfmt @@ -48,11 +48,13 @@ extract_lang_po2msg () 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" >$MSGS_PATH - echo "%% DO NOT EDIT: run \`make translations\` instead" >>$MSGS_PATH - echo "%% To improve translations please read:" >>$MSGS_PATH - echo "%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/" >>$MSGS_PATH - echo "" >>$MSGS_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 @@ -68,29 +70,29 @@ extract_lang_updateall () echo "Generating POT..." extract_lang_src2pot - cd $MSGS_DIR + cd $MSGS_DIR || exit echo "" - echo -e "File Missing (fuzzy) Language Last translator" - echo -e "---- ------- ------- -------- ---------------" - for i in $( ls *.msg ) ; do + echo "File Missing (fuzzy) Language Last translator" + echo "---- ------- ------- -------- ---------------" + for i in *.msg ; do LANG_CODE=${i%.msg} - echo -n $LANG_CODE | awk '{printf "%-6s", $1 }' + 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 }'` - echo -n " $MISSING" + MISSING=$(msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4+$7 }') + printf " %s" "$MISSING" - FUZZY=`msgfmt --statistics $PO 2>&1 | awk '{printf "%7s", $4 }'` - echo -n " $FUZZY" + 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}'` - echo -n " $LANGUAGE" + 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'` + LASTAUTH=$(grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\n\"//g') echo " $LASTAUTH" done echo "" @@ -101,7 +103,7 @@ extract_lang_updateall () cd .. } -EJA_DIR=`pwd` +EJA_DIR=$(pwd) PROJECT=ejabberd DEPS_DIR=$1 MSGS_DIR=$EJA_DIR/priv/msgs 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/xml_compress_gen.erl b/tools/xml_compress_gen.erl index 157e6cc8a..d331d7533 100644 --- a/tools/xml_compress_gen.erl +++ b/tools/xml_compress_gen.erl @@ -4,7 +4,7 @@ %% Created : 14 Sep 2018 Pawel Chmielowski %% %% -%% ejabberd, Copyright (C) 2002-2022 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 @@ -93,41 +93,46 @@ 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" - " {El, _} = decode(Rest, <<\"jabber:client\">>, J1, J2),~n" - " El.~n~n", [VerId]), + " 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: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: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" + 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" + 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" + 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" + 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) ->~n" - " decode(Other, PNs, J1, J2).~n~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) end, Data).~n~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" @@ -153,7 +158,7 @@ gen_decode(Dev, Data, VerId) -> fun({Ns, Els}) -> lists:foreach( fun({Name, Id, Attrs, Text}) -> - io:format(Dev, "decode(<<~s, Rest/binary>>, PNs, J1, J2) ->~n" + io:format(Dev, "decode(<<~s, Rest/binary>>, PNs, J1, J2, _) ->~n" " Ns = ~p,~n", [Id, Ns]), case Attrs of [] -> @@ -209,14 +214,14 @@ gen_decode(Dev, Data, VerId) -> end, Text), io:format(Dev, " (Other) ->~n" - " decode_child(Other, Ns, J1, J2)~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) ->~n" - " decode_child(Other, PNs, J1, J2).~n~n", []). + io:format(Dev, "decode(Other, PNs, J1, J2, Loop) ->~n" + " decode_child(Other, PNs, J1, J2, Loop).~n~n", []). gen_encode(Dev, Data, VerId) -> diff --git a/vars.config.in b/vars.config.in index e80e49782..ded059b3e 100644 --- a/vars.config.in +++ b/vars.config.in @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2022 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,7 +24,6 @@ {debug, @debug@}. {new_sql_schema, @new_sql_schema@}. -%% Ad-hoc directories with source files {tools, @tools@}. %% Dependencies @@ -55,6 +54,7 @@ {installuser, "@INSTALLUSER@"}. {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"}.