fix: vendor vnctalk-prosody source as regular files (was a submodule gitlink)
Build + push image / build-and-push (push) Successful in 1m8s
The repo tracked vnctalk-prosody/ as a submodule gitlink with no .gitmodules URL, so the CI checkout left it empty and kaniko could not find the Dockerfile. Vendor the source in-tree so the Gitea Actions build has a self-contained context.
@@ -0,0 +1,82 @@
|
||||
image: docker:20.10.17
|
||||
|
||||
variables:
|
||||
DOCKER_HOST: tcp://localhost:2375
|
||||
DOCKER_TLS_CERTDIR: ''
|
||||
DEV_CONTAINER_REGISTRY: eu.gcr.io/vnc-development/vnctalk-prosody:development
|
||||
IMAGE_TAG: eu.gcr.io/vnc-development/vnctalk-prosody:development-$CI_COMMIT_SHORT_SHA
|
||||
IMAGE_TAG_PROD: eu.gcr.io/vnc-dev-275609/vnctalk-prosody:production
|
||||
IMAGE_TAG_STABLE: eu.gcr.io/vnc-stable/vnctalk-prosody:stable
|
||||
|
||||
services:
|
||||
- name: docker:20.10.17-dind
|
||||
alias: docker
|
||||
|
||||
stages:
|
||||
- docker
|
||||
- scan
|
||||
- notification
|
||||
|
||||
|
||||
dev_deploy:
|
||||
before_script:
|
||||
- until docker info; do sleep 1; done
|
||||
- base64 -d $SA_Development | docker login -u _json_key --password-stdin https://eu.gcr.io
|
||||
stage: docker
|
||||
artifacts:
|
||||
public: false
|
||||
expire_in: 12h
|
||||
paths:
|
||||
- scan
|
||||
script:
|
||||
- docker build -t $IMAGE_TAG .
|
||||
- docker push $IMAGE_TAG
|
||||
- docker image save $IMAGE_TAG -o scan/vnctalk-prosody.tgz
|
||||
only:
|
||||
- main
|
||||
|
||||
dev_trivy:
|
||||
image: aquasec/trivy:latest
|
||||
stage: scan
|
||||
script:
|
||||
- trivy image --input scan/vnctalk-prosody.tgz
|
||||
only:
|
||||
- main
|
||||
|
||||
prod_deploy:
|
||||
before_script:
|
||||
- until docker info; do sleep 1; done
|
||||
- base64 -d $SA_Production | docker login -u _json_key --password-stdin https://eu.gcr.io
|
||||
stage: docker
|
||||
script:
|
||||
- echo $CI_COMMIT_TAG
|
||||
- COMMIT_VERSION=$(echo $CI_COMMIT_TAG | awk -F'prod-' '{print $2}')
|
||||
- echo $COMMIT_VERSION
|
||||
|
||||
- DEV_TAG=$DEV_CONTAINER_REGISTRY-$COMMIT_VERSION
|
||||
- echo $DEV_TAG
|
||||
- docker pull $DEV_TAG
|
||||
- PROD_IMAGE=$IMAGE_TAG_PROD-$COMMIT_VERSION
|
||||
- docker tag $DEV_TAG $PROD_IMAGE
|
||||
- docker push $PROD_IMAGE
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG =~ /^prod-.*/
|
||||
|
||||
stable_deploy:
|
||||
before_script:
|
||||
- until docker info; do sleep 1; done
|
||||
- base64 -d $SA_Production | docker login -u _json_key --password-stdin https://eu.gcr.io
|
||||
stage: docker
|
||||
script:
|
||||
- echo $CI_COMMIT_TAG
|
||||
- COMMIT_VERSION=$(echo $CI_COMMIT_TAG | awk -F'stable-' '{print $2}')
|
||||
- echo $COMMIT_VERSION
|
||||
|
||||
- DEV_TAG=$DEV_CONTAINER_REGISTRY-$COMMIT_VERSION
|
||||
- echo $DEV_TAG
|
||||
- docker pull $DEV_TAG
|
||||
- STABLE_IMAGE=$IMAGE_TAG_STABLE-$COMMIT_VERSION
|
||||
- docker tag $DEV_TAG $STABLE_IMAGE
|
||||
- docker push $STABLE_IMAGE
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG =~ /^stable-.*/
|
||||
@@ -0,0 +1,61 @@
|
||||
FROM alpine:3.23.4 AS builder
|
||||
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add gettext lua5.2 lua5.2-bitop lua5.2-socket lua5.2-dbi-postgresql lua5.2-expat lua5.2-sql-postgres luarocks5.2 \
|
||||
lua5.2-filesize lua5.2-lpeg_patterns lua5.2-hiredis lua5.2-socket lua5.2-lpeg lua5.2-filesystem lua5.2-ldap \
|
||||
lua5.2-rapidjson lua5.2-redis lua5.2-sec lua5.2-lzlib lua5.2-cjson lua5.2-dev \
|
||||
libidn-dev libidn icu icu-dev libpq postgresql-dev make gcc expat expat-dev \
|
||||
libxmlb-dev libc-dev openssl openssl-dev htop nodejs nodejs-dev \
|
||||
libc-dev g++ yarn nagios-plugins-tcp && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
RUN wget https://prosody.im/downloads/source/prosody-0.11.6.tar.gz && tar xvfz prosody-0.11.6.tar.gz
|
||||
|
||||
RUN cd prosody-0.11.6/ && ./configure --sysconfdir="/etc/prosody" --no-example-certs && \
|
||||
make && make install
|
||||
|
||||
RUN mkdir /vnc && mkdir -p /var/run/prosody/ && \
|
||||
mkdir -p /var/log/prosody/ && \
|
||||
mkdir -p /etc/prosody/public && mkdir -p /etc/tls-update
|
||||
|
||||
RUN yarn global add http-server
|
||||
|
||||
FROM alpine:3.23.4
|
||||
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add gettext lua5.2 lua5.2-bitop lua5.2-socket lua5.2-dbi-postgresql lua5.2-expat lua5.2-sql-postgres luarocks5.2 \
|
||||
lua5.2-filesize lua5.2-lpeg_patterns lua5.2-hiredis lua5.2-socket lua5.2-lpeg lua5.2-filesystem lua5.2-ldap \
|
||||
lua5.2-rapidjson lua5.2-redis lua5.2-sec lua5.2-lzlib lua5.2-cjson lua5.2-dev \
|
||||
libidn-dev libidn icu libpq expat \
|
||||
openssl nodejs nagios-plugins-tcp && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --from=builder /usr/local/ /usr/local/
|
||||
|
||||
RUN mkdir /vnc && mkdir -p /var/log/prosody/ && \
|
||||
mkdir -p /etc/prosody/public && mkdir -p /etc/tls-update
|
||||
|
||||
|
||||
RUN addgroup -g 1001 prosody
|
||||
RUN adduser -D -u 1001 -S -h /var/lib/prosody -H -G prosody prosody
|
||||
|
||||
ADD patches /vnc/patches
|
||||
RUN cp /vnc/patches/mod_carbons.lua /usr/local/lib/prosody/modules/mod_carbons.lua && \
|
||||
cp /vnc/patches/mod_mam.lua /usr/local/lib/prosody/modules/mod_mam/mod_mam.lua && \
|
||||
cp /vnc/patches/mod_muc_mam.lua /usr/local/lib/prosody/modules/mod_muc_mam.lua && \
|
||||
cp /vnc/patches/mod_muc.lua /usr/local/lib/prosody/modules/muc/mod_muc.lua && \
|
||||
cp /vnc/patches/mod_muc_unique.lua /usr/local/lib/prosody/modules/mod_muc_unique.lua && \
|
||||
cp /vnc/patches/moduleapi.lua /usr/local/lib/prosody/core/moduleapi.lua && \
|
||||
cp /vnc/patches/muc.lib.lua /usr/local/lib/prosody/modules/muc/muc.lib.lua
|
||||
RUN cp /vnc/patches/hidden.lib.lua /usr/local/lib/prosody/modules/muc/hidden.lib.lua && \
|
||||
cp /vnc/patches/register.lib.lua /usr/local/lib/prosody/modules/muc/register.lib.lua && \
|
||||
cp /vnc/patches/mod_admin_telnet.lua /usr/local/lib/prosody/modules/mod_admin_telnet.lua && \
|
||||
cp /vnc/patches/portmanager.lua /usr/local/lib/prosody/core/portmanager.lua
|
||||
ADD vnctalk /vnc/vnctalk
|
||||
RUN cd /vnc/vnctalk && cp -Rp * /usr/local/lib/prosody/modules/
|
||||
ADD config /vnc/config
|
||||
|
||||
USER prosody
|
||||
ENV __FLUSH_LOG=yes
|
||||
|
||||
CMD ["/vnc/config/startup.sh"]
|
||||
@@ -0,0 +1,4 @@
|
||||
# Vnctalk Prosody
|
||||
|
||||
dockerized build for vnctalk prosody
|
||||
(commit for rebuild)
|
||||
@@ -0,0 +1,14 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
root /var/www/localhost/htdocs;
|
||||
# Everything is a 404
|
||||
location / {
|
||||
try_files / /index.html =404;
|
||||
}
|
||||
|
||||
# You may need this to prevent return 404 recursion.
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
# rationale: the preferred method for automated cert updates is to have virtual hosts with required
|
||||
# hostnames and certs automatically updated with ingress and cert manager.
|
||||
# This script checks if the certs in the container are updated and if not, it fails the healthcheck to
|
||||
# trigger a restart of the container with the new certs. If the certs are updated, it checks if prosody
|
||||
#is running and healthy by checking if the TCP port 5582 is open and responsive.
|
||||
|
||||
if [ -f /etc/tls-update/tls.crt ]; then
|
||||
OLDHASH=$(md5sum /etc/prosody/certs/prosody-ssl.pem | awk '{print $1}')
|
||||
NEWHASH=$(md5sum /etc/tls-update/tls.crt | awk '{print $1}')
|
||||
if [ "x$OLDHASH" != "x$NEWHASH" ]; then
|
||||
# cert in secret is changed - fail healthcheck to restart with new cert
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
/usr/lib/nagios/plugins/check_tcp -H 127.0.0.1 -p 5582 -w 6 -c 3
|
||||
@@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head><title>VNClagoon redirect</title></head>
|
||||
<script type="text/javascript">
|
||||
window.location.replace("${REDIRECT_TARGET}");
|
||||
</script>
|
||||
<body>
|
||||
|
||||
<center><h1>VNClagoon will</h1></center>
|
||||
<hr><center>redirect</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,328 @@
|
||||
--------------------------------------------------------------------------------
|
||||
---- VNCtalk Prosody/XMPP Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
daemonize = false;
|
||||
|
||||
pidfile = "/var/log/prosody/prosody.pid"; -- Required for init scripts and prosodyctl
|
||||
|
||||
-- Enable use of libevent for better performance under high load
|
||||
-- For more information see: http://prosody.im/doc/libevent
|
||||
-- use_libevent = true;
|
||||
network_backend = "epoll";
|
||||
|
||||
|
||||
allow_registration = false;
|
||||
|
||||
network_default_read_size = 8192; -- Increase buffer size to allow jitsi meet and file transfers. Default 4096.
|
||||
|
||||
-- for docker only!
|
||||
run_as_root = true;
|
||||
|
||||
admins = {
|
||||
"admin@${prosodyDomain}"
|
||||
}
|
||||
|
||||
plugin_paths = {
|
||||
"/usr/local/lib/prosody/modules",
|
||||
"/usr/lib/prosody/modules",
|
||||
}
|
||||
|
||||
modules_enabled = {
|
||||
"roster"; -- Allow users to have a roster. Recommended ;)
|
||||
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
|
||||
"tls"; -- Add support for secure TLS on c2s/s2s connections
|
||||
"dialback"; -- s2s dialback support
|
||||
"disco"; -- Service discovery
|
||||
"private"; -- Private XML storage (for room bookmarks, etc.)
|
||||
"vcard"; -- Allow users to set vCards
|
||||
"blocklist"; -- Allow users to block communications with other users
|
||||
"smacks";
|
||||
"smacks_offline";
|
||||
"carbons";
|
||||
"carbons_copies";
|
||||
"mam";
|
||||
"vnc_lastactivity";
|
||||
"offline"; -- Store offline messages
|
||||
"pubsub";
|
||||
"version"; -- Replies to server version requests
|
||||
"uptime"; -- Report how long server has been running
|
||||
"time"; -- Let others know the time here on this server
|
||||
"ping"; -- Replies to XMPP pings with pongs
|
||||
"pep"; -- Enables users to publish their mood, activity, playing music and more
|
||||
"vnc_vcard_avatar"; -- publish avatars
|
||||
|
||||
"adhoc";
|
||||
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
|
||||
"admin_telnet"; -- Opens telnet console interface on localhost port 5582
|
||||
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
|
||||
"bosh"; -- Enable mod_bosh
|
||||
"websocket";
|
||||
"http_altconnect";
|
||||
"idlecompat";
|
||||
-- "groups"; -- Shared roster support
|
||||
|
||||
-- Other specific functionality
|
||||
"announce"; -- Send announcement to all online users
|
||||
"watchregistrations"; -- Alert admins of registrations
|
||||
"motd"; -- Send a message to users when they log in
|
||||
"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
|
||||
"webpresence";
|
||||
"http_rest";
|
||||
"csi";
|
||||
-- "throttle_presence";
|
||||
"filter_chatstates";
|
||||
-- s2s keepalive with default jitsi
|
||||
"s2s_keepalive";
|
||||
|
||||
-- VNCtalk modules
|
||||
"vnc_receipts";
|
||||
"vnc_remotemucstore";
|
||||
|
||||
-- debug
|
||||
"log_slow_events";
|
||||
}
|
||||
|
||||
modules_disabled = {
|
||||
-- "c2s"; -- Handle client connections
|
||||
-- "s2s"; -- Handle server-to-server connections
|
||||
|
||||
"archive";
|
||||
"register"; -- Allow users to register on this server using a client and change passwords
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- Logging configuration
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- The supported levels are: "debug", "info", "warn", "error".
|
||||
log = {
|
||||
-- levels = { "info", "warn", "error" }, to = "console" ;
|
||||
info = "/var/log/prosody/prosody.log";
|
||||
error = "/var/log/prosody/prosody.err";
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- Components Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
component_ports = { 5347 }
|
||||
component_interface = "0.0.0.0";
|
||||
--component_interface = "192.168.0.10"; -- Prosody to listen for component connections on port XXXX, coming only to the IP address 192.168.0.10.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- BOSH Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
bosh_ports = { 5280 }
|
||||
bosh_max_inactivity = 60; --Maximum amount of time in seconds a client may remain silent for, with no requests
|
||||
cross_domain_bosh = true; --Allow access from scripts on any site with no proxy (requires a modern browser)
|
||||
consider_bosh_secure = true; --Use if proxying HTTPS->HTTP on the server side
|
||||
bosh_default_hold = 1; --Maximum number of requests the server will hold open for a client
|
||||
bosh_max_requests = 2; --Maximum number of requests a client may make to the server at a time
|
||||
-- 2check: internel network in k8s ?
|
||||
trusted_proxies = { "127.0.0.1" } --A list of proxies to trust, used to find a client's real IP
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- Websocket Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
cross_domain_websocket = true; --Allow access from scripts on any site with no proxy (requires a modern browser)
|
||||
consider_websocket_secure = true; --Use if proxying HTTPS->HTTP on the server side
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- SSL/TLS Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
ssl = {
|
||||
key = "/etc/prosody/certs/prosody-ssl.key";
|
||||
certificate = "/etc/prosody/certs/prosody-ssl.pem";
|
||||
}
|
||||
|
||||
c2s_require_encryption = true; -- Force clients to use encrypted connections
|
||||
s2s_secure_auth = false; -- Force certificate authentication for server-to-server connections
|
||||
|
||||
--s2s_secure_domains = { "jabber.org" }
|
||||
-- list of domains here that will not be required to authenticate using certificates
|
||||
--s2s_insecure_domains = { "gmail.com" }
|
||||
|
||||
allow_anonymous_s2s = true
|
||||
|
||||
keepalive_servers = { "${DEFAULT_JITSI_CONFERENCE}" }
|
||||
keepalive_interval = 90 -- (in seconds, default is 60 )
|
||||
keepalive_timeout = 300 -- (in seconds, default is 593 )
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- Authentication Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
authentication = "internal_hashed"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- Storage Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
default_storage = "sql"
|
||||
storage = "sql"
|
||||
|
||||
sql = {
|
||||
driver = "PostgreSQL",
|
||||
host = "${prosodyDBhost}",
|
||||
database = "${prosodyDBname}",
|
||||
username = "${prosodyDBuser}",
|
||||
password = "${prosodyDBpass}",
|
||||
}
|
||||
|
||||
-- storage = { archive2 = "sql2" }
|
||||
|
||||
sql_manage_tables = true; --allow auto create table
|
||||
|
||||
-- PEP limit
|
||||
|
||||
pep_max_items = 256
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- MAM XEP-0313 Settings
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
max_archive_query_results = 20;
|
||||
archive_cleanup_interval = 4*60*60;
|
||||
archive_expires_after = "never"
|
||||
default_archive_policy = "roster"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- SMACKS config
|
||||
--------------------------------------------------------------------------------
|
||||
smacks_hibernation_time = 300;
|
||||
smacks_enabled_s2s = false;
|
||||
smacks_max_unacked_stanzas = 5;
|
||||
smacks_max_ack_delay = 60;
|
||||
smacks_max_hibernated_sessions = 10;
|
||||
smacks_max_old = 10;
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
log_slow_events_threshold = ${log_slow_events_threshold}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
---- MOTD config
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
motd_jid = "admin@${prosodyDomain}"
|
||||
-- motd_text = "Currently the XMPP service is under maintence."
|
||||
|
||||
|
||||
----------- Virtual hosts -----------
|
||||
|
||||
VirtualHost "${prosodyDomain}"
|
||||
enabled = true; -- Remove this line to enable this host
|
||||
|
||||
admins = { "admin@${prosodyDomain}" }
|
||||
|
||||
authentication = "http_async"
|
||||
http_auth_url = "${hybridaAuthUrl}"
|
||||
|
||||
fcm_api_key = "${fcmApiKey}"
|
||||
fcm_api_url = "${fcm_api_url}"
|
||||
del_api_url = "${del_api_url}"
|
||||
|
||||
|
||||
modules_enabled = {
|
||||
"bosh";
|
||||
"pubsub";
|
||||
"ping"; -- Enable mod_ping
|
||||
-- "auto_accept_subscriptions";
|
||||
"vnc_delfile";
|
||||
"vnc_fcm";
|
||||
"http_upload_external";
|
||||
"alias";
|
||||
"vnc_timestamp";
|
||||
}
|
||||
|
||||
modules_disabled = {
|
||||
"discoitems"; --manually override the service discovery items for a host
|
||||
}
|
||||
|
||||
http_upload_external_base_url = "${fileShareBaseUrl}"
|
||||
http_upload_external_secret = "${fileShareSecret}"
|
||||
http_upload_external_file_size_limit = 104857600 -- bytes
|
||||
|
||||
-- avatar_upload_url = "https://vnctalk.dev.vnc.de/avatarupload/"
|
||||
avatar_upload_url = "${avatarUploadUrl}"
|
||||
avatar_upload_user = "${avatarUploadUser}"
|
||||
avatar_upload_pass = "${avatarUploadPass}"
|
||||
|
||||
ssl = {
|
||||
key = "/etc/prosody/certs/prosody-ssl.key";
|
||||
certificate = "/etc/prosody/certs/prosody-ssl.pem";
|
||||
}
|
||||
|
||||
|
||||
|
||||
disco_items = {
|
||||
-- Do not display/publicy any disco item to avoid information leaking due to public reachable chatroom service.
|
||||
}
|
||||
|
||||
VirtualHost "anon.${prosodyDomain}"
|
||||
enabled = true;
|
||||
authentication = "anonymous"
|
||||
modules_enabled = {
|
||||
"bosh";
|
||||
"pubsub";
|
||||
"ping"; -- Enable mod_ping
|
||||
"http_upload_external";
|
||||
"websocket";
|
||||
}
|
||||
|
||||
modules_disabled = {
|
||||
"discoitems"; --manually override the service discovery items for a host
|
||||
}
|
||||
|
||||
http_upload_external_base_url = "${fileShareBaseUrl}"
|
||||
http_upload_external_secret = "${fileShareSecret}"
|
||||
http_upload_external_file_size_limit = 104857600 -- bytes
|
||||
|
||||
ssl = {
|
||||
key = "/etc/prosody/certs/prosody-ssl.key";
|
||||
certificate = "/etc/prosody/certs/prosody-ssl.pem";
|
||||
}
|
||||
|
||||
|
||||
Component "conference.${prosodyDomain}" "muc"
|
||||
-- TODO: create random secret in startup
|
||||
component_secret = "Weew0Ooheivei3aizohmalohLieshohs"
|
||||
modules_enabled = {
|
||||
"muc_mam",
|
||||
"vnc_muc_automember",
|
||||
"vnc_muc_fcm",
|
||||
"vnc_remotemucinvite",
|
||||
"vnc_delfile",
|
||||
"vnc_e2ehints",
|
||||
"vnc_muc_hook",
|
||||
"vnc_muc_data",
|
||||
"vnc_track_kicks",
|
||||
"vcard_muc"; -- allow vcards for muc
|
||||
}
|
||||
storage = {
|
||||
-- This makes mod_mam_muc use the sql2 storage backend (others will use internal)
|
||||
-- which at the time of this writing is the only one supporting stanza archives
|
||||
muc_log = "sql";
|
||||
}
|
||||
muc_log_by_default = true; -- Enable logging by default (can be disabled in room config)
|
||||
muc_log_all_rooms = true; -- set to true to force logging of all rooms
|
||||
muc_log_expires_after = "never";
|
||||
|
||||
-- This is the largest number of messages that are allowed to be retrieved in one MAM request.
|
||||
max_archive_query_results = 20;
|
||||
|
||||
-- This is the largest number of messages that are allowed to be retrieved when joining a room.
|
||||
max_history_messages = 1000;
|
||||
fcm_api_key = "${fcmApiKey}"
|
||||
fcm_api_url = "${fcm_api_url}"
|
||||
del_api_url = "${del_api_url}"
|
||||
|
||||
avatar_upload_url = "${avatarUploadUrl}"
|
||||
avatar_upload_user = "${avatarUploadUser}"
|
||||
avatar_upload_pass = "${avatarUploadPass}"
|
||||
storage_host = "${prosodyDomain}";
|
||||
|
||||
Component "broadcast@${prosodyDomain}" "vnc_broadcast"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "running startup"
|
||||
|
||||
if [ -z "$redirectTarget" ]; then
|
||||
REDIRECT_TARGET="https://vnclagoon.com"
|
||||
else
|
||||
REDIRECT_TARGET="$redirectTarget"
|
||||
fi
|
||||
|
||||
echo "redirect: $REDIRECT_TARGET"
|
||||
|
||||
export REDIRECT_TARGET
|
||||
|
||||
envsubst < /vnc/config/index.html.template > /etc/prosody/public/index.html
|
||||
cat /vnc/config/status.html > /etc/prosody/public/status.html
|
||||
|
||||
|
||||
/usr/local/bin/http-server /etc/prosody/public/ -p 8080 -r --proxy http://localhost:8080?
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "running startup"
|
||||
mkdir -p /etc/prosody
|
||||
chmod 755 /etc/prosody
|
||||
|
||||
mkdir -p /etc/prosody/certs
|
||||
|
||||
|
||||
defaultProsodySSLkey="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRRHdqakRJL2hiWERXZTQKRlB0RGtCbG9wUjBJSTQ2U25tY001R3ZQSzlXVkx6dXM0YlBsTmF5Y2dVVFhGTUR2OThmTTdCa3pYaEFuRGorTAo4VTQ1dnp6bW1STHQ3aDBHdStud0t1dXg2Z2MvTW9XVjJYd3JlY1U0bUp2Z0IvTW9QZ05ReGNVVlhHYTF6VFJLCm5VMDJDa28zUEZaNmxWNkFEN3dISGR0cXhTbGQxdTVxMnZMa3VHM2psellqZFJmc2tTdjNLS1BJS3FKdUszM0sKUnRDb1J1TzBvZFNzOHN2MmhmYVdEangvZEEyQ29ubXh3RTlIa3VCUFhIWnQzM2s3ajdTWkh6ZnZxZ1BibVRXNwpuTmhTWVRCa05LRXpHU051a0gvQTcrS0VUWXJoVFcwTUNVSkNIbE5oSlY5MmYxaFBjeGVHMGhJV0J0bEoyS1RuClNHQUI3S0FaQWdNQkFBRUNnZ0VBQ3g4bjFrTVpIenc1YldOYThwYTQ5Z09jUWdJUzEyaUhieXNvYU4ybjlFdkkKd0lkMjM4UUpoUHJkQ2V5bHR2S0h4RXV1SnBmVXUxbUhTcHNQZU5BRXFzeXMrVjhtN0diSHZCamY2TXZmSmViYQpFa0dKNGlDeEk1cTZxakx6NVFwbzhOVVQ5WmJlRU0yNSt6RW1ucmIvRTJxd3ZONG5EbGlyNHRCWWtxa203bldvCmdjbFk4RHpWSy9qcHZpSThSVmkzckFBTGlPbU50NWgwUHJGN0dFYVBRQ3Z4eVVEa1VZVGRaUjB1bTFMalZmZjUKcEMvVlRhUzFsdkNCd3JTb0FRdUlnQVRjQ0xGTDRpS0FIQjFHdnEwK2FsQXBCaWFpY1Z0WGQ5c0tBalBsS3R2dgpOamJFc3BBUzJFSVBBRHRZNmg4Yzd3R016NGxCenEzSm5oaVppUFJkZVFLQmdRRDVqMGtpbmFORitXR3dFbVVYCjBLWkk2dnM3R0YwelVQaW1FaFBPRmxrYVczcTZYUXFpN3ArbVpJUUhreEpmWlVMcEdtUm1rL3JsMlBHUnZzTzkKWHAyamVTbmQyRnlGdnRkYUpTcXMyOTNVRC8vZmtuQTQ5U0E2NW5tSkYrdUFkMlRBQW5JOFNmTVZHV2hLNGltbQo0aU1BNzRKM2RvUG1XdzFHWm9JZzZ6NzVCd0tCZ1FEMncyc09md2lJVlNvT3JqWitNMzJrUytmVWFjRUxxSmNQCk4wc0Z6Um1YY0RPY1VyamlCcDNwOXRzcmxqNzc4TkpWUkVSTG1PVHRleVFJWjVpTVBLQW15cTNleHBhY2FFSHIKN1M3WUtVcXBRRGZhbkNCVkdYcFdmcW84bDZZeEM3SVhKSFY5SjVZV05HWWVzWW5QWVh0dEdPQVlRQWlxUnpRYwpHdHpycHRMMTN3S0JnUURzNmVIOFFycWx5MEc1N1R5Ymh6RFZPSDZZYzJESFBxOU91UXFrTXdNVHlwU1FKNVBaCmFiK1dMd1pJU282K2tWc2NBRG9nUjR1QWVKdWNTUVY1MlVDOTJHQ3VncUFUT0VFcEN1K2dlbm4wdjl1aFcxdSsKTnJjS001SmRXNS91cldtdi9xS0VLWS9qRCtPNmMwb25hN2JmUklFTytDdnZtN0RXaktvc3BpZDZNUUtCZ1FEWgpIcGpUNkhKWmdwVysxUXFkZ0RETW52MGNxUEJ5a2FSWWZ3NUR6SEw0RElGY3lteGljKy8rTEhZcm84ZEhabDJyCktLWHUzbGlQbkFuWVcwNmVGYm5VYUFFVVF5ZkI5dm9KM0dlYlNybW1zaHB5LzJ4emREZHdZNExtVFlpdEh0MVQKLzRCZzNyUHdWYXFFMXpHcWh0dFM0dEJLaGcwWnhnelRyYzgwaVZTVmdRS0JnUUNCZ1pYaVEwY3FpWkpHdWkrcwoxRXhWdnNBdjh4NnJLMmEzc0ZvOXE1WEc4NnZuVEdrVElNNmZCWXo2Qm1lMTdiTDgrYmVKcjRmeVc4WWlENGVWCjFrQWxuaUdlYnZDcTNrSlh4U0VhdjQxM0dLQzhkMFZqWVpFbUVaN1hkU1dxcWVWa0xpUGNNVFdnaC9DdmpLU3YKVnpnOGFyN3VZaUh3WGdPQXNFVWJpWllNaUE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg=="
|
||||
|
||||
defaultProsodySSLcert="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ5ekNDQXQrZ0F3SUJBZ0lVZSsyVmpVM3psWDVzUHVqWGFZNnY0UWZOK2VRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2JqRUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQW9NQ2xCeWIzTnZaSGtnU1UweExEQXFCZ05WQkFzTQpJMmgwZEhCek9pOHZjSEp2YzI5a2VTNXBiUzlrYjJNdlkyVnlkR2xtYVdOaGRHVnpNUnd3R2dZRFZRUUREQk5GCmVHRnRjR3hsSUdObGNuUnBabWxqWVhSbE1CNFhEVEl5TURrd01qRXlNVGt3T1ZvWERUSXpNRGt3TWpFeU1Ua3cKT1Zvd2JqRUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQW9NQ2xCeWIzTnZaSGtnU1UweExEQXFCZ05WQkFzTQpJMmgwZEhCek9pOHZjSEp2YzI5a2VTNXBiUzlrYjJNdlkyVnlkR2xtYVdOaGRHVnpNUnd3R2dZRFZRUUREQk5GCmVHRnRjR3hsSUdObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0MKQVFFQThJNHd5UDRXMXcxbnVCVDdRNUFaYUtVZENDT09rcDVuRE9Scnp5dlZsUzg3ck9HejVUV3NuSUZFMXhUQQo3L2ZIek93Wk0xNFFKdzQvaS9GT09iODg1cGtTN2U0ZEJydnA4Q3Jyc2VvSFB6S0ZsZGw4SzNuRk9KaWI0QWZ6CktENERVTVhGRlZ4bXRjMDBTcDFOTmdwS056eFdlcFZlZ0ErOEJ4M2Jhc1VwWGRidWF0cnk1TGh0NDVjMkkzVVgKN0pFcjl5aWp5Q3FpYml0OXlrYlFxRWJqdEtIVXJQTEw5b1gybGc0OGYzUU5ncUo1c2NCUFI1TGdUMXgyYmQ5NQpPNCswbVI4Mzc2b0QyNWsxdTV6WVVtRXdaRFNoTXhramJwQi93Ty9paEUySzRVMXREQWxDUWg1VFlTVmZkbjlZClQzTVhodElTRmdiWlNkaWs1MGhnQWV5Z0dRSURBUUFCbzRHTU1JR0pNQXdHQTFVZEV3UUZNQU1CQWY4d2VRWUQKVlIwUkJISXdjSUlKYkc5allXeG9iM04wb0NRR0NDc0dBUVVGQndnSG9CZ1dGbDk0YlhCd0xXTnNhV1Z1ZEM1cwpiMk5oYkdodmMzU2dKQVlJS3dZQkJRVUhDQWVnR0JZV1gzaHRjSEF0YzJWeWRtVnlMbXh2WTJGc2FHOXpkS0FYCkJnZ3JCZ0VGQlFjSUJhQUxEQWxzYjJOaGJHaHZjM1F3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUpGK0dBZ28KSW8vWlBqVWM4YWJQbXZCMHZNTlhRVURYVGlzdkxRdDg5MGJmNzVDWkM2SFVGekw5ZjljamQ1NU1ZTG80QllxZQpVcjZOL3R3ZGRHWVNwb3JBY1dQWmtIb3F5bmQ5QXhIR0VpQUtvV2s5VXB0U2crcWhRd1JLQ1owQnBKemcrOENqCmtUdHBiWmdMa0s5RWhmSWlXbW1LSG41cHovYkhaK0FHaU9ESzcrYmsxZk1IZ0dDemFjVWVsU2lwSS9WOE1jY3IKbitwaGRRc2pjN1Vpd05zNlFEL2dxSS9GME1pV2FCd3ZiSjg0dWk3YkF4OFJqZ29OZC9ya3htUGhyQzBtL2VYYgpjUGFTOXpEMGFuUFN3ZUQ5VEQzUXBvNXZaNlJMempzNjZRR09VK0Y0a1dIU2NMTmVhaW9mOUV1aEMxQ0VrMDk4CmxQZ0F2SEQydTlKcURLdz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
|
||||
|
||||
|
||||
if [ -n "$prosodySSLkey" ]; then
|
||||
echo "$prosodySSLkey" | base64 -d > /etc/prosody/certs/prosody-ssl.key
|
||||
else
|
||||
echo "$defaultProsodySSLkey" | base64 -d > /etc/prosody/certs/prosody-ssl.key
|
||||
fi
|
||||
|
||||
if [ -n "$prosodySSLcert" ]; then
|
||||
echo "$prosodySSLcert" | base64 -d > /etc/prosody/certs/prosody-ssl.pem
|
||||
else
|
||||
echo "$defaultProsodySSLcert" | base64 -d > /etc/prosody/certs/prosody-ssl.pem
|
||||
fi
|
||||
|
||||
if [ -f /etc/tls-update/tls.crt ]; then
|
||||
cat /etc/tls-update/tls.crt > /etc/prosody/certs/prosody-ssl.pem
|
||||
fi
|
||||
|
||||
if [ -f /etc/tls-update/tls.key ]; then
|
||||
cat /etc/tls-update/tls.key > /etc/prosody/certs/prosody-ssl.key
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$log_slow_events_threshold" ]; then
|
||||
log_slow_events_threshold="1.5"
|
||||
fi
|
||||
|
||||
if [ -z "$DEFAULT_JITSI_CONFERENCE" ]; then
|
||||
DEFAULT_JITSI_CONFERENCE="conference.jitsi.dev.vnc.de"
|
||||
export DEFAULT_JITSI_CONFERENCE
|
||||
fi
|
||||
|
||||
chmod 444 /etc/prosody/certs/prosody-ssl.pem
|
||||
chmod 444 /etc/prosody/certs/prosody-ssl.key
|
||||
|
||||
envsubst < /vnc/config/prosody.cfg.lua.template > /etc/prosody/prosody.cfg.lua
|
||||
|
||||
cat /etc/prosody/prosody.cfg.lua
|
||||
|
||||
# su prosody -c /usr/local/bin/prosody -F
|
||||
env
|
||||
|
||||
touch /var/log/prosody/prosody.log
|
||||
tail -f /var/log/prosody/prosody.log &
|
||||
|
||||
/usr/local/bin/prosody -F
|
||||
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head><title>VNClagoon status</title></head>
|
||||
<body>
|
||||
<center><h1>VNClagoon status</h1></center>
|
||||
<hr><center>OK</center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2014 Daurnimator
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local restrict_public = not module:get_option_boolean("muc_room_allow_public", true);
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
|
||||
local function get_hidden(room)
|
||||
return room._data.hidden;
|
||||
end
|
||||
|
||||
local function set_hidden(room, hidden)
|
||||
hidden = hidden and true or nil;
|
||||
if get_hidden(room) == hidden then return false; end
|
||||
room._data.hidden = hidden;
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("muc-config-form", function(event)
|
||||
-- if restrict_public and not um_is_admin(event.actor, module.host) then
|
||||
if restrict_public then
|
||||
-- Don't show option if public rooms are restricted and user is not admin of this host
|
||||
return;
|
||||
end
|
||||
table.insert(event.form, {
|
||||
name = "muc#roomconfig_publicroom";
|
||||
type = "boolean";
|
||||
label = "Include room information in public lists";
|
||||
desc = "Enable this to allow people to find the room";
|
||||
value = not get_hidden(event.room);
|
||||
});
|
||||
end, 100-9);
|
||||
|
||||
module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event)
|
||||
if restrict_public and not um_is_admin(event.actor, module.host) then
|
||||
return; -- Not allowed
|
||||
end
|
||||
if set_hidden(event.room, not event.value) then
|
||||
event.status_codes["104"] = true;
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("muc-disco#info", function(event)
|
||||
event.reply:tag("feature", {var = get_hidden(event.room) and "muc_hidden" or "muc_public"}):up();
|
||||
end);
|
||||
|
||||
return {
|
||||
get = get_hidden;
|
||||
set = set_hidden;
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
-- XEP-0280: Message Carbons implementation for Prosody
|
||||
-- Copyright (C) 2011-2016 Kim Alvefur
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
|
||||
local st = require "util.stanza";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local xmlns_carbons = "urn:xmpp:carbons:2";
|
||||
local xmlns_forward = "urn:xmpp:forward:0";
|
||||
local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions;
|
||||
|
||||
local function toggle_carbons(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local state = stanza.tags[1].name;
|
||||
module:log("debug", "%s %sd carbons", origin.full_jid, state);
|
||||
origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns;
|
||||
origin.send(st.reply(stanza));
|
||||
return true;
|
||||
end
|
||||
module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons);
|
||||
module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons);
|
||||
|
||||
local function message_handler(event, c2s)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local orig_type = stanza.attr.type or "normal";
|
||||
local orig_from = stanza.attr.from;
|
||||
local bare_from = jid_bare(orig_from);
|
||||
local orig_to = stanza.attr.to;
|
||||
local bare_to = jid_bare(orig_to);
|
||||
|
||||
if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body"))) then
|
||||
return -- Only chat type messages
|
||||
end
|
||||
|
||||
-- Stanza sent by a local client
|
||||
local bare_jid = bare_from; -- JID of the local user
|
||||
local target_session = origin;
|
||||
local top_priority = false;
|
||||
local user_sessions = bare_sessions[bare_from];
|
||||
|
||||
-- Stanza about to be delivered to a local client
|
||||
if not c2s then
|
||||
bare_jid = bare_to;
|
||||
target_session = full_sessions[orig_to];
|
||||
user_sessions = bare_sessions[bare_jid];
|
||||
if not target_session and user_sessions then
|
||||
-- The top resources will already receive this message per normal routing rules,
|
||||
-- so we are going to skip them in order to avoid sending duplicated messages.
|
||||
local top_resources = user_sessions.top_resources;
|
||||
top_priority = top_resources and top_resources[1].priority
|
||||
end
|
||||
end
|
||||
|
||||
if not user_sessions then
|
||||
module:log("debug", "Skip carbons for offline user");
|
||||
return -- No use in sending carbons to an offline user
|
||||
end
|
||||
|
||||
if stanza:get_child("private", xmlns_carbons) then
|
||||
if not c2s then
|
||||
stanza:maptags(function(tag)
|
||||
if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then
|
||||
return tag;
|
||||
end
|
||||
end);
|
||||
end
|
||||
module:log("debug", "Message tagged private, ignoring");
|
||||
return
|
||||
elseif stanza:get_child("no-copy", "urn:xmpp:hints") then
|
||||
module:log("debug", "Message has no-copy hint, ignoring");
|
||||
return
|
||||
elseif not c2s and bare_jid ~= orig_to and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
|
||||
module:log("debug", "MUC PM, ignoring");
|
||||
return
|
||||
end
|
||||
|
||||
-- Create the carbon copy and wrap it as per the Stanza Forwarding XEP
|
||||
local copy = st.clone(stanza);
|
||||
if c2s and not orig_to then
|
||||
stanza.attr.to = bare_from;
|
||||
end
|
||||
copy.attr.xmlns = "jabber:client";
|
||||
local carbon = st.message{ from = bare_jid, type = orig_type, }
|
||||
:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons })
|
||||
:tag("forwarded", { xmlns = xmlns_forward })
|
||||
:add_child(copy):reset();
|
||||
|
||||
user_sessions = user_sessions and user_sessions.sessions;
|
||||
for _, session in pairs(user_sessions) do
|
||||
-- Carbons are sent to resources that have enabled it
|
||||
if session.want_carbons
|
||||
-- but not the resource that sent the message, or the one that it's directed to
|
||||
and session ~= target_session
|
||||
-- and isn't among the top resources that would receive the message per standard routing rules
|
||||
and (c2s or session.priority ~= top_priority) then
|
||||
carbon.attr.to = session.full_jid;
|
||||
module:log("debug", "Sending carbon to %s", session.full_jid);
|
||||
session.send(carbon);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function c2s_message_handler(event)
|
||||
return message_handler(event, true)
|
||||
end
|
||||
|
||||
-- Stanzas sent by local clients
|
||||
module:hook("pre-message/host", c2s_message_handler, -0.5);
|
||||
module:hook("pre-message/bare", c2s_message_handler, -0.5);
|
||||
module:hook("pre-message/full", c2s_message_handler, -0.5);
|
||||
module:hook("vnc-rest-message", c2s_message_handler, -0.5);
|
||||
-- Stanzas to local clients
|
||||
module:hook("message/bare", message_handler, -0.5);
|
||||
module:hook("message/full", message_handler, -0.5);
|
||||
|
||||
module:add_feature(xmlns_carbons);
|
||||
@@ -0,0 +1,416 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2017 Matthew Wild
|
||||
-- Copyright (C) 2008-2017 Waqas Hussain
|
||||
-- Copyright (C) 2011-2017 Kim Alvefur
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- XEP-0313: Message Archive Management for Prosody
|
||||
--
|
||||
|
||||
local xmlns_mam = "urn:xmpp:mam:2";
|
||||
local xmlns_delay = "urn:xmpp:delay";
|
||||
local xmlns_forward = "urn:xmpp:forward:0";
|
||||
local xmlns_st_id = "urn:xmpp:sid:0";
|
||||
|
||||
local um = require "core.usermanager";
|
||||
local st = require "util.stanza";
|
||||
local rsm = require "util.rsm";
|
||||
local get_prefs = module:require"mamprefs".get;
|
||||
local set_prefs = module:require"mamprefs".set;
|
||||
local prefs_to_stanza = module:require"mamprefsxml".tostanza;
|
||||
local prefs_from_stanza = module:require"mamprefsxml".fromstanza;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prepped_split = require "util.jid".prepped_split;
|
||||
local dataform = require "util.dataforms".new;
|
||||
local host = module.host;
|
||||
|
||||
local rm_load_roster = require "core.rostermanager".load_roster;
|
||||
|
||||
local is_stanza = st.is_stanza;
|
||||
local tostring = tostring;
|
||||
local time_now = os.time;
|
||||
local m_min = math.min;
|
||||
local timestamp, timestamp_parse, datestamp = import( "util.datetime", "datetime", "parse", "date");
|
||||
local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
|
||||
local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
|
||||
|
||||
local archive_store = module:get_option_string("archive_store", "archive");
|
||||
local archive = module:open_store(archive_store, "archive");
|
||||
|
||||
if not archive.find then
|
||||
error("mod_"..(archive._provided_by or archive.name and "storage_"..archive.name).." does not support archiving\n"
|
||||
.."See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
|
||||
end
|
||||
local use_total = module:get_option_boolean("mam_include_total", true);
|
||||
|
||||
function schedule_cleanup()
|
||||
-- replaced by non-noop later if cleanup is enabled
|
||||
end
|
||||
|
||||
-- Handle prefs.
|
||||
module:hook("iq/self/"..xmlns_mam..":prefs", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local user = origin.username;
|
||||
if stanza.attr.type == "set" then
|
||||
local new_prefs = stanza:get_child("prefs", xmlns_mam);
|
||||
local prefs = prefs_from_stanza(new_prefs);
|
||||
local ok, err = set_prefs(user, prefs);
|
||||
if not ok then
|
||||
origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err)));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
local prefs = prefs_to_stanza(get_prefs(user, true));
|
||||
local reply = st.reply(stanza):add_child(prefs);
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
|
||||
local query_form = dataform {
|
||||
{ name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; };
|
||||
{ name = "with"; type = "jid-single"; };
|
||||
{ name = "start"; type = "text-single" };
|
||||
{ name = "end"; type = "text-single"; };
|
||||
};
|
||||
|
||||
-- Serve form
|
||||
module:hook("iq-get/self/"..xmlns_mam..":query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
get_prefs(origin.username, true);
|
||||
origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form()));
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- Handle archive queries
|
||||
module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local query = stanza.tags[1];
|
||||
local qid = query.attr.queryid;
|
||||
|
||||
origin.mam_requested = true;
|
||||
|
||||
get_prefs(origin.username, true);
|
||||
|
||||
-- Search query parameters
|
||||
local qwith, qstart, qend;
|
||||
local form = query:get_child("x", "jabber:x:data");
|
||||
if form then
|
||||
local err;
|
||||
form, err = query_form:data(form);
|
||||
if err then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
|
||||
return true;
|
||||
end
|
||||
qwith, qstart, qend = form["with"], form["start"], form["end"];
|
||||
qwith = qwith and jid_bare(qwith); -- dataforms does jidprep
|
||||
end
|
||||
|
||||
if qstart or qend then -- Validate timestamps
|
||||
local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend));
|
||||
if (qstart and not vstart) or (qend and not vend) then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
|
||||
return true;
|
||||
end
|
||||
qstart, qend = vstart, vend;
|
||||
end
|
||||
|
||||
module:log("debug", "Archive query, id %s with %s from %s until %s",
|
||||
tostring(qid), qwith or "anyone",
|
||||
qstart and timestamp(qstart) or "the dawn of time",
|
||||
qend and timestamp(qend) or "now");
|
||||
|
||||
-- RSM stuff
|
||||
local qset = rsm.get(query);
|
||||
local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
|
||||
local reverse = qset and qset.before or false;
|
||||
local before, after = qset and qset.before, qset and qset.after;
|
||||
if type(before) ~= "string" then before = nil; end
|
||||
|
||||
-- Load all the data!
|
||||
local data, err = archive:find(origin.username, {
|
||||
start = qstart; ["end"] = qend; -- Time range
|
||||
with = qwith;
|
||||
limit = qmax == 0 and 0 or qmax + 1;
|
||||
before = before; after = after;
|
||||
reverse = reverse;
|
||||
total = use_total or qmax == 0;
|
||||
});
|
||||
|
||||
if not data then
|
||||
origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
|
||||
return true;
|
||||
end
|
||||
local total = tonumber(err);
|
||||
|
||||
local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
|
||||
|
||||
local results = {};
|
||||
|
||||
-- Wrap it in stuff and deliver
|
||||
local first, last;
|
||||
local count = 0;
|
||||
local complete = "true";
|
||||
for id, item, when in data do
|
||||
count = count + 1;
|
||||
if count > qmax then
|
||||
-- We requested qmax+1 items. If that many items are retrieved then
|
||||
-- there are more results to page through, so:
|
||||
complete = nil;
|
||||
break;
|
||||
end
|
||||
local fwd_st = st.message(msg_reply_attr)
|
||||
:tag("result", { xmlns = xmlns_mam, queryid = qid, id = id })
|
||||
:tag("forwarded", { xmlns = xmlns_forward })
|
||||
:tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
|
||||
|
||||
if not is_stanza(item) then
|
||||
item = st.deserialize(item);
|
||||
end
|
||||
item.attr.xmlns = "jabber:client";
|
||||
fwd_st:add_child(item);
|
||||
|
||||
if not first then first = id; end
|
||||
last = id;
|
||||
|
||||
if reverse then
|
||||
results[count] = fwd_st;
|
||||
else
|
||||
origin.send(fwd_st);
|
||||
end
|
||||
end
|
||||
|
||||
if reverse then
|
||||
for i = #results, 1, -1 do
|
||||
origin.send(results[i]);
|
||||
end
|
||||
first, last = last, first;
|
||||
end
|
||||
|
||||
-- That's all folks!
|
||||
module:log("debug", "Archive query %s completed", tostring(qid));
|
||||
|
||||
origin.send(st.reply(stanza)
|
||||
:tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
|
||||
:add_child(rsm.generate {
|
||||
first = first, last = last, count = total }));
|
||||
return true;
|
||||
end);
|
||||
|
||||
local function has_in_roster(user, who)
|
||||
local roster = rm_load_roster(user, host);
|
||||
module:log("debug", "%s has %s in roster? %s", user, who, roster[who] and "yes" or "no");
|
||||
return roster[who];
|
||||
end
|
||||
|
||||
local function shall_store(user, who)
|
||||
return true;
|
||||
end
|
||||
|
||||
local function strip_stanza_id(stanza, user)
|
||||
if stanza:get_child("stanza-id", xmlns_st_id) then
|
||||
stanza = st.clone(stanza);
|
||||
stanza:maptags(function (tag)
|
||||
if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id then
|
||||
local by_user, by_host, res = jid_prepped_split(tag.attr.by);
|
||||
if not res and by_host == host and by_user == user then
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
return tag;
|
||||
end);
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- Handle messages
|
||||
local function message_handler(event, c2s, vnc_rest)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local log = c2s and origin.log or module._log;
|
||||
local orig_type = stanza.attr.type or "normal";
|
||||
local orig_from = stanza.attr.from;
|
||||
local orig_to = stanza.attr.to or orig_from;
|
||||
-- Stanza without 'to' are treated as if it was to their own bare jid
|
||||
|
||||
-- Whos storage do we put it in?
|
||||
local store_user = c2s and origin.username or jid_split(orig_to);
|
||||
if vnc_rest then
|
||||
store_user = jid_split(orig_from);
|
||||
end
|
||||
-- And who are they chatting with?
|
||||
local with = jid_bare(c2s and orig_to or orig_from);
|
||||
|
||||
-- Filter out <stanza-id> that claim to be from us
|
||||
event.stanza = strip_stanza_id(stanza, store_user);
|
||||
|
||||
-- We store chat messages or normal messages that have a body
|
||||
if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then
|
||||
log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
|
||||
return;
|
||||
end
|
||||
|
||||
-- or if hints suggest we shouldn't
|
||||
if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
|
||||
if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
|
||||
or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
|
||||
log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
local clone_for_storage;
|
||||
if not strip_tags:empty() then
|
||||
clone_for_storage = st.clone(stanza);
|
||||
clone_for_storage:maptags(function (tag)
|
||||
if strip_tags:contains(tag.attr.xmlns) then
|
||||
return nil;
|
||||
else
|
||||
return tag;
|
||||
end
|
||||
end);
|
||||
if #clone_for_storage.tags == 0 then
|
||||
log("debug", "Not archiving stanza: %s (empty when stripped)", stanza:top_tag());
|
||||
return;
|
||||
end
|
||||
else
|
||||
clone_for_storage = stanza;
|
||||
end
|
||||
|
||||
local storebody = stanza:get_child("body") and stanza:get_child("body")[1];
|
||||
|
||||
-- Check with the users preferences
|
||||
if (storebody and shall_store(store_user, with)) then
|
||||
log("debug", "Archiving stanza: %s", stanza:top_tag());
|
||||
|
||||
-- And stash it
|
||||
local ok, err = archive:append(store_user, nil, clone_for_storage, time_now(), with);
|
||||
if ok then
|
||||
local clone_for_other_handlers = st.clone(stanza);
|
||||
local id = ok;
|
||||
clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up();
|
||||
event.stanza = clone_for_other_handlers;
|
||||
schedule_cleanup(store_user);
|
||||
module:fire_event("archive-message-added", { origin = origin, stanza = clone_for_storage, for_user = store_user, id = id });
|
||||
else
|
||||
log("error", "Could not archive stanza: %s", err);
|
||||
end
|
||||
else
|
||||
log("debug", "Not archiving stanza: %s (prefs)", stanza:top_tag());
|
||||
end
|
||||
end
|
||||
|
||||
local function c2s_message_handler(event)
|
||||
return message_handler(event, true);
|
||||
end
|
||||
|
||||
local function vnc_message_handler(event)
|
||||
return message_handler(event, true, true);
|
||||
end
|
||||
|
||||
-- Filter out <stanza-id> before the message leaves the server to prevent privacy leak.
|
||||
local function strip_stanza_id_after_other_events(event)
|
||||
event.stanza = strip_stanza_id(event.stanza, event.origin.username);
|
||||
end
|
||||
|
||||
module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1);
|
||||
module:hook("pre-message/full", strip_stanza_id_after_other_events, -1);
|
||||
|
||||
local cleanup_after = module:get_option_string("archive_expires_after", "1w");
|
||||
local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
|
||||
if cleanup_after ~= "never" then
|
||||
local cleanup_storage = module:open_store("archive_cleanup");
|
||||
local cleanup_map = module:open_store("archive_cleanup", "map");
|
||||
|
||||
local day = 86400;
|
||||
local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
|
||||
local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
|
||||
if not n then
|
||||
module:log("error", "Could not parse archive_expires_after string %q", cleanup_after);
|
||||
return false;
|
||||
end
|
||||
|
||||
cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
|
||||
|
||||
module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after);
|
||||
|
||||
if not archive.delete then
|
||||
module:log("error", "archive_expires_after set but mod_%s does not support deleting", archive._provided_by);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- For each day, store a set of users that have new messages. To expire
|
||||
-- messages, we collect the union of sets of users from dates that fall
|
||||
-- outside the cleanup range.
|
||||
|
||||
local last_date = require "util.cache".new(module:get_option_number("archive_cleanup_date_cache_size", 1000));
|
||||
function schedule_cleanup(username, date)
|
||||
date = date or datestamp();
|
||||
if last_date:get(username) == date then return end
|
||||
local ok = cleanup_map:set(date, username, true);
|
||||
if ok then
|
||||
last_date:set(username, date);
|
||||
end
|
||||
end
|
||||
|
||||
local async = require "util.async";
|
||||
cleanup_runner = async.runner(function ()
|
||||
local users = {};
|
||||
local cut_off = datestamp(os.time() - cleanup_after);
|
||||
for date in cleanup_storage:users() do
|
||||
if date <= cut_off then
|
||||
module:log("debug", "Messages from %q should be expired", date);
|
||||
local messages_this_day = cleanup_storage:get(date);
|
||||
if messages_this_day then
|
||||
for user in pairs(messages_this_day) do
|
||||
users[user] = true;
|
||||
end
|
||||
if date < cut_off then
|
||||
-- Messages from the same day as the cut-off might not have expired yet,
|
||||
-- but all earlier will have, so clear storage for those days.
|
||||
cleanup_storage:set(date, nil);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local sum, num_users = 0, 0;
|
||||
for user in pairs(users) do
|
||||
local ok, err = archive:delete(user, { ["end"] = os.time() - cleanup_after; })
|
||||
if ok then
|
||||
num_users = num_users + 1;
|
||||
sum = sum + (tonumber(ok) or 0);
|
||||
else
|
||||
cleanup_map:set(cut_off, user, true);
|
||||
module:log("error", "Could not delete messages for user '%s': %s", user, err);
|
||||
end
|
||||
local wait, done = async.waiter();
|
||||
module:add_timer(0.01, done);
|
||||
wait();
|
||||
end
|
||||
module:log("info", "Deleted %d expired messages for %d users", sum, num_users);
|
||||
end);
|
||||
|
||||
cleanup_task = module:add_timer(1, function ()
|
||||
cleanup_runner:run(true);
|
||||
return cleanup_interval;
|
||||
end);
|
||||
else
|
||||
module:log("debug", "Archive expiry disabled");
|
||||
-- Don't ask the backend to count the potentially unbounded number of items,
|
||||
-- it'll get slow.
|
||||
use_total = module:get_option_boolean("mam_include_total", false);
|
||||
end
|
||||
|
||||
-- Stanzas sent by local clients
|
||||
module:hook("pre-message/bare", c2s_message_handler, 0);
|
||||
module:hook("pre-message/full", c2s_message_handler, 0);
|
||||
module:hook("vnc-rest-message", vnc_message_handler, 0);
|
||||
-- Stanzas to local clients
|
||||
module:hook("message/bare", message_handler, 0);
|
||||
module:hook("message/full", message_handler, 0);
|
||||
|
||||
module:hook("account-disco-info", function(event)
|
||||
(event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up();
|
||||
(event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up();
|
||||
end);
|
||||
@@ -0,0 +1,535 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
-- Exposed functions:
|
||||
--
|
||||
-- create_room(jid) -> room
|
||||
-- track_room(room)
|
||||
-- delete_room(room)
|
||||
-- forget_room(room)
|
||||
-- get_room_from_jid(jid) -> room
|
||||
-- each_room(live_only) -> () -> room [DEPRECATED]
|
||||
-- all_rooms() -> room
|
||||
-- live_rooms() -> room
|
||||
-- shutdown_component()
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
if module:get_host_type() ~= "component" then
|
||||
error("MUC should be loaded as a component, please see https://prosody.im/doc/components", 0);
|
||||
end
|
||||
|
||||
local muclib = module:require "muc";
|
||||
room_mt = muclib.room_mt; -- Yes, global.
|
||||
new_room = muclib.new_room;
|
||||
|
||||
local name = module:require "muc/name";
|
||||
room_mt.get_name = name.get;
|
||||
room_mt.set_name = name.set;
|
||||
|
||||
local description = module:require "muc/description";
|
||||
room_mt.get_description = description.get;
|
||||
room_mt.set_description = description.set;
|
||||
|
||||
local language = module:require "muc/language";
|
||||
room_mt.get_language = language.get;
|
||||
room_mt.set_language = language.set;
|
||||
|
||||
local hidden = module:require "muc/hidden";
|
||||
room_mt.get_hidden = hidden.get;
|
||||
room_mt.set_hidden = hidden.set;
|
||||
function room_mt:get_public()
|
||||
return not self:get_hidden();
|
||||
end
|
||||
function room_mt:set_public(public)
|
||||
return self:set_hidden(not public);
|
||||
end
|
||||
|
||||
local password = module:require "muc/password";
|
||||
room_mt.get_password = password.get;
|
||||
room_mt.set_password = password.set;
|
||||
|
||||
local members_only = module:require "muc/members_only";
|
||||
room_mt.get_members_only = members_only.get;
|
||||
room_mt.set_members_only = members_only.set;
|
||||
room_mt.get_allow_member_invites = members_only.get_allow_member_invites;
|
||||
room_mt.set_allow_member_invites = members_only.set_allow_member_invites;
|
||||
|
||||
local moderated = module:require "muc/moderated";
|
||||
room_mt.get_moderated = moderated.get;
|
||||
room_mt.set_moderated = moderated.set;
|
||||
|
||||
local request = module:require "muc/request";
|
||||
room_mt.handle_role_request = request.handle_request;
|
||||
|
||||
local persistent = module:require "muc/persistent";
|
||||
room_mt.get_persistent = persistent.get;
|
||||
room_mt.set_persistent = persistent.set;
|
||||
|
||||
local subject = module:require "muc/subject";
|
||||
room_mt.get_changesubject = subject.get_changesubject;
|
||||
room_mt.set_changesubject = subject.set_changesubject;
|
||||
room_mt.get_subject = subject.get;
|
||||
room_mt.set_subject = subject.set;
|
||||
room_mt.send_subject = subject.send;
|
||||
|
||||
local history = module:require "muc/history";
|
||||
room_mt.send_history = history.send;
|
||||
room_mt.get_historylength = history.get_length;
|
||||
room_mt.set_historylength = history.set_length;
|
||||
|
||||
local register = module:require "muc/register";
|
||||
room_mt.get_registered_nick = register.get_registered_nick;
|
||||
room_mt.get_registered_jid = register.get_registered_jid;
|
||||
room_mt.handle_register_iq = register.handle_register_iq;
|
||||
room_mt.handle_unregister_iq = register.handle_unregister_iq;
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local st = require "util.stanza";
|
||||
local cache = require "util.cache";
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
|
||||
module:require "muc/config_form_sections";
|
||||
|
||||
module:depends("disco");
|
||||
module:add_identity("conference", "text", module:get_option_string("name", "Prosody Chatrooms"));
|
||||
module:add_feature("http://jabber.org/protocol/muc");
|
||||
module:depends "muc_unique"
|
||||
module:require "muc/lock";
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
if module:get_option_boolean("component_admins_as_room_owners", true) then
|
||||
-- Monkey patch to make server admins room owners
|
||||
local _get_affiliation = room_mt.get_affiliation;
|
||||
function room_mt:get_affiliation(jid)
|
||||
if is_admin(jid) then return "owner"; end
|
||||
return _get_affiliation(self, jid);
|
||||
end
|
||||
|
||||
local _set_affiliation = room_mt.set_affiliation;
|
||||
function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
|
||||
if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end
|
||||
return _set_affiliation(self, actor, jid, affiliation, reason, data);
|
||||
end
|
||||
end
|
||||
|
||||
local persistent_rooms_storage = module:open_store("persistent");
|
||||
local persistent_rooms = module:open_store("persistent", "map");
|
||||
local room_configs = module:open_store("config");
|
||||
local room_state = module:open_store("state");
|
||||
|
||||
local room_items_cache = {};
|
||||
|
||||
local function room_save(room, forced, savestate)
|
||||
local node = jid_split(room.jid);
|
||||
local is_persistent = persistent.get(room);
|
||||
room_items_cache[room.jid] = room:get_public() and room:get_name() or nil;
|
||||
if is_persistent or savestate then
|
||||
persistent_rooms:set(nil, room.jid, true);
|
||||
local data, state = room:freeze(savestate);
|
||||
room_state:set(node, state);
|
||||
return room_configs:set(node, data);
|
||||
elseif forced then
|
||||
persistent_rooms:set(nil, room.jid, nil);
|
||||
room_state:set(node, nil);
|
||||
return room_configs:set(node, nil);
|
||||
end
|
||||
end
|
||||
|
||||
local max_rooms = module:get_option_number("muc_max_rooms");
|
||||
local max_live_rooms = module:get_option_number("muc_room_cache_size", 100);
|
||||
|
||||
local room_hit = module:measure("room_hit", "rate");
|
||||
local room_miss = module:measure("room_miss", "rate")
|
||||
local room_eviction = module:measure("room_eviction", "rate");
|
||||
local rooms = cache.new(max_rooms or max_live_rooms, function (jid, room)
|
||||
if max_rooms then
|
||||
module:log("info", "Room limit of %d reached, no new rooms allowed", max_rooms);
|
||||
return false;
|
||||
end
|
||||
module:log("debug", "Evicting room %s", jid);
|
||||
room_eviction();
|
||||
room_items_cache[room.jid] = room:get_public() and room:get_name() or nil;
|
||||
local ok, err = room_save(room, nil, true); -- Force to disk
|
||||
if not ok then
|
||||
module:log("error", "Failed to swap inactive room %s to disk: %s", jid, err);
|
||||
return false;
|
||||
end
|
||||
end);
|
||||
|
||||
-- Automatically destroy empty non-persistent rooms
|
||||
module:hook("muc-occupant-left",function(event)
|
||||
local room = event.room
|
||||
if room.destroying then return end
|
||||
if not room:has_occupant() and not persistent.get(room) then -- empty, non-persistent room
|
||||
module:log("debug", "%q empty, destroying", room.jid);
|
||||
module:fire_event("muc-room-destroyed", { room = room });
|
||||
end
|
||||
end, -1);
|
||||
|
||||
function track_room(room)
|
||||
if rooms:set(room.jid, room) then
|
||||
-- When room is created, over-ride 'save' method
|
||||
room.save = room_save;
|
||||
return room;
|
||||
end
|
||||
-- Resource limit reached
|
||||
return false;
|
||||
end
|
||||
|
||||
local function handle_broken_room(room, origin, stanza)
|
||||
module:log("debug", "Returning error from broken room %s", room.jid);
|
||||
origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local function restore_room(jid)
|
||||
local node = jid_split(jid);
|
||||
local data, err = room_configs:get(node);
|
||||
if data then
|
||||
module:log("debug", "Restoring room %s from storage", jid);
|
||||
if module:fire_event("muc-room-pre-restore", { jid = jid, data = data }) == false then
|
||||
return false;
|
||||
end
|
||||
local state, s_err = room_state:get(node);
|
||||
if not state and s_err then
|
||||
module:log("debug", "Could not restore state of room %s: %s", jid, s_err);
|
||||
end
|
||||
local room = muclib.restore_room(data, state);
|
||||
if track_room(room) then
|
||||
room_state:set(node, nil);
|
||||
module:fire_event("muc-room-restored", { jid = jid, room = room });
|
||||
return room;
|
||||
else
|
||||
return false;
|
||||
end
|
||||
elseif err then
|
||||
module:log("error", "Error restoring room %s from storage: %s", jid, err);
|
||||
local room = muclib.new_room(jid, { locked = math.huge });
|
||||
room.handle_normal_presence = handle_broken_room;
|
||||
room.handle_first_presence = handle_broken_room;
|
||||
return room;
|
||||
end
|
||||
end
|
||||
|
||||
-- Removes a room from memory, without saving it (save first if required)
|
||||
function forget_room(room)
|
||||
module:log("debug", "Forgetting %s", room.jid);
|
||||
rooms.save = nil;
|
||||
rooms:set(room.jid, nil);
|
||||
end
|
||||
|
||||
-- Removes a room from the database (may remain in memory)
|
||||
function delete_room(room)
|
||||
module:log("debug", "Deleting %s", room.jid);
|
||||
room_configs:set(jid_split(room.jid), nil);
|
||||
room_state:set(jid_split(room.jid), nil);
|
||||
persistent_rooms:set(nil, room.jid, nil);
|
||||
room_items_cache[room.jid] = nil;
|
||||
end
|
||||
|
||||
function module.unload()
|
||||
for room in live_rooms() do
|
||||
room:save(nil, true);
|
||||
forget_room(room);
|
||||
end
|
||||
end
|
||||
|
||||
function get_room_from_jid(room_jid)
|
||||
local room = rooms:get(room_jid);
|
||||
if room then
|
||||
room_hit();
|
||||
rooms:set(room_jid, room); -- bump to top;
|
||||
return room;
|
||||
end
|
||||
room_miss();
|
||||
return restore_room(room_jid);
|
||||
end
|
||||
|
||||
local function set_room_defaults(room, lang)
|
||||
room:set_public(module:get_option_boolean("muc_room_default_public", false));
|
||||
room:set_persistent(module:get_option_boolean("muc_room_default_persistent", room:get_persistent()));
|
||||
room:set_members_only(module:get_option_boolean("muc_room_default_members_only", room:get_members_only()));
|
||||
room:set_allow_member_invites(module:get_option_boolean("muc_room_default_allow_member_invites",
|
||||
room:get_allow_member_invites()));
|
||||
room:set_moderated(module:get_option_boolean("muc_room_default_moderated", room:get_moderated()));
|
||||
room:set_whois(module:get_option_boolean("muc_room_default_public_jids",
|
||||
room:get_whois() == "anyone") and "anyone" or "moderators");
|
||||
room:set_changesubject(module:get_option_boolean("muc_room_default_change_subject", room:get_changesubject()));
|
||||
room:set_historylength(module:get_option_number("muc_room_default_history_length", room:get_historylength()));
|
||||
room:set_language(lang or module:get_option_string("muc_room_default_language"));
|
||||
end
|
||||
|
||||
function create_room(room_jid, config)
|
||||
local exists = get_room_from_jid(room_jid);
|
||||
if exists then
|
||||
return nil, "room-exists";
|
||||
end
|
||||
local room = muclib.new_room(room_jid, config);
|
||||
if not config then
|
||||
set_room_defaults(room);
|
||||
end
|
||||
module:fire_event("muc-room-created", {
|
||||
room = room;
|
||||
});
|
||||
return track_room(room);
|
||||
end
|
||||
|
||||
function all_rooms()
|
||||
return coroutine.wrap(function ()
|
||||
local seen = {}; -- Don't iterate over persistent rooms twice
|
||||
for room in live_rooms() do
|
||||
coroutine.yield(room);
|
||||
seen[room.jid] = true;
|
||||
end
|
||||
local all_persistent_rooms, err = persistent_rooms_storage:get(nil);
|
||||
if not all_persistent_rooms then
|
||||
if err then
|
||||
module:log("error", "Error loading list of persistent rooms, only rooms live in memory were iterated over");
|
||||
module:log("debug", "%s", debug.traceback(err));
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
for room_jid in pairs(all_persistent_rooms) do
|
||||
if not seen[room_jid] then
|
||||
local room = restore_room(room_jid);
|
||||
if room then
|
||||
coroutine.yield(room);
|
||||
else
|
||||
module:log("error", "Missing data for room '%s', omitting from iteration", room_jid);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
function live_rooms()
|
||||
return rooms:values();
|
||||
end
|
||||
|
||||
function each_room(live_only)
|
||||
if live_only then
|
||||
return live_rooms();
|
||||
end
|
||||
return all_rooms();
|
||||
end
|
||||
|
||||
module:hook("host-disco-items", function(event)
|
||||
local reply = event.reply;
|
||||
module:log("debug", "host-disco-items called");
|
||||
if next(room_items_cache) ~= nil then
|
||||
for jid, room_name in pairs(room_items_cache) do
|
||||
reply:tag("item", { jid = jid, name = room_name }):up();
|
||||
end
|
||||
else
|
||||
for room in all_rooms() do
|
||||
if not room:get_hidden() then
|
||||
local jid, room_name = room.jid, room:get_name();
|
||||
room_items_cache[jid] = room_name;
|
||||
reply:tag("item", { jid = jid, name = room_name }):up();
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("muc-room-pre-create", function (event)
|
||||
set_room_defaults(event.room, event.stanza.attr["xml:lang"]);
|
||||
end, 1);
|
||||
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if not track_room(event.room) then
|
||||
origin.send(st.error_reply(stanza, "wait", "resource-constraint"));
|
||||
return true;
|
||||
end
|
||||
end, -1000);
|
||||
|
||||
module:hook("muc-room-destroyed",function(event)
|
||||
local room = event.room;
|
||||
forget_room(room);
|
||||
delete_room(room);
|
||||
end);
|
||||
|
||||
if module:get_option_boolean("muc_tombstones", true) then
|
||||
|
||||
local ttl = module:get_option_number("muc_tombstone_expiry", 86400 * 31);
|
||||
|
||||
module:hook("muc-room-destroyed",function(event)
|
||||
local room = event.room;
|
||||
if not room:get_persistent() then return end
|
||||
if room._data.destroyed then
|
||||
return -- Allow destruction of tombstone
|
||||
end
|
||||
|
||||
local tombstone = new_room(room.jid, {
|
||||
locked = os.time() + ttl;
|
||||
destroyed = true;
|
||||
reason = event.reason;
|
||||
newjid = event.newjid;
|
||||
-- password?
|
||||
});
|
||||
tombstone.save = room_save;
|
||||
tombstone:set_persistent(true);
|
||||
tombstone:set_hidden(true);
|
||||
tombstone:save(true);
|
||||
return true;
|
||||
end, -10);
|
||||
end
|
||||
|
||||
do
|
||||
local restrict_room_creation = module:get_option("restrict_room_creation");
|
||||
if restrict_room_creation == true then
|
||||
restrict_room_creation = "admin";
|
||||
end
|
||||
if restrict_room_creation then
|
||||
local host_suffix = module.host:gsub("^[^%.]+%.", "");
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local user_jid = stanza.attr.from;
|
||||
if not is_admin(user_jid) and not (
|
||||
restrict_room_creation == "local" and
|
||||
select(2, jid_split(user_jid)) == host_suffix
|
||||
) then
|
||||
origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted"));
|
||||
return true;
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
for event_name, method in pairs {
|
||||
-- Normal room interactions
|
||||
["iq-get/bare/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ;
|
||||
["iq-get/bare/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ;
|
||||
["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["iq-get/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ;
|
||||
["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
|
||||
["iq-get/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ;
|
||||
["message/bare"] = "handle_message_to_room" ;
|
||||
["presence/bare"] = "handle_presence_to_room" ;
|
||||
["iq/bare/jabber:iq:register:query"] = "handle_register_iq";
|
||||
["iq-set/bare/xmpp:vnctalk:unregister:query"] = "handle_unregister_iq";
|
||||
-- Host room
|
||||
["iq-get/host/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ;
|
||||
["iq-get/host/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ;
|
||||
["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["iq-get/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ;
|
||||
["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
|
||||
["iq-get/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ;
|
||||
["message/host"] = "handle_message_to_room" ;
|
||||
["presence/host"] = "handle_presence_to_room" ;
|
||||
-- Direct to occupant (normal rooms and host room)
|
||||
["presence/full"] = "handle_presence_to_occupant" ;
|
||||
["iq/full"] = "handle_iq_to_occupant" ;
|
||||
["message/full"] = "handle_message_to_occupant" ;
|
||||
} do
|
||||
module:hook(event_name, function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = jid_bare(stanza.attr.to);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if room and room._data.destroyed then
|
||||
if room._data.locked < os.time()
|
||||
or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then
|
||||
-- Allow the room to be recreated by admin or after time has passed
|
||||
delete_room(room);
|
||||
room = nil;
|
||||
else
|
||||
if stanza.attr.type ~= "error" then
|
||||
local reply = st.error_reply(stanza, "cancel", "gone", room._data.reason)
|
||||
if room._data.newjid then
|
||||
local uri = "xmpp:"..room._data.newjid.."?join";
|
||||
reply:get_child("error"):child_with_name("gone"):text(uri);
|
||||
end
|
||||
event.origin.send(reply);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
if room == nil then
|
||||
-- Watch presence to create rooms
|
||||
if stanza.attr.type == nil and stanza.name == "presence" then
|
||||
room = muclib.new_room(room_jid);
|
||||
return room:handle_first_presence(origin, stanza);
|
||||
elseif stanza.attr.type ~= "error" then
|
||||
origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
|
||||
return true;
|
||||
else
|
||||
return;
|
||||
end
|
||||
elseif room == false then -- Error loading room
|
||||
origin.send(st.error_reply(stanza, "wait", "resource-constraint"));
|
||||
return true;
|
||||
end
|
||||
return room[method](room, origin, stanza);
|
||||
end, -2)
|
||||
end
|
||||
|
||||
function shutdown_component()
|
||||
for room in live_rooms() do
|
||||
room:save(nil, true);
|
||||
end
|
||||
end
|
||||
module:hook_global("server-stopping", shutdown_component, -300);
|
||||
|
||||
do -- Ad-hoc commands
|
||||
module:depends "adhoc";
|
||||
local t_concat = table.concat;
|
||||
local adhoc_new = module:require "adhoc".new;
|
||||
local adhoc_initial = require "util.adhoc".new_initial_data_form;
|
||||
local array = require "util.array";
|
||||
local dataforms_new = require "util.dataforms".new;
|
||||
|
||||
local destroy_rooms_layout = dataforms_new {
|
||||
title = "Destroy rooms";
|
||||
instructions = "Select the rooms to destroy";
|
||||
|
||||
{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" };
|
||||
{ name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"};
|
||||
};
|
||||
|
||||
local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function()
|
||||
return { rooms = array.collect(all_rooms()):pluck("jid"):sort(); };
|
||||
end, function(fields, errors)
|
||||
if errors then
|
||||
local errmsg = {};
|
||||
for field, err in pairs(errors) do
|
||||
errmsg[#errmsg + 1] = field .. ": " .. err;
|
||||
end
|
||||
return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
|
||||
end
|
||||
for _, room in ipairs(fields.rooms) do
|
||||
get_room_from_jid(room):destroy();
|
||||
end
|
||||
return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") };
|
||||
end);
|
||||
local destroy_rooms_desc = adhoc_new("Destroy Rooms",
|
||||
"http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
|
||||
|
||||
module:provides("adhoc", destroy_rooms_desc);
|
||||
end
|
||||
@@ -0,0 +1,485 @@
|
||||
-- XEP-0313: Message Archive Management for Prosody MUC
|
||||
-- Copyright (C) 2011-2017 Kim Alvefur
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
|
||||
if module:get_host_type() ~= "component" then
|
||||
module:log("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module.name);
|
||||
return;
|
||||
end
|
||||
|
||||
local xmlns_mam = "urn:xmpp:mam:2";
|
||||
local xmlns_delay = "urn:xmpp:delay";
|
||||
local xmlns_forward = "urn:xmpp:forward:0";
|
||||
local xmlns_st_id = "urn:xmpp:sid:0";
|
||||
local xmlns_muc_user = "http://jabber.org/protocol/muc#user";
|
||||
local muc_form_enable = "muc#roomconfig_enablearchiving"
|
||||
|
||||
local st = require "util.stanza";
|
||||
local rsm = require "util.rsm";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local dataform = require "util.dataforms".new;
|
||||
|
||||
local mod_muc = module:depends"muc";
|
||||
local get_room_from_jid = mod_muc.get_room_from_jid;
|
||||
|
||||
local is_stanza = st.is_stanza;
|
||||
local tostring = tostring;
|
||||
local time_now = os.time;
|
||||
local m_min = math.min;
|
||||
local timestamp, timestamp_parse, datestamp = import( "util.datetime", "datetime", "parse", "date");
|
||||
local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
|
||||
|
||||
local default_history_length = 20;
|
||||
local max_history_length = module:get_option_number("max_history_messages", math.huge);
|
||||
|
||||
local function get_historylength(room)
|
||||
return math.min(room._data.history_length or default_history_length, max_history_length);
|
||||
end
|
||||
|
||||
function schedule_cleanup()
|
||||
-- replaced by non-noop later if cleanup is enabled
|
||||
end
|
||||
|
||||
local log_all_rooms = module:get_option_boolean("muc_log_all_rooms", false);
|
||||
local log_by_default = module:get_option_boolean("muc_log_by_default", true);
|
||||
|
||||
local archive_store = "muc_log";
|
||||
local archive = module:open_store(archive_store, "archive");
|
||||
|
||||
if archive.name == "null" or not archive.find then
|
||||
if not archive.find then
|
||||
module:log("error", "Attempt to open archive storage returned a driver without archive API support");
|
||||
module:log("error", "mod_%s does not support archiving",
|
||||
archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or "<unknown>");
|
||||
else
|
||||
module:log("error", "Attempt to open archive storage returned null driver");
|
||||
end
|
||||
module:log("info", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
|
||||
return false;
|
||||
end
|
||||
|
||||
local function archiving_enabled(room)
|
||||
if log_all_rooms then
|
||||
return true;
|
||||
end
|
||||
local enabled = room._data.archiving;
|
||||
if enabled == nil then
|
||||
return log_by_default;
|
||||
end
|
||||
return enabled;
|
||||
end
|
||||
|
||||
if not log_all_rooms then
|
||||
module:hook("muc-config-form", function(event)
|
||||
local room, form = event.room, event.form;
|
||||
table.insert(form,
|
||||
{
|
||||
name = muc_form_enable,
|
||||
type = "boolean",
|
||||
label = "Enable archiving?",
|
||||
value = archiving_enabled(room),
|
||||
}
|
||||
);
|
||||
end);
|
||||
|
||||
module:hook("muc-config-submitted/"..muc_form_enable, function(event)
|
||||
event.room._data.archiving = event.value;
|
||||
event.status_codes[event.value and "170" or "171"] = true;
|
||||
end);
|
||||
end
|
||||
|
||||
-- Note: We ignore the 'with' field as this is internally used for stanza types
|
||||
local query_form = dataform {
|
||||
{ name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; };
|
||||
{ name = "with"; type = "jid-single"; };
|
||||
{ name = "start"; type = "text-single" };
|
||||
{ name = "end"; type = "text-single"; };
|
||||
};
|
||||
|
||||
-- Serve form
|
||||
module:hook("iq-get/bare/"..xmlns_mam..":query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
origin.send(st.reply(stanza):tag("query", { xmlns = xmlns_mam }):add_child(query_form:form()));
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- Handle archive queries
|
||||
module:hook("iq-set/bare/"..xmlns_mam..":query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = stanza.attr.to;
|
||||
local room_node = jid_split(room_jid);
|
||||
local orig_from = stanza.attr.from;
|
||||
local query = stanza.tags[1];
|
||||
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
origin.send(st.error_reply(stanza, "cancel", "item-not-found"))
|
||||
return true;
|
||||
end
|
||||
local from = jid_bare(orig_from);
|
||||
|
||||
-- Banned or not a member of a members-only room?
|
||||
local from_affiliation = room:get_affiliation(from);
|
||||
if from_affiliation == "outcast" -- banned
|
||||
or room:get_members_only() and not from_affiliation then -- members-only, not a member
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"))
|
||||
return true;
|
||||
end
|
||||
|
||||
local qid = query.attr.queryid;
|
||||
|
||||
-- Search query parameters
|
||||
local qstart, qend;
|
||||
local form = query:get_child("x", "jabber:x:data");
|
||||
if form then
|
||||
local err;
|
||||
form, err = query_form:data(form);
|
||||
if err then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
|
||||
return true;
|
||||
end
|
||||
qstart, qend = form["start"], form["end"];
|
||||
end
|
||||
|
||||
if qstart or qend then -- Validate timestamps
|
||||
local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend))
|
||||
if (qstart and not vstart) or (qend and not vend) then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
|
||||
return true;
|
||||
end
|
||||
qstart, qend = vstart, vend;
|
||||
end
|
||||
|
||||
module:log("debug", "Archive query id %s from %s until %s)",
|
||||
tostring(qid),
|
||||
qstart and timestamp(qstart) or "the dawn of time",
|
||||
qend and timestamp(qend) or "now");
|
||||
|
||||
-- RSM stuff
|
||||
local qset = rsm.get(query);
|
||||
local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
|
||||
local reverse = qset and qset.before or false;
|
||||
|
||||
local before, after = qset and qset.before, qset and qset.after;
|
||||
if type(before) ~= "string" then before = nil; end
|
||||
|
||||
-- Load all the data!
|
||||
local data, err = archive:find(room_node, {
|
||||
start = qstart; ["end"] = qend; -- Time range
|
||||
limit = qmax + 1;
|
||||
before = before; after = after;
|
||||
reverse = reverse;
|
||||
with = "message<groupchat";
|
||||
});
|
||||
|
||||
if not data then
|
||||
origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
|
||||
return true;
|
||||
end
|
||||
local total = tonumber(err);
|
||||
|
||||
local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
|
||||
|
||||
local results = {};
|
||||
|
||||
-- Wrap it in stuff and deliver
|
||||
local first, last;
|
||||
local count = 0;
|
||||
local complete = "true";
|
||||
for id, item, when in data do
|
||||
count = count + 1;
|
||||
if count > qmax then
|
||||
complete = nil;
|
||||
break;
|
||||
end
|
||||
local fwd_st = st.message(msg_reply_attr)
|
||||
:tag("result", { xmlns = xmlns_mam, queryid = qid, id = id })
|
||||
:tag("forwarded", { xmlns = xmlns_forward })
|
||||
:tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
|
||||
|
||||
-- Strip <x> tag, containing the original senders JID, unless the room makes this public
|
||||
if room:get_whois() ~= "anyone" then
|
||||
item:maptags(function (tag)
|
||||
if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then
|
||||
return nil;
|
||||
end
|
||||
return tag;
|
||||
end);
|
||||
end
|
||||
if not is_stanza(item) then
|
||||
item = st.deserialize(item);
|
||||
end
|
||||
item.attr.to = nil;
|
||||
item.attr.xmlns = "jabber:client";
|
||||
fwd_st:add_child(item);
|
||||
|
||||
if not first then first = id; end
|
||||
last = id;
|
||||
|
||||
if reverse then
|
||||
results[count] = fwd_st;
|
||||
else
|
||||
origin.send(fwd_st);
|
||||
end
|
||||
end
|
||||
|
||||
if reverse then
|
||||
for i = #results, 1, -1 do
|
||||
origin.send(results[i]);
|
||||
end
|
||||
first, last = last, first;
|
||||
end
|
||||
|
||||
-- That's all folks!
|
||||
module:log("debug", "Archive query %s completed", tostring(qid));
|
||||
|
||||
origin.send(st.reply(stanza)
|
||||
:tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
|
||||
:add_child(rsm.generate {
|
||||
first = first, last = last, count = total }));
|
||||
return true;
|
||||
end);
|
||||
|
||||
module:hook("muc-get-history", function (event)
|
||||
local room = event.room;
|
||||
if not archiving_enabled(room) then return end
|
||||
local room_jid = room.jid;
|
||||
local maxstanzas = event.maxstanzas;
|
||||
local maxchars = event.maxchars;
|
||||
local since = event.since;
|
||||
local to = event.to;
|
||||
|
||||
if maxstanzas == 0 or maxchars == 0 then
|
||||
return -- No history requested
|
||||
end
|
||||
|
||||
if not maxstanzas or maxstanzas > get_historylength(room) then
|
||||
maxstanzas = get_historylength(room);
|
||||
end
|
||||
|
||||
if room._history and #room._history >= maxstanzas then
|
||||
return -- It can deal with this itself
|
||||
end
|
||||
|
||||
-- Load all the data!
|
||||
local query = {
|
||||
limit = maxstanzas;
|
||||
start = since;
|
||||
reverse = true;
|
||||
with = "message<groupchat";
|
||||
}
|
||||
local data, err = archive:find(jid_split(room_jid), query);
|
||||
|
||||
if not data then
|
||||
module:log("error", "Could not fetch history: %s", tostring(err));
|
||||
return
|
||||
end
|
||||
|
||||
local history, i = {}, 1;
|
||||
|
||||
for id, item, when in data do
|
||||
item.attr.to = to;
|
||||
item:tag("delay", { xmlns = "urn:xmpp:delay", from = room_jid, stamp = timestamp(when) }):up(); -- XEP-0203
|
||||
item:tag("stanza-id", { xmlns = xmlns_st_id, by = room_jid, id = id }):up();
|
||||
if room:get_whois() ~= "anyone" then
|
||||
item:maptags(function (tag)
|
||||
if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then
|
||||
return nil;
|
||||
end
|
||||
return tag;
|
||||
end);
|
||||
end
|
||||
if maxchars then
|
||||
local chars = #tostring(item);
|
||||
if maxchars - chars < 0 then
|
||||
break
|
||||
end
|
||||
maxchars = maxchars - chars;
|
||||
end
|
||||
history[i], i = item, i+1;
|
||||
-- module:log("debug", tostring(item));
|
||||
end
|
||||
function event.next_stanza()
|
||||
i = i - 1;
|
||||
return history[i];
|
||||
end
|
||||
return true;
|
||||
end, 1);
|
||||
|
||||
module:hook("muc-broadcast-message", function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
|
||||
-- Filter out <stanza-id> that claim to be from us
|
||||
stanza:maptags(function (tag)
|
||||
if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id
|
||||
and jid_prep(tag.attr.by) == room.jid then
|
||||
return nil;
|
||||
end
|
||||
return tag;
|
||||
end);
|
||||
|
||||
end, 1);
|
||||
|
||||
-- Handle messages
|
||||
local function save_to_history(self, stanza)
|
||||
local room_node, room_host = jid_split(self.jid);
|
||||
module:log("info", "processing stanza for archive: %s @ %s", room_node, room_host);
|
||||
|
||||
local stored_stanza = stanza;
|
||||
|
||||
if stanza.name == "message" and self:get_whois() == "anyone" then
|
||||
stored_stanza = st.clone(stanza);
|
||||
stored_stanza.attr.to = nil;
|
||||
local occupant = self._occupants[stanza.attr.from];
|
||||
if occupant then
|
||||
local actor = jid_bare(occupant.jid);
|
||||
local affiliation = self:get_affiliation(actor) or "none";
|
||||
local role = self:get_role(actor) or self:get_default_role(affiliation);
|
||||
stored_stanza:add_direct_child(st.stanza("x", { xmlns = xmlns_muc_user })
|
||||
:tag("item", { affiliation = affiliation; role = role; jid = actor }));
|
||||
end
|
||||
end
|
||||
|
||||
-- Policy check
|
||||
if not archiving_enabled(self) then return end -- Don't log
|
||||
|
||||
-- Save the type in the 'with' field, allows storing presence without conflicts
|
||||
local with = stanza.name
|
||||
if stanza.attr.type then
|
||||
with = with .. "<" .. stanza.attr.type
|
||||
end
|
||||
|
||||
module:log("info", "adding stanza to for archive: %s @ %s", room_node, room_host);
|
||||
-- And stash it
|
||||
local id, err = archive:append(room_node, nil, stored_stanza, time_now(), with);
|
||||
|
||||
if id then
|
||||
module:log("info", "stanza added to for archive: %s @ %s", room_node, room_host);
|
||||
-- schedule_cleanup(room_node);
|
||||
stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id }));
|
||||
else
|
||||
module:log("error", "Could not archive stanza: %s", err);
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("muc-add-history", function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
save_to_history(room, stanza);
|
||||
end);
|
||||
|
||||
if module:get_option_boolean("muc_log_presences", false) then
|
||||
module:hook("muc-occupant-joined", function (event)
|
||||
save_to_history(event.room, st.stanza("presence", { from = event.nick }):tag("x", { xmlns = "http://jabber.org/protocol/muc" }));
|
||||
end);
|
||||
module:hook("muc-occupant-left", function (event)
|
||||
save_to_history(event.room, st.stanza("presence", { type = "unavailable", from = event.nick }));
|
||||
end);
|
||||
end
|
||||
|
||||
if not archive.delete then
|
||||
module:log("warn", "Storage driver %s does not support deletion", archive._provided_by);
|
||||
module:log("warn", "Archived message will persist after a room has been destroyed");
|
||||
else
|
||||
module:hook("muc-room-destroyed", function(event)
|
||||
module:log("info", "Archived message will persist after a room has been destroyed");
|
||||
-- local room_node = jid_split(event.room.jid);
|
||||
-- archive:delete(room_node);
|
||||
end);
|
||||
end
|
||||
|
||||
-- And role/affiliation changes?
|
||||
|
||||
module:add_feature(xmlns_mam);
|
||||
|
||||
module:hook("muc-disco#info", function(event)
|
||||
event.reply:tag("feature", {var=xmlns_mam}):up();
|
||||
event.reply:tag("feature", {var=xmlns_st_id}):up();
|
||||
end);
|
||||
|
||||
-- Cleanup
|
||||
|
||||
local cleanup_after = module:get_option_string("muc_log_expires_after", "1w");
|
||||
local cleanup_interval = module:get_option_number("muc_log_cleanup_interval", 4 * 60 * 60);
|
||||
|
||||
if cleanup_after ~= "never" then
|
||||
local cleanup_storage = module:open_store("muc_log_cleanup");
|
||||
local cleanup_map = module:open_store("muc_log_cleanup", "map");
|
||||
|
||||
local day = 86400;
|
||||
local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
|
||||
local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
|
||||
if not n then
|
||||
module:log("error", "Could not parse muc_log_expires_after string %q", cleanup_after);
|
||||
return false;
|
||||
end
|
||||
|
||||
cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
|
||||
|
||||
module:log("debug", "muc_log_expires_after = %d -- in seconds", cleanup_after);
|
||||
|
||||
if not archive.delete then
|
||||
module:log("error", "muc_log_expires_after set but mod_%s does not support deleting", archive._provided_by);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- For each day, store a set of rooms that have new messages. To expire
|
||||
-- messages, we collect the union of sets of rooms from dates that fall
|
||||
-- outside the cleanup range.
|
||||
|
||||
local last_date = require "util.cache".new(module:get_option_number("muc_log_cleanup_date_cache_size", 1000));
|
||||
function schedule_cleanup(roomname, date)
|
||||
date = date or datestamp();
|
||||
if last_date:get(roomname) == date then return end
|
||||
local ok = cleanup_map:set(date, roomname, true);
|
||||
if ok then
|
||||
last_date:set(roomname, date);
|
||||
end
|
||||
end
|
||||
|
||||
local async = require "util.async";
|
||||
cleanup_runner = async.runner(function ()
|
||||
local rooms = {};
|
||||
local cut_off = datestamp(os.time() - cleanup_after);
|
||||
for date in cleanup_storage:users() do
|
||||
if date <= cut_off then
|
||||
module:log("debug", "Messages from %q should be expired", date);
|
||||
local messages_this_day = cleanup_storage:get(date);
|
||||
if messages_this_day then
|
||||
for room in pairs(messages_this_day) do
|
||||
rooms[room] = true;
|
||||
end
|
||||
if date < cut_off then
|
||||
-- Messages from the same day as the cut-off might not have expired yet,
|
||||
-- but all earlier will have, so clear storage for those days.
|
||||
cleanup_storage:set(date, nil);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local sum, num_rooms = 0, 0;
|
||||
for room in pairs(rooms) do
|
||||
local ok, err = archive:delete(room, { ["end"] = os.time() - cleanup_after; })
|
||||
if ok then
|
||||
num_rooms = num_rooms + 1;
|
||||
sum = sum + (tonumber(ok) or 0);
|
||||
else
|
||||
cleanup_map:set(cut_off, room, true);
|
||||
module:log("error", "Could not delete messages for room '%s': %s", room, err);
|
||||
end
|
||||
local wait, done = async.waiter();
|
||||
module:add_timer(0.01, done);
|
||||
wait();
|
||||
end
|
||||
module:log("info", "Deleted %d expired messages for %d rooms", sum, num_rooms);
|
||||
end);
|
||||
|
||||
cleanup_task = module:add_timer(1, function ()
|
||||
cleanup_runner:run(true);
|
||||
return cleanup_interval;
|
||||
end);
|
||||
else
|
||||
module:log("debug", "Archive expiry disabled");
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
-- XEP-0307: Unique Room Names for Multi-User Chat
|
||||
local st = require "util.stanza";
|
||||
local unique_name = require "util.id".medium;
|
||||
module:add_feature "http://jabber.org/protocol/muc#unique"
|
||||
|
||||
module:log("info", "uniqe activated");
|
||||
|
||||
local function handle_iq(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local uname = unique_name():lower();
|
||||
local origto = stanza.attr.to or nil;
|
||||
|
||||
-- module:log("info", "orig to: %s", origto);
|
||||
-- module:log("info", "uname: %s", uname);
|
||||
|
||||
|
||||
origin.send(st.reply(stanza)
|
||||
:tag("unique", {xmlns = "http://jabber.org/protocol/muc#unique"})
|
||||
:text(unique_name():lower())
|
||||
);
|
||||
|
||||
end;
|
||||
|
||||
local function handle_iq_tobare(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local uname = unique_name():lower();
|
||||
local origto = stanza.attr.to or nil;
|
||||
|
||||
-- module:log("info", "orig to: %s", origto);
|
||||
-- module:log("info", "uname: %s", uname);
|
||||
|
||||
origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
|
||||
|
||||
end;
|
||||
|
||||
module:hook("iq-get/bare/http://jabber.org/protocol/muc#unique:unique", handle_iq_tobare, 1);
|
||||
module:hook("iq-get/host/http://jabber.org/protocol/muc#unique:unique", handle_iq, -1);
|
||||
@@ -0,0 +1,440 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2012 Matthew Wild
|
||||
-- Copyright (C) 2008-2012 Waqas Hussain
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local array = require "util.array";
|
||||
local set = require "util.set";
|
||||
local it = require "util.iterators";
|
||||
local logger = require "util.logger";
|
||||
local pluginloader = require "util.pluginloader";
|
||||
local timer = require "util.timer";
|
||||
local resolve_relative_path = require"util.paths".resolve_relative_path;
|
||||
local st = require "util.stanza";
|
||||
|
||||
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
|
||||
local error, setmetatable, type = error, setmetatable, type;
|
||||
local ipairs, pairs, select = ipairs, pairs, select;
|
||||
local tonumber, tostring = tonumber, tostring;
|
||||
local require = require;
|
||||
local pack = table.pack or function(...) return {n=select("#",...), ...}; end -- table.pack is only in 5.2
|
||||
local unpack = table.unpack or unpack; --luacheck: ignore 113 -- renamed in 5.2
|
||||
|
||||
local prosody = prosody;
|
||||
local hosts = prosody.hosts;
|
||||
|
||||
-- FIXME: This assert() is to try and catch an obscure bug (2013-04-05)
|
||||
local core_post_stanza = assert(prosody.core_post_stanza,
|
||||
"prosody.core_post_stanza is nil, please report this as a bug");
|
||||
|
||||
-- Registry of shared module data
|
||||
local shared_data = setmetatable({}, { __mode = "v" });
|
||||
|
||||
local NULL = {};
|
||||
|
||||
local api = {};
|
||||
|
||||
-- Returns the name of the current module
|
||||
function api:get_name()
|
||||
return self.name;
|
||||
end
|
||||
|
||||
-- Returns the host that the current module is serving
|
||||
function api:get_host()
|
||||
return self.host;
|
||||
end
|
||||
|
||||
function api:get_host_type()
|
||||
return (self.host == "*" and "global") or hosts[self.host].type or "local";
|
||||
end
|
||||
|
||||
function api:set_global()
|
||||
self.host = "*";
|
||||
-- Update the logger
|
||||
local _log = logger.init("mod_"..self.name);
|
||||
self.log = function (self, ...) return _log(...); end; --luacheck: ignore self
|
||||
self._log = _log;
|
||||
self.global = true;
|
||||
end
|
||||
|
||||
function api:add_feature(xmlns)
|
||||
self:add_item("feature", xmlns);
|
||||
end
|
||||
function api:add_identity(category, identity_type, name)
|
||||
self:add_item("identity", {category = category, type = identity_type, name = name});
|
||||
end
|
||||
function api:add_extension(data)
|
||||
self:add_item("extension", data);
|
||||
end
|
||||
|
||||
function api:fire_event(...)
|
||||
return (hosts[self.host] or prosody).events.fire_event(...);
|
||||
end
|
||||
|
||||
function api:hook_object_event(object, event, handler, priority)
|
||||
self.event_handlers:set(object, event, handler, true);
|
||||
return object.add_handler(event, handler, priority);
|
||||
end
|
||||
|
||||
function api:unhook_object_event(object, event, handler)
|
||||
self.event_handlers:set(object, event, handler, nil);
|
||||
return object.remove_handler(event, handler);
|
||||
end
|
||||
|
||||
function api:hook(event, handler, priority)
|
||||
return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority);
|
||||
end
|
||||
|
||||
function api:hook_global(event, handler, priority)
|
||||
return self:hook_object_event(prosody.events, event, handler, priority);
|
||||
end
|
||||
|
||||
function api:hook_tag(xmlns, name, handler, priority)
|
||||
if not handler and type(name) == "function" then
|
||||
-- If only 2 options then they specified no xmlns
|
||||
xmlns, name, handler, priority = nil, xmlns, name, handler;
|
||||
elseif not (handler and name) then
|
||||
self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
|
||||
return;
|
||||
end
|
||||
return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name,
|
||||
function (data) return handler(data.origin, data.stanza, data); end, priority);
|
||||
end
|
||||
api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9
|
||||
|
||||
function api:unhook(event, handler)
|
||||
return self:unhook_object_event((hosts[self.host] or prosody).events, event, handler);
|
||||
end
|
||||
|
||||
function api:wrap_object_event(events_object, event, handler)
|
||||
return self:hook_object_event(assert(events_object.wrappers, "no wrappers"), event, handler);
|
||||
end
|
||||
|
||||
function api:wrap_event(event, handler)
|
||||
return self:wrap_object_event((hosts[self.host] or prosody).events, event, handler);
|
||||
end
|
||||
|
||||
function api:wrap_global(event, handler)
|
||||
return self:hook_object_event(prosody.events, event, handler);
|
||||
end
|
||||
|
||||
function api:require(lib)
|
||||
local f, n = pluginloader.load_code_ext(self.name, lib, "lib.lua", self.environment);
|
||||
if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
|
||||
return f();
|
||||
end
|
||||
|
||||
function api:depends(name)
|
||||
local modulemanager = require"core.modulemanager";
|
||||
if self:get_option_inherited_set("modules_disabled", {}):contains(name) then
|
||||
error("Dependency on disabled module mod_"..name);
|
||||
end
|
||||
if not self.dependencies then
|
||||
self.dependencies = {};
|
||||
self:hook("module-reloaded", function (event)
|
||||
if self.dependencies[event.module] and not self.reloading then
|
||||
self:log("info", "Auto-reloading due to reload of %s:%s", event.host, event.module);
|
||||
modulemanager.reload(self.host, self.name);
|
||||
return;
|
||||
end
|
||||
end);
|
||||
self:hook("module-unloaded", function (event)
|
||||
if self.dependencies[event.module] then
|
||||
self:log("info", "Auto-unloading due to unload of %s:%s", event.host, event.module);
|
||||
modulemanager.unload(self.host, self.name);
|
||||
end
|
||||
end);
|
||||
end
|
||||
local mod = modulemanager.get_module(self.host, name) or modulemanager.get_module("*", name);
|
||||
if mod and mod.module.host == "*" and self.host ~= "*"
|
||||
and modulemanager.module_has_method(mod, "add_host") then
|
||||
mod = nil; -- Target is a shared module, so we still want to load it on our host
|
||||
end
|
||||
if not mod then
|
||||
local err;
|
||||
mod, err = modulemanager.load(self.host, name);
|
||||
if not mod then
|
||||
return error(("Unable to load required module, mod_%s: %s"):format(name, ((err or "unknown error"):gsub("%-", " ")) ));
|
||||
end
|
||||
end
|
||||
self.dependencies[name] = true;
|
||||
return mod;
|
||||
end
|
||||
|
||||
local function get_shared_table_from_path(module, tables, path)
|
||||
if path:sub(1,1) ~= "/" then -- Prepend default components
|
||||
local default_path_components = { module.host, module.name };
|
||||
local n_components = select(2, path:gsub("/", "%1"));
|
||||
path = (n_components<#default_path_components and "/" or "")
|
||||
..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path;
|
||||
end
|
||||
local shared = tables[path];
|
||||
if not shared then
|
||||
shared = {};
|
||||
if path:match("%-cache$") then
|
||||
setmetatable(shared, { __mode = "kv" });
|
||||
end
|
||||
tables[path] = shared;
|
||||
end
|
||||
return shared;
|
||||
end
|
||||
|
||||
-- Returns a shared table at the specified virtual path
|
||||
-- Intentionally does not allow the table to be _set_, it
|
||||
-- is auto-created if it does not exist.
|
||||
function api:shared(path)
|
||||
if not self.shared_data then self.shared_data = {}; end
|
||||
local shared = get_shared_table_from_path(self, shared_data, path);
|
||||
self.shared_data[path] = shared;
|
||||
return shared;
|
||||
end
|
||||
|
||||
function api:get_option(name, default_value)
|
||||
local config = require "core.configmanager";
|
||||
local value = config.get(self.host, name);
|
||||
if value == nil then
|
||||
value = default_value;
|
||||
end
|
||||
return value;
|
||||
end
|
||||
|
||||
function api:get_option_scalar(name, default_value)
|
||||
local value = self:get_option(name, default_value);
|
||||
if type(value) == "table" then
|
||||
if #value > 1 then
|
||||
self:log("error", "Config option '%s' does not take a list, using just the first item", name);
|
||||
end
|
||||
value = value[1];
|
||||
end
|
||||
return value;
|
||||
end
|
||||
|
||||
function api:get_option_string(name, default_value)
|
||||
local value = self:get_option_scalar(name, default_value);
|
||||
if value == nil then
|
||||
return nil;
|
||||
end
|
||||
return tostring(value);
|
||||
end
|
||||
|
||||
function api:get_option_number(name, ...)
|
||||
local value = self:get_option_scalar(name, ...);
|
||||
local ret = tonumber(value);
|
||||
if value ~= nil and ret == nil then
|
||||
self:log("error", "Config option '%s' not understood, expecting a number", name);
|
||||
end
|
||||
return ret;
|
||||
end
|
||||
|
||||
function api:get_option_boolean(name, ...)
|
||||
local value = self:get_option_scalar(name, ...);
|
||||
if value == nil then
|
||||
return nil;
|
||||
end
|
||||
local ret = value == true or value == "true" or value == 1 or nil;
|
||||
if ret == nil then
|
||||
ret = (value == false or value == "false" or value == 0);
|
||||
if ret then
|
||||
ret = false;
|
||||
else
|
||||
ret = nil;
|
||||
end
|
||||
end
|
||||
if ret == nil then
|
||||
self:log("error", "Config option '%s' not understood, expecting true/false", name);
|
||||
end
|
||||
return ret;
|
||||
end
|
||||
|
||||
function api:get_option_array(name, ...)
|
||||
local value = self:get_option(name, ...);
|
||||
|
||||
if value == nil then
|
||||
return nil;
|
||||
end
|
||||
|
||||
if type(value) ~= "table" then
|
||||
return array{ value }; -- Assume any non-list is a single-item list
|
||||
end
|
||||
|
||||
return array():append(value); -- Clone
|
||||
end
|
||||
|
||||
function api:get_option_set(name, ...)
|
||||
local value = self:get_option_array(name, ...);
|
||||
|
||||
if value == nil then
|
||||
return nil;
|
||||
end
|
||||
|
||||
return set.new(value);
|
||||
end
|
||||
|
||||
function api:get_option_inherited_set(name, ...)
|
||||
local value = self:get_option_set(name, ...);
|
||||
local global_value = self:context("*"):get_option_set(name, ...);
|
||||
if not value then
|
||||
return global_value;
|
||||
elseif not global_value then
|
||||
return value;
|
||||
end
|
||||
value:include(global_value);
|
||||
return value;
|
||||
end
|
||||
|
||||
function api:get_option_path(name, default, parent)
|
||||
if parent == nil then
|
||||
parent = self:get_directory();
|
||||
elseif prosody.paths[parent] then
|
||||
parent = prosody.paths[parent];
|
||||
end
|
||||
local value = self:get_option_string(name, default);
|
||||
if value == nil then
|
||||
return nil;
|
||||
end
|
||||
return resolve_relative_path(parent, value);
|
||||
end
|
||||
|
||||
|
||||
function api:context(host)
|
||||
return setmetatable({host=host or "*"}, {__index=self,__newindex=self});
|
||||
end
|
||||
|
||||
function api:add_item(key, value)
|
||||
self.items = self.items or {};
|
||||
self.items[key] = self.items[key] or {};
|
||||
t_insert(self.items[key], value);
|
||||
self:fire_event("item-added/"..key, {source = self, item = value});
|
||||
end
|
||||
function api:remove_item(key, value)
|
||||
local t = self.items and self.items[key] or NULL;
|
||||
for i = #t,1,-1 do
|
||||
if t[i] == value then
|
||||
t_remove(self.items[key], i);
|
||||
self:fire_event("item-removed/"..key, {source = self, item = value});
|
||||
return value;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function api:get_host_items(key)
|
||||
local modulemanager = require"core.modulemanager";
|
||||
local result = modulemanager.get_items(key, self.host) or {};
|
||||
return result;
|
||||
end
|
||||
|
||||
function api:handle_items(item_type, added_cb, removed_cb, existing)
|
||||
self:hook("item-added/"..item_type, added_cb);
|
||||
self:hook("item-removed/"..item_type, removed_cb);
|
||||
if existing ~= false then
|
||||
for _, item in ipairs(self:get_host_items(item_type)) do
|
||||
added_cb({ item = item });
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function api:provides(name, item)
|
||||
-- if not item then item = setmetatable({}, { __index = function(t,k) return rawget(self.environment, k); end }); end
|
||||
if not item then
|
||||
item = {}
|
||||
for k,v in pairs(self.environment) do
|
||||
if k ~= "module" then item[k] = v; end
|
||||
end
|
||||
end
|
||||
if not item.name then
|
||||
local item_name = self.name;
|
||||
-- Strip a provider prefix to find the item name
|
||||
-- (e.g. "auth_foo" -> "foo" for an auth provider)
|
||||
if item_name:find(name.."_", 1, true) == 1 then
|
||||
item_name = item_name:sub(#name+2);
|
||||
end
|
||||
item.name = item_name;
|
||||
end
|
||||
item._provided_by = self.name;
|
||||
self:add_item(name.."-provider", item);
|
||||
end
|
||||
|
||||
function api:send(stanza, origin)
|
||||
return core_post_stanza(origin or hosts[self.host], stanza);
|
||||
end
|
||||
|
||||
function api:broadcast(jids, stanza, iter)
|
||||
for jid in (iter or it.values)(jids) do
|
||||
local new_stanza = st.clone(stanza);
|
||||
new_stanza.attr.to = jid;
|
||||
self:send(new_stanza);
|
||||
end
|
||||
end
|
||||
|
||||
local timer_methods = { }
|
||||
local timer_mt = {
|
||||
__index = timer_methods;
|
||||
}
|
||||
function timer_methods:stop( )
|
||||
timer.stop(self.id);
|
||||
end
|
||||
timer_methods.disarm = timer_methods.stop
|
||||
function timer_methods:reschedule(delay)
|
||||
timer.reschedule(self.id, delay)
|
||||
end
|
||||
|
||||
local function timer_callback(now, id, t) --luacheck: ignore 212/id
|
||||
if t.module_env.loaded == false then return; end
|
||||
return t.callback(now, unpack(t, 1, t.n));
|
||||
end
|
||||
|
||||
function api:add_timer(delay, callback, ...)
|
||||
local t = pack(...)
|
||||
t.module_env = self;
|
||||
t.callback = callback;
|
||||
t.id = timer.add_task(delay, timer_callback, t);
|
||||
return setmetatable(t, timer_mt);
|
||||
end
|
||||
|
||||
local path_sep = package.config:sub(1,1);
|
||||
function api:get_directory()
|
||||
return self.path and (self.path:gsub("%"..path_sep.."[^"..path_sep.."]*$", "")) or nil;
|
||||
end
|
||||
|
||||
function api:load_resource(path, mode)
|
||||
path = resolve_relative_path(self:get_directory(), path);
|
||||
return io.open(path, mode);
|
||||
end
|
||||
|
||||
function api:open_store(name, store_type)
|
||||
return require"core.storagemanager".open(self.host, name or self.name, store_type);
|
||||
end
|
||||
|
||||
function api:open_host_store(host, name, store_type)
|
||||
return require"core.storagemanager".open(host, name or self.name, store_type);
|
||||
end
|
||||
|
||||
|
||||
function api:measure(name, stat_type)
|
||||
local measure = require "core.statsmanager".measure;
|
||||
return measure(stat_type, "/"..self.host.."/mod_"..self.name.."/"..name);
|
||||
end
|
||||
|
||||
function api:measure_object_event(events_object, event_name, stat_name)
|
||||
local m = self:measure(stat_name or event_name, "times");
|
||||
local function handler(handlers, _event_name, _event_data)
|
||||
local finished = m();
|
||||
local ret = handlers(_event_name, _event_data);
|
||||
finished();
|
||||
return ret;
|
||||
end
|
||||
return self:hook_object_event(events_object, event_name, handler);
|
||||
end
|
||||
|
||||
function api:measure_event(event_name, stat_name)
|
||||
return self:measure_object_event((hosts[self.host] or prosody).events.wrappers, event_name, stat_name);
|
||||
end
|
||||
|
||||
function api:measure_global_event(event_name, stat_name)
|
||||
return self:measure_object_event(prosody.events.wrappers, event_name, stat_name);
|
||||
end
|
||||
|
||||
return api;
|
||||
@@ -0,0 +1,5 @@
|
||||
mod_mam.lua /usr/lib/prosody/modules/mod_mam/mod_mam.lua
|
||||
mod_muc_unique.lua usr/lib/prosody/modules/mod_muc_unique.lua
|
||||
moduleapi.lua usr/lib/prosody/core/moduleapi.lua
|
||||
mod_muc.lua usr/lib/prosody/modules/muc/mod_muc.lua
|
||||
register.lib.lua /usr/lib/prosody/modules/muc/register.lib.lua
|
||||
@@ -0,0 +1,244 @@
|
||||
local config = require "core.configmanager";
|
||||
local certmanager = require "core.certmanager";
|
||||
local server = require "net.server";
|
||||
local socket = require "socket";
|
||||
|
||||
local log = require "util.logger".init("portmanager");
|
||||
local multitable = require "util.multitable";
|
||||
local set = require "util.set";
|
||||
|
||||
local table = table;
|
||||
local setmetatable, rawset, rawget = setmetatable, rawset, rawget;
|
||||
local type, tonumber, tostring, ipairs = type, tonumber, tostring, ipairs;
|
||||
|
||||
local prosody = prosody;
|
||||
local fire_event = prosody.events.fire_event;
|
||||
|
||||
local _ENV = nil;
|
||||
-- luacheck: std none
|
||||
|
||||
--- Config
|
||||
|
||||
local default_interfaces = { };
|
||||
local default_local_interfaces = { };
|
||||
if config.get("*", "use_ipv4") ~= false then
|
||||
table.insert(default_interfaces, "*");
|
||||
table.insert(default_local_interfaces, "*");
|
||||
end
|
||||
if socket.tcp6 and config.get("*", "use_ipv6") ~= false then
|
||||
table.insert(default_interfaces, "::");
|
||||
table.insert(default_local_interfaces, "::");
|
||||
end
|
||||
|
||||
local default_mode = config.get("*", "network_default_read_size") or 4096;
|
||||
|
||||
--- Private state
|
||||
|
||||
-- service_name -> { service_info, ... }
|
||||
local services = setmetatable({}, { __index = function (t, k) rawset(t, k, {}); return rawget(t, k); end });
|
||||
|
||||
-- service_name, interface (string), port (number)
|
||||
local active_services = multitable.new();
|
||||
|
||||
--- Private helpers
|
||||
|
||||
local function error_to_friendly_message(service_name, port, err) --luacheck: ignore 212/service_name
|
||||
local friendly_message = err;
|
||||
if err:match(" in use") then
|
||||
-- FIXME: Use service_name here
|
||||
if port == 5222 or port == 5223 or port == 5269 then
|
||||
friendly_message = "check that Prosody or another XMPP server is "
|
||||
.."not already running and using this port";
|
||||
elseif port == 80 or port == 81 then
|
||||
friendly_message = "check that a HTTP server is not already using "
|
||||
.."this port";
|
||||
elseif port == 5280 then
|
||||
friendly_message = "check that Prosody or a BOSH connection manager "
|
||||
.."is not already running";
|
||||
else
|
||||
friendly_message = "this port is in use by another application";
|
||||
end
|
||||
elseif err:match("permission") then
|
||||
friendly_message = "Prosody does not have sufficient privileges to use this port";
|
||||
end
|
||||
return friendly_message;
|
||||
end
|
||||
|
||||
--- Public API
|
||||
|
||||
local function activate(service_name)
|
||||
local service_info = services[service_name][1];
|
||||
if not service_info then
|
||||
return nil, "Unknown service: "..service_name;
|
||||
end
|
||||
|
||||
local listener = service_info.listener;
|
||||
|
||||
local config_prefix = (service_info.config_prefix or service_name).."_";
|
||||
if config_prefix == "_" then
|
||||
config_prefix = "";
|
||||
end
|
||||
|
||||
local bind_interfaces = config.get("*", config_prefix.."interfaces")
|
||||
or config.get("*", config_prefix.."interface") -- COMPAT w/pre-0.9
|
||||
or (service_info.private and (config.get("*", "local_interfaces") or default_local_interfaces))
|
||||
or config.get("*", "interfaces")
|
||||
or config.get("*", "interface") -- COMPAT w/pre-0.9
|
||||
or listener.default_interface -- COMPAT w/pre0.9
|
||||
or default_interfaces
|
||||
bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces);
|
||||
|
||||
local bind_ports = config.get("*", config_prefix.."ports")
|
||||
or service_info.default_ports
|
||||
or {service_info.default_port
|
||||
or listener.default_port -- COMPAT w/pre-0.9
|
||||
}
|
||||
bind_ports = set.new(type(bind_ports) ~= "table" and { bind_ports } or bind_ports );
|
||||
|
||||
local mode, ssl = listener.default_mode or default_mode;
|
||||
local hooked_ports = {};
|
||||
|
||||
for interface in bind_interfaces do
|
||||
for port in bind_ports do
|
||||
local port_number = tonumber(port);
|
||||
if not port_number then
|
||||
log("error", "Invalid port number specified for service '%s': %s", service_info.name, tostring(port));
|
||||
elseif #active_services:search(nil, interface, port_number) > 0 then
|
||||
log("error", "Multiple services configured to listen on the same port ([%s]:%d): %s, %s", interface, port,
|
||||
active_services:search(nil, interface, port)[1][1].service.name or "<unnamed>", service_name or "<unnamed>");
|
||||
else
|
||||
local err;
|
||||
-- Create SSL context for this service/port
|
||||
if service_info.encryption == "ssl" then
|
||||
local global_ssl_config = config.get("*", "ssl") or {};
|
||||
local prefix_ssl_config = config.get("*", config_prefix.."ssl") or global_ssl_config;
|
||||
log("debug", "Creating context for direct TLS service %s on port %d", service_info.name, port);
|
||||
ssl, err = certmanager.create_context(service_info.name.." port "..port, "server",
|
||||
prefix_ssl_config[interface],
|
||||
prefix_ssl_config[port],
|
||||
prefix_ssl_config,
|
||||
service_info.ssl_config or {},
|
||||
global_ssl_config[interface],
|
||||
global_ssl_config[port]);
|
||||
if not ssl then
|
||||
log("error", "Error binding encrypted port for %s: %s", service_info.name,
|
||||
error_to_friendly_message(service_name, port_number, err) or "unknown error");
|
||||
end
|
||||
end
|
||||
if not err then
|
||||
-- Start listening on interface+port
|
||||
local handler, err = server.addserver(interface, port_number, listener, mode, ssl);
|
||||
if not handler then
|
||||
log("error", "Failed to open server port %d on %s, %s", port_number, interface,
|
||||
error_to_friendly_message(service_name, port_number, err));
|
||||
else
|
||||
table.insert(hooked_ports, "["..interface.."]:"..port_number);
|
||||
log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port_number);
|
||||
active_services:add(service_name, interface, port_number, {
|
||||
server = handler;
|
||||
service = service_info;
|
||||
});
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
log("info", "Activated service '%s' on %s", service_name,
|
||||
#hooked_ports == 0 and "no ports" or table.concat(hooked_ports, ", "));
|
||||
return true;
|
||||
end
|
||||
|
||||
local close; -- forward declaration
|
||||
|
||||
local function deactivate(service_name, service_info)
|
||||
for name, interface, port, n, active_service --luacheck: ignore 213/name 213/n
|
||||
in active_services:iter(service_name or service_info and service_info.name, nil, nil, nil) do
|
||||
if service_info == nil or active_service.service == service_info then
|
||||
close(interface, port);
|
||||
end
|
||||
end
|
||||
log("info", "Deactivated service '%s'", service_name or service_info.name);
|
||||
end
|
||||
|
||||
local function register_service(service_name, service_info)
|
||||
table.insert(services[service_name], service_info);
|
||||
|
||||
if not active_services:get(service_name) then
|
||||
log("debug", "No active service for %s, activating...", service_name);
|
||||
local ok, err = activate(service_name);
|
||||
if not ok then
|
||||
log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error");
|
||||
end
|
||||
end
|
||||
|
||||
fire_event("service-added", { name = service_name, service = service_info });
|
||||
return true;
|
||||
end
|
||||
|
||||
local function unregister_service(service_name, service_info)
|
||||
log("debug", "Unregistering service: %s", service_name);
|
||||
local service_info_list = services[service_name];
|
||||
for i, service in ipairs(service_info_list) do
|
||||
if service == service_info then
|
||||
table.remove(service_info_list, i);
|
||||
end
|
||||
end
|
||||
deactivate(nil, service_info);
|
||||
if #service_info_list > 0 then -- Other services registered with this name
|
||||
activate(service_name); -- Re-activate with the next available one
|
||||
end
|
||||
fire_event("service-removed", { name = service_name, service = service_info });
|
||||
end
|
||||
|
||||
local get_service_at -- forward declaration
|
||||
|
||||
function close(interface, port)
|
||||
local service, service_server = get_service_at(interface, port);
|
||||
if not service then
|
||||
return false, "port-not-open";
|
||||
end
|
||||
service_server:close();
|
||||
active_services:remove(service.name, interface, port);
|
||||
log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port);
|
||||
return true;
|
||||
end
|
||||
|
||||
function get_service_at(interface, port)
|
||||
local data = active_services:search(nil, interface, port)[1][1];
|
||||
return data.service, data.server;
|
||||
end
|
||||
|
||||
local function get_service(service_name)
|
||||
return (services[service_name] or {})[1];
|
||||
end
|
||||
|
||||
local function get_active_services()
|
||||
return active_services;
|
||||
end
|
||||
|
||||
local function get_registered_services()
|
||||
return services;
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
|
||||
prosody.events.add_handler("item-added/net-provider", function (event)
|
||||
local item = event.item;
|
||||
register_service(item.name, item);
|
||||
end);
|
||||
prosody.events.add_handler("item-removed/net-provider", function (event)
|
||||
local item = event.item;
|
||||
unregister_service(item.name, item);
|
||||
end);
|
||||
|
||||
return {
|
||||
activate = activate;
|
||||
deactivate = deactivate;
|
||||
register_service = register_service;
|
||||
unregister_service = unregister_service;
|
||||
close = close;
|
||||
get_service_at = get_service_at;
|
||||
get_service = get_service;
|
||||
get_active_services = get_active_services;
|
||||
get_registered_services = get_registered_services;
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local resourceprep = require "util.encodings".stringprep.resourceprep;
|
||||
local st = require "util.stanza";
|
||||
local dataforms = require "util.dataforms";
|
||||
|
||||
local allow_unaffiliated = module:get_option_boolean("allow_unaffiliated_register", false);
|
||||
|
||||
local enforce_nick = module:get_option_boolean("enforce_registered_nickname", false);
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
-- reserved_nicks[nick] = jid
|
||||
local function get_reserved_nicks(room)
|
||||
if room._reserved_nicks then
|
||||
return room._reserved_nicks;
|
||||
end
|
||||
module:log("debug", "Refreshing reserved nicks...");
|
||||
local reserved_nicks = {};
|
||||
for jid in room:each_affiliation() do
|
||||
local data = room._affiliation_data[jid];
|
||||
local nick = data and data.reserved_nickname;
|
||||
module:log("debug", "Refreshed for %s: %s", jid, nick);
|
||||
if nick then
|
||||
reserved_nicks[nick] = jid;
|
||||
end
|
||||
end
|
||||
room._reserved_nicks = reserved_nicks;
|
||||
return reserved_nicks;
|
||||
end
|
||||
|
||||
-- Returns the registered nick, if any, for a JID
|
||||
-- Note: this is just the *nick* part, i.e. the resource of the in-room JID
|
||||
local function get_registered_nick(room, jid)
|
||||
local registered_data = room._affiliation_data[jid];
|
||||
if not registered_data then
|
||||
return;
|
||||
end
|
||||
return registered_data.reserved_nickname;
|
||||
end
|
||||
|
||||
-- Returns the JID, if any, that registered a nick (not in-room JID)
|
||||
local function get_registered_jid(room, nick)
|
||||
local reserved_nicks = get_reserved_nicks(room);
|
||||
return reserved_nicks[nick];
|
||||
end
|
||||
|
||||
module:hook("muc-set-affiliation", function (event)
|
||||
-- Clear reserved nick cache
|
||||
event.room._reserved_nicks = nil;
|
||||
end);
|
||||
|
||||
module:hook("muc-disco#info", function (event)
|
||||
event.reply:tag("feature", { var = "jabber:iq:register" }):up();
|
||||
end);
|
||||
|
||||
local registration_form = dataforms.new {
|
||||
{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#register" },
|
||||
{ name = "muc#register_roomnick", type = "text-single", label = "Nickname"},
|
||||
};
|
||||
|
||||
local function enforce_nick_policy(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room = assert(event.room); -- FIXME
|
||||
if not room then return; end
|
||||
|
||||
-- Check if the chosen nickname is reserved
|
||||
local requested_nick = jid_resource(stanza.attr.to);
|
||||
local reserved_by = get_registered_jid(room, requested_nick);
|
||||
if reserved_by and reserved_by ~= jid_bare(stanza.attr.from) then
|
||||
module:log("debug", "%s attempted to use nick %s reserved by %s", stanza.attr.from, requested_nick, reserved_by);
|
||||
local reply = st.error_reply(stanza, "cancel", "conflict"):up();
|
||||
origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Check if the occupant has a reservation they must use
|
||||
if enforce_nick then
|
||||
local nick = get_registered_nick(room, jid_bare(stanza.attr.from));
|
||||
if nick then
|
||||
if event.occupant then
|
||||
event.occupant.nick = jid_bare(event.occupant.nick) .. "/" .. nick;
|
||||
elseif event.dest_occupant.nick ~= jid_bare(event.dest_occupant.nick) .. "/" .. nick then
|
||||
module:log("debug", "Attempt by %s to join as %s, but their reserved nick is %s", stanza.attr.from, requested_nick, nick);
|
||||
local reply = st.error_reply(stanza, "cancel", "not-acceptable"):up();
|
||||
origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("muc-occupant-pre-join", enforce_nick_policy);
|
||||
module:hook("muc-occupant-pre-change", enforce_nick_policy);
|
||||
|
||||
-- Discovering Reserved Room Nickname
|
||||
-- http://xmpp.org/extensions/xep-0045.html#reservednick
|
||||
module:hook("muc-disco#info/x-roomuser-item", function (event)
|
||||
local nick = get_registered_nick(event.room, jid_bare(event.stanza.attr.from));
|
||||
if nick then
|
||||
event.reply:tag("identity", { category = "conference", type = "text", name = nick })
|
||||
end
|
||||
end);
|
||||
|
||||
local function handle_register_iq(room, origin, stanza)
|
||||
local user_jid = jid_bare(stanza.attr.from)
|
||||
local affiliation = room:get_affiliation(user_jid);
|
||||
if affiliation == "outcast" then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
elseif not (affiliation or allow_unaffiliated) then
|
||||
origin.send(st.error_reply(stanza, "auth", "registration-required"));
|
||||
return true;
|
||||
end
|
||||
local reply = st.reply(stanza);
|
||||
local registered_nick = get_registered_nick(room, user_jid);
|
||||
if stanza.attr.type == "get" then
|
||||
reply:query("jabber:iq:register");
|
||||
if registered_nick then
|
||||
reply:tag("registered"):up();
|
||||
reply:tag("username"):text(registered_nick);
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
reply:add_child(registration_form:form());
|
||||
else -- type == set -- handle registration form
|
||||
local query = stanza.tags[1];
|
||||
if query:get_child("remove") then
|
||||
-- Remove "member" affiliation, but preserve if any other
|
||||
local new_affiliation = affiliation ~= "member" and affiliation;
|
||||
local ok, err_type, err_condition = room:set_affiliation(true, user_jid, new_affiliation, nil, false);
|
||||
if not ok then
|
||||
origin.send(st.error_reply(stanza, err_type, err_condition));
|
||||
return true;
|
||||
end
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
local form_tag = query:get_child("x", "jabber:x:data");
|
||||
local reg_data = form_tag and registration_form:data(form_tag);
|
||||
if not reg_data then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Error in form"));
|
||||
return true;
|
||||
end
|
||||
-- Is the nickname valid?
|
||||
local desired_nick = resourceprep(reg_data["muc#register_roomnick"]);
|
||||
if not desired_nick then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid Nickname"));
|
||||
return true;
|
||||
end
|
||||
-- Is the nickname currently in use by another user?
|
||||
local current_occupant = room:get_occupant_by_nick(room.jid.."/"..desired_nick);
|
||||
if current_occupant and current_occupant.bare_jid ~= user_jid then
|
||||
origin.send(st.error_reply(stanza, "cancel", "conflict"));
|
||||
return true;
|
||||
end
|
||||
-- Is the nickname currently reserved by another user?
|
||||
local reserved_by = get_registered_jid(room, desired_nick);
|
||||
if reserved_by and reserved_by ~= user_jid then
|
||||
origin.send(st.error_reply(stanza, "cancel", "conflict"));
|
||||
return true;
|
||||
end
|
||||
|
||||
if enforce_nick then
|
||||
-- Kick any sessions that are not using this nick before we register it
|
||||
local required_room_nick = room.jid.."/"..desired_nick;
|
||||
for room_nick, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == user_jid and room_nick ~= required_room_nick then
|
||||
room:set_role(true, room_nick, nil); -- Kick (TODO: would be nice to use 333 code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Checks passed, save the registration
|
||||
if registered_nick ~= desired_nick then
|
||||
local registration_data = { reserved_nickname = desired_nick };
|
||||
local ok, err_type, err_condition = room:set_affiliation(true, user_jid, affiliation or "member", nil, registration_data);
|
||||
if not ok then
|
||||
origin.send(st.error_reply(stanza, err_type, err_condition));
|
||||
return true;
|
||||
end
|
||||
module:log("debug", "Saved nick registration for %s: %s", user_jid, desired_nick);
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
|
||||
local function handle_unregister_iq(room, origin, stanza)
|
||||
local user_jid = jid_bare(stanza.attr.from)
|
||||
local affiliation = room:get_affiliation(user_jid);
|
||||
local affs = room._affiliations;
|
||||
local occupants = room._occupants;
|
||||
module:fire_event("vnc-muc-kick", { room = stanza.attr.to, who = user_jid });
|
||||
|
||||
-- for nick, occupant in room:each_occupant() do
|
||||
-- module:log("info", "occupant bare: %s", occupant.bare_jid);
|
||||
-- end
|
||||
if (affiliation ~= "") then
|
||||
module:log("info","removing affiliaton from %s for %s", stanza.attr.to, user_jid);
|
||||
affs[user_jid] = nil;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
return {
|
||||
get_registered_nick = get_registered_nick;
|
||||
get_registered_jid = get_registered_jid;
|
||||
handle_register_iq = handle_register_iq;
|
||||
handle_unregister_iq = handle_unregister_iq;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
docker run -e avatarUploadPass="123" \
|
||||
-e avatarUploadUser="avatar" \
|
||||
-e fcmApiKey="asdfghjkl" \
|
||||
-e fcmApiUrl="http://fcmp:80/notify" \
|
||||
-e fcmDelUrl="http://fcmp:80/delete" \
|
||||
-e fileShareBaseUrl="https://phpfile.uxf.example.com/share.php/" \
|
||||
-e fileShareSecret="g3h31m!" \
|
||||
-e hybridaAuthUrl="http://192.168.23.3:80/" \
|
||||
-e prosodyDBhost="172.17.0.1" \
|
||||
-e prosodyDBname="prosody" \
|
||||
-e prosodyDBpass="q3tx65test" \
|
||||
-e prosodyDBuser="prosody" \
|
||||
-e prosodyDomain="example.com" \
|
||||
-p 5280:5280 -p 5281:5281 -p 5222:5222 -p 5269:5269 -p 5582:5582 "$1"
|
||||
@@ -0,0 +1,44 @@
|
||||
-- Copyright (C) 2015 Travis Burtrum
|
||||
-- This file is MIT/X11 licensed.
|
||||
|
||||
-- set like so in prosody config, works on full or bare jids, or hosts:
|
||||
--aliases = {
|
||||
-- ["old@example.net"] = "new@example.net";
|
||||
-- ["you@example.com"] = "you@example.net";
|
||||
-- ["conference.example.com"] = "conference.example.net";
|
||||
--}
|
||||
|
||||
local aliases = module:get_option("aliases", {});
|
||||
local bcaddr = module:get_option("bc_prefix", "");
|
||||
local alias_response = module:get_option("alias_response", "User $alias can be contacted at $target");
|
||||
|
||||
local st = require "util.stanza";
|
||||
local jid_split = require "util.jid".split;
|
||||
|
||||
local function starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
function handle_alias(event)
|
||||
|
||||
if ((event.stanza.attr.type ~= "error") and (event.stanza.attr.type == "normal")) then
|
||||
local alias = event.stanza.attr.to;
|
||||
local forward_stanza = event.stanza;
|
||||
-- module:log("info", "alias handling - checking %s", alias);
|
||||
local tonode, tohost = jid_split(alias);
|
||||
if tonode ~= nil then
|
||||
if starts_with(tonode, "broadcast-") then
|
||||
module:log("info", "determined %s as broadcast", alias);
|
||||
local target = "broadcast@"..tohost;
|
||||
forward_stanza.attr.to = target;
|
||||
forward_stanza.attr.origtarget = alias;
|
||||
module:send(forward_stanza);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module:hook("message/bare", handle_alias, 300);
|
||||
module:hook("message/full", handle_alias, 300);
|
||||
module:hook("message/host", handle_alias, 300);
|
||||
@@ -0,0 +1,51 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- luacheck: ignore 212
|
||||
|
||||
local datamanager = require "util.datamanager";
|
||||
local new_sasl = require "util.sasl".new;
|
||||
|
||||
local host = module.host;
|
||||
local provider = { name = "any" };
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return true;
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
local account = datamanager.load(username, host, "accounts");
|
||||
if account then
|
||||
account.password = password;
|
||||
return datamanager.store(username, host, "accounts", account);
|
||||
end
|
||||
return nil, "Account not available.";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return true;
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return datamanager.store(username, host, "accounts", {password = password});
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
return datamanager.store(username, host, "accounts", nil);
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler()
|
||||
local getpass_authentication_profile = {
|
||||
plain_test = function(sasl, username, password, realm)
|
||||
return true, true;
|
||||
end
|
||||
};
|
||||
return new_sasl(module.host, getpass_authentication_profile);
|
||||
end
|
||||
|
||||
module:add_item("auth-provider", provider);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
labels:
|
||||
- Stage-Alpha
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
This is an experimental authentication module that does an asynchronous
|
||||
HTTP call to verify username and password.
|
||||
|
||||
Details
|
||||
=======
|
||||
|
||||
When a user attempts to authenticate to Prosody, this module takes the
|
||||
username and password and does a HTTP GET request with [Basic
|
||||
authentication][rfc7617] to the configured `http_auth_url`.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
``` lua
|
||||
VirtualHost "example.com"
|
||||
authentication = "http_async"
|
||||
http_auth_url = "http://example.com/auth"
|
||||
```
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
Requires Prosody trunk
|
||||
@@ -0,0 +1,128 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2013 Matthew Wild
|
||||
-- Copyright (C) 2008-2013 Waqas Hussain
|
||||
-- Copyright (C) 2014 Kim Alvefur
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local base64 = require "util.encodings".base64.encode;
|
||||
local have_async, async = pcall(require, "util.async");
|
||||
-- local have_async = false;
|
||||
|
||||
local log = module._log;
|
||||
local host = module.host;
|
||||
|
||||
local api_base = module:get_option_string("http_auth_url", ""):gsub("$host", host);
|
||||
if api_base == "" then error("http_auth_url required") end
|
||||
|
||||
local provider = {};
|
||||
|
||||
-- globals required by socket.http
|
||||
if rawget(_G, "PROXY") == nil then
|
||||
rawset(_G, "PROXY", false)
|
||||
end
|
||||
if rawget(_G, "base_parsed") == nil then
|
||||
rawset(_G, "base_parsed", false)
|
||||
end
|
||||
if not have_async then -- FINE! Set your globals then
|
||||
prosody.unlock_globals()
|
||||
require "ltn12"
|
||||
require "socket"
|
||||
require "socket.http"
|
||||
require "ssl.https"
|
||||
prosody.lock_globals()
|
||||
end
|
||||
|
||||
local function async_http_auth(url, username, password)
|
||||
module:log("debug", "async_http_auth()");
|
||||
local http = require "net.http";
|
||||
local wait, done = async.waiter();
|
||||
local content, code, request, response;
|
||||
local ex = {
|
||||
headers = { Authorization = "Basic "..base64(username.."@"..host..":"..password); };
|
||||
}
|
||||
local function cb(content_, code_, request_, response_)
|
||||
content, code, request, response = content_, code_, request_, response_;
|
||||
done();
|
||||
end
|
||||
module:log("info", "HTTP auth provider url: %s", url);
|
||||
http.request(url, ex, cb);
|
||||
wait();
|
||||
if code >= 200 and code <= 299 then
|
||||
-- module:log("debug", "HTTP auth provider confirmed valid password");
|
||||
return true;
|
||||
else
|
||||
module:log("info", "HTTP auth provider returned status code %d", code);
|
||||
end
|
||||
return nil, "Auth failed. Invalid username or password.";
|
||||
end
|
||||
|
||||
local function sync_http_auth(url,username, password)
|
||||
module:log("debug", "sync_http_auth()");
|
||||
require "ltn12";
|
||||
local http = require "socket.http";
|
||||
local https = require "ssl.https";
|
||||
local request;
|
||||
module:log("info", "HTTP auth provider url: %s", url);
|
||||
if string.sub(url, 1, string.len('https')) == 'https' then
|
||||
request = https.request;
|
||||
else
|
||||
request = http.request;
|
||||
end
|
||||
local _, code, headers, status = request{
|
||||
url = url,
|
||||
headers = { Authorization = "Basic "..base64(username.."@"..host..":"..password); }
|
||||
};
|
||||
if type(code) == "number" and code >= 200 and code <= 299 then
|
||||
-- module:log("debug", "HTTP sync auth provider confirmed valid password");
|
||||
return true;
|
||||
else
|
||||
module:log("info", "HTTP sync auth provider returned status code: "..code);
|
||||
end
|
||||
return nil, "Auth failed. Invalid username or password.";
|
||||
end
|
||||
|
||||
function provider.test_password(username, password)
|
||||
local url = api_base:gsub("$user", username):gsub("$password", password);
|
||||
log("info", "Testing password for user %s at host %s with URL %s", username, host, url);
|
||||
if (have_async) then
|
||||
return async_http_auth(url, username, password);
|
||||
else
|
||||
return sync_http_auth(url, username, password);
|
||||
end
|
||||
end
|
||||
|
||||
function provider.users()
|
||||
return function()
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
return nil, "Changing passwords not supported";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return true;
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return nil, "User creation not supported";
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
return nil , "User deletion not supported";
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler()
|
||||
return new_sasl(host, {
|
||||
plain_test = function(sasl, username, password, realm)
|
||||
return provider.test_password(username, password), true;
|
||||
end
|
||||
});
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Beta'
|
||||
summary: Automatically accept incoming subscription requests on behalf of users
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
In some environments where all users on the system have mutual trust in
|
||||
each other, it's sometimes fine to skip the usual authorization process
|
||||
to add someone to your contact list and see their status.
|
||||
|
||||
This module sets Prosody to automatically accept incoming subscription
|
||||
authorization requests, and add the contact to the user's contact list,
|
||||
without intervention from the user.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Simply add the module to your modules\_enabled list like any other
|
||||
module:
|
||||
|
||||
modules_enabled = {
|
||||
...
|
||||
"auto_accept_subscriptions";
|
||||
...
|
||||
}
|
||||
|
||||
This module has no further configuration.
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
------- -------
|
||||
trunk Works
|
||||
0.9 Works
|
||||
0.8 Works
|
||||
------- -------
|
||||
@@ -0,0 +1,27 @@
|
||||
#summary Automatically accept incoming subscription requests on behalf of users
|
||||
#labels Stage-Beta
|
||||
|
||||
= Introduction =
|
||||
|
||||
In some environments where all users on the system have mutual trust in each other, it's sometimes fine to skip the usual authorization process to
|
||||
add someone to your contact list and see their status.
|
||||
|
||||
This module sets Prosody to automatically accept incoming subscription authorization requests, and add the contact to the user's contact list, without intervention from the user.
|
||||
|
||||
= Configuration =
|
||||
Simply add the module to your modules_enabled list like any other module:
|
||||
|
||||
{{{
|
||||
modules_enabled = {
|
||||
...
|
||||
"auto_accept_subscriptions";
|
||||
...
|
||||
}
|
||||
}}}
|
||||
|
||||
This module has no further configuration.
|
||||
|
||||
= Compatibility =
|
||||
||trunk||Works||
|
||||
||0.9||Works||
|
||||
||0.8||Works||
|
||||
@@ -0,0 +1,96 @@
|
||||
local usermanager = require "core.usermanager";
|
||||
local rostermanager = require "core.rostermanager";
|
||||
local storagemanager = require "core.storagemanager";
|
||||
local jid = require "util.jid";
|
||||
local st = require "util.stanza";
|
||||
local core_post_stanza = prosody.core_post_stanza;
|
||||
|
||||
-- Unsubscribes user from contact (not contact from user, if subscribed).
|
||||
function unsubscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribe(user_username, user_host, contact_jid);
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribed(contact_username, contact_host, user_jid);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Cancel any subscription in either direction.
|
||||
function unsubscribe_both(jid1, jid2)
|
||||
unsubscribe(jid1, jid2);
|
||||
unsubscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
|
||||
local function handle_inbound_subscription_request(origin, stanza)
|
||||
local to_bare, from_bare = jid.bare(stanza.attr.to), jid.bare(stanza.attr.from);
|
||||
local node, host = jid.split(to_bare);
|
||||
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
|
||||
module:log("info", "Auto-accepting inbound subscription request from %s to %s", tostring(from_bare), tostring(to_bare));
|
||||
|
||||
local do_process = host or nil;
|
||||
if host then
|
||||
if not rostermanager.is_contact_subscribed(node, host, from_bare) then
|
||||
core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
|
||||
-- module:log("info", "receipt acknowledged");
|
||||
if rostermanager.set_contact_pending_in(node, host, from_bare) then
|
||||
-- module:log("info", "set pending in");
|
||||
if rostermanager.subscribed(node, host, from_bare) then
|
||||
-- module:log("info", "set subscribed");
|
||||
rostermanager.roster_push(node, host, to_bare);
|
||||
-- module:log("info", "pushed roster item");
|
||||
local subscribed_stanza = st.reply(stanza);
|
||||
subscribed_stanza.attr.type = "subscribed";
|
||||
core_post_stanza(hosts[host], subscribed_stanza);
|
||||
-- module:log("info", "sent subscribed");
|
||||
hosts[host].modules.presence.send_presence_of_available_resources(node, host, to_bare, origin);
|
||||
-- module:log("info", "sent available presence of all resources");
|
||||
-- Add return subscription from user to contact
|
||||
local subscribe_stanza = st.reply(stanza);
|
||||
subscribe_stanza.attr.type = "subscribe";
|
||||
if rostermanager.set_contact_pending_out(node, host, from_bare) then
|
||||
rostermanager.roster_push(node, host, from_bare);
|
||||
end
|
||||
core_post_stanza(hosts[host], subscribe_stanza);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
module:log("warn", "Failed to auto-accept subscription request from %s to %s", tostring(from_bare), tostring(to_bare));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_inbound_unsubscription_request(origin, stanza)
|
||||
local to_bare, from_bare = jid.bare(stanza.attr.to), jid.bare(stanza.attr.from);
|
||||
local node, host = jid.split(to_bare);
|
||||
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
|
||||
-- module:log("info", "auto-unsubscribing subscription request from %s to %s", tostring(from_bare), tostring(to_bare));
|
||||
unsubscribe_both(from_bare, to_bare);
|
||||
end
|
||||
|
||||
module:hook("presence/bare", function (event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.type == "subscribe" then
|
||||
handle_inbound_subscription_request(event.origin, stanza);
|
||||
return true;
|
||||
elseif stanza.attr.type == "unsubscribed" then
|
||||
handle_inbound_unsubscription_request(event.origin, stanza);
|
||||
end
|
||||
end, 0.1);
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Send carbons v0 style copies of incoming messages to clients which
|
||||
-- are not (yet) capable of Message Carbons (XEP-0280).
|
||||
--
|
||||
-- This extension integrates with the mod_carbons plugin in such a way
|
||||
-- that a client capable of Message Carbons will not get a v0 copy.
|
||||
--
|
||||
-- This extension can be enabled for all users by default by setting
|
||||
-- carbons_copies_default = true.
|
||||
--
|
||||
-- Alternatively or additionally setting carbons_copies_adhoc = true
|
||||
-- will allow the user to enable or disable copies through Adhoc
|
||||
-- commands.
|
||||
--
|
||||
-- Copyright (C) 2012 Michael Holzt
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local dm_load = require "util.datamanager".load;
|
||||
local dm_store = require "util.datamanager".store;
|
||||
local adhoc_new = module:require "adhoc".new;
|
||||
local xmlns_carbons_v0 = "urn:xmpp:carbons:0";
|
||||
local storename = "mod_carbons_copies";
|
||||
|
||||
local function toggle_copies(data, on)
|
||||
local username, hostname, resource = jid_split(data.from);
|
||||
dm_store(username, hostname, storename, { enabled = on });
|
||||
end
|
||||
|
||||
local function adhoc_enable_copies(self, data, state)
|
||||
toggle_copies(data, true);
|
||||
return { info = "Copies are enabled for you now.\nPlease restart/reconnect clients.", status = "completed" };
|
||||
end
|
||||
|
||||
local function adhoc_disable_copies(self, data, state)
|
||||
toggle_copies(data, false);
|
||||
return { info = "Copies are disabled for you now.\nPlease restart/reconnect clients.", status = "completed" };
|
||||
end
|
||||
|
||||
module:hook("resource-bind", function(event)
|
||||
local session = event.session;
|
||||
local username, hostname, resource = jid_split(session.full_jid);
|
||||
|
||||
local store = dm_load(username, hostname, storename) or
|
||||
{ enabled =
|
||||
module:get_option_boolean("carbons_copies_default") };
|
||||
|
||||
if store.enabled then
|
||||
session.want_carbons = xmlns_carbons_v0;
|
||||
module:log("debug", "%s enabling copies", session.full_jid);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Adhoc-Support
|
||||
if module:get_option_boolean("carbons_copies_adhoc") then
|
||||
local enable_desc = adhoc_new("Carbons: Enable Copies",
|
||||
"mod_carbons_copies#enable", adhoc_enable_copies);
|
||||
local disable_desc = adhoc_new("Carbons: Disable Copies",
|
||||
"mod_carbons_copies#disable", adhoc_disable_copies);
|
||||
|
||||
module:add_item("adhoc", enable_desc);
|
||||
module:add_item("adhoc", disable_desc);
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Beta'
|
||||
summary: Manually override the list of service discovery items
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
This Prosody plugin lets you manually override the service discovery
|
||||
items for a host.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Simply add `"discoitems"` to your modules\_enabled list. Then add the
|
||||
`disco_items` option to hosts for which you wish to override the default
|
||||
response.
|
||||
|
||||
Note: mod\_disco in Prosody 0.8+ supports the `disco_items` option; this
|
||||
plugin changes the behavior from appending items to replacing items
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The `disco_items` option can be added to relevant hosts:
|
||||
|
||||
disco_items = {
|
||||
{"proxy.eu.jabber.org", "Jabber.org SOCKS5 service"};
|
||||
{"conference.jabber.org", "The Jabber.org MUC"};
|
||||
}
|
||||
|
||||
The format for individual items is `{JID, display-name}`. The
|
||||
display-name can be omitted: `{JID}`.
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
----- -------------
|
||||
0.8 Works
|
||||
0.7 Works
|
||||
0.6 Works
|
||||
0.5 Should work
|
||||
----- -------------
|
||||
@@ -0,0 +1,32 @@
|
||||
#summary Manually override the list of service discovery items
|
||||
#labels Stage-Beta
|
||||
|
||||
= Introduction =
|
||||
|
||||
This Prosody plugin lets you manually override the service discovery items for a host.
|
||||
|
||||
= Usage =
|
||||
|
||||
Simply add `"discoitems"` to your modules_enabled list. Then add the `disco_items` option to hosts for which you wish to override the default response.
|
||||
|
||||
Note: mod_disco in Prosody 0.8+ supports the `disco_items` option; this plugin changes the behavior from appending items to replacing items
|
||||
|
||||
= Configuration =
|
||||
|
||||
The `disco_items` option can be added to relevant hosts:
|
||||
|
||||
{{{
|
||||
disco_items = {
|
||||
{"proxy.eu.jabber.org", "Jabber.org SOCKS5 service"};
|
||||
{"conference.jabber.org", "The Jabber.org MUC"};
|
||||
}
|
||||
}}}
|
||||
|
||||
The format for individual items is `{JID, display-name}`. The display-name can be omitted: `{JID}`.
|
||||
|
||||
= Compatibility =
|
||||
||0.8||Works||
|
||||
||0.7||Works||
|
||||
||0.6||Works||
|
||||
||0.5||Should work||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
-- mod_discoitems.lua
|
||||
--
|
||||
-- In the config, you can add:
|
||||
--
|
||||
-- disco_items = {
|
||||
-- {"proxy.eu.jabber.org", "Jabber.org SOCKS5 service"};
|
||||
-- {"conference.jabber.org", "The Jabber.org MUC"};
|
||||
-- };
|
||||
--
|
||||
|
||||
local st = require "util.stanza";
|
||||
|
||||
local result_query = st.stanza("query", {xmlns="http://jabber.org/protocol/disco#items"});
|
||||
for _, item in ipairs(module:get_option("disco_items") or {}) do
|
||||
result_query:tag("item", {jid=item[1], name=item[2]}):up();
|
||||
end
|
||||
|
||||
module:hook('iq/host/http://jabber.org/protocol/disco#items:query', function(event)
|
||||
local stanza = event.stanza;
|
||||
local query = stanza.tags[1];
|
||||
if stanza.attr.type == 'get' and not query.attr.node then
|
||||
event.origin.send(st.reply(stanza):add_child(result_query));
|
||||
return true;
|
||||
end
|
||||
end, 100);
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
summary: Drop chat states from messages to inactive sessions
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
Some mobile XMPP client developers consider [Chat State
|
||||
Notifications](http://xmpp.org/extensions/xep-0085.html) to be a waste
|
||||
of power and bandwidth, especially when the user is not actively looking
|
||||
at their device. This module will filter them out while the session is
|
||||
considered inactive. It depends on [mod\_csi](/mod_csi.html) for
|
||||
deciding when to begin and end filtering.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
There is no configuration for this module, just add it to
|
||||
modules\_enabled as normal.
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
----- -------
|
||||
0.9 Works
|
||||
----- -------
|
||||
@@ -0,0 +1,18 @@
|
||||
#summary Drop chat states from messages to inactive sessions
|
||||
|
||||
= Introduction =
|
||||
|
||||
Some mobile XMPP client developers consider
|
||||
[http://xmpp.org/extensions/xep-0085.html Chat State Notifications] to be
|
||||
a waste of power and bandwidth, especially when the user is not actively
|
||||
looking at their device. This module will filter them out while the
|
||||
session is considered inactive. It depends on `mod_csi` for deciding
|
||||
when to begin and end filtering.
|
||||
|
||||
= Configuration =
|
||||
|
||||
There is no configuration for this module, just add it to modules_enabled as normal.
|
||||
|
||||
= Compatibility =
|
||||
||0.9||Works||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
local filters = require "util.filters";
|
||||
local st = require "util.stanza";
|
||||
|
||||
module:depends("csi");
|
||||
|
||||
local function chatstate_tag_filter(tag)
|
||||
if tag.attr.xmlns ~= "http://jabber.org/protocol/chatstates" then
|
||||
return tag;
|
||||
end
|
||||
end
|
||||
|
||||
local function filter_chatstates(stanza)
|
||||
if stanza.name == "message" then
|
||||
stanza = st.clone(stanza);
|
||||
stanza:maptags(chatstate_tag_filter);
|
||||
if #stanza.tags == 0 then
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
module:hook("csi-client-inactive", function (event)
|
||||
local session = event.origin;
|
||||
filters.add_filter(session, "stanzas/out", filter_chatstates);
|
||||
end);
|
||||
|
||||
module:hook("csi-client-active", function (event)
|
||||
local session = event.origin;
|
||||
filters.remove_filter(session, "stanzas/out", filter_chatstates);
|
||||
end);
|
||||
@@ -0,0 +1,60 @@
|
||||
-- mod_http_altconnect
|
||||
-- XEP-0156: Discovering Alternative XMPP Connection Methods
|
||||
|
||||
module:depends"http";
|
||||
|
||||
local json = require"util.json";
|
||||
local st = require"util.stanza";
|
||||
local array = require"util.array";
|
||||
|
||||
local host_modules = hosts[module.host].modules;
|
||||
|
||||
local function get_supported()
|
||||
local uris = array();
|
||||
if host_modules["bosh"] then
|
||||
uris:push({ rel = "urn:xmpp:alt-connections:xbosh", href = module:http_url("bosh", "/http-bind") });
|
||||
end
|
||||
if host_modules["websocket"] then
|
||||
uris:push({ rel = "urn:xmpp:alt-connections:websocket", href = module:http_url("websocket", "xmpp-websocket"):gsub("^http", "ws") });
|
||||
end
|
||||
return uris;
|
||||
end
|
||||
|
||||
|
||||
local function GET_xml(event)
|
||||
local request, response = event.request, event.response;
|
||||
local xrd = st.stanza("XRD", { xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0' });
|
||||
local uris = get_supported();
|
||||
for i, method in ipairs(uris) do
|
||||
xrd:tag("Link", method):up();
|
||||
end
|
||||
response.headers.content_type = "application/xrd+xml"
|
||||
response.headers.access_control_allow_origin = "*";
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>' .. tostring(xrd);
|
||||
end
|
||||
|
||||
local function GET_json(event)
|
||||
local request, response = event.request, event.response;
|
||||
local jrd = { links = get_supported() };
|
||||
response.headers.content_type = "application/json"
|
||||
response.headers.access_control_allow_origin = "*";
|
||||
return json.encode(jrd);
|
||||
end;
|
||||
|
||||
local function GET_either(event)
|
||||
local accept_type = event.request.headers.accept or "";
|
||||
if ( accept_type:find("xml") or #accept_type ) < ( accept_type:find("json") or #accept_type+1 ) then
|
||||
return GET_xml(event);
|
||||
else
|
||||
return GET_json(event);
|
||||
end
|
||||
end;
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/.well-known";
|
||||
route = {
|
||||
["GET /host-meta"] = GET_either;
|
||||
-- ["GET /host-meta.xml"] = GET_xml; -- Hmmm
|
||||
["GET /host-meta.json"] = GET_json;
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="generator" value="prosody/{prosody_version} mod_{mod_name}">
|
||||
<link rel="canonical" href="{canonical}">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
:link,:visited{color:#3465a4;text-decoration:none;}
|
||||
:link:hover,:visited:hover{color:#6197df;}
|
||||
body{background-color:#eeeeec;margin:1ex 0;padding-bottom:3em;font-family:Arial,Helvetica,sans-serif;}
|
||||
ul,ol{padding:0;}
|
||||
li{list-style:none;}
|
||||
hr{visibility:hidden;clear:both;}
|
||||
br{clear:both;}
|
||||
header,footer{margin:1ex 1em;}
|
||||
footer{font-size:smaller;color:#babdb6;}
|
||||
nav{font-size:large;margin:1ex 1ex;clear:both;line-height:1.5em;}
|
||||
footer nav .up{display:none;}
|
||||
@media screen and (min-width: 460px) {
|
||||
nav {font-size:x-large;margin:1ex 1em;}
|
||||
}
|
||||
nav a{padding:1ex}
|
||||
nav li,nav dt{margin:1ex}
|
||||
.content{background-color:white;padding:1em;list-style-position:inside;}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html{color:#eee}
|
||||
body{background-color:#161616}
|
||||
.content{background-color:#1c1c1c}
|
||||
footer{color:#444}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Prosody IM</h1>
|
||||
<h2>HTTP Services</h2>
|
||||
</header>
|
||||
<hr>
|
||||
<div class="content">
|
||||
<nav>
|
||||
<ul>{items#
|
||||
<li><a href="{item.url}" title="{item.module}">{item.title?{item.name}}</a></li>}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<hr>
|
||||
<footer>
|
||||
<br>
|
||||
<div class="powered-by">Prosody {prosody_version?}</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
local url = require"socket.url";
|
||||
local render = require"util.interpolation".new("%b{}", require"util.stanza".xml_escape);
|
||||
|
||||
module:depends"http";
|
||||
|
||||
local show_all = module:get_option_boolean(module.name .. "_show_all", false);
|
||||
|
||||
local base_template;
|
||||
do
|
||||
local template_file = module:get_option_string(module.name .. "_template", module.name .. ".html");
|
||||
template_file = assert(module:load_resource(template_file));
|
||||
base_template = template_file:read("*a");
|
||||
template_file:close();
|
||||
end
|
||||
|
||||
local canonical = module:http_url(nil, "/");
|
||||
|
||||
local function relative(base, link)
|
||||
base = url.parse(base);
|
||||
link = url.parse(link);
|
||||
for k,v in pairs(base) do
|
||||
if link[k] == v then
|
||||
link[k] = nil;
|
||||
end
|
||||
end
|
||||
return url.build(link);
|
||||
end
|
||||
|
||||
local function handler(event)
|
||||
local host_items = module:get_host_items("http-provider");
|
||||
local http_apps = {}
|
||||
for _, item in ipairs(host_items) do
|
||||
if module.name ~= item._provided_by and (show_all or item.title) then
|
||||
table.insert(http_apps, {
|
||||
title = item.title or item.name;
|
||||
name = item.name;
|
||||
module = "mod_" .. item._provided_by;
|
||||
url = relative(canonical, module:http_url(item.name, item.default_path));
|
||||
});
|
||||
end
|
||||
end
|
||||
table.sort(http_apps, function (a, b) return a.name < b.name; end);
|
||||
event.response.headers.content_type = "text/html";
|
||||
return render(base_template, {
|
||||
title = "Prosody IM - HTTP Services";
|
||||
items = http_apps;
|
||||
prosody_version = prosody.version;
|
||||
mod_name = module.name;
|
||||
canonical = canonical;
|
||||
});
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET /"] = handler;
|
||||
};
|
||||
default_path = "/";
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Alpha'
|
||||
summary: Send XMPP stanzas via REST/HTTP
|
||||
...
|
||||
|
||||
This module provides a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)ful
|
||||
method for sending XMPP stanzas.
|
||||
|
||||
This enables you to send stanzas by making HTTP requests to `http://${prosody-url}/rest`.
|
||||
|
||||
**DANGER/ACHTUNG!: This module does NOT enforce any authentication or user-checking.
|
||||
This means that by default stanzas can be sent *anyone* on behalf of *any* user.**
|
||||
|
||||
You should enable [mod_http_authentication](https://modules.prosody.im/mod_http_authentication.html),
|
||||
to require authentication for calls made to this module, or alternatively, you
|
||||
could use a reverse proxy like Nginx.
|
||||
|
||||
# To enable this module
|
||||
|
||||
Add `"http_rest"` to `modules_enabled`, either globally or for a particular virtual
|
||||
host.
|
||||
|
||||
# How to test:
|
||||
|
||||
You can use curl to make the HTTP request to Prosody, to test whether this
|
||||
module is working properly:
|
||||
|
||||
curl -k http://localhost:5280/rest -u username:password -H "Content-Type: text/xml" -d '<iq to="pubsub.localhost" type="set" id="4dd1a1e3-ef91-4017-a5aa-eaba0a82eb94-1" from="user@localhost"><pubsub xmlns="http://jabber.org/protocol/pubsub"><publish node="Test mod_rest.lua"><item>Hello World!</item></publish></pubsub></iq>'
|
||||
@@ -0,0 +1,50 @@
|
||||
module:depends"http"
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local stanza = require "util.stanza";
|
||||
local test_password = require "core.usermanager".test_password;
|
||||
local b64_decode = require "util.encodings".base64.decode;
|
||||
local formdecode = require "net.http".formdecode;
|
||||
-- local xml = require"util.xml";
|
||||
local xml_parse = require "util.xml".parse;
|
||||
|
||||
local function handle_post(event, path, authed_user)
|
||||
local request = event.request;
|
||||
local headers = request.headers;
|
||||
local body_type = headers.content_type;
|
||||
-- module:log("info","got body type: %s", body_type);
|
||||
-- module:log("info","request body: %s", request.body);
|
||||
if body_type == "text/xml" and request.body then
|
||||
local parsed, err = xml_parse(request.body);
|
||||
module:log("info","parsed: %s", parsed);
|
||||
if parsed then
|
||||
module:log("info", "Sending %s", parsed);
|
||||
local parsed_stanza = stanza.deserialize(parsed);
|
||||
module:fire_event("vnc-rest-message", { stanza = parsed_stanza , origin = "" } );
|
||||
module:send(parsed);
|
||||
return 201;
|
||||
end
|
||||
else
|
||||
module:log("info","rest got error: %s", err);
|
||||
return 415;
|
||||
end
|
||||
module:log("info", "rest will return 422 with type: %s", body_type);
|
||||
module:log("info", "rest will return 422 with body: %s", request.body);
|
||||
-- module:log("info","got body type: %s", body_type);
|
||||
-- module:log("info","request body: %s", request.body);
|
||||
return 422;
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/rest";
|
||||
route = {
|
||||
["POST"] = handle_post;
|
||||
OPTIONS = function(e)
|
||||
local headers = e.response.headers;
|
||||
headers.allow = "POST";
|
||||
headers.accept = "text/xml";
|
||||
return 200;
|
||||
end;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
description: HTTP File Upload (external service)
|
||||
labels: 'Stage-Alpha'
|
||||
---
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
This module implements [XEP-0363], which lets clients upload files
|
||||
over HTTP to an external web server.
|
||||
|
||||
This module generates URLs that are signed using a HMAC. Any web service that can authenticate
|
||||
these URLs can be used. There is a PHP implementation available
|
||||
[here](https://hg.prosody.im/prosody-modules/raw-file/tip/mod_http_upload_external/share.php). To
|
||||
implement your own service compatible with this module, check out the implementation notes below (and if you
|
||||
publish your implementation - let us know!).
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Add `"http_upload_external"` to modules_enabled in your global section, or under the host(s) you wish
|
||||
to use it on.
|
||||
|
||||
External URL
|
||||
------------
|
||||
|
||||
You need to provide the path to the external service. Ensure it ends with '/'.
|
||||
|
||||
For example, to use the PHP implementation linked above, you might set it to:
|
||||
|
||||
``` {.lua}
|
||||
http_upload_external_base_url = "https://your.example.com/path/to/share.php/"
|
||||
```
|
||||
|
||||
Secret
|
||||
------
|
||||
|
||||
Set a long and unpredictable string as your secret. This is so the upload service can verify that
|
||||
the upload comes from mod_http_upload_external, and random strangers can't upload to your server.
|
||||
|
||||
``` {.lua}
|
||||
http_upload_external_secret = "this is a secret string!"
|
||||
```
|
||||
|
||||
You need to set exactly the same secret string in your external service.
|
||||
|
||||
Limits
|
||||
------
|
||||
|
||||
A maximum file size can be set by:
|
||||
|
||||
``` {.lua}
|
||||
http_upload_external_file_size_limit = 123 -- bytes
|
||||
```
|
||||
|
||||
Default is 100MB (100\*1024\*1024).
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
Works with Prosody 0.9.x and later.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
To implement your own external service that is compatible with this module, you need to expose a
|
||||
simple API that allows the HTTP GET, HEAD and PUT methods on arbitrary URLs located on your service.
|
||||
|
||||
For example, if http_upload_external_base_url is set to `https://example.com/upload/` then your service
|
||||
might receive the following requests:
|
||||
|
||||
Upload a new file:
|
||||
|
||||
```
|
||||
PUT https://example.com/upload/foo/bar.jpg?v=49e9309ff543ace93d25be90635ba8e9965c4f23fc885b2d86c947a5d59e55b2
|
||||
```
|
||||
|
||||
Recipient checks the file size and other headers:
|
||||
|
||||
```
|
||||
HEAD https://example.com/upload/foo/bar.jpg
|
||||
```
|
||||
|
||||
Recipient downloads the file:
|
||||
|
||||
```
|
||||
GET https://example.com/upload/foo/bar.jpg
|
||||
```
|
||||
|
||||
The only tricky logic is in validation of the PUT request. Firstly, don't overwrite existing files (return 409 Conflict).
|
||||
|
||||
Then you need to validate the auth token. This will be in the URL query parameter 'v'. If it is absent, fail with 403 Forbidden.
|
||||
|
||||
Calculate the expected auth token by reading the value of the Content-Length header of the PUT request. E.g. for a 1MB file
|
||||
will have a Content-Length of '1048576'. Append this to the uploaded file name, separated by a space (0x20) character.
|
||||
|
||||
For the above example, you would end up with the following string: "foo/bar.jpg 1048576"
|
||||
|
||||
The auth token is a SHA256 HMAC of this string, using the configured secret as the key. E.g.
|
||||
|
||||
```
|
||||
calculated_auth_token = hmac_sha256("foo/bar.jpg 1048576", "secret string")
|
||||
```
|
||||
|
||||
If this is not equal to the 'v' parameter provided in the upload URL, reject the upload with 403 Forbidden.
|
||||
|
||||
Note: your language/environment may provide a function for doing a constant-time comparison of these, to guard against
|
||||
timing attacks that may be used to discover the secret key.
|
||||
@@ -0,0 +1,126 @@
|
||||
-- mod_http_upload_external
|
||||
--
|
||||
-- Copyright (C) 2015-2016 Kim Alvefur
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
--
|
||||
|
||||
-- imports
|
||||
local st = require"util.stanza";
|
||||
local uuid = require"util.uuid".generate;
|
||||
local http = require "util.http";
|
||||
local dataform = require "util.dataforms".new;
|
||||
local HMAC = require "util.hashes".hmac_sha256;
|
||||
|
||||
-- config
|
||||
local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 100 * 1024 * 1024); -- 100 MB
|
||||
local base_url = assert(module:get_option_string(module.name .. "_base_url"), module.name .. "_base_url is a required option");
|
||||
local secret = assert(module:get_option_string(module.name .. "_secret"), module.name .. "_secret is a required option");
|
||||
|
||||
-- depends
|
||||
module:depends("disco");
|
||||
|
||||
-- namespace
|
||||
local legacy_namespace = "urn:xmpp:http:upload";
|
||||
local namespace = "urn:xmpp:http:upload:0";
|
||||
|
||||
-- identity and feature advertising
|
||||
module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload"))
|
||||
module:add_feature(namespace);
|
||||
module:add_feature(legacy_namespace);
|
||||
|
||||
module:add_extension(dataform {
|
||||
{ name = "FORM_TYPE", type = "hidden", value = namespace },
|
||||
{ name = "max-file-size", type = "text-single" },
|
||||
}:form({ ["max-file-size"] = tostring(file_size_limit) }, "result"));
|
||||
|
||||
module:add_extension(dataform {
|
||||
{ name = "FORM_TYPE", type = "hidden", value = legacy_namespace },
|
||||
{ name = "max-file-size", type = "text-single" },
|
||||
}:form({ ["max-file-size"] = tostring(file_size_limit) }, "result"));
|
||||
|
||||
local function magic_crypto_dust(random, filename, filesize)
|
||||
local message = string.format("%s/%s %d", random, filename, filesize);
|
||||
local digest = HMAC(secret, message, true);
|
||||
random, filename = http.urlencode(random), http.urlencode(filename);
|
||||
return base_url .. random .. "/" .. filename, "?v=" .. digest;
|
||||
end
|
||||
|
||||
local function handle_request(origin, stanza, xmlns, filename, filesize)
|
||||
-- local clients only
|
||||
if origin.type ~= "c2s" then
|
||||
module:log("debug", "Request for upload slot from a %s", origin.type);
|
||||
origin.send(st.error_reply(stanza, "cancel", "not-authorized"));
|
||||
return nil, nil;
|
||||
end
|
||||
-- validate
|
||||
if not filename or filename:find("/") then
|
||||
module:log("debug", "Filename %q not allowed", filename or "");
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid filename"));
|
||||
return nil, nil;
|
||||
end
|
||||
if not filesize then
|
||||
module:log("debug", "Missing file size");
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing or invalid file size"));
|
||||
return nil, nil;
|
||||
elseif filesize > file_size_limit then
|
||||
module:log("debug", "File too large (%d > %d)", filesize, file_size_limit);
|
||||
origin.send(st.error_reply(stanza, "modify", "not-acceptable", "File too large",
|
||||
st.stanza("file-too-large", {xmlns=xmlns})
|
||||
:tag("max-size"):text(tostring(file_size_limit))));
|
||||
return nil, nil;
|
||||
end
|
||||
local random = uuid();
|
||||
local get_url, verify = magic_crypto_dust(random, filename, filesize);
|
||||
local put_url = get_url .. verify;
|
||||
|
||||
module:log("info", "Handing out upload slot %s to %s@%s", get_url, origin.username, origin.host);
|
||||
|
||||
return get_url, put_url;
|
||||
end
|
||||
|
||||
-- hooks
|
||||
module:hook("iq/host/"..legacy_namespace..":request", function (event)
|
||||
local stanza, origin = event.stanza, event.origin;
|
||||
local request = stanza.tags[1];
|
||||
local filename = request:get_child_text("filename");
|
||||
local filesize = tonumber(request:get_child_text("size"));
|
||||
|
||||
local get_url, put_url = handle_request(
|
||||
origin, stanza, legacy_namespace, filename, filesize);
|
||||
|
||||
if not get_url then
|
||||
-- error was already sent
|
||||
return true;
|
||||
end
|
||||
|
||||
local reply = st.reply(stanza)
|
||||
:tag("slot", { xmlns = legacy_namespace })
|
||||
:tag("get"):text(get_url):up()
|
||||
:tag("put"):text(put_url):up()
|
||||
:up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
|
||||
module:hook("iq/host/"..namespace..":request", function (event)
|
||||
local stanza, origin = event.stanza, event.origin;
|
||||
local request = stanza.tags[1];
|
||||
local filename = request.attr.filename;
|
||||
local filesize = tonumber(request.attr.size);
|
||||
local get_url, put_url = handle_request(
|
||||
origin, stanza, legacy_namespace, filename, filesize);
|
||||
|
||||
if not get_url then
|
||||
-- error was already sent
|
||||
return true;
|
||||
end
|
||||
|
||||
local reply = st.reply(stanza)
|
||||
:tag("slot", { xmlns = namespace})
|
||||
:tag("get", { url = get_url }):up()
|
||||
:tag("put", { url = put_url }):up()
|
||||
:up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
PHP script to handle file uploads and downloads for Prosody's mod_http_upload_external
|
||||
|
||||
Tested with Apache 2.2+ and PHP 5.3+
|
||||
|
||||
** Why this script?
|
||||
|
||||
This script only allows uploads that have been authorized by mod_http_upload_external. It
|
||||
attempts to make the upload/download as safe as possible, considering that there are *many*
|
||||
security concerns involved with allowing arbitrary file upload/download on a web server.
|
||||
|
||||
With that said, I do not consider myself a PHP developer, and at the time of writing, this
|
||||
code has had no external review. Use it at your own risk. I make no claims that this code
|
||||
is secure.
|
||||
|
||||
** How to use?
|
||||
|
||||
Drop this file somewhere it will be served by your web server. Edit the config options below.
|
||||
|
||||
In Prosody set:
|
||||
|
||||
http_upload_external_base_url = "https://your.example.com/path/to/share.php/"
|
||||
http_upload_external_secret = "this is your secret string"
|
||||
|
||||
** License
|
||||
|
||||
(C) 2016 Matthew Wild <mwild1@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/
|
||||
/* CONFIGURATION OPTIONS */
|
||||
/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/
|
||||
|
||||
/* Change this to a directory that is writable by your web server, but is outside your web root */
|
||||
$CONFIG_STORE_DIR = '/tmp';
|
||||
|
||||
/* This must be the same as 'http_upload_external_secret' that you set in Prosody's config file */
|
||||
$CONFIG_SECRET = 'this is your secret string';
|
||||
|
||||
/* For people who need options to tweak that they don't understand... here you are */
|
||||
$CONFIG_CHUNK_SIZE = 4096;
|
||||
|
||||
/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/
|
||||
/* END OF CONFIGURATION */
|
||||
/*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/
|
||||
|
||||
/* Do not edit below this line unless you know what you are doing (spoiler: nobody does) */
|
||||
|
||||
$upload_file_name = substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME'])+1);
|
||||
$store_file_name = $CONFIG_STORE_DIR . '/store-' . hash('sha256', $upload_file_name);
|
||||
|
||||
$request_method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if(array_key_exists('v', $_GET) === TRUE && $request_method === 'PUT') {
|
||||
$headers = getallheaders();
|
||||
|
||||
$upload_file_size = $headers['Content-Length'];
|
||||
$upload_token = $_GET['v'];
|
||||
|
||||
$calculated_token = hash_hmac('sha256', "$upload_file_name $upload_file_size", $CONFIG_SECRET);
|
||||
if($upload_token !== $calculated_token) {
|
||||
header('HTTP/1.0 403 Forbidden');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* Open a file for writing */
|
||||
$store_file = fopen($store_file_name, 'x');
|
||||
|
||||
if($store_file === FALSE) {
|
||||
header('HTTP/1.0 409 Conflict');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* PUT data comes in on the stdin stream */
|
||||
$incoming_data = fopen('php://input', 'r');
|
||||
|
||||
/* Read the data a chunk at a time and write to the file */
|
||||
while ($data = fread($incoming_data, $CONFIG_CHUNK_SIZE)) {
|
||||
fwrite($store_file, $data);
|
||||
}
|
||||
|
||||
/* Close the streams */
|
||||
fclose($incoming_data);
|
||||
fclose($store_file);
|
||||
} else if($request_method === 'GET' || $request_method === 'HEAD') {
|
||||
// Send file (using X-Sendfile would be nice here...)
|
||||
if(file_exists($store_file_name)) {
|
||||
header('Content-Disposition: attachment');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Length: '.filesize($store_file_name));
|
||||
if($request_method !== 'HEAD') {
|
||||
readfile($store_file_name);
|
||||
}
|
||||
} else {
|
||||
header('HTTP/1.0 404 Not Found');
|
||||
}
|
||||
} else {
|
||||
header('HTTP/1.0 400 Bad Request');
|
||||
}
|
||||
|
||||
exit;
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Beta'
|
||||
summary: 'XEP-0319 compatibility module'
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
This module adds [XEP-0319](http://xmpp.org/extensions/xep-0319.html)
|
||||
idle tags to presence stanzas containing [XEP-0012: Last
|
||||
Activity](http://xmpp.org/extensions/xep-0012.html) tags for idle
|
||||
indication (e.g. supported by libpurple clients). It works on outgoing
|
||||
and incoming presence stanzas.
|
||||
|
||||
Install and enable it like any other module. It has no configuration.
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
------- -------
|
||||
trunk Works
|
||||
0.9 Works
|
||||
------- -------
|
||||
@@ -0,0 +1,13 @@
|
||||
#summary XEP-0319 compatibility module
|
||||
#labels Stage-Beta
|
||||
|
||||
= Introduction =
|
||||
|
||||
This module adds [http://xmpp.org/extensions/xep-0319.html XEP-0319] idle tags to presence stanzas containing [http://xmpp.org/extensions/xep-0012.html XEP-0012: Last Activity] tags for idle indication (e.g. supported by libpurple clients). It works on outgoing and incoming presence stanzas.
|
||||
|
||||
Install and enable it like any other module. It has no configuration.
|
||||
|
||||
= Compatibility =
|
||||
|
||||
||trunk||Works||
|
||||
||0.9||Works||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Last User Interaction in Presence via Last Activity compatibility module
|
||||
-- http://xmpp.org/extensions/xep-0319.html
|
||||
-- http://xmpp.org/extensions/xep-0256.html
|
||||
-- Copyright (C) 2014 Tobias Markmann
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
|
||||
local st = require "util.stanza";
|
||||
local datetime = require "util.datetime";
|
||||
|
||||
local function on_presence(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
local last_activity = stanza:get_child("query", "jabber:iq:last");
|
||||
local has_idle = stanza:get_child("idle", "urn:xmpp:idle:1");
|
||||
if last_activity and not has_idle then
|
||||
module:log("debug", "Adding XEP-0319 tag from Last Activity.");
|
||||
local seconds = last_activity.attr.seconds;
|
||||
local last_userinteraction = datetime.datetime(os.time() - seconds);
|
||||
stanza:tag("idle", { xmlns = "urn:xmpp:idle:1", since = last_userinteraction }):up();
|
||||
end
|
||||
end
|
||||
|
||||
-- incoming
|
||||
module:hook("presence/full", on_presence, 900);
|
||||
module:hook("presence/bare", on_presence, 900);
|
||||
|
||||
-- outgoing
|
||||
module:hook("pre-presence/bare", on_presence, 900);
|
||||
module:hook("pre-presence/full", on_presence, 900);
|
||||
module:hook("pre-presence/host", on_presence, 900);
|
||||
@@ -0,0 +1,63 @@
|
||||
module:set_global();
|
||||
|
||||
local time = require "socket".gettime;
|
||||
local base64_decode = require "util.encodings".base64.decode;
|
||||
|
||||
local max_seconds = module:get_option_number("log_slow_events_threshold", 0.5);
|
||||
|
||||
local measure_slow_event = module:measure("slow_events", "rate");
|
||||
|
||||
function event_wrapper(handlers, event_name, event_data)
|
||||
local start = time();
|
||||
local ret = handlers(event_name, event_data);
|
||||
local duration = time()-start;
|
||||
if duration > max_seconds then
|
||||
local data = {};
|
||||
if event_data then
|
||||
local function log_data(name, value)
|
||||
if value then
|
||||
table.insert(data, ("%s=%q"):format(name, value));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
local sess = event_data.origin or event_data.session;
|
||||
if sess then
|
||||
log_data("ip", sess.ip);
|
||||
if not log_data("full_jid", sess.full_jid) then
|
||||
log_data("username", sess.username);
|
||||
end
|
||||
log_data("type", sess.type);
|
||||
log_data("host", sess.host);
|
||||
end
|
||||
local stanza = event_data.stanza;
|
||||
if stanza then
|
||||
log_data("stanza", tostring(stanza));
|
||||
else
|
||||
local request = event_data.request;
|
||||
if request then
|
||||
log_data("http_method", request.method);
|
||||
log_data("http_path", request.path);
|
||||
local auth = request.headers.authorization;
|
||||
if auth then
|
||||
local creds = auth:match("^Basic +(.+)$");
|
||||
if creds then
|
||||
local user = string.match(base64_decode(creds) or "", "^([^:]+):");
|
||||
log_data("http_user", user);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
measure_slow_event();
|
||||
module:log("warn", "Slow event '%s' took %0.2fs: %s", event_name, duration, next(data) and table.concat(data, ", ") or "no recognised data");
|
||||
end
|
||||
return ret;
|
||||
end
|
||||
|
||||
local http_events = require "net.http.server"._events;
|
||||
module:wrap_object_event(http_events, false, event_wrapper);
|
||||
|
||||
module:wrap_event(false, event_wrapper);
|
||||
function module.add_host(module)
|
||||
module:wrap_event(false, event_wrapper);
|
||||
end
|
||||
@@ -0,0 +1,165 @@
|
||||
-----------------------------------------------------------
|
||||
-- mod_roster_command: Manage rosters through prosodyctl
|
||||
-- version 0.02
|
||||
-----------------------------------------------------------
|
||||
-- Copyright (C) 2011 Matthew Wild
|
||||
-- Copyright (C) 2011 Adam Nielsen
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
-----------------------------------------------------------
|
||||
|
||||
if module.host ~= "*" then
|
||||
module:log("error", "Do not load this module in Prosody, for correct usage see: https://modules.prosody.im/mod_roster_command.html");
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- Workaround for lack of util.startup...
|
||||
local prosody = _G.prosody;
|
||||
local hosts = prosody.hosts;
|
||||
prosody.bare_sessions = prosody.bare_sessions or {};
|
||||
_G.bare_sessions = _G.bare_sessions or prosody.bare_sessions;
|
||||
|
||||
local usermanager = require "core.usermanager";
|
||||
local rostermanager = require "core.rostermanager";
|
||||
local storagemanager = require "core.storagemanager";
|
||||
local jid = require "util.jid";
|
||||
local warn = require"util.prosodyctl".show_warning;
|
||||
|
||||
-- Make a *one-way* subscription. User will see when contact is online,
|
||||
-- contact will not see when user is online.
|
||||
function subscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription request is pending. Bare hosts (e.g. components) don't have rosters.
|
||||
if user_username ~= nil then
|
||||
rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
end
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_in(contact_username, contact_host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, contact_host, user_jid);
|
||||
-- Update user's roster to say subscription request approved. Bare hosts (e.g. components) don't have rosters.
|
||||
if user_username ~= nil then
|
||||
rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Make a mutual subscription between jid1 and jid2. Each JID will see
|
||||
-- when the other one is online.
|
||||
function subscribe_both(jid1, jid2)
|
||||
subscribe(jid1, jid2);
|
||||
subscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
-- Unsubscribes user from contact (not contact from user, if subscribed).
|
||||
function unsubscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribe(user_username, user_host, contact_jid);
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribed(contact_username, contact_host, user_jid);
|
||||
end
|
||||
end
|
||||
|
||||
-- Cancel any subscription in either direction.
|
||||
function unsubscribe_both(jid1, jid2)
|
||||
unsubscribe(jid1, jid2);
|
||||
unsubscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
-- Set the name shown and group used in the contact list
|
||||
function rename(user_jid, contact_jid, contact_nick, contact_group)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
|
||||
-- Load user's roster and find the contact
|
||||
local roster = rostermanager.load_roster(user_username, user_host);
|
||||
local item = roster[contact_jid];
|
||||
if item then
|
||||
if contact_nick then
|
||||
item.name = contact_nick;
|
||||
end
|
||||
if contact_group then
|
||||
item.groups = {}; -- Remove from all current groups
|
||||
item.groups[contact_group] = true;
|
||||
end
|
||||
rostermanager.save_roster(user_username, user_host, roster);
|
||||
end
|
||||
end
|
||||
|
||||
function remove(user_jid, contact_jid)
|
||||
unsubscribe_both(user_jid, contact_jid);
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local roster = rostermanager.load_roster(user_username, user_host);
|
||||
roster[contact_jid] = nil;
|
||||
rostermanager.save_roster(user_username, user_host, roster);
|
||||
end
|
||||
|
||||
function module.command(arg)
|
||||
local command = arg[1];
|
||||
if not command then
|
||||
warn("Valid subcommands: (un)subscribe(_both) | rename");
|
||||
return 0;
|
||||
end
|
||||
table.remove(arg, 1);
|
||||
if command == "subscribe" then
|
||||
subscribe(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "subscribe_both" then
|
||||
subscribe_both(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "unsubscribe" then
|
||||
unsubscribe(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "unsubscribe_both" then
|
||||
unsubscribe_both(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "remove" then
|
||||
remove(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "rename" then
|
||||
rename(arg[1], arg[2], arg[3], arg[4]);
|
||||
return 0;
|
||||
else
|
||||
warn("Unknown command: %s", command);
|
||||
return 1;
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
local st = require "util.stanza";
|
||||
local watchdog = require "util.watchdog";
|
||||
local dt = require "util.datetime";
|
||||
|
||||
local keepalive_servers = module:get_option_set("keepalive_servers");
|
||||
local keepalive_interval = module:get_option_number("keepalive_interval", 60);
|
||||
local keepalive_timeout = module:get_option_number("keepalive_timeout", 593);
|
||||
|
||||
local host = module.host;
|
||||
local s2sout = prosody.hosts[host].s2sout;
|
||||
|
||||
local function send_pings()
|
||||
local ping_hosts = {};
|
||||
|
||||
for remote_domain, session in pairs(s2sout) do
|
||||
if session.type ~= "s2sout_unauthed"
|
||||
and (not(keepalive_servers) or keepalive_servers:contains(remote_domain)) then
|
||||
session.sends2s(st.iq({ to = remote_domain, type = "get", from = host, id = "keepalive:"..dt.datetime()})
|
||||
:tag("ping", { xmlns = "urn:xmpp:ping" })
|
||||
);
|
||||
end
|
||||
end
|
||||
|
||||
for session in pairs(prosody.incoming_s2s) do
|
||||
if session.type ~= "s2sin_unauthed"
|
||||
and not session.notopen
|
||||
and session.to_host == host
|
||||
and (not(keepalive_servers) or keepalive_servers:contains(session.from_host)) then
|
||||
if not s2sout[session.from_host] then ping_hosts[session.from_host] = true; end
|
||||
session.sends2s " ";
|
||||
-- If the connection is dead, this should make it time out.
|
||||
end
|
||||
end
|
||||
|
||||
-- ping remotes we only have s2sin from
|
||||
for remote_domain in pairs(ping_hosts) do
|
||||
module:send(st.iq({ to = remote_domain, type = "get", from = host, id = "keepalive:"..dt.datetime() })
|
||||
:tag("ping", { xmlns = "urn:xmpp:ping" })
|
||||
);
|
||||
end
|
||||
|
||||
return keepalive_interval;
|
||||
end
|
||||
|
||||
module:hook("s2sin-established", function (event)
|
||||
local session = event.session;
|
||||
if session.watchdog_keepalive then return end -- in case mod_bidi fires this twice
|
||||
if keepalive_servers and not keepalive_servers:contains(session.from_host) then return end
|
||||
session.watchdog_keepalive = watchdog.new(keepalive_timeout, function ()
|
||||
session.log("info", "Keepalive ping timed out, closing connection");
|
||||
session:close("connection-timeout");
|
||||
end);
|
||||
end);
|
||||
|
||||
module:hook("s2sout-established", function (event)
|
||||
local session = event.session;
|
||||
if session.watchdog_keepalive then return end -- in case mod_bidi fires this twice
|
||||
if keepalive_servers and not keepalive_servers:contains(session.from_host) then return end
|
||||
session.watchdog_keepalive = watchdog.new(keepalive_timeout, function ()
|
||||
session.log("info", "Keepalive ping timed out, closing connection");
|
||||
session:close("connection-timeout");
|
||||
end);
|
||||
end);
|
||||
|
||||
module:hook("iq/host", function (event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then
|
||||
return -- not a reply iq stanza
|
||||
end
|
||||
if not (stanza.attr.id and stanza.attr.id:sub(1, #"keepalive:") == "keepalive:") then
|
||||
return -- not a reply to this module
|
||||
end
|
||||
if stanza.attr.type == "error" then
|
||||
local err = stanza:get_child("error");
|
||||
local err_by = err and err.attr.by;
|
||||
if err_by and prosody.hosts[err_by] then
|
||||
return -- error produced by the local host
|
||||
end
|
||||
end
|
||||
|
||||
local origin = event.origin;
|
||||
if origin.dummy then return end -- Probably a sendq bounce
|
||||
if origin.watchdog_keepalive then
|
||||
origin.log("debug", "Resetting keepalive watchdog")
|
||||
origin.watchdog_keepalive:reset();
|
||||
end
|
||||
if s2sout[origin.from_host] and s2sout[origin.from_host].watchdog_keepalive then
|
||||
s2sout[origin.from_host].watchdog_keepalive:reset();
|
||||
end
|
||||
return true;
|
||||
end);
|
||||
|
||||
module:add_timer(keepalive_interval, send_pings);
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Alpha'
|
||||
summary: 'XEP-0198: Reliability and fast reconnects for XMPP'
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
By default XMPP is as reliable as your network is. Unfortunately in some
|
||||
cases that is not very reliable - in some network conditions disconnects
|
||||
can be frequent and message loss can occur.
|
||||
|
||||
To overcome this, XMPP has an optional extension (XEP-0198: Stream
|
||||
Management) which, when supported by both the client and server, can
|
||||
allow a client to resume a disconnected session, and prevent message
|
||||
loss.
|
||||
|
||||
Details
|
||||
=======
|
||||
|
||||
When using XEP-0198 both the client and the server keep a queue of the
|
||||
most recently sent stanzas - this is cleared when the other end
|
||||
acknowledges they have received the stanzas. If the client disconnects,
|
||||
instead of marking the user offline the server pretends the client is
|
||||
still online for a short (configurable) period of time. If the client
|
||||
reconnects within this period, any stanzas in the queue that the client
|
||||
did not receive are re-sent.
|
||||
|
||||
If the client fails to reconnect before the timeout then it is marked
|
||||
offline as normal, and any stanzas in the queue are returned to the
|
||||
sender as a "recipient-unavailable" error.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Option Default Description
|
||||
------------------------------ ----------------- -----------------------------------------------------------------------------------------
|
||||
`smacks_hibernation_time` 300 (5 minutes) The number of seconds a disconnected session should stay alive for (to allow reconnect)
|
||||
`smacks_enabled_s2s` false Enable Stream Management on server connections? *Experimental*
|
||||
`smacks_max_unacked_stanzas` 0 How many stanzas to send before requesting acknowledgement
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
----- -----------------------------------
|
||||
0.9 Works
|
||||
0.8 Works, use version [7693724881b3]
|
||||
----- -----------------------------------
|
||||
|
||||
|
||||
Clients
|
||||
=======
|
||||
|
||||
Clients that support XEP-0198:
|
||||
|
||||
- Gajim
|
||||
- Swift (but not resumption, as of version 2.0 and alphas of 3.0)
|
||||
- Psi (in an unreleased branch)
|
||||
- Conversations
|
||||
- Yaxim
|
||||
|
||||
[7693724881b3]: //hg.prosody.im/prosody-modules/raw-file/7693724881b3/mod_smacks/mod_smacks.lua
|
||||
@@ -0,0 +1,31 @@
|
||||
#summary XEP-0198: Reliability and fast reconnects for XMPP
|
||||
#labels Stage-Beta
|
||||
|
||||
= Introduction =
|
||||
|
||||
By default XMPP is as reliable as your network is. Unfortunately in some cases that is not very reliable - in some network conditions disconnects can be frequent and message loss can occur.
|
||||
|
||||
To overcome this, XMPP has an optional extension (XEP-0198: Stream Management) which, when supported by both the client and server, can allow a client to resume a disconnected session, and prevent message loss.
|
||||
|
||||
= Details =
|
||||
|
||||
When using XEP-0198 both the client and the server keep a queue of the most recently sent stanzas - this is cleared when the other end acknowledges they have received the stanzas. If the client disconnects, instead of marking the user offline the server pretends the client is still online for a short (configurable) period of time. If the client reconnects within this period, any stanzas in the queue that the client did not receive are re-sent.
|
||||
|
||||
If the client fails to reconnect before the timeout then it is marked offline as normal, and any stanzas in the queue are returned to the sender as a "recipient-unavailable" error.
|
||||
|
||||
= Configuration =
|
||||
|
||||
|| *Option* || *Default* || *Description* ||
|
||||
|| smacks_hibernation_time || 300 (5 minutes) || The number of seconds a disconnected session should stay alive for (to allow reconnect) ||
|
||||
|
||||
= Compatibility =
|
||||
||0.9||Works||
|
||||
||0.8||Works, use version [http://prosody-modules.googlecode.com/hg-history/7693724881b3f3cdafa35763f00dd040d02313bf/mod_smacks/mod_smacks.lua 7693724881b3]||
|
||||
|
||||
= Clients =
|
||||
Clients that support XEP-0198:
|
||||
* Gajim
|
||||
* Swift (but not resumption, as of version 2.0 and alphas of 3.0)
|
||||
* Psi (in an unreleased branch)
|
||||
* Yaxim
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
-- XEP-0198: Stream Management for Prosody IM
|
||||
--
|
||||
-- Copyright (C) 2010-2015 Matthew Wild
|
||||
-- Copyright (C) 2010 Waqas Hussain
|
||||
-- Copyright (C) 2012-2015 Kim Alvefur
|
||||
-- Copyright (C) 2012 Thijs Alkemade
|
||||
-- Copyright (C) 2014 Florian Zeitz
|
||||
-- Copyright (C) 2016-2020 Thilo Molitor
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local st = require "util.stanza";
|
||||
local dep = require "util.dependencies";
|
||||
local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
|
||||
local uuid_generate = require "util.uuid".generate;
|
||||
local jid = require "util.jid";
|
||||
|
||||
local t_remove = table.remove;
|
||||
local math_min = math.min;
|
||||
local math_max = math.max;
|
||||
local os_time = os.time;
|
||||
local tonumber, tostring = tonumber, tostring;
|
||||
local add_filter = require "util.filters".add_filter;
|
||||
local timer = require "util.timer";
|
||||
local datetime = require "util.datetime";
|
||||
|
||||
local xmlns_sm2 = "urn:xmpp:sm:2";
|
||||
local xmlns_sm3 = "urn:xmpp:sm:3";
|
||||
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
|
||||
local xmlns_delay = "urn:xmpp:delay";
|
||||
|
||||
local sm2_attr = { xmlns = xmlns_sm2 };
|
||||
local sm3_attr = { xmlns = xmlns_sm3 };
|
||||
|
||||
local resume_timeout = module:get_option_number("smacks_hibernation_time", 600);
|
||||
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
|
||||
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
|
||||
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
|
||||
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
|
||||
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
|
||||
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
|
||||
local core_process_stanza = prosody.core_process_stanza;
|
||||
local sessionmanager = require"core.sessionmanager";
|
||||
|
||||
assert(max_hibernated_sessions > 0, "smacks_max_hibernated_sessions must be greater than 0");
|
||||
assert(max_old_sessions > 0, "smacks_max_old_sessions must be greater than 0");
|
||||
|
||||
local c2s_sessions = module:shared("/*/c2s/sessions");
|
||||
|
||||
local function init_session_cache(max_entries, evict_callback)
|
||||
-- old prosody version < 0.10 (no limiting at all!)
|
||||
if not cache then
|
||||
local store = {};
|
||||
return {
|
||||
get = function(user, key)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
return store[key];
|
||||
end;
|
||||
set = function(user, key, value)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
store[key] = value;
|
||||
end;
|
||||
};
|
||||
end
|
||||
|
||||
-- use per user limited cache for prosody >= 0.10
|
||||
local stores = {};
|
||||
return {
|
||||
get = function(user, key)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
if not stores[user] then
|
||||
stores[user] = cache.new(max_entries, evict_callback);
|
||||
end
|
||||
return stores[user]:get(key);
|
||||
end;
|
||||
set = function(user, key, value)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end
|
||||
stores[user]:set(key, value);
|
||||
-- remove empty caches completely
|
||||
if not stores[user]:count() then stores[user] = nil; end
|
||||
end;
|
||||
};
|
||||
end
|
||||
local old_session_registry = init_session_cache(max_old_sessions, nil);
|
||||
local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session)
|
||||
if session.destroyed then return true; end -- destroyed session can always be removed from cache
|
||||
session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token);
|
||||
-- store old session's h values on force delete
|
||||
-- save only actual h value and username/host (for security)
|
||||
old_session_registry.set(session.username, resumption_token, {
|
||||
h = session.handled_stanza_count,
|
||||
username = session.username,
|
||||
host = session.host
|
||||
});
|
||||
return true; -- allow session to be removed from full cache to make room for new one
|
||||
end);
|
||||
|
||||
local function stoppable_timer(delay, callback)
|
||||
local stopped = false;
|
||||
local timer = module:add_timer(delay, function (t)
|
||||
if stopped then return; end
|
||||
return callback(t);
|
||||
end);
|
||||
if timer and timer.stop then return timer; end -- new prosody api includes stop() function
|
||||
return {
|
||||
stop = function(self) stopped = true end;
|
||||
timer;
|
||||
};
|
||||
end
|
||||
|
||||
local function delayed_ack_function(session)
|
||||
-- fire event only if configured to do so and our session is not already hibernated or destroyed
|
||||
if delayed_ack_timeout > 0 and session.awaiting_ack
|
||||
and not session.hibernating and not session.destroyed then
|
||||
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d",
|
||||
session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0);
|
||||
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue});
|
||||
end
|
||||
session.delayed_ack_timer = nil;
|
||||
end
|
||||
|
||||
local function can_do_smacks(session, advertise_only)
|
||||
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
|
||||
|
||||
local session_type = session.type;
|
||||
if session.username then
|
||||
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
|
||||
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
|
||||
end
|
||||
return true;
|
||||
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
|
||||
return true;
|
||||
end
|
||||
return false, "service-unavailable", "Stream management is not available for this stream";
|
||||
end
|
||||
|
||||
module:hook("stream-features",
|
||||
function (event)
|
||||
if can_do_smacks(event.origin, true) then
|
||||
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
|
||||
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("s2s-stream-features",
|
||||
function (event)
|
||||
if can_do_smacks(event.origin, true) then
|
||||
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
|
||||
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
|
||||
end
|
||||
end);
|
||||
|
||||
local function request_ack_if_needed(session, force, reason)
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
local expected_h = session.last_acknowledged_stanza + #queue;
|
||||
-- session.log("debug", "*** SMACKS(1) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
|
||||
if session.awaiting_ack == nil and not session.hibernating then
|
||||
-- this check of last_requested_h prevents ack-loops if missbehaving clients report wrong
|
||||
-- stanza counts. it is set when an <r> is really sent (e.g. inside timer), preventing any
|
||||
-- further requests until a higher h-value would be expected.
|
||||
-- session.log("debug", "*** SMACKS(2) ***: #queue=%s, max_unacked_stanzas=%s, expected_h=%s, last_requested_h=%s", tostring(#queue), tostring(max_unacked_stanzas), tostring(expected_h), tostring(session.last_requested_h));
|
||||
if (#queue > max_unacked_stanzas and expected_h ~= session.last_requested_h) or force then
|
||||
session.log("debug", "Queuing <r> (in a moment) from %s - #queue=%d", reason, #queue);
|
||||
session.awaiting_ack = false;
|
||||
session.awaiting_ack_timer = stoppable_timer(1e-06, function ()
|
||||
-- session.log("debug", "*** SMACKS(3) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
|
||||
-- only request ack if needed and our session is not already hibernated or destroyed
|
||||
if not session.awaiting_ack and not session.hibernating and not session.destroyed then
|
||||
session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, #queue);
|
||||
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
|
||||
session.awaiting_ack = true;
|
||||
-- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile)
|
||||
session.last_requested_h = session.last_acknowledged_stanza + #queue;
|
||||
session.log("debug", "Sending <r> (inside timer, after send) from %s - #queue=%d", reason, #queue);
|
||||
if not session.delayed_ack_timer then
|
||||
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
|
||||
delayed_ack_function(session);
|
||||
end);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
-- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue
|
||||
-- and there isn't already a timer for this event running.
|
||||
-- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event
|
||||
-- would not trigger this event (again).
|
||||
if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then
|
||||
session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)");
|
||||
delayed_ack_function(session);
|
||||
end
|
||||
end
|
||||
|
||||
local function outgoing_stanza_filter(stanza, session)
|
||||
-- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's
|
||||
-- supposed to be nil.
|
||||
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
|
||||
-- stanzas/out filter can get called before this one and adds the xmlns.
|
||||
local is_stanza = stanza.attr and
|
||||
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
|
||||
and not stanza.name:find":";
|
||||
|
||||
if is_stanza and not stanza._cached then
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
local cached_stanza = st.clone(stanza);
|
||||
cached_stanza._cached = true;
|
||||
|
||||
if cached_stanza and cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then
|
||||
cached_stanza = cached_stanza:tag("delay", {
|
||||
xmlns = xmlns_delay,
|
||||
from = jid.bare(session.full_jid or session.host),
|
||||
stamp = datetime.datetime()
|
||||
});
|
||||
end
|
||||
|
||||
queue[#queue+1] = cached_stanza;
|
||||
if session.hibernating then
|
||||
session.log("debug", "hibernating, stanza queued");
|
||||
module:fire_event("smacks-hibernation-stanza-queued", {origin = session, queue = queue, stanza = cached_stanza});
|
||||
return nil;
|
||||
end
|
||||
request_ack_if_needed(session, false, "outgoing_stanza_filter");
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function count_incoming_stanzas(stanza, session)
|
||||
if not stanza.attr.xmlns then
|
||||
session.handled_stanza_count = session.handled_stanza_count + 1;
|
||||
session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count);
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function wrap_session_out(session, resume)
|
||||
if not resume then
|
||||
session.outgoing_stanza_queue = {};
|
||||
session.last_acknowledged_stanza = 0;
|
||||
end
|
||||
|
||||
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
|
||||
|
||||
local session_close = session.close;
|
||||
function session.close(...)
|
||||
if session.resumption_token then
|
||||
session_registry.set(session.username, session.resumption_token, nil);
|
||||
old_session_registry.set(session.username, session.resumption_token, nil);
|
||||
session.resumption_token = nil;
|
||||
end
|
||||
-- send out last ack as per revision 1.5.2 of XEP-0198
|
||||
if session.smacks and session.conn then
|
||||
(session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = string.format("%d", session.handled_stanza_count) }));
|
||||
end
|
||||
return session_close(...);
|
||||
end
|
||||
return session;
|
||||
end
|
||||
|
||||
local function wrap_session_in(session, resume)
|
||||
if not resume then
|
||||
session.handled_stanza_count = 0;
|
||||
end
|
||||
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
|
||||
|
||||
return session;
|
||||
end
|
||||
|
||||
local function wrap_session(session, resume)
|
||||
wrap_session_out(session, resume);
|
||||
wrap_session_in(session, resume);
|
||||
return session;
|
||||
end
|
||||
|
||||
function handle_enable(session, stanza, xmlns_sm)
|
||||
local ok, err, err_text = can_do_smacks(session);
|
||||
if not ok then
|
||||
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
|
||||
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
|
||||
return true;
|
||||
end
|
||||
|
||||
module:log("debug", "Enabling stream management");
|
||||
session.smacks = xmlns_sm;
|
||||
|
||||
wrap_session(session, false);
|
||||
|
||||
local resume_token;
|
||||
local resume = stanza.attr.resume;
|
||||
if resume == "true" or resume == "1" then
|
||||
resume_token = uuid_generate();
|
||||
session_registry.set(session.username, resume_token, session);
|
||||
session.resumption_token = resume_token;
|
||||
end
|
||||
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = tostring(resume_timeout) }));
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
|
||||
module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
|
||||
|
||||
module:hook_stanza("http://etherx.jabber.org/streams", "features",
|
||||
function (session, stanza)
|
||||
stoppable_timer(1e-6, function ()
|
||||
if can_do_smacks(session) then
|
||||
if stanza:get_child("sm", xmlns_sm3) then
|
||||
session.sends2s(st.stanza("enable", sm3_attr));
|
||||
session.smacks = xmlns_sm3;
|
||||
elseif stanza:get_child("sm", xmlns_sm2) then
|
||||
session.sends2s(st.stanza("enable", sm2_attr));
|
||||
session.smacks = xmlns_sm2;
|
||||
else
|
||||
return;
|
||||
end
|
||||
wrap_session_out(session, false);
|
||||
end
|
||||
end);
|
||||
end);
|
||||
|
||||
function handle_enabled(session, stanza, xmlns_sm)
|
||||
module:log("debug", "Enabling stream management");
|
||||
session.smacks = xmlns_sm;
|
||||
|
||||
wrap_session_in(session, false);
|
||||
|
||||
-- FIXME Resume?
|
||||
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100);
|
||||
module:hook_stanza(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100);
|
||||
|
||||
function handle_r(origin, stanza, xmlns_sm)
|
||||
if not origin.smacks then
|
||||
module:log("debug", "Received ack request from non-smack-enabled session");
|
||||
return;
|
||||
end
|
||||
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
|
||||
-- Reply with <a>
|
||||
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = string.format("%d", origin.handled_stanza_count) }));
|
||||
-- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h)
|
||||
local expected_h = origin.last_acknowledged_stanza + #origin.outgoing_stanza_queue;
|
||||
if #origin.outgoing_stanza_queue > 0 and expected_h ~= origin.last_requested_h then
|
||||
request_ack_if_needed(origin, true, "piggybacked by handle_r");
|
||||
end
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end);
|
||||
module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end);
|
||||
|
||||
function handle_a(origin, stanza)
|
||||
if not origin.smacks then return; end
|
||||
origin.awaiting_ack = nil;
|
||||
if origin.awaiting_ack_timer then
|
||||
origin.awaiting_ack_timer:stop();
|
||||
end
|
||||
if origin.delayed_ack_timer then
|
||||
origin.delayed_ack_timer:stop();
|
||||
origin.delayed_ack_timer = nil;
|
||||
end
|
||||
-- Remove handled stanzas from outgoing_stanza_queue
|
||||
-- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
|
||||
local h = tonumber(stanza.attr.h);
|
||||
if not h then
|
||||
origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; };
|
||||
return;
|
||||
end
|
||||
local handled_stanza_count = h-origin.last_acknowledged_stanza;
|
||||
local queue = origin.outgoing_stanza_queue;
|
||||
if handled_stanza_count > #queue then
|
||||
origin.log("warn", "The client says it handled %d new stanzas, but we only sent %d :)",
|
||||
handled_stanza_count, #queue);
|
||||
origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), origin.last_acknowledged_stanza);
|
||||
for i=1,#queue do
|
||||
origin.log("debug", "Q item %d: %s", i, tostring(queue[i]));
|
||||
end
|
||||
end
|
||||
|
||||
for i=1,math_min(handled_stanza_count,#queue) do
|
||||
local handled_stanza = t_remove(origin.outgoing_stanza_queue, 1);
|
||||
module:fire_event("delivery/success", { session = origin, stanza = handled_stanza });
|
||||
end
|
||||
|
||||
origin.log("debug", "#queue = %d", #queue);
|
||||
origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count;
|
||||
request_ack_if_needed(origin, false, "handle_a")
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "a", handle_a);
|
||||
module:hook_stanza(xmlns_sm3, "a", handle_a);
|
||||
|
||||
--TODO: Optimise... incoming stanzas should be handled by a per-session
|
||||
-- function that has a counter as an upvalue (no table indexing for increments,
|
||||
-- and won't slow non-198 sessions). We can also then remove the .handled flag
|
||||
-- on stanzas
|
||||
|
||||
local function handle_unacked_stanzas(session)
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
local error_attr = { type = "cancel" };
|
||||
if #queue > 0 then
|
||||
session.outgoing_stanza_queue = {};
|
||||
for i=1,#queue do
|
||||
if not module:fire_event("delivery/failure", { session = session, stanza = queue[i] }) then
|
||||
if queue[i].attr.type ~= "error" then
|
||||
local reply = st.reply(queue[i]);
|
||||
if reply.attr.to ~= session.full_jid then
|
||||
reply.attr.type = "error";
|
||||
reply:tag("error", error_attr)
|
||||
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
|
||||
core_process_stanza(session, reply);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- don't send delivery errors for messages which will be delivered by mam later on
|
||||
-- check if stanza was archived --> this will allow us to send back errors for stanzas not archived
|
||||
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
|
||||
local function get_stanza_id(stanza, by_jid)
|
||||
for tag in stanza:childtags("stanza-id", "urn:xmpp:sid:0") do
|
||||
if tag.attr.by == by_jid then
|
||||
return tag.attr.id;
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
module:hook("delivery/failure", function(event)
|
||||
local session, stanza = event.session, event.stanza;
|
||||
-- Only deal with authenticated (c2s) sessions
|
||||
if session.username then
|
||||
if stanza.name == "message" and stanza.attr.xmlns == nil and
|
||||
( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then
|
||||
-- do nothing here for normal messages and don't send out "message delivery errors",
|
||||
-- because messages are already in MAM at this point (no need to frighten users)
|
||||
local stanza_id = get_stanza_id(stanza, jid.bare(session.full_jid));
|
||||
if session.mam_requested and stanza_id ~= nil then
|
||||
session.log("debug", "mod_smacks delivery/failuere returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id));
|
||||
return true; -- stanza handled, don't send an error
|
||||
end
|
||||
-- store message in offline store, if this client does not use mam *and* was the last client online
|
||||
local sessions = prosody.hosts[module.host].sessions[session.username] and
|
||||
prosody.hosts[module.host].sessions[session.username].sessions or nil;
|
||||
if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then
|
||||
local ok = module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
|
||||
session.log("debug", "mod_smacks delivery/failuere returning %s for offline-handled stanza", tostring(ok));
|
||||
return ok; -- if stanza was handled, don't send an error
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("pre-resource-unbind", function (event)
|
||||
local session, err = event.session, event.error;
|
||||
if session.smacks then
|
||||
if not session.resumption_token then
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
if #queue > 0 then
|
||||
session.log("debug", "Destroying session with %d unacked stanzas", #queue);
|
||||
handle_unacked_stanzas(session);
|
||||
end
|
||||
else
|
||||
session.log("debug", "mod_smacks hibernating session for up to %d seconds", resume_timeout);
|
||||
local hibernate_time = os_time(); -- Track the time we went into hibernation
|
||||
session.hibernating = hibernate_time;
|
||||
local resumption_token = session.resumption_token;
|
||||
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
|
||||
timer.add_task(resume_timeout, function ()
|
||||
session.log("debug", "mod_smacks hibernation timeout reached...");
|
||||
-- We need to check the current resumption token for this resource
|
||||
-- matches the smacks session this timer is for in case it changed
|
||||
-- (for example, the client may have bound a new resource and
|
||||
-- started a new smacks session, or not be using smacks)
|
||||
local curr_session = full_sessions[session.full_jid];
|
||||
if session.destroyed then
|
||||
session.log("debug", "The session has already been destroyed");
|
||||
elseif curr_session and curr_session.resumption_token == resumption_token
|
||||
-- Check the hibernate time still matches what we think it is,
|
||||
-- otherwise the session resumed and re-hibernated.
|
||||
and session.hibernating == hibernate_time then
|
||||
-- wait longer if the timeout isn't reached because push was enabled for this session
|
||||
-- session.first_hibernated_push is the starting point for hibernation timeouts of those push enabled clients
|
||||
-- wait for an additional resume_timeout seconds if no push occured since hibernation at all
|
||||
local current_time = os_time();
|
||||
local timeout_start = math_max(session.hibernating, session.first_hibernated_push or session.hibernating);
|
||||
if session.push_identifier ~= nil and not session.first_hibernated_push then
|
||||
session.log("debug", "No push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout);
|
||||
return resume_timeout;
|
||||
end
|
||||
if current_time-timeout_start < resume_timeout and session.push_identifier ~= nil then
|
||||
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", current_time-timeout_start);
|
||||
return current_time-timeout_start; -- time left to wait
|
||||
end
|
||||
session.log("debug", "Destroying session for hibernating too long");
|
||||
session_registry.set(session.username, session.resumption_token, nil);
|
||||
-- save only actual h value and username/host (for security)
|
||||
old_session_registry.set(session.username, session.resumption_token, {
|
||||
h = session.handled_stanza_count,
|
||||
username = session.username,
|
||||
host = session.host
|
||||
});
|
||||
session.resumption_token = nil;
|
||||
sessionmanager.destroy_session(session);
|
||||
else
|
||||
session.log("debug", "Session resumed before hibernation timeout, all is well")
|
||||
end
|
||||
end);
|
||||
return true; -- Postpone destruction for now
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
local function handle_s2s_destroyed(event)
|
||||
local session = event.session;
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
if queue and #queue > 0 then
|
||||
session.log("warn", "Destroying session with %d unacked stanzas", #queue);
|
||||
if s2s_resend then
|
||||
for i = 1, #queue do
|
||||
module:send(queue[i]);
|
||||
end
|
||||
session.outgoing_stanza_queue = nil;
|
||||
else
|
||||
handle_unacked_stanzas(session);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("s2sout-destroyed", handle_s2s_destroyed);
|
||||
module:hook("s2sin-destroyed", handle_s2s_destroyed);
|
||||
|
||||
local function get_session_id(session)
|
||||
return session.id or (tostring(session):match("[a-f0-9]+$"));
|
||||
end
|
||||
|
||||
function handle_resume(session, stanza, xmlns_sm)
|
||||
if session.full_jid then
|
||||
session.log("warn", "Tried to resume after resource binding");
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("unexpected-request", { xmlns = xmlns_errors })
|
||||
);
|
||||
return true;
|
||||
end
|
||||
|
||||
local id = stanza.attr.previd;
|
||||
local original_session = session_registry.get(session.username, id);
|
||||
if not original_session then
|
||||
session.log("debug", "Tried to resume non-existent session with id %s", id);
|
||||
local old_session = old_session_registry.get(session.username, id);
|
||||
if old_session and session.username == old_session.username
|
||||
and session.host == old_session.host
|
||||
and old_session.h then
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = string.format("%d", old_session.h) })
|
||||
:tag("item-not-found", { xmlns = xmlns_errors })
|
||||
);
|
||||
else
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("item-not-found", { xmlns = xmlns_errors })
|
||||
);
|
||||
end;
|
||||
elseif session.username == original_session.username
|
||||
and session.host == original_session.host then
|
||||
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
|
||||
original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session));
|
||||
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
|
||||
if original_session.conn then
|
||||
original_session.log("debug", "mod_smacks closing an old connection for this session");
|
||||
local conn = original_session.conn;
|
||||
c2s_sessions[conn] = nil;
|
||||
conn:close();
|
||||
end
|
||||
local migrated_session_log = session.log;
|
||||
original_session.ip = session.ip;
|
||||
original_session.conn = session.conn;
|
||||
original_session.send = session.send;
|
||||
original_session.close = session.close;
|
||||
original_session.filter = session.filter;
|
||||
original_session.filter.session = original_session;
|
||||
original_session.filters = session.filters;
|
||||
original_session.stream = session.stream;
|
||||
original_session.secure = session.secure;
|
||||
original_session.hibernating = nil;
|
||||
session.log = original_session.log;
|
||||
session.type = original_session.type;
|
||||
wrap_session(original_session, true);
|
||||
-- Inform xmppstream of the new session (passed to its callbacks)
|
||||
original_session.stream:set_session(original_session);
|
||||
-- Similar for connlisteners
|
||||
c2s_sessions[session.conn] = original_session;
|
||||
|
||||
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
|
||||
h = string.format("%d", original_session.handled_stanza_count), previd = id }));
|
||||
|
||||
-- Fake an <a> with the h of the <resume/> from the client
|
||||
original_session:dispatch_stanza(st.stanza("a", { xmlns = xmlns_sm,
|
||||
h = stanza.attr.h }));
|
||||
|
||||
-- Ok, we need to re-send any stanzas that the client didn't see
|
||||
-- ...they are what is now left in the outgoing stanza queue
|
||||
-- We have to use the send of "session" because we don't want to add our resent stanzas
|
||||
-- to the outgoing queue again
|
||||
local queue = original_session.outgoing_stanza_queue;
|
||||
session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", #queue);
|
||||
for i=1,#queue do
|
||||
session.send(queue[i]);
|
||||
end
|
||||
session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", #queue);
|
||||
function session.send(stanza)
|
||||
migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
|
||||
return false;
|
||||
end
|
||||
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue});
|
||||
request_ack_if_needed(original_session, true, "handle_resume");
|
||||
else
|
||||
module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]",
|
||||
session.username or "?", session.host or "?", session.type,
|
||||
original_session.username or "?", original_session.host or "?", original_session.type);
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("not-authorized", { xmlns = xmlns_errors }));
|
||||
end
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
|
||||
module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
|
||||
|
||||
local function handle_read_timeout(event)
|
||||
local session = event.session;
|
||||
if session.smacks then
|
||||
if session.awaiting_ack then
|
||||
if session.awaiting_ack_timer then
|
||||
session.awaiting_ack_timer:stop();
|
||||
end
|
||||
if session.delayed_ack_timer then
|
||||
session.delayed_ack_timer:stop();
|
||||
session.delayed_ack_timer = nil;
|
||||
end
|
||||
return false; -- Kick the session
|
||||
end
|
||||
session.log("debug", "Sending <r> (read timeout)");
|
||||
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
|
||||
session.awaiting_ack = true;
|
||||
if not session.delayed_ack_timer then
|
||||
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
|
||||
delayed_ack_function(session);
|
||||
end);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("s2s-read-timeout", handle_read_timeout);
|
||||
module:hook("c2s-read-timeout", handle_read_timeout);
|
||||
@@ -0,0 +1,586 @@
|
||||
-- XEP-0198: Stream Management for Prosody IM
|
||||
--
|
||||
-- Copyright (C) 2010-2015 Matthew Wild
|
||||
-- Copyright (C) 2010 Waqas Hussain
|
||||
-- Copyright (C) 2012-2015 Kim Alvefur
|
||||
-- Copyright (C) 2012 Thijs Alkemade
|
||||
-- Copyright (C) 2014 Florian Zeitz
|
||||
-- Copyright (C) 2016-2017 Thilo Molitor
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local st = require "util.stanza";
|
||||
local dep = require "util.dependencies";
|
||||
local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
|
||||
local uuid_generate = require "util.uuid".generate;
|
||||
local jid = require "util.jid";
|
||||
|
||||
local t_insert, t_remove = table.insert, table.remove;
|
||||
local math_min = math.min;
|
||||
local math_max = math.max;
|
||||
local os_time = os.time;
|
||||
local tonumber, tostring = tonumber, tostring;
|
||||
local add_filter = require "util.filters".add_filter;
|
||||
local timer = require "util.timer";
|
||||
local datetime = require "util.datetime";
|
||||
|
||||
local xmlns_sm2 = "urn:xmpp:sm:2";
|
||||
local xmlns_sm3 = "urn:xmpp:sm:3";
|
||||
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
|
||||
local xmlns_delay = "urn:xmpp:delay";
|
||||
|
||||
local sm2_attr = { xmlns = xmlns_sm2 };
|
||||
local sm3_attr = { xmlns = xmlns_sm3 };
|
||||
|
||||
local resume_timeout = module:get_option_number("smacks_hibernation_time", 300);
|
||||
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
|
||||
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
|
||||
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
|
||||
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60);
|
||||
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
|
||||
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
|
||||
local core_process_stanza = prosody.core_process_stanza;
|
||||
local sessionmanager = require"core.sessionmanager";
|
||||
|
||||
local c2s_sessions = module:shared("/*/c2s/sessions");
|
||||
|
||||
local function init_session_cache(max_entries, evict_callback)
|
||||
-- old prosody version < 0.10 (no limiting at all!)
|
||||
if not cache then
|
||||
local store = {};
|
||||
return {
|
||||
get = function(user, key)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
return store[key];
|
||||
end;
|
||||
set = function(user, key, value)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
store[key] = value;
|
||||
end;
|
||||
};
|
||||
end
|
||||
|
||||
-- use per user limited cache for prosody >= 0.10
|
||||
local stores = {};
|
||||
return {
|
||||
get = function(user, key)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
if not stores[user] then
|
||||
stores[user] = cache.new(max_entries, evict_callback);
|
||||
end
|
||||
return stores[user]:get(key);
|
||||
end;
|
||||
set = function(user, key, value)
|
||||
if not user then return nil; end
|
||||
if not key then return nil; end
|
||||
if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end
|
||||
stores[user]:set(key, value);
|
||||
-- remove empty caches completely
|
||||
if not stores[user]:count() then stores[user] = nil; end
|
||||
end;
|
||||
};
|
||||
end
|
||||
local old_session_registry = init_session_cache(max_old_sessions, nil);
|
||||
local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session)
|
||||
if session.destroyed then return true; end -- destroyed session can always be removed from cache
|
||||
session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token);
|
||||
-- store old session's h values on force delete
|
||||
-- save only actual h value and username/host (for security)
|
||||
old_session_registry.set(session.username, resumption_token, {
|
||||
h = session.handled_stanza_count,
|
||||
username = session.username,
|
||||
host = session.host
|
||||
});
|
||||
return true; -- allow session to be removed from full cache to make room for new one
|
||||
end);
|
||||
|
||||
local function stoppable_timer(delay, callback)
|
||||
local stopped = false;
|
||||
return {
|
||||
stop = function () stopped = true end;
|
||||
module:add_timer(delay, function (t)
|
||||
if stopped then return; end
|
||||
return callback(t);
|
||||
end);
|
||||
};
|
||||
end
|
||||
|
||||
local function delayed_ack_function(session)
|
||||
-- fire event only if configured to do so and our session is not hibernated or destroyed
|
||||
if delayed_ack_timeout > 0 and session.awaiting_ack
|
||||
and not session.hibernating and not session.destroyed then
|
||||
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d",
|
||||
session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0);
|
||||
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue});
|
||||
end
|
||||
session.delayed_ack_timer = nil;
|
||||
end
|
||||
|
||||
local function can_do_smacks(session, advertise_only)
|
||||
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
|
||||
|
||||
local session_type = session.type;
|
||||
if session.username then
|
||||
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
|
||||
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
|
||||
end
|
||||
return true;
|
||||
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
|
||||
return true;
|
||||
end
|
||||
return false, "service-unavailable", "Stream management is not available for this stream";
|
||||
end
|
||||
|
||||
module:hook("stream-features",
|
||||
function (event)
|
||||
if can_do_smacks(event.origin, true) then
|
||||
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
|
||||
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("s2s-stream-features",
|
||||
function (event)
|
||||
if can_do_smacks(event.origin, true) then
|
||||
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
|
||||
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
|
||||
end
|
||||
end);
|
||||
|
||||
local function request_ack_if_needed(session, force)
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
if session.awaiting_ack == nil and not session.hibernating then
|
||||
if (#queue > max_unacked_stanzas and session.last_queue_count ~= #queue) or force then
|
||||
session.log("debug", "Queuing <r> (in a moment)");
|
||||
session.awaiting_ack = false;
|
||||
session.awaiting_ack_timer = stoppable_timer(1e-06, function ()
|
||||
if not session.awaiting_ack and not session.hibernating then
|
||||
session.log("debug", "Sending <r> (inside timer, before send)");
|
||||
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
|
||||
session.log("debug", "Sending <r> (inside timer, after send)");
|
||||
session.awaiting_ack = true;
|
||||
if not session.delayed_ack_timer then
|
||||
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
|
||||
delayed_ack_function(session);
|
||||
end);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
-- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue
|
||||
-- and there isn't already a timer for this event running.
|
||||
-- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event
|
||||
-- would not trigger this event (again).
|
||||
if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then
|
||||
session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)");
|
||||
delayed_ack_function(session);
|
||||
end
|
||||
|
||||
session.last_queue_count = #queue;
|
||||
end
|
||||
|
||||
local function outgoing_stanza_filter(stanza, session)
|
||||
local is_stanza = stanza.attr and not stanza.attr.xmlns and not stanza.name:find":";
|
||||
if is_stanza and not stanza._cached then -- Stanza in default stream namespace
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
local cached_stanza = st.clone(stanza);
|
||||
cached_stanza._cached = true;
|
||||
|
||||
if cached_stanza and cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then
|
||||
cached_stanza = cached_stanza:tag("delay", {
|
||||
xmlns = xmlns_delay,
|
||||
from = jid.bare(session.full_jid or session.host),
|
||||
stamp = datetime.datetime()
|
||||
});
|
||||
end
|
||||
|
||||
queue[#queue+1] = cached_stanza;
|
||||
session.log("debug", "#queue = %d", #queue);
|
||||
if session.hibernating then
|
||||
session.log("debug", "hibernating, stanza queued");
|
||||
return nil;
|
||||
end
|
||||
request_ack_if_needed(session, false);
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function count_incoming_stanzas(stanza, session)
|
||||
if not stanza.attr.xmlns then
|
||||
session.handled_stanza_count = session.handled_stanza_count + 1;
|
||||
session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count);
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function wrap_session_out(session, resume)
|
||||
if not resume then
|
||||
session.outgoing_stanza_queue = {};
|
||||
session.last_acknowledged_stanza = 0;
|
||||
end
|
||||
|
||||
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
|
||||
|
||||
local session_close = session.close;
|
||||
function session.close(...)
|
||||
if session.resumption_token then
|
||||
session_registry.set(session.username, session.resumption_token, nil);
|
||||
old_session_registry.set(session.username, session.resumption_token, nil);
|
||||
session.resumption_token = nil;
|
||||
end
|
||||
-- send out last ack as per revision 1.5.2 of XEP-0198
|
||||
if session.smacks and session.conn then
|
||||
(session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = string.format("%d", session.handled_stanza_count) ,t = tostring(os.time())) }));
|
||||
end
|
||||
return session_close(...);
|
||||
end
|
||||
return session;
|
||||
end
|
||||
|
||||
local function wrap_session_in(session, resume)
|
||||
if not resume then
|
||||
session.handled_stanza_count = 0;
|
||||
end
|
||||
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
|
||||
|
||||
return session;
|
||||
end
|
||||
|
||||
local function wrap_session(session, resume)
|
||||
wrap_session_out(session, resume);
|
||||
wrap_session_in(session, resume);
|
||||
return session;
|
||||
end
|
||||
|
||||
function handle_enable(session, stanza, xmlns_sm)
|
||||
local ok, err, err_text = can_do_smacks(session);
|
||||
if not ok then
|
||||
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
|
||||
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
|
||||
return true;
|
||||
end
|
||||
|
||||
module:log("debug", "Enabling stream management");
|
||||
session.smacks = xmlns_sm;
|
||||
|
||||
wrap_session(session, false);
|
||||
|
||||
local resume_token;
|
||||
local resume = stanza.attr.resume;
|
||||
if resume == "true" or resume == "1" then
|
||||
resume_token = uuid_generate();
|
||||
session_registry.set(session.username, resume_token, session);
|
||||
session.resumption_token = resume_token;
|
||||
end
|
||||
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = tostring(resume_timeout) }));
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
|
||||
module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
|
||||
|
||||
module:hook_stanza("http://etherx.jabber.org/streams", "features",
|
||||
function (session, stanza)
|
||||
stoppable_timer(1e-6, function ()
|
||||
if can_do_smacks(session) then
|
||||
if stanza:get_child("sm", xmlns_sm3) then
|
||||
session.sends2s(st.stanza("enable", sm3_attr));
|
||||
session.smacks = xmlns_sm3;
|
||||
elseif stanza:get_child("sm", xmlns_sm2) then
|
||||
session.sends2s(st.stanza("enable", sm2_attr));
|
||||
session.smacks = xmlns_sm2;
|
||||
else
|
||||
return;
|
||||
end
|
||||
wrap_session_out(session, false);
|
||||
end
|
||||
end);
|
||||
end);
|
||||
|
||||
function handle_enabled(session, stanza, xmlns_sm)
|
||||
module:log("debug", "Enabling stream management");
|
||||
session.smacks = xmlns_sm;
|
||||
|
||||
wrap_session_in(session, false);
|
||||
|
||||
-- FIXME Resume?
|
||||
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100);
|
||||
module:hook_stanza(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100);
|
||||
|
||||
function handle_r(origin, stanza, xmlns_sm)
|
||||
if not origin.smacks then
|
||||
module:log("debug", "Received ack request from non-smack-enabled session");
|
||||
return;
|
||||
end
|
||||
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
|
||||
-- Reply with <a>
|
||||
-- ssa: add t with os.time
|
||||
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = string.format("%d", origin.handled_stanza_count), t = tostring(os.time()) }));
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end);
|
||||
module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end);
|
||||
|
||||
function handle_a(origin, stanza)
|
||||
if not origin.smacks then return; end
|
||||
origin.awaiting_ack = nil;
|
||||
if origin.awaiting_ack_timer then
|
||||
origin.awaiting_ack_timer:stop();
|
||||
end
|
||||
if origin.delayed_ack_timer then
|
||||
origin.delayed_ack_timer:stop();
|
||||
origin.delayed_ack_timer = nil;
|
||||
end
|
||||
-- Remove handled stanzas from outgoing_stanza_queue
|
||||
-- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
|
||||
local h = tonumber(stanza.attr.h);
|
||||
if not h then
|
||||
origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; };
|
||||
end
|
||||
local handled_stanza_count = h-origin.last_acknowledged_stanza;
|
||||
local queue = origin.outgoing_stanza_queue;
|
||||
if handled_stanza_count > #queue then
|
||||
origin.log("warn", "The client says it handled %d new stanzas, but we only sent %d :)",
|
||||
handled_stanza_count, #queue);
|
||||
origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), origin.last_acknowledged_stanza);
|
||||
for i=1,#queue do
|
||||
origin.log("debug", "Q item %d: %s", i, tostring(queue[i]));
|
||||
end
|
||||
end
|
||||
for i=1,math_min(handled_stanza_count,#queue) do
|
||||
t_remove(origin.outgoing_stanza_queue, 1);
|
||||
end
|
||||
origin.log("debug", "#queue = %d", #queue);
|
||||
origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count;
|
||||
request_ack_if_needed(origin, false)
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "a", handle_a);
|
||||
module:hook_stanza(xmlns_sm3, "a", handle_a);
|
||||
|
||||
--TODO: Optimise... incoming stanzas should be handled by a per-session
|
||||
-- function that has a counter as an upvalue (no table indexing for increments,
|
||||
-- and won't slow non-198 sessions). We can also then remove the .handled flag
|
||||
-- on stanzas
|
||||
|
||||
function handle_unacked_stanzas(session)
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
local error_attr = { type = "cancel" };
|
||||
if #queue > 0 then
|
||||
session.outgoing_stanza_queue = {};
|
||||
for i=1,#queue do
|
||||
local reply = st.reply(queue[i]);
|
||||
if reply.attr.to ~= session.full_jid then
|
||||
reply.attr.type = "error";
|
||||
reply:tag("error", error_attr)
|
||||
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
|
||||
core_process_stanza(session, reply);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("pre-resource-unbind", function (event)
|
||||
local session, err = event.session, event.error;
|
||||
if session.smacks then
|
||||
if not session.resumption_token then
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
if #queue > 0 then
|
||||
session.log("debug", "Destroying session with %d unacked stanzas", #queue);
|
||||
handle_unacked_stanzas(session);
|
||||
end
|
||||
else
|
||||
session.log("debug", "mod_smacks hibernating session for up to %d seconds", resume_timeout);
|
||||
local hibernate_time = os_time(); -- Track the time we went into hibernation
|
||||
session.hibernating = hibernate_time;
|
||||
local resumption_token = session.resumption_token;
|
||||
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
|
||||
timer.add_task(resume_timeout, function ()
|
||||
session.log("debug", "mod_smacks hibernation timeout reached...");
|
||||
-- We need to check the current resumption token for this resource
|
||||
-- matches the smacks session this timer is for in case it changed
|
||||
-- (for example, the client may have bound a new resource and
|
||||
-- started a new smacks session, or not be using smacks)
|
||||
local curr_session = full_sessions[session.full_jid];
|
||||
if session.destroyed then
|
||||
session.log("debug", "The session has already been destroyed");
|
||||
elseif curr_session and curr_session.resumption_token == resumption_token
|
||||
-- Check the hibernate time still matches what we think it is,
|
||||
-- otherwise the session resumed and re-hibernated.
|
||||
and session.hibernating == hibernate_time then
|
||||
-- wait longer if the timeout isn't reached because push was enabled for this session
|
||||
-- session.first_hibernated_push is the starting point for hibernation timeouts of those push enabled clients
|
||||
-- wait for an additional resume_timeout seconds if no push occured since hibernation at all
|
||||
local current_time = os_time();
|
||||
local timeout_start = math_max(session.hibernating, session.first_hibernated_push or session.hibernating);
|
||||
if session.push_identifier ~= nil and not session.first_hibernated_push then
|
||||
session.log("debug", "No push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout);
|
||||
return resume_timeout;
|
||||
end
|
||||
if current_time-timeout_start < resume_timeout and session.push_identifier ~= nil then
|
||||
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", current_time-timeout_start);
|
||||
return current_time-timeout_start; -- time left to wait
|
||||
end
|
||||
session.log("debug", "Destroying session for hibernating too long");
|
||||
session_registry.set(session.username, session.resumption_token, nil);
|
||||
-- save only actual h value and username/host (for security)
|
||||
old_session_registry.set(session.username, session.resumption_token, {
|
||||
h = session.handled_stanza_count,
|
||||
username = session.username,
|
||||
host = session.host
|
||||
});
|
||||
session.resumption_token = nil;
|
||||
sessionmanager.destroy_session(session);
|
||||
else
|
||||
session.log("debug", "Session resumed before hibernation timeout, all is well")
|
||||
end
|
||||
end);
|
||||
return true; -- Postpone destruction for now
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
local function handle_s2s_destroyed(event)
|
||||
local session = event.session;
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
if queue and #queue > 0 then
|
||||
session.log("warn", "Destroying session with %d unacked stanzas", #queue);
|
||||
if s2s_resend then
|
||||
for i = 1, #queue do
|
||||
module:send(queue[i]);
|
||||
end
|
||||
session.outgoing_stanza_queue = nil;
|
||||
else
|
||||
handle_unacked_stanzas(session);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("s2sout-destroyed", handle_s2s_destroyed);
|
||||
module:hook("s2sin-destroyed", handle_s2s_destroyed);
|
||||
|
||||
local function get_session_id(session)
|
||||
return session.id or (tostring(session):match("[a-f0-9]+$"));
|
||||
end
|
||||
|
||||
function handle_resume(session, stanza, xmlns_sm)
|
||||
if session.full_jid then
|
||||
session.log("warn", "Tried to resume after resource binding");
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("unexpected-request", { xmlns = xmlns_errors })
|
||||
);
|
||||
return true;
|
||||
end
|
||||
|
||||
local id = stanza.attr.previd;
|
||||
local original_session = session_registry.get(session.username, id);
|
||||
if not original_session then
|
||||
session.log("debug", "Tried to resume non-existent session with id %s", id);
|
||||
local old_session = old_session_registry.get(session.username, id);
|
||||
if old_session and session.username == old_session.username
|
||||
and session.host == old_session.host
|
||||
and old_session.h then
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = string.format("%d", old_session.h) })
|
||||
:tag("item-not-found", { xmlns = xmlns_errors })
|
||||
);
|
||||
else
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("item-not-found", { xmlns = xmlns_errors })
|
||||
);
|
||||
end;
|
||||
elseif session.username == original_session.username
|
||||
and session.host == original_session.host then
|
||||
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
|
||||
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
|
||||
if original_session.conn then
|
||||
original_session.log("debug", "mod_smacks closing an old connection for this session");
|
||||
local conn = original_session.conn;
|
||||
c2s_sessions[conn] = nil;
|
||||
conn:close();
|
||||
end
|
||||
original_session.ip = session.ip;
|
||||
original_session.conn = session.conn;
|
||||
original_session.send = session.send;
|
||||
original_session.close = session.close;
|
||||
original_session.filter = session.filter;
|
||||
original_session.filter.session = original_session;
|
||||
original_session.filters = session.filters;
|
||||
original_session.stream = session.stream;
|
||||
original_session.secure = session.secure;
|
||||
original_session.hibernating = nil;
|
||||
wrap_session(original_session, true);
|
||||
-- Inform xmppstream of the new session (passed to its callbacks)
|
||||
original_session.stream:set_session(original_session);
|
||||
-- Similar for connlisteners
|
||||
c2s_sessions[session.conn] = original_session;
|
||||
|
||||
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
|
||||
h = string.format("%d", original_session.handled_stanza_count), previd = id }));
|
||||
|
||||
-- Fake an <a> with the h of the <resume/> from the client
|
||||
original_session:dispatch_stanza(st.stanza("a", { xmlns = xmlns_sm,
|
||||
h = stanza.attr.h }));
|
||||
|
||||
-- Ok, we need to re-send any stanzas that the client didn't see
|
||||
-- ...they are what is now left in the outgoing stanza queue
|
||||
local queue = original_session.outgoing_stanza_queue;
|
||||
original_session.log("debug", "#queue = %d", #queue);
|
||||
for i=1,#queue do
|
||||
original_session.send(queue[i]);
|
||||
end
|
||||
original_session.log("debug", "#queue = %d -- after send", #queue);
|
||||
function session.send(stanza)
|
||||
session.log("warn", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
|
||||
return false;
|
||||
end
|
||||
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue});
|
||||
request_ack_if_needed(original_session, true);
|
||||
else
|
||||
module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]",
|
||||
session.username or "?", session.host or "?", session.type,
|
||||
original_session.username or "?", original_session.host or "?", original_session.type);
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("not-authorized", { xmlns = xmlns_errors }));
|
||||
end
|
||||
return true;
|
||||
end
|
||||
module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
|
||||
module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
|
||||
|
||||
local function handle_read_timeout(event)
|
||||
local session = event.session;
|
||||
if session.smacks then
|
||||
if session.awaiting_ack then
|
||||
if session.awaiting_ack_timer then
|
||||
session.awaiting_ack_timer:stop();
|
||||
end
|
||||
if session.delayed_ack_timer then
|
||||
session.delayed_ack_timer:stop();
|
||||
session.delayed_ack_timer = nil;
|
||||
end
|
||||
return false; -- Kick the session
|
||||
end
|
||||
session.log("debug", "Sending <r> (read timeout)");
|
||||
session.awaiting_ack = false;
|
||||
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
|
||||
session.awaiting_ack = true;
|
||||
if not session.delayed_ack_timer then
|
||||
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
|
||||
delayed_ack_function(session);
|
||||
end);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("s2s-read-timeout", handle_read_timeout);
|
||||
module:hook("c2s-read-timeout", handle_read_timeout);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- this module is deprecated, log an error and load the superseding module instead
|
||||
module:depends"smacks"
|
||||
|
||||
module:log("info", "mod_smacks_offline is no lobger required.");
|
||||
@@ -0,0 +1,174 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2018 Michel Le Bihan
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local st = require "util.stanza"
|
||||
local jid_split = require "util.jid".split;
|
||||
local base64 = require"util.encodings".base64;
|
||||
local sha1 = require"util.hashes".sha1;
|
||||
local http = require "net.http";
|
||||
|
||||
local mod_muc = module:depends"muc";
|
||||
|
||||
local vcards = module:open_store();
|
||||
|
||||
local avatar_upload_url = module:get_option("avatar_upload_url", "");
|
||||
local avatar_upload_user = module:get_option("avatar_upload_user", "");
|
||||
local avatar_upload_pass = module:get_option("avatar_upload_pass", "");
|
||||
|
||||
local avatar_update_store = module:open_store("avatarupdate", "map");
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function putGlobalAvatar(_room_jid, _photo)
|
||||
local to_url = avatar_upload_url.._room_jid;
|
||||
module:log("info", "upload target URL: %s", to_url);
|
||||
local httpRequestOptions = {
|
||||
method = "PUT",
|
||||
body = _photo,
|
||||
headers = {
|
||||
["Content-Type"] = "image/png"
|
||||
}
|
||||
}
|
||||
|
||||
if avatar_upload_user ~= "" then
|
||||
httpRequestOptions = {
|
||||
method = "PUT",
|
||||
body = _photo,
|
||||
headers = {
|
||||
["Content-Type"] = "image/png",
|
||||
["Authorization"] = "Basic "..base64.encode(avatar_upload_user..":"..avatar_upload_pass)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
local function cb(content_, code_, request_, response_)
|
||||
content, code, request, response = content_, code_, request_, response_;
|
||||
module:log("info", "response code is: %s", code);
|
||||
--done();
|
||||
end
|
||||
|
||||
http.request(to_url, httpRequestOptions, cb);
|
||||
|
||||
module:log("info", "avatar upload for %s", _room_jid);
|
||||
end
|
||||
|
||||
|
||||
module:add_feature("vcard-temp");
|
||||
|
||||
local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or
|
||||
function (jid)
|
||||
local rooms = rawget(mod_muc, "rooms");
|
||||
return rooms[jid];
|
||||
end
|
||||
|
||||
local function broadcast_presence(room_jid, to)
|
||||
local room = get_room_from_jid(room_jid);
|
||||
local room_node = jid_split(room_jid);
|
||||
local vcard = st.deserialize(vcards:get(room_node));
|
||||
local photo_hash;
|
||||
|
||||
if vcard then
|
||||
local photo = vcard:get_child("PHOTO");
|
||||
|
||||
if photo then
|
||||
local photo_b64 = photo:get_child_text("BINVAL");
|
||||
local photo_raw = photo_b64 and base64.decode(photo_b64);
|
||||
photo_hash = sha1(photo_raw, true);
|
||||
end
|
||||
end
|
||||
|
||||
local presence_vcard = st.presence({to = to, from = room_jid})
|
||||
:tag("x", { xmlns = "vcard-temp:x:update" })
|
||||
:tag("photo"):text(photo_hash):up();
|
||||
|
||||
if to == nil then
|
||||
room:broadcast_message(presence_vcard);
|
||||
else
|
||||
module:send(presence_vcard);
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_vcard(event)
|
||||
local session, stanza = event.origin, event.stanza;
|
||||
|
||||
local room_jid = stanza.attr.to;
|
||||
local room_node = jid_split(room_jid);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
local from = stanza.attr.from;
|
||||
local from_affiliation = room:get_affiliation(from);
|
||||
|
||||
if stanza.attr.type == "get" then
|
||||
local vCard;
|
||||
vCard = st.deserialize(vcards:get(room_node));
|
||||
|
||||
if vCard then
|
||||
session.send(st.reply(stanza):add_child(vCard)); -- send vCard!
|
||||
else
|
||||
session.send(st.error_reply(stanza, "cancel", "item-not-found"));
|
||||
end
|
||||
else
|
||||
if from_affiliation == "owner" then
|
||||
if vcards:set(room_node, st.preserialize(stanza.tags[1])) then
|
||||
session.send(st.reply(stanza):tag("vCard", { xmlns = "vcard-temp" }));
|
||||
broadcast_presence(room_jid, nil)
|
||||
local vcrd = st.deserialize(vcards:get(room_node));
|
||||
if vcrd then
|
||||
local photo = vcrd:get_child("PHOTO");
|
||||
if photo then
|
||||
local photo_b64 = photo:get_child_text("BINVAL");
|
||||
local photo_raw = photo_b64 and base64.decode(photo_b64);
|
||||
if room_jid then
|
||||
local s = os.time();
|
||||
avatar_update_store:set(room_node, "updated", s);
|
||||
putGlobalAvatar(room_jid, photo_raw);
|
||||
module:log("info", "setting new avatar for %s @ %s", room_jid);
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
else
|
||||
-- TODO unable to write file, file may be locked, etc, what's the correct error?
|
||||
session.send(st.error_reply(stanza, "wait", "internal-server-error"));
|
||||
end
|
||||
else
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
|
||||
module:hook("iq/bare/vcard-temp:vCard", handle_vcard);
|
||||
module:hook("iq/host/vcard-temp:vCard", handle_vcard);
|
||||
|
||||
module:hook("muc-disco#info", function(event)
|
||||
event.reply:tag("feature", { var = "vcard-temp" }):up();
|
||||
end);
|
||||
|
||||
-- module:hook("muc-occupant-session-new", function(event)
|
||||
-- module:log("info", "skipping vcard photo presence");
|
||||
-- broadcast_presence(event.room.jid, event.jid);
|
||||
-- end)
|
||||
@@ -0,0 +1,106 @@
|
||||
local is_admin = require "core.usermanager".is_admin;
|
||||
local allowed_senders = module:get_option_set("broadcast_senders", {});
|
||||
local from_address = module:get_option_string("broadcast_from");
|
||||
local st = require "util.stanza";
|
||||
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid = require "util.jid";
|
||||
local id = require "util.id"
|
||||
|
||||
local mod_muc = module:depends"muc";
|
||||
|
||||
local roster_manager = require("core.rostermanager");
|
||||
|
||||
|
||||
function send_bc_message(from, to, bcid, body, title, origtarget, vnctalk_avatarup)
|
||||
local msgid = id.long();
|
||||
local message = "";
|
||||
if (vnctalk_avatarup) then
|
||||
message = st.message({ from = from, to = to, id = msgid,
|
||||
type = "normal" }):body(body):up():tag("vncTalkBroadcast", { xmlns = "xmpp:vnctalk", id=bcid, title=title, origtarget = origtarget, avatarup = "true"}):up();
|
||||
else
|
||||
message = st.message({ from = from, to = to, id = msgid,
|
||||
type = "normal" }):body(body):up():tag("vncTalkBroadcast", { xmlns = "xmpp:vnctalk", id=bcid, title=title, origtarget = origtarget}):up();
|
||||
-- module:log("info", "built bcmessage: %s", tostring(message));
|
||||
end
|
||||
module:send(message);
|
||||
end
|
||||
|
||||
|
||||
function send_message(event)
|
||||
local stanza = event.stanza;
|
||||
local from = jid.bare(stanza.attr.from);
|
||||
local fnode, fhost = jid.split(from);
|
||||
-- module:log("info", "event.stanza is %s", stanza:get_child_text("body"));
|
||||
|
||||
local body = stanza:get_child_text("body");
|
||||
local vnctalk_broadcast = stanza:get_child("vncTalkBroadcast", "xmpp:vnctalk");
|
||||
local vnctalk_avatarup = stanza:get_child("vnctalkAvatarUpdate", "xmpp:vnctalk" ) or false;
|
||||
-- module:log("info", "vnctalkavatarup %s", vnctalk_avatarup);
|
||||
local vnctalk_broadcast_childid = vnctalk_broadcast.attr.id;
|
||||
|
||||
if vnctalk_broadcast_childid ~= nil then
|
||||
module:log("info", "not casting - is already casted message %s", stanza.attr.body);
|
||||
else
|
||||
new_id = stanza.attr.id;
|
||||
local origtarget = stanza.attr.origtarget;
|
||||
-- module:log("info", "casting broadcast orig target: %s", stanza.attr.origtarget);
|
||||
local bctitle = vnctalk_broadcast.attr.title;
|
||||
local bc_to = stanza:get_child("vncTalkBroadcast", "xmpp:vnctalk"):childtags("to", "xmpp:vnctalk")[1];
|
||||
local t_receipients = {};
|
||||
for ato in vnctalk_broadcast:childtags("to") do
|
||||
local to = ato:get_text();
|
||||
-- module:log("info", "found to: %s", to);
|
||||
if string.find(to, "@conference") then
|
||||
-- module:log("info","add affiliates of %s", to);
|
||||
local muc, host = jid.split(to);
|
||||
-- module:log("info", "got muc %s from host %s", muc, host);
|
||||
if prosody.hosts[host] then
|
||||
local affs = prosody.hosts[host].modules.muc.rooms[to]._affiliations;
|
||||
for _aff, role in pairs(affs) do
|
||||
-- module:log("info", "got muc _aff %s from muc %s", _aff, to);
|
||||
if from ~= _aff then
|
||||
t_receipients[_aff] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
t_receipients[to] = true;
|
||||
end
|
||||
end
|
||||
|
||||
local target_groups = {};
|
||||
|
||||
for rgroup in vnctalk_broadcast:childtags("roster") do
|
||||
local rgrp = rgroup:get_text();
|
||||
-- module:log("info", "found targeted roster group: %s", rgrp);
|
||||
target_groups[rgrp] = true;
|
||||
end
|
||||
|
||||
local roster = roster_manager.load_roster(fnode, fhost);
|
||||
for contact, croster in pairs(roster) do
|
||||
local cgroups = croster.groups;
|
||||
if cgroups ~= nil then
|
||||
for a1, a2 in pairs (cgroups) do
|
||||
if target_groups[a1] then
|
||||
-- module:log("info","adding roster contact from group: %s ==> %s", a1, contact);
|
||||
if from ~= contact then
|
||||
t_receipients[contact] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for receipient, _x in pairs(t_receipients) do
|
||||
module:log("info","sending broadcast message from: %s to: %s with childid %s and body: %s and avatarup: %s", from, receipient, new_id, body, vnctalk_avatarup);
|
||||
send_bc_message(from, receipient, new_id, body, bctitle, origtarget, vnctalk_avatarup);
|
||||
end
|
||||
-- prevent further handling (i.e. storing offline)
|
||||
-- return 1
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- execute first
|
||||
module:hook("message/bare", send_message, 5);
|
||||
@@ -0,0 +1,123 @@
|
||||
--require "socket"
|
||||
--rawset(_G, "PROXY", false); -- socket module accesses global variable PROXY
|
||||
|
||||
local http = require "net.http"
|
||||
local json = require "util.json"
|
||||
-- local ltn12 = require "ltn12"
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local timer = require "util.timer";
|
||||
local st = require "util.stanza";
|
||||
local time_now = os.time;
|
||||
|
||||
local delAPIURL = module:get_option_string("del_api_url");
|
||||
|
||||
local moduleIsActive = delAPIURL;
|
||||
|
||||
local function contains(list, x)
|
||||
if (list) then
|
||||
for _, v in pairs(list) do
|
||||
if (v == x) then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
local function trim(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("^%s*(.-)%s*$", "%1"))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function getPostCallback()
|
||||
return function(body, code, response)
|
||||
if code ~= 200 then
|
||||
module:log("error", "DEL result: HTTP %s", tostring(code))
|
||||
-- if body then module:log("error", body); end
|
||||
end
|
||||
return false -- close request
|
||||
end
|
||||
end
|
||||
|
||||
local function postDEL(_from, _replace)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
|
||||
local delRequest = {
|
||||
from = _from,
|
||||
msgid = _replace
|
||||
};
|
||||
|
||||
local data = json.encode(delRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
module:log("info", "sending delete for message id %s from %s", _replace, _from);
|
||||
--module:log("info", "sendingHTTP request to %s", delAPIURL);
|
||||
http.request(delAPIURL, httpRequestOptions, getPostCallback())
|
||||
end
|
||||
|
||||
|
||||
local function message_handler(event, fromLocal)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local message_id = event.stanza.attr.id;
|
||||
local type = stanza.attr.type or "normal";
|
||||
local from = jid_bare(stanza.attr.from);
|
||||
local to = jid_bare(stanza.attr.to) or from;
|
||||
|
||||
if (to ~= from)
|
||||
then
|
||||
|
||||
local content = trim(stanza:find("body#"));
|
||||
local isMessageCorrection = stanza:find("{urn:xmpp:message-correct:0}replace@id");
|
||||
-- module:log("info", "got replaceid: %s", isMessageCorrection);
|
||||
if (isMessageCorrection) then
|
||||
postDEL(from, isMessageCorrection);
|
||||
end
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
local function message_handler_from_local(event)
|
||||
return message_handler(event, true);
|
||||
end
|
||||
|
||||
|
||||
if (moduleIsActive)
|
||||
then
|
||||
module:hook("pre-message/bare", message_handler_from_local, 5);
|
||||
module:hook("pre-message/full", message_handler_from_local, 5);
|
||||
|
||||
module:log("info", "DEL HTTP upload files ACTIVATED");
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
local jid_split = require "util.jid".split;
|
||||
module:depends"muc";
|
||||
|
||||
|
||||
local function get_e2e(room)
|
||||
local e2e = room._data.e2e;
|
||||
if e2e then -- explicitly configured
|
||||
return e2e;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
module:hook("muc-config-form", function(event)
|
||||
local room, form = event.room, event.form;
|
||||
local re2e = get_e2e(room);
|
||||
table.insert(form, {
|
||||
name = "muc#roomconfig_e2e",
|
||||
type = "boolean",
|
||||
label = "if the room is E2E ecnrypted",
|
||||
value = re2e,
|
||||
});
|
||||
-- value = get_e2e(room),
|
||||
end);
|
||||
|
||||
module:hook("muc-config-submitted", function(event)
|
||||
local room, fields, changed = event.room, event.fields, event.changed;
|
||||
local new = fields["muc#roomconfig_e2e"];
|
||||
if new ~= get_e2e(room) then
|
||||
if new == get_e2e(room) then
|
||||
room._data.e2e = false;
|
||||
else
|
||||
room._data.e2e = new;
|
||||
end
|
||||
if type(changed) == "table" then
|
||||
changed["muc#roomconfig_e2e"] = true;
|
||||
else
|
||||
event.changed = true;
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("muc-disco#info", function (event)
|
||||
local room, form, formdata = event.room, event.form, event.formdata;
|
||||
|
||||
local e2e = get_e2e(room);
|
||||
module:log("info", "got e2e as %s", e2e);
|
||||
-- if not e2e or e2e == "" then
|
||||
--return;
|
||||
-- end
|
||||
table.insert(form, {
|
||||
name = "muc#roominfo_e2e",
|
||||
});
|
||||
formdata["muc#roominfo_e2e"] = e2e;
|
||||
end);
|
||||
@@ -0,0 +1,932 @@
|
||||
--require "socket"
|
||||
--rawset(_G, "PROXY", false); -- socket module accesses global variable PROXY
|
||||
|
||||
local http = require "net.http"
|
||||
local json = require "util.json"
|
||||
-- local ltn12 = require "ltn12"
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local timer = require "util.timer";
|
||||
local st = require "util.stanza";
|
||||
local time_now = os.time;
|
||||
|
||||
local private_storage = module:open_store("private");
|
||||
local vcard_storage = module:open_store("vcard");
|
||||
local config_store = nil;
|
||||
local fcm_token_store = module:open_store("fcmtoken");
|
||||
local fcm_token_map_store = module:open_store("fcmtoken", "map");
|
||||
|
||||
local fcmAPIKey = module:get_option_string("fcm_api_key");
|
||||
local fcmAPIURL = module:get_option_string("fcm_api_url");
|
||||
|
||||
local moduleIsActive = fcmAPIURL and fcmAPIKey;
|
||||
|
||||
local isMUC = module:get_host_type() == "component";
|
||||
local ThisDomain = module.host;
|
||||
|
||||
local xmlns_sm2 = "urn:xmpp:sm:2";
|
||||
local xmlns_sm3 = "urn:xmpp:sm:3";
|
||||
|
||||
local ThisBroadcast = "broadcast@"..ThisDomain;
|
||||
|
||||
module:add_feature("urn:xmpp:vnctalk:fcm");
|
||||
|
||||
local inactive_devices = {};
|
||||
|
||||
local UDomain = ThisDomain;
|
||||
|
||||
if isMUC then
|
||||
local storage_host = module:get_option_string("storage_host");
|
||||
UDomain = storage_host;
|
||||
module:log("debug", "opening stores for host %s", storage_host);
|
||||
timer.add_task(3, function ()
|
||||
private_storage = module:open_store(storage_host, "private");
|
||||
vcard_storage = module:open_store(storage_host, "vcard");
|
||||
fcm_token_store = module:open_store(storage_host, "fcmtoken");
|
||||
fcm_token_map_store = module:open_store(storage_host, "fcmtoken", "map");
|
||||
if not private_storage or not vcard_storage then
|
||||
module:log("debug", "private/vcard_store %s/%s not found? - will try again", private_storage, vcard_storage);
|
||||
return 3;
|
||||
else
|
||||
module:log("debug", "private/vcard_store are now %s/%s", private_storage, vcard_storage);
|
||||
end
|
||||
end
|
||||
);
|
||||
config_store = module:open_store("config");
|
||||
end
|
||||
|
||||
local function contains(list, x)
|
||||
if (list) then
|
||||
for _, v in pairs(list) do
|
||||
if (v == x) then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function deserializeDataElement(pdElement, expectedName)
|
||||
--module:log("debug", "deserializeDataElement("..expectedName..", "..dumpTable(pdElement)..")");
|
||||
if (not pdElement) then return nil end
|
||||
if (type(pdElement) ~= "table") then return nil end
|
||||
|
||||
if (pdElement[1] and pdElement["attr"] and pdElement["attr"]["key"] and (pdElement["attr"]["key"] == expectedName))
|
||||
then
|
||||
local result = json.decode(pdElement[1]);
|
||||
--module:log("debug", "deserializeDataElement() = '"..dumpTable(result).."'");
|
||||
return result;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
|
||||
local function trim(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("^%s*(.-)%s*$", "%1"))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function collapseSpaces(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("%s+", " "))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function prettyUsername(userName)
|
||||
userName = string.upper(string.sub(userName, 1, 1))..string.sub(userName, 2, -1) -- upcase first char in any case
|
||||
if string.find(userName, "%.") -- contains dot
|
||||
then
|
||||
local i = 1
|
||||
while true do
|
||||
local j = string.find(userName, "%.", i)
|
||||
if j == nil or #userName == i then break end
|
||||
-- replace dot with space and upcase next char
|
||||
userName = string.sub(userName, 1, j-1).." "..string.upper(string.sub(userName, j+1, j+1))..string.sub(userName, j+2, -1)
|
||||
if #userName <= j then break end
|
||||
i = j
|
||||
end
|
||||
end
|
||||
return trim(collapseSpaces(userName));
|
||||
end
|
||||
|
||||
|
||||
local function getDisplayName(userName)
|
||||
local vCard, err = vcard_storage:get(userName);
|
||||
if vCard then
|
||||
vCard = st.deserialize(vCard);
|
||||
end
|
||||
local name = nil;
|
||||
if not vCard or err then
|
||||
module:log("error", "Unable to get vCard for %s : %s ", userName, tostring(err));
|
||||
else
|
||||
name = vCard:find("{vcard-temp}FN#");
|
||||
end
|
||||
if not name then name = prettyUsername(userName) or userName end
|
||||
module:log("debug", "name for %s : %s", userName, name)
|
||||
return name;
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function removeKeyFromUser(userName, fcmId)
|
||||
local pd, pfFetchError = private_storage:get(userName);
|
||||
if (pfFetchError) then
|
||||
module:log(
|
||||
"error",
|
||||
"Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError)
|
||||
);
|
||||
return 0,{};
|
||||
elseif (pd) then
|
||||
local removed = false;
|
||||
for key,value in pairs(pd)
|
||||
do
|
||||
--module:log("debug", "PDD("..userName..") : "..tostring(key).." => "..tostring(value));
|
||||
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
|
||||
--module:log("debug", "PDD.stanza("..userName..") : "..json.encode(value));
|
||||
for k2,v2 in pairs(value) do
|
||||
local fcm = deserializeDataElement(v2, "fcm");
|
||||
if (fcm)
|
||||
then
|
||||
module:log("debug", "removing fcmID %s from private data record: %s", fcmId, json.encode(fcm));
|
||||
fcm[fcmId] = nil;
|
||||
v2[1]=json.encode(fcm);
|
||||
removed = true;
|
||||
--module:log("debug", "AFTER PDD.stanza: "..json.encode(v2));
|
||||
--module:log("debug", "AFTER PDD: "..json.encode(value));
|
||||
--module:log("debug", "removed outdated key");
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if (removed)
|
||||
then
|
||||
local ok, errmsg = private_storage:set(userName, pd);
|
||||
if (not ok)
|
||||
then
|
||||
module:log("error", "failed to store private data for %s : '%s'", userName, tostring(errmsg));
|
||||
end
|
||||
else
|
||||
module:log("debug", "unable to remove fcmID %s from %s. Not found in data: %s", fcmId, userName, tostring(pd));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function getPostCallback(_fcmId, _userName)
|
||||
return function(body, code, response)
|
||||
local shortKey = string.sub(_fcmId, 1, 6)
|
||||
if code ~= 200 then
|
||||
module:log("error", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||||
-- if body then module:log("error", body); end
|
||||
else
|
||||
module:log("debug", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||||
local response2 = json.decode(body)
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "NotRegistered"
|
||||
then
|
||||
-- module:log("debug", "%s key %s is outdated", _userName, shortKey);
|
||||
removeKeyFromUser(_userName, _fcmId);
|
||||
end
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "MissingRegistration"
|
||||
then
|
||||
-- module:log("debug", "%s key %s is never registered", _userName, shortKey);
|
||||
removeKeyFromUser(_userName, _fcmId);
|
||||
end
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "InvalidRegistration"
|
||||
then
|
||||
-- module:log("debug", "%s key %s is invalid", _userName, shortKey);
|
||||
removeKeyFromUser(_userName, _fcmId);
|
||||
end
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "InvalidPackageName"
|
||||
then
|
||||
-- module:log("debug", "%s key %s is using invalid FCM package name", _userName, shortKey);
|
||||
removeKeyFromUser(_userName, _fcmId);
|
||||
end
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "error:MessageTooBig"
|
||||
then
|
||||
-- module:log("debug", "%s key %s : message too big", _userName, shortKey);
|
||||
end
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "InvalidDataKey"
|
||||
then
|
||||
-- module:log("debug", "%s key %s is using invalid data key", _userName, shortKey);
|
||||
end
|
||||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "InvalidTtl"
|
||||
then
|
||||
--- module:log("debug", "%s key %s is using invalid TTL", _userName, shortKey);
|
||||
end
|
||||
end
|
||||
return false -- close request
|
||||
--return true -- keep request open
|
||||
end
|
||||
end
|
||||
|
||||
local function postFCM(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
local domainname = "";
|
||||
local v_attachment = "";
|
||||
if isMUC then
|
||||
domainname = module:get_option_string("storage_host");
|
||||
else
|
||||
domainname = module.host;
|
||||
end
|
||||
|
||||
if (_attachment_type ~= "") and (_attachment_type ~=nil) then
|
||||
v_attachment = _attachment_type;
|
||||
end
|
||||
|
||||
local lsource = _source;
|
||||
local ltype = _type;
|
||||
local signal = nil;
|
||||
local nfrom = _source;
|
||||
local nJitsiURL = "";
|
||||
local nJitsiRoom = "";
|
||||
|
||||
if (_vncTalkConferenceId ~= nil) then
|
||||
if string.match(_vncTalkConferenceId, "@") then
|
||||
lsource = _vncTalkConferenceId;
|
||||
end
|
||||
end
|
||||
|
||||
if (_vncTalkConferenceEtype ~=nil) then
|
||||
signal = "1";
|
||||
ltype = _vncTalkConferenceEtype;
|
||||
end
|
||||
|
||||
if (_nFromJid ~= nil) then
|
||||
nfrom = _nFromJid;
|
||||
end
|
||||
|
||||
if (_jitsiURL ~= nil) then
|
||||
nJitsiURL = _jitsiURL;
|
||||
end
|
||||
|
||||
if (_jitsiRoom ~= nil) then
|
||||
nJitsiRoom = _jitsiRoom;
|
||||
end
|
||||
|
||||
local stamp = os.time();
|
||||
|
||||
local fcmRequest = {
|
||||
to = _fcmId,
|
||||
content_available = true,
|
||||
priority = "high",
|
||||
data = {
|
||||
nType = "local_notification", -- notification type
|
||||
eType = ltype, -- event type
|
||||
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
|
||||
nfrom = nfrom,
|
||||
nto = _userName.."@"..domainname,
|
||||
name = _senderName, -- group chat nick name or single chat peer vCard name",
|
||||
gt = _groupTopic, -- topic of room iff groupchat
|
||||
aft = v_attachment,
|
||||
callSignal = signal,
|
||||
jitsiURL = nJitsiURL,
|
||||
jitsiRoom = nJitsiRoom,
|
||||
t = stamp,
|
||||
lang = _lang,
|
||||
msgid = _msgid,
|
||||
body = _body -- notification body/text content
|
||||
}
|
||||
};
|
||||
|
||||
fcmRequest["android"] = { priority = "high", ttl = 345600 };
|
||||
fcmRequest["webpush"] = { headers = { Urgency = "high", TTL = 345600 } };
|
||||
|
||||
local data = json.encode(fcmRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Authorization"] = "key="..fcmAPIKey,
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
module:log("info", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body);
|
||||
-- ssl does not seem to work with net.http, so we proxy it
|
||||
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
|
||||
module:log("info", "sendingHTTP request to %s", fcmAPIURL);
|
||||
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||||
end
|
||||
|
||||
local function postFCMIOS(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
local domainname = "";
|
||||
local ntitle = "";
|
||||
local nbody = "";
|
||||
local v_attachment = "";
|
||||
local nJitsiURL = "";
|
||||
local nJitsiRoom = "";
|
||||
|
||||
if isMUC then
|
||||
domainname = module:get_option_string("storage_host");
|
||||
if (_groupTopic ~= "") and (_groupTopic ~= nil) then
|
||||
ntitle = _groupTopic;
|
||||
else
|
||||
ntitle = jid_split(_source);
|
||||
end
|
||||
else
|
||||
domainname = module.host;
|
||||
if (_groupTopic ~= "") and (_groupTopic ~=nil) then
|
||||
ntitle = _groupTopic
|
||||
else
|
||||
if (_senderName ~= "") and (_senderName ~= nil) then
|
||||
ntitle = _senderName;
|
||||
else
|
||||
ntitle = _source;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (_attachment_type ~= "") and (_attachment_type ~=nil) then
|
||||
v_attachment = _attachment_type;
|
||||
end
|
||||
|
||||
local _click_action = _type:upper();
|
||||
|
||||
if (_click_action == "NORMAL") then
|
||||
_click_action = "CHAT";
|
||||
end
|
||||
|
||||
local lsource = _source;
|
||||
local ltype = _type;
|
||||
local signal = nil;
|
||||
local nfrom = _source;
|
||||
|
||||
if (_vncTalkConferenceId ~= nil) then
|
||||
if string.match(_vncTalkConferenceId, "@") then
|
||||
lsource = _vncTalkConferenceId;
|
||||
end
|
||||
end
|
||||
|
||||
if (_vncTalkConferenceEtype ~=nil) then
|
||||
signal = "1";
|
||||
ltype = _vncTalkConferenceEtype;
|
||||
end
|
||||
|
||||
if (_nFromJid ~= nil) then
|
||||
nfrom = _nFromJid;
|
||||
end
|
||||
|
||||
if (_jitsiURL ~= nil) then
|
||||
nJitsiURL = _jitsiURL;
|
||||
end
|
||||
|
||||
if (_jitsiRoom ~= nil) then
|
||||
nJitsiRoom = _jitsiRoom;
|
||||
end
|
||||
|
||||
local fcmRequest = {
|
||||
to = _fcmId,
|
||||
priority = "high",
|
||||
notification = {
|
||||
title = ntitle,
|
||||
body = _body,
|
||||
mutable_content = true,
|
||||
sound = "incoming-call-loop.caff",
|
||||
thread_id = lsource,
|
||||
click_action = _click_action
|
||||
},
|
||||
data = {
|
||||
nType = "local_notification", -- notification type
|
||||
eType = ltype, -- event type
|
||||
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
|
||||
nfrom = nfrom,
|
||||
nto = _userName.."@"..domainname,
|
||||
name = _senderName, -- group chat nick name or single chat peer vCard name",
|
||||
gt = ntitle, -- topic of room iff groupchat
|
||||
aft = v_attachment,
|
||||
callSignal = signal,
|
||||
jitsiURL = nJitsiURL,
|
||||
jitsiRoom = nJitsiRoom,
|
||||
lang = _lang,
|
||||
msgid = _msgid,
|
||||
body = _body -- notification body/text content
|
||||
}
|
||||
};
|
||||
|
||||
local data = json.encode(fcmRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Authorization"] = "key="..fcmAPIKey,
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
-- module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, string.sub(_body, 20).."...");
|
||||
module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body.."...");
|
||||
-- ssl does not seem to work with net.http, so we proxy it
|
||||
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
|
||||
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||||
end
|
||||
|
||||
|
||||
local function getNotifyOptionsForUser(userName)
|
||||
--module:log("debug", "getFCMtokenForUser("..userName..")");
|
||||
local pd, pfFetchError = private_storage:get(userName);
|
||||
if (pfFetchError) then
|
||||
module:log(
|
||||
"error",
|
||||
"Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError)
|
||||
);
|
||||
return 0,{};
|
||||
elseif (pd) then
|
||||
local tokens;
|
||||
local nType;
|
||||
local nLang;
|
||||
for key,value in pairs(pd)
|
||||
do
|
||||
-- module:log("debug", "PD("..userName..") : "..tostring(key).." => "..tostring(value));
|
||||
module:log("debug", "PD( %s) : %s => %s ", userName,tostring(key),tostring(value));
|
||||
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
|
||||
--module:log("debug", "PD("..userName..") : "..json.encode(value));
|
||||
-- module:log("debug", "PD( %s ) : %s ", userName ,json.encode(value));
|
||||
for k2,v2 in pairs(value) do
|
||||
if (not tokens) then
|
||||
local fcm = deserializeDataElement(v2, "fcm");
|
||||
if (type(fcm) == "table") then
|
||||
tokens = {};
|
||||
for token,x in pairs(fcm) do
|
||||
-- module:log("debug", "token: %s - x[os]: %s", token, x["os"]);
|
||||
if (x["os"] ~= nil) then
|
||||
table.insert(tokens, token.."@"..x["os"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if (not nType) then
|
||||
local options = deserializeDataElement(v2, "notifyOptions");
|
||||
if (options and (type(options) == "table") and options["type"]) then
|
||||
nType = tonumber(options["type"]);
|
||||
end
|
||||
end
|
||||
if (not nLang) then
|
||||
local options = deserializeDataElement(v2, "lang");
|
||||
-- module:log("debug", "PDLang: %s => %s", userName, json.encode(options));
|
||||
if (options) and (type(options) == "string") then
|
||||
nLang = options;
|
||||
end;
|
||||
-- module:log("debug", "type of options: %s", type(options));
|
||||
end
|
||||
end
|
||||
break; -- we can stop after the documents:stanza:io:json element
|
||||
end
|
||||
end
|
||||
--module:log("debug", "getNotifyOptionsForUser("..userName..") = "..tostring(tokens).." => "..tostring(nType));
|
||||
|
||||
local map_res = fcm_token_store:get(userName);
|
||||
|
||||
-- module:log("debug", "result from map store for %s is: %s", userName, dumpTable(map_res));
|
||||
|
||||
if (map_res) then
|
||||
for _device, _tkr in pairs(map_res) do
|
||||
-- module:log("debug", "got for %s device %s => %s", userName, _device, dumpTable(_tkr));
|
||||
-- module:log("debug", "got for %s device %s => token: %s - os: %s", userName, _device, _tkr.token, _tkr.os);
|
||||
local sessions = prosody.bare_sessions;
|
||||
local ubjid = userName.."@"..UDomain;
|
||||
local u_sessions = sessions[ubjid] or nil;
|
||||
local deviceonline = false;
|
||||
if (u_sessions) then
|
||||
local u_sessions_device = u_sessions["sessions"] or nil;
|
||||
if (u_sessions_device) then
|
||||
module:log("debug", "devise is %s", _device);
|
||||
-- module:log("debug", "u_sessions_device %s", dumpTable(u_sessions_device));
|
||||
local dsession = u_sessions_device[_device] or nil;
|
||||
-- module:log("debug", "dsession %s", dumpTable(dsession));
|
||||
if (dsession) then
|
||||
local hibernated = dsession["hibernated"] or nil;
|
||||
local hibernating = dsession["hibernating"] or nil;
|
||||
local awaiting_ack = dsession["awaiting_ack"] or nil;
|
||||
module:log("info", "found session for _device %s - hibernating: %s, hibernated %s", _device, hibernating, hibernated);
|
||||
if not ((hibernated) or (hibernating)) then
|
||||
local dev_in_background = inactive_devices[_device] or nil;
|
||||
if dev_in_background then
|
||||
if awaiting_ack then
|
||||
module:log("info", "device %s is still connected, but inactive and awaiting_ack", _device);
|
||||
else
|
||||
module:log("info", "device %s is still connected, but inactive", _device);
|
||||
deviceonline = true;
|
||||
end
|
||||
else
|
||||
if awaiting_ack then
|
||||
module:log("info", "device %s is connected and active, but awaiting_ack", _device);
|
||||
else
|
||||
module:log("info", "device %s is connected and active", _device);
|
||||
deviceonline = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not(deviceonline) then
|
||||
local already_added = contains(tokens, _tkr.token.."@".._tkr.os);
|
||||
module:log("debug","already_added: %s", _tkr.token.."@".._tkr.os);
|
||||
if not(tokens) then
|
||||
tokens = {};
|
||||
end
|
||||
if not(already_added) then
|
||||
table.insert(tokens, _tkr.token.."@".._tkr.os);
|
||||
end
|
||||
end
|
||||
-- module:log("debug","tokens table is now: %s", dumpTable(tokens));
|
||||
end
|
||||
end
|
||||
|
||||
if (not tokens) then tokens = {}; end;
|
||||
if (not nType) then nType = 0; end;
|
||||
--module:log("debug", "getNotifyOptionsForUser(%s) = %i, %i tokens", userName, nType, table.getn(tokens));
|
||||
-- module:log("debug", "getNotifyOptionsForUser(%s) = %i, %i tokens, lang %s", userName, nType, table.getn(tokens), nLang);
|
||||
local sessions = prosody.bare_sessions;
|
||||
local ubjid = userName.."@"..ThisDomain;
|
||||
for _user, _resdetails in pairs(sessions) do
|
||||
if (_user == ubjid) then
|
||||
for _res, _detail1 in pairs(_resdetails["sessions"]) do
|
||||
local hibernated = _detail1["hibernated"] or _detail1["hibernating"] or nil;
|
||||
if (hibernated) then
|
||||
module:log("info", "for _user: %s and _res: %s got hibernated since: %s", _user, _res, hibernated);
|
||||
else
|
||||
module:log("info", "for _user: %s and _res: %s got active session?", _user, _res);
|
||||
--module:log("debug", "for _user: %s and _res: %s got detail: %s", _user, _res, dumpTable(_detail1));
|
||||
end
|
||||
end
|
||||
-- module:log("debug", "session for _res %s is: %s", _res, dumpTable(_resdetails));
|
||||
end
|
||||
end
|
||||
-- module:log("debug", "sessions: %s", dumpTable(sessions[ubjid]));
|
||||
return nType, tokens, nLang;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function fcm_notify(userName, title, body, type, _senderName, _groupTopic, vnc_attachment_type, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom)
|
||||
if (not moduleIsActive) then return end;
|
||||
local nType, tokens, lang = getNotifyOptionsForUser(userName);
|
||||
if (tokens and (#tokens) > 0) and ((nType == 1) or (nType == 2) or vncTalkConferenceEtype)
|
||||
then
|
||||
module:log(
|
||||
"info", "in fcm_notify(%s, %s, %s) => type=%s, %s tokens",
|
||||
userName, title, body, tostring(nType), tostring((#tokens))
|
||||
);
|
||||
local sName = _senderName;
|
||||
local gTopic = _groupTopic;
|
||||
sName = "Neue Nachricht";
|
||||
gTopic = "Neue Nachricht";
|
||||
|
||||
if (lang == "en") then
|
||||
sName = "new message";
|
||||
gTopic = "new message";
|
||||
end;
|
||||
|
||||
if (lang == "fr") then
|
||||
sName = "Nouveau message";
|
||||
gTopic = "Nouveau message";
|
||||
end;
|
||||
|
||||
body = "";
|
||||
for i,token in pairs(tokens) do
|
||||
if (token)
|
||||
then
|
||||
local rtoken, ros = jid_split(token);
|
||||
-- module:log("debug","got rtoken: %s - os: %s ", rtoken, ros);
|
||||
if (ros == "ios") then
|
||||
-- module:log("debug", "postFCMIOS with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang);
|
||||
postFCMIOS(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom);
|
||||
else
|
||||
-- module:log("debug", "postFCM with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang, _nFrom);
|
||||
postFCM(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom);
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
module:log("debug", "fcm_notify(%s, %s, %s) => type=%s, no tokens", userName, title, body, tostring(nType));
|
||||
end
|
||||
end
|
||||
|
||||
-- build map from room nicknames to real JIDs
|
||||
local room_nick_jid = {} -- key1=roomJid, key2=occupantNickname, value=occupantJid
|
||||
local room_afffiliations = {} -- key1=roomJid, key2=occupantNickname, value=occupantJid
|
||||
module:hook("muc-room-changed", function (event)
|
||||
|
||||
--module:log("debug", "muc-room-changed "..event.room.jid.."\n"..tostring(event.room));
|
||||
|
||||
--if not room_nick_jid[event.room.jid]
|
||||
--then
|
||||
-- module:log("debug", "muc: new room "..tostring(event.room))
|
||||
--else
|
||||
-- module:log("debug", "muc: "..tostring(event.room).." changed")
|
||||
--end
|
||||
|
||||
room_nick_jid[event.room.jid] = {} -- reset mapping table for the current room
|
||||
local nick_jid = room_nick_jid[event.room.jid] -- mapping table for the current room
|
||||
|
||||
for nick, odata in pairs(event.room._occupants)
|
||||
do
|
||||
local jid = jid_bare(odata["jid"])
|
||||
nick_jid[nick]=jid
|
||||
-- module:log("debug", "muc nick "..nick.." => "..jid.." in "..event.room.jid);
|
||||
end
|
||||
|
||||
-- copy affiliations table
|
||||
room_afffiliations[event.room.jid] = {}
|
||||
for bareJid, role in pairs(event.room._affiliations)
|
||||
do
|
||||
room_afffiliations[event.room.jid][bareJid] = role;
|
||||
-- module:log("debug", "muc role %s %s %s", event.room.jid, bareJid, role);
|
||||
end
|
||||
end);
|
||||
|
||||
local function message_handler(event, fromLocal)
|
||||
--TODO; use stanza:find here
|
||||
-- module:log("debug", "MH0");
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local message_id = event.stanza.attr.id;
|
||||
-- module:log("debug","message id: %s", event.stanza.attr.id);
|
||||
local type = stanza.attr.type or "normal";
|
||||
local from = jid_bare(stanza.attr.from);
|
||||
local to = jid_bare(stanza.attr.to) or from;
|
||||
|
||||
local vncTalkConferenceEtype = stanza:find("{xmpp:vnctalk}vncTalkConference/eventType#") or nil;
|
||||
|
||||
local vnctalk_broadcast = stanza:find("{xmpp:vnctalk}vncTalkBroadcast", "xmpp:vnctalk" ) or false;
|
||||
-- module:log("info", "vnctalk_avatarup broadcast: %s", vnctalk_broadcast);
|
||||
local vnctalk_avatarup = false;
|
||||
if (vnctalk_broadcast) then
|
||||
vnctalk_avatarup = vnctalk_broadcast.attr.avatarup or false;
|
||||
end
|
||||
if (to == ThisBroadcast) then
|
||||
vnctalk_avatarup = true;
|
||||
end
|
||||
|
||||
-- module:log("info", "handling message from=%s, to=%s", from, to);
|
||||
|
||||
if ((to ~= from) or ((to == from) and vncTalkConferenceEtype))
|
||||
then
|
||||
|
||||
local content = trim(stanza:find("body#"));
|
||||
local delayedDelivery = stanza:get_child("delay", "urn:xmpp:delay");
|
||||
|
||||
-- if we have a vncTalk incoming or whiteboard element, it overrides the type
|
||||
local vncTalkIncomingType = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceType#");
|
||||
|
||||
local vncTalkConferenceId = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceId#") or nil;
|
||||
local jitsiURL = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiURL#") or nil;
|
||||
local jitsiRoom = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiRoom#") or nil;
|
||||
-- module:log("debug", "vncTalkIncomingType: %s", vncTalkIncomingType);
|
||||
local vncTalkWhiteboard = stanza:find("{xmpp:vnctalk}whiteboard");
|
||||
local isSentCarbonMessage = stanza:get_child("sent", "urn:xmpp:carbons:2");
|
||||
local vnc_attachment_type = stanza:find("{xmpp:vnctalk}attachment/fileType#");
|
||||
|
||||
local vnctalk_broadcast = stanza:get_child("vncTalkBroadcast", "xmpp:vnctalk");
|
||||
local vnctalk_broadcast_childid = nil;
|
||||
if vnctalk_broadcast then
|
||||
if starts_with(to, "broadcast") then
|
||||
-- module:log("message is to broadcast - bailing out");
|
||||
return nil;
|
||||
end
|
||||
vnctalk_broadcast_childid = vnctalk_broadcast.attr.id;
|
||||
-- module:log("debug", "this is a broadcast message: %s", tostring(stanza));
|
||||
local vnctalk_broadcast_title = vnctalk_broadcast.attr.title;
|
||||
-- module:log("debug", "this is a broadcast title: %s", vnctalk_broadcast_title);
|
||||
local vnctalk_broadcast_target = vnctalk_broadcast.attr.origtarget;
|
||||
-- module:log("debug", "this is a broadcast target: %s", vnctalk_broadcast_target);
|
||||
local body = stanza:get_child("body");
|
||||
-- module:log("debug", "broadcast body: %s", content);
|
||||
-- module:log("debug", "jid_split(to): %s", jid_split(to));
|
||||
if (not(vnctalk_avatarup)) then
|
||||
fcm_notify(jid_split(to), vnctalk_broadcast_target, content, type, getDisplayName(jid_split(from)), "Broadcast: "..vnctalk_broadcast_title, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom);
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
--local isMessageCorrection = stanza:find("{urn:xmpp:message-correct:0}replace@id");
|
||||
|
||||
-- debug from here
|
||||
module:log("debug","continue to process message with body: %s", content);
|
||||
|
||||
local auxType = vncTalkIncomingType or (vncTalkWhiteboard and "whiteboard");
|
||||
|
||||
if (auxType)
|
||||
then
|
||||
content = auxType;
|
||||
type = auxType;
|
||||
end;
|
||||
|
||||
local doNotify = content and (content ~= " ") and (content ~= "") and (not(vnctalk_avatarup)) and (delayedDelivery == nil) and (isSentCarbonMessage == nil) and (
|
||||
(type == "chat") or (type == "groupchat") or (type == "audio") or (type == "video") or (type == "whiteboard") or (type == "screen")
|
||||
);
|
||||
|
||||
if (doNotify)
|
||||
then
|
||||
module:log("debug", "536-doNotify: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s",
|
||||
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify));
|
||||
if (not isMUC)
|
||||
then
|
||||
module:log("debug", "537-doNotify not isMUC: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s",
|
||||
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify));
|
||||
--if (fromLocal)
|
||||
--then
|
||||
-- module:log("debug", " ignoring message from local client");
|
||||
--module:log("debug", "not sending fromLocal from=%s, to=%s, type=%s, content=%s, isSentCarbonMessage=%s", from, to, type, content, isSentCarbonMessage);
|
||||
if fromLocal and (type ~= "groupchat") then
|
||||
fcm_notify(jid_split(to), from, content, type, getDisplayName(jid_split(from)), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom);
|
||||
else
|
||||
local fromuser2, fromdomain2 = jid_split(stanza.attr.from);
|
||||
if (type ~= "groupchat") and (fromdomain2 ~= ThisDomain) then
|
||||
fcm_notify(jid_split(to), from, content, type, getDisplayName(jid_split(from)), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom);
|
||||
else
|
||||
-- notify when not from a local domain
|
||||
if not (prosody.hosts[fromdomain2]) and type == "groupchat" then
|
||||
local rjnode, rjhost, rjres = jid_split(stanza.attr.from);
|
||||
local rdn, rdh = jid_split(rjres);
|
||||
local tdn, tdh, tdr = jid_split(stanza.attr.to);
|
||||
if ((rdn == tdn) and (rdh == tdh)) then
|
||||
module:log("info", "766 ... rdn=%s, tdn=%s, rdh=%s, tdh=%s", rdn, tdn, rdh, tdh);
|
||||
else
|
||||
module:log("info", "541-doNotify IOM: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s, rdn=%s",
|
||||
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify), rdn);
|
||||
|
||||
fcm_notify(jid_split(to), from, content, type, getDisplayName(rdn), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom);
|
||||
end
|
||||
else
|
||||
module:log("debug", " ignoring message");
|
||||
end
|
||||
end
|
||||
end
|
||||
--end
|
||||
|
||||
else -- MUC
|
||||
module:log("debug", "538-doNotify isMUC: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s",
|
||||
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify));
|
||||
|
||||
local muc, host = jid_split(to);
|
||||
if prosody.hosts[host] then
|
||||
module:log("info","muc conf: %s", dumpTable(prosody.hosts[host].modules.muc));
|
||||
-- module:log("debug","muc conf: %s", dumpTable(prosody.hosts[host].modules.muc.rooms[to]));
|
||||
-- module:log("debug","muc affiliations: %s", dumpTable(prosody.hosts[host].modules.muc.rooms[to]._affiliations));
|
||||
module.log("info", "processing to and found local muc component: %s", to);
|
||||
local affs = prosody.hosts[host].modules.muc.rooms[to]._affiliations;
|
||||
if affs then
|
||||
for _aff, role in pairs(affs) do
|
||||
local now = time_now();
|
||||
local roomTopic = prosody.hosts[host].modules.muc.rooms[to]._data.subject or nil;
|
||||
module:log("info", "got muc _aff %s from muc %s", _aff, to);
|
||||
if from ~= _aff then
|
||||
module:log("info", " notify MUC affilaite Jid=%s", _aff);
|
||||
module:log("debug", "muc notifiy from: %s => to: %s", from, to);
|
||||
local roomJid = to;
|
||||
fcm_notify(jid_split(_aff), roomJid, content, type, getDisplayName(jid_split(from)), roomTopic, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, from, jitsiURL, jitsiRoom);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
module:log("debug", "ignoring from=%s, to=%s, type=%s, content=%s", from, to, type, content);
|
||||
end
|
||||
end
|
||||
|
||||
return;
|
||||
end
|
||||
|
||||
local function iom_muc_handler(event)
|
||||
-- module:log("debug", "iom muc handler - event: %s", dumpTable(event));
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local message_id = event.stanza.attr.id;
|
||||
-- module:log("debug","message id: %s", event.stanza.attr.id);
|
||||
local type = stanza.attr.type or "normal";
|
||||
local from = jid_bare(stanza.attr.from);
|
||||
local to = jid_bare(stanza.attr.to) or from;
|
||||
local fromloc,fromhost = jid_split(from);
|
||||
local toloc,tohost = jid_split(to);
|
||||
if (prosody.hosts[tohost] and (prosody.hosts[fromhost] == nil)) then
|
||||
module:log("debug", "iom message type %s from host %s - to host %s", type, fromhost, tohost);
|
||||
return message_handler(event, false);
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function message_handler_to_local(event)
|
||||
return message_handler(event, false);
|
||||
end
|
||||
|
||||
local function message_handler_from_local(event)
|
||||
return message_handler(event, true);
|
||||
end
|
||||
|
||||
local function logCsiEvent(event, active)
|
||||
local csires = event.origin.resource or nil;
|
||||
if active
|
||||
then
|
||||
inactive_devices[csires] = nil;
|
||||
module:log("debug", "ACTIVE: %s", tostring(event.origin));
|
||||
module:log("debug", "ACTIVE resource %s", csires);
|
||||
-- module:log("debug", "ACTIVE: %s", dumpTable(event.origin));
|
||||
else
|
||||
module:log("debug", "INACTIVE: %s", tostring(event.origin));
|
||||
-- module:log("debug", "INACTIVE: %s", dumpTable(event.origin));
|
||||
module:log("debug", "INACTIVE resource %s", csires);
|
||||
local t = os.time();
|
||||
inactive_devices[csires] = t;
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_iq(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local from_node, from_host, from_res = jid_split(stanza.attr.from);
|
||||
local reply = st.reply(stanza);
|
||||
local res = false;
|
||||
if stanza.attr.type == "set" then
|
||||
local fcm_child = stanza:get_child("add", "xmpp:vnctalk:fcm") or nil;
|
||||
if (fcm_child) then
|
||||
local fcm_token = fcm_child:get_child("fcm", "xmpp:vnctalk:fcm") or nil;
|
||||
if (fcm_token) then
|
||||
local _device = fcm_token.attr.device or nil;
|
||||
local _token = fcm_token.attr.token or nil;
|
||||
local _os = fcm_token.attr.os or nil;
|
||||
module:log("debug", "going to add token for %s - device %s, token: %s ", from_node, _device, _token);
|
||||
local do_add_token = (_device) and (_token) and (_os);
|
||||
if (do_add_token) then
|
||||
local tk_os = { token = _token , os = _os };
|
||||
local _store_fcm = fcm_token_map_store:set(from_node, _device, tk_os);
|
||||
res = true;
|
||||
origin.send(st.reply(stanza):tag('add', {xmlns='xmpp:vnctalk:fcm'}):text("ok"));
|
||||
return true;
|
||||
-- module:log("debug", "going to add token for ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if res then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
if (moduleIsActive)
|
||||
then
|
||||
-- Stanszas to local clients
|
||||
module:hook("message/bare", message_handler_to_local, 2);
|
||||
module:hook("message/full", iom_muc_handler, 2);
|
||||
--module:hook("message/full", message_handler_to_local, 2);
|
||||
-- Stanzas sent by local clients
|
||||
module:hook("pre-message/bare", message_handler_from_local, 2);
|
||||
module:hook("pre-message/full", message_handler_from_local, 2);
|
||||
|
||||
module:hook("csi-client-active", function(event) logCsiEvent(event, true); end);
|
||||
module:hook("csi-client-inactive", function(event) logCsiEvent(event, false); end);
|
||||
|
||||
-- to track new connection
|
||||
module:hook("resource-bind", function(event)
|
||||
-- module:log("debug", "resource bind event %s", dumpTable(event.session.resource));
|
||||
local resbind = event.session.resource;
|
||||
inactive_devices[resbind] = nil;
|
||||
end);
|
||||
|
||||
module:hook_stanza(xmlns_sm3, "resume", function (event)
|
||||
-- module:log("debug", "resource bind event %s", dumpTable(event.session.resource));
|
||||
end);
|
||||
|
||||
-- module:hook("pre-iq/full", handle_iq, 1);
|
||||
module:hook("pre-iq/bare", handle_iq, 100);
|
||||
-- module:hook("pre-iq/host", handle_iq, 1);
|
||||
|
||||
-- module:hook("iq/self/urn:xmpp:vnctalk:fcm", handle_iq, -1);
|
||||
-- module:hook("iq/bare/urn:xmpp:vnctalk:fcm", handle_iq, -1);
|
||||
-- module:hook("iq/host/urn:xmpp:vnctalk:fcm", handle_iq, -1);
|
||||
|
||||
module:log("info", "FCM notifications ACTIVATED");
|
||||
end
|
||||
@@ -0,0 +1,415 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local st = require "util.stanza";
|
||||
local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local base64 = require"util.encodings".base64;
|
||||
local sha1 = require"util.hashes".sha1;
|
||||
|
||||
local my_host = module:get_host();
|
||||
local my_host_type = module:get_host_type();
|
||||
|
||||
local avt_hash_table = {};
|
||||
local remote_act_cache = {};
|
||||
local remote_act_sessions = {};
|
||||
|
||||
module:depends"vcard";
|
||||
local vcard_storage = module:open_store("vcard");
|
||||
local activity_store = module:open_store("activity", "map");
|
||||
local remote_activity_store = module:open_store("remote_activity", "map");
|
||||
local remote_avatar_update_store = module:open_store("remote_avatar", "map");
|
||||
|
||||
local function getTableLng(tbl)
|
||||
local getN = 0
|
||||
for n in pairs(tbl) do
|
||||
getN = getN + 1
|
||||
end
|
||||
return getN
|
||||
end
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function get_vcard(username)
|
||||
local vcard, err = vcard_storage:get(username);
|
||||
if vcard then
|
||||
vcard = st.deserialize(vcard);
|
||||
end
|
||||
if not vcard then
|
||||
vcard = st.stanza("vCard", { xmlns = "vcard-temp" });
|
||||
end
|
||||
return vcard, err;
|
||||
end
|
||||
|
||||
|
||||
local function calculate_avatar_hash(username)
|
||||
local vcard = get_vcard(username)
|
||||
local photo = vcard and vcard:get_child("PHOTO")
|
||||
if photo then
|
||||
local photo_type = photo:get_child_text("TYPE");
|
||||
local photo_b64 = photo:get_child_text("BINVAL");
|
||||
local photo_raw = photo_b64 and base64.decode(photo_b64);
|
||||
if photo_raw and photo_type then
|
||||
return sha1(photo_raw, true);
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
local function get_avatar_hash(username, host)
|
||||
if (my_host ~= host) then
|
||||
return nil
|
||||
end
|
||||
local session = nil
|
||||
if (prosody.hosts[host].sessions ~= nil) then
|
||||
session = prosody.hosts[host] and prosody.hosts[host].sessions[username]
|
||||
end
|
||||
local avt_hash = nil;
|
||||
if not avt_hash_table[username] then
|
||||
avt_hash = calculate_avatar_hash(username);
|
||||
avt_hash_table[username] = avt_hash;
|
||||
-- -- module:log("info", "DEBUG get_avatar_hash returning result after new calc");
|
||||
else
|
||||
-- -- module:log("info", "DEBUG get_avatar_hash returning result from cache");
|
||||
avt_hash = avt_hash_table[username];
|
||||
end;
|
||||
return avt_hash
|
||||
end
|
||||
|
||||
module:add_feature("jabber:iq:last");
|
||||
module:add_feature("jabber:iq:batch");
|
||||
|
||||
local map = {};
|
||||
|
||||
|
||||
module:hook("authentication-success", function(event)
|
||||
local session = event.session;
|
||||
if session.username then
|
||||
local pers = activity_store:set(session.username, "last", 0);
|
||||
end
|
||||
end, 10);
|
||||
|
||||
|
||||
module:hook("presence/bare", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local typ = stanza.attr.type;
|
||||
-- module:log("info", "Cache hook, got raw %s ", dumpTable(stanza));
|
||||
-- module:log("info", "Cache hook, got %s from a %s - %s", stanza:top_tag(), origin.type, typ);
|
||||
|
||||
if origin.type == "s2sin" and ( typ == nil or typ == "unavailable" ) then
|
||||
|
||||
local contact_full = stanza.attr.from;
|
||||
local contact_bare = jid_bare(contact_full);
|
||||
local contact_user, contact_host, contact_res = jid_split(contact_full);
|
||||
local contact_sessions = remote_act_sessions[contact_bare] or {};
|
||||
|
||||
-- module:log("info", "contact_sessions from %s is %s", contact_bare, dumpTable(remote_act_sessions[contact_bare]));
|
||||
|
||||
if typ == "unavailable" then -- set current time in cache
|
||||
local t = os.time();
|
||||
local s = "";
|
||||
if contact_sessions[contact_full] then
|
||||
contact_sessions[contact_full] = nil;
|
||||
remote_act_sessions[contact_bare] = contact_sessions;
|
||||
local contact_session_count = getTableLng(contact_sessions);
|
||||
-- module:log("info", "storing online state for %s in after removing contact_full, net length %s, %s", contact_full, contact_session_count, dumpTable(contact_sessions));
|
||||
if ( contact_session_count == 0 ) then
|
||||
remote_act_sessions[contact_bare] = nil;
|
||||
remote_act_cache[contact_bare] = t;
|
||||
-- module:log("info", "remote_act_cache for %s is now %s", contact_bare, remote_act_cache[contact_bare]);
|
||||
remote_activity_store:set(contact_bare, "last", t);
|
||||
end
|
||||
end
|
||||
else
|
||||
-- set 0 (online) in cache
|
||||
-- module:log("info", "storing online state for %s in remote_act_cache %s", contact_bare, dumpTable(remote_act_sessions[contact_bare]));
|
||||
-- remote_act_cache[contact_bare] = 0;
|
||||
local contact_sessions = remote_act_sessions[contact_bare] or {};
|
||||
-- module:log("info", "storing online state for %s in pre local contact_sessions %s", contact_bare, dumpTable(contact_sessions));
|
||||
contact_sessions[contact_full] = 1;
|
||||
-- module:log("info", "storing online state for %s in podt local contact_sessions %s", contact_bare, dumpTable(contact_sessions));
|
||||
remote_act_sessions[contact_bare] = contact_sessions;
|
||||
local contact_session_count = getTableLng(contact_sessions);
|
||||
-- module:log("info", "contact_sessions afterstore from %s is new length %s, %s", contact_bare, contact_session_count, dumpTable(remote_act_sessions[contact_bare]));
|
||||
|
||||
local x_update = stanza:get_child("x", "vcard-temp:x:update") or nil;
|
||||
if (x_update) then
|
||||
local x_photo_update = x_update:get_child_text("photo") or nil;
|
||||
-- module:log("info", "Cache hook1 debug: %s", x_photo_update);
|
||||
if (x_photo_update) then
|
||||
-- module:log("info", "avatarupdate - need to store %s update for %s", x_photo_update, contact_bare);
|
||||
local t = os.time();
|
||||
remote_avatar_update_store:set(contact_bare, "avatar", t);
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end, 10);
|
||||
|
||||
module:hook("pre-presence/bare", function(event)
|
||||
local stanza = event.stanza;
|
||||
-- module:log("info", "presence hook - event.origin.username: %s", event.origin.username);
|
||||
if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
|
||||
-- module:log("info", "unavailable hook - event.origin.username: %s", event.origin.username);
|
||||
local s = os.time();
|
||||
local t = stanza:get_child_text("status");
|
||||
map[event.origin.username] = {s = s, t = t};
|
||||
local lastsession = true;
|
||||
local luser, lhost = jid_split(stanza.attr.from);
|
||||
if luser and lhost then
|
||||
local user_sessions = hosts[lhost] and hosts[lhost].sessions[luser];
|
||||
if user_sessions then
|
||||
lastsession = false;
|
||||
end
|
||||
end
|
||||
if lastsession then
|
||||
-- module:log("info", "last session gone - storing to activity_store for %s => %s", event.origin.username, s);
|
||||
local pers = activity_store:set(event.origin.username, "last", s);
|
||||
end
|
||||
end
|
||||
end, 10);
|
||||
|
||||
module:hook("iq/bare/jabber:iq:batch:query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if stanza.attr.type == "get" then
|
||||
-- -- module:log("info", "got stanza: %s", dumpTable(stanza));
|
||||
local querychild = stanza:get_child("query", "jabber:iq:batch");
|
||||
local reply = st.reply(stanza):tag('query', {xmlns='jabber:iq:last'});
|
||||
-- :childtags("jid", "jabber:iq:batch");
|
||||
-- -- module:log("info", "querychilds: %s", dumpTable(querychild));
|
||||
for ajid in querychild:childtags("jid") do
|
||||
local abjid = ajid:get_text();
|
||||
-- module:log("info", "got jid: %s", abjid);
|
||||
local username = jid_split(abjid);
|
||||
local luser, lhost = jid_split(abjid);
|
||||
|
||||
local seconds, text = "-1", "";
|
||||
local hash = "";
|
||||
|
||||
-- -- module:log("info", "map is now: %s", dumpTable(map));
|
||||
if map[username] then
|
||||
-- module:log("info", "found map[ %s ] => t: %s", username, map[username].t );
|
||||
-- module:log("info", "found map[ %s ] => s: %s", username, map[username].s );
|
||||
local mt = tonumber(map[username].s);
|
||||
seconds = tostring(os.difftime(os.time(), mt));
|
||||
text = map[username].s;
|
||||
end
|
||||
if luser and lhost then
|
||||
-- -- module:log("info", "luser: %s ++ lhost %s", luser, lhost);
|
||||
local sessions_avail = false;
|
||||
if (hosts[lhost]) then
|
||||
-- module:log("info", "lhost exists: %s", lhost);
|
||||
if (hosts[lhost].sessions) then
|
||||
-- module:log("info", "got session object for lhost %s", lhost);
|
||||
sessions_avail = true;
|
||||
end
|
||||
end
|
||||
if sessions_avail then
|
||||
local user_sessions = hosts[lhost] and hosts[lhost].sessions[luser];
|
||||
local latest_hibernated_stamp = 0;
|
||||
local all_hibernated = true;
|
||||
if user_sessions then
|
||||
for x,s in pairs(user_sessions.sessions) do
|
||||
if (s.hibernating) then
|
||||
-- module:log("info", "user_sessions s.hibernated %s is %s", s, s.hibernating);
|
||||
if (s.hibernating > latest_hibernated_stamp) then
|
||||
latest_hibernated_stamp = s.hibernating;
|
||||
end
|
||||
else
|
||||
-- module:log("info", "user_sessions %s is active", s);
|
||||
all_hibernated = false;
|
||||
end
|
||||
end
|
||||
-- module:log("info","latest_hibernated_stamp is %s", latest_hibernated_stamp);
|
||||
-- module:log("info","all_hibernated is %s", all_hibernated);
|
||||
if (latest_hibernated_stamp > 0 and all_hibernated) then
|
||||
seconds = os.difftime(os.time(), latest_hibernated_stamp)
|
||||
else
|
||||
seconds = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
if not (prosody.hosts[lhost]) then
|
||||
local contact_bare = luser.."@"..lhost;
|
||||
-- module:log("info","getting act for remote %s", contact_bare);
|
||||
if remote_act_sessions[contact_bare] then
|
||||
local contact_sessions = getTableLng(remote_act_sessions[contact_bare]);
|
||||
if ( contact_sessions == 0 ) then
|
||||
-- module:log("info","setting seconds to 0 for remote_act_cache %s -> ", contact_bare, remote_act_cache[contact_bare]);
|
||||
seconds = tostring(os.difftime(os.time(), remote_act_cache[contact_bare]));
|
||||
else
|
||||
seconds = 0;
|
||||
-- module:log("info","setting seconds to 0 for remote %s", contact_bare);
|
||||
end
|
||||
else
|
||||
if remote_act_cache[contact_bare] then
|
||||
if ( remote_act_cache[contact_bare] > 0) then
|
||||
seconds = tostring(os.difftime(os.time(), remote_act_cache[contact_bare]));
|
||||
else
|
||||
seconds = 0;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (seconds == "-1") then
|
||||
if (prosody.hosts[lhost]) then
|
||||
local stored_seconds = activity_store:get(luser, "last") or false;
|
||||
if stored_seconds then
|
||||
-- module:log("info", "read persistant lastactivity for %s as %s", luser, dumpTable(stored_seconds));
|
||||
local nseconds = tonumber(stored_seconds);
|
||||
if (nseconds == 0) then
|
||||
-- module:log("info","using restored for %s @ %s ", luser, lhost);
|
||||
map[event.origin.username] = {s = "0", t = text};
|
||||
else
|
||||
seconds = tostring(os.difftime(os.time(), nseconds));
|
||||
-- module:log("info","using restored for %s @ %s => %s", luser, lhost, nseconds);
|
||||
map[event.origin.username] = {s = nseconds, t = text};
|
||||
end
|
||||
end
|
||||
else
|
||||
if (luser ~= nil) then
|
||||
local contact_bare = luser.."@"..lhost;
|
||||
local stored_seconds = remote_activity_store:get(contact_bare, "last") or false;
|
||||
if stored_seconds then
|
||||
-- module:log("info", "read persistant lastactivity for %s as %s", luser, dumpTable(stored_seconds));
|
||||
local nseconds = tonumber(stored_seconds);
|
||||
if (nseconds == 0) then
|
||||
-- module:log("info","using restored for %s @ %s ", luser, lhost);
|
||||
map[event.origin.username] = {s = "0", t = text};
|
||||
else
|
||||
seconds = tostring(os.difftime(os.time(), nseconds));
|
||||
-- module:log("info","using restored for %s @ %s => %s", luser, lhost, nseconds);
|
||||
map[event.origin.username] = {s = nseconds, t = text};
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- hash = get_avatar_hash(luser, lhost);
|
||||
-- reply:add_child():tag("result", {jid=abjid, seconds=seconds, photo=hash}):up();
|
||||
-- module:log("info","determined for %s => %s seconds", abjid, seconds);
|
||||
-- module:log("info","determined for %s => %s seconds", abjid, tostring(seconds));
|
||||
-- module:log("info","type jid: %s", type(abjid));
|
||||
-- module:log("info","type seconds: %s", type(seconds));
|
||||
-- module:log("info"," reply before add: %s", reply);
|
||||
|
||||
-- reply:add_child():tag("result", { jid=abjid; seconds=tostring(seconds); } ):up();
|
||||
local rtag = st.stanza("result", { jid=abjid; seconds=tostring(seconds); });
|
||||
reply:add_child(rtag);
|
||||
end
|
||||
-- module:log("info", "sending reply %s", reply);
|
||||
origin.send(reply);
|
||||
end
|
||||
return true;
|
||||
end);
|
||||
|
||||
module:hook("iq/bare/jabber:iq:last:query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if stanza.attr.type == "get" then
|
||||
local username = jid_split(stanza.attr.to) or origin.username;
|
||||
local luser, lhost = jid_split(stanza.attr.to);
|
||||
-- if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
|
||||
if stanza.attr.to then
|
||||
local seconds, text = "-1", "";
|
||||
local hash = "";
|
||||
if map[username] then
|
||||
mseconds = map[username].s or 0;
|
||||
-- module:log("info", "mseconds: %s", mseconds);
|
||||
if (mseconds == 0) then
|
||||
seconds=0;
|
||||
map[username].s=os.time();
|
||||
else
|
||||
-- module:log("info", "difftime from map for user %s is %s", username, dumpTable(map[username]));
|
||||
-- module:log("info", "difftime will be called for %s == type %s", map[username].s, type(map[username].s));
|
||||
seconds = tostring(os.difftime(os.time(), map[username].s));
|
||||
-- module:log("info","using2 for %s @ %s => %s --- stored was %s", luser, lhost, seconds, map[username].s);
|
||||
-- text = map[username].s;
|
||||
end
|
||||
end
|
||||
if luser and lhost then
|
||||
local user_sessions = hosts[lhost] and hosts[lhost].sessions[luser];
|
||||
if user_sessions then
|
||||
local latest_hibernated_stamp = 0;
|
||||
local all_hibernated = true;
|
||||
for x,s in pairs(user_sessions.sessions) do
|
||||
if (s.hibernating) then
|
||||
-- module:log("info", "user_sessions s.hibernated %s is %s", s, s.hibernating);
|
||||
if (s.hibernating > latest_hibernated_stamp) then
|
||||
latest_hibernated_stamp = s.hibernating;
|
||||
end
|
||||
else
|
||||
-- module:log("info", "user_sessions %s is active", s);
|
||||
all_hibernated = false;
|
||||
end
|
||||
end
|
||||
-- module:log("info","latest_hibernated_stamp is %s", latest_hibernated_stamp);
|
||||
-- module:log("info","all_hibernated is %s", all_hibernated);
|
||||
if (latest_hibernated_stamp > 0 and all_hibernated) then
|
||||
seconds = os.difftime(os.time(), latest_hibernated_stamp)
|
||||
else
|
||||
seconds = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
if (seconds == "-1") then
|
||||
local stored_seconds = activity_store:get(luser, "last") or -1;
|
||||
if (stored_seconds ~= 0) then
|
||||
-- -- module:log("info", "read persistant lastactivity for %s as %s", luser, dumpTable(stored_seconds));
|
||||
if (stored_seconds == -1) then
|
||||
seconds = -1
|
||||
else
|
||||
seconds = tostring(os.difftime(os.time(), stored_seconds));
|
||||
end;
|
||||
-- module:log("info","using4 for %s @ %s => %s", luser, lhost, seconds);
|
||||
if event.origin.username ~= nil then
|
||||
map[event.origin.username] = {s = seconds, t = text};
|
||||
end
|
||||
else
|
||||
-- local now = os.time();
|
||||
-- module:log("info", "no stored data for %s @ %s", luser, lhost);
|
||||
-- local upers = activity_store:set(luser, "last", now);
|
||||
-- seconds = 0;
|
||||
end
|
||||
end
|
||||
-- module:log("info","sending reply with: seconds=%s text=%s", seconds, text);
|
||||
origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=tostring(seconds)}):text(tostring(text)));
|
||||
else
|
||||
origin.send(st.error_reply(stanza, 'auth', 'forbidden'));
|
||||
end
|
||||
return true;
|
||||
end
|
||||
end);
|
||||
|
||||
module.save = function()
|
||||
return {map = map};
|
||||
end
|
||||
module.restore = function(data)
|
||||
map = data.map or {};
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
-- mod_vnc_muc_automember
|
||||
--
|
||||
|
||||
-- Copyright (C) 2020 VNC - Virtual Network Consult AG
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
--
|
||||
-- A module to automatically add invited users as members
|
||||
--
|
||||
|
||||
local st = require"util.stanza"
|
||||
|
||||
local muc_util = module:require "muc/util";
|
||||
local valid_affiliations = muc_util.valid_affiliations;
|
||||
|
||||
-- When an invite is sent; add an affiliation for the invitee
|
||||
module:hook("muc-invite", function(event)
|
||||
local room = event.room;
|
||||
local stanza = event.stanza;
|
||||
local invitee = stanza.attr.to;
|
||||
local affiliation = room:get_affiliation(invitee);
|
||||
local invited_unaffiliated = valid_affiliations[affiliation or "none"] <= valid_affiliations.none;
|
||||
if invited_unaffiliated then
|
||||
local from = stanza:get_child("x", "http://jabber.org/protocol/muc#user")
|
||||
:get_child("invite").attr.from;
|
||||
module:log("debug", "%s invited %s into members only room %s, granting membership",
|
||||
from, invitee, room.jid);
|
||||
-- This might fail; ignore for now
|
||||
room:set_affiliation(true, invitee, "member", "Invited by " .. from);
|
||||
room:save();
|
||||
end
|
||||
end);
|
||||
@@ -0,0 +1,92 @@
|
||||
local jid_split = require "util.jid".split;
|
||||
local st = require "util.stanza";
|
||||
local json = require "util.json";
|
||||
|
||||
|
||||
module:depends"muc";
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
local function get_vdata(room)
|
||||
local vdata = room._data.vdata;
|
||||
if vdata then -- explicitly configured
|
||||
return vdata;
|
||||
end
|
||||
return "{ }";
|
||||
end
|
||||
|
||||
module:hook("muc-config-form", function(event)
|
||||
local room, form = event.room, event.form;
|
||||
local vdata = get_vdata(room);
|
||||
table.insert(form, {
|
||||
name = "muc#roomconfig_vdata",
|
||||
type = "text-single",
|
||||
label = "muc data",
|
||||
value = vdata,
|
||||
});
|
||||
-- value = get_e2e(room),
|
||||
end);
|
||||
|
||||
|
||||
local function broadcast_presence(room)
|
||||
local data = room._data;
|
||||
local aff = room._affiliations;
|
||||
module:log("info", "updated config for room %s", room);
|
||||
module:log("info", "new data: %s", dumpTable(data));
|
||||
module:log("info", "new aff: %s", dumpTable(aff));
|
||||
end
|
||||
|
||||
module:hook("muc-config-submitted", function(event)
|
||||
local room, fields, changed = event.room, event.fields, event.changed;
|
||||
local data = room._data;
|
||||
local affiliations = room._affiliations;
|
||||
-- module:log("info", "updated config for room %s", room);
|
||||
-- module:log("info", "new data: %s", dumpTable(data));
|
||||
-- module:log("info", "new aff: %s", dumpTable(affiliations));
|
||||
-- module:log("info", "event.fields: %s", dumpTable(fields));
|
||||
-- module:log("info", "event.changed: %s", dumpTable(changed));
|
||||
|
||||
room._data.vdata = fields["muc#roomconfig_vdata"];
|
||||
if type(changed) == "table" then
|
||||
changed["muc#roomconfig_vdata"] = true;
|
||||
else
|
||||
event.changed = true;
|
||||
end
|
||||
local msg = st.message({type='groupchat', from=room.jid})
|
||||
:tag('x', {xmlns='xmpp:vnctalk:update'})
|
||||
:tag("affiliations"):text(json.encode(affiliations)):up()
|
||||
:tag("data"):text(json.encode(data)):up();
|
||||
module:log("info","st: %s", tostring(msg));
|
||||
room:broadcast_message(msg);
|
||||
|
||||
end, -1);
|
||||
|
||||
|
||||
module:hook("muc-disco#info", function (event)
|
||||
local room, form, formdata = event.room, event.form, event.formdata;
|
||||
|
||||
local vdata = get_vdata(room);
|
||||
module:log("info", "got disco vdata as %s", vdata);
|
||||
-- if not e2e or e2e == "" then
|
||||
--return;
|
||||
-- end
|
||||
table.insert(form, {
|
||||
name = "muc#roominfo_vdata",
|
||||
});
|
||||
formdata["muc#roominfo_vdata"] = vdata;
|
||||
end);
|
||||
@@ -0,0 +1,706 @@
|
||||
-- mod_muc_notifications
|
||||
--
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
--
|
||||
-- A module to notify non-present members of messages in a group chat
|
||||
--
|
||||
|
||||
local http = require "net.http";
|
||||
local json = require "util.json";
|
||||
-- local ltn12 = require "ltn12"
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid = require "util.jid";
|
||||
local timer = require "util.timer";
|
||||
local id = require"util.id"
|
||||
local st = require"util.stanza"
|
||||
local storage_host = module:get_option_string("storage_host");
|
||||
|
||||
module:log("info","founs storage_host: %s", storage_host);
|
||||
|
||||
|
||||
local fcmAPIKey = module:get_option_string("fcm_api_key");
|
||||
local fcmAPIURL = module:get_option_string("fcm_api_url");
|
||||
|
||||
local moduleIsActive = fcmAPIURL and fcmAPIKey;
|
||||
|
||||
local inactive_devices = {};
|
||||
|
||||
local ThisDomain = module.host;
|
||||
|
||||
local xmlns_sm2 = "urn:xmpp:sm:2";
|
||||
local xmlns_sm3 = "urn:xmpp:sm:3";
|
||||
|
||||
|
||||
local fcm_token_store = module:open_store("fcmtoken");
|
||||
local fcm_token_map_store = module:open_store("fcmtoken", "map");
|
||||
local vcard_storage = module:open_store("vcard");
|
||||
local private_storage = module:open_store("private");
|
||||
|
||||
timer.add_task(3, function ()
|
||||
private_storage = module:open_host_store(storage_host, "private");
|
||||
vcard_storage = module:open_host_store(storage_host, "vcard");
|
||||
fcm_token_store = module:open_host_store(storage_host, "fcmtoken");
|
||||
fcm_token_map_store = module:open_host_store(storage_host, "fcmtoken", "map");
|
||||
if not private_storage or not vcard_storage then
|
||||
module:log("info", "private/vcard_store %s/%s not found? - will try again", private_storage, vcard_storage);
|
||||
return 3;
|
||||
else
|
||||
module:log("info", "private/vcard_store are now %s/%s", private_storage, vcard_storage);
|
||||
end
|
||||
end
|
||||
);
|
||||
|
||||
|
||||
-- some helper functions
|
||||
local function deserializeDataElement(pdElement, expectedName)
|
||||
--module:log("debug", "deserializeDataElement("..expectedName..", "..dumpTable(pdElement)..")");
|
||||
if (not pdElement) then return nil end
|
||||
if (type(pdElement) ~= "table") then return nil end
|
||||
|
||||
if (pdElement[1] and pdElement["attr"] and pdElement["attr"]["key"] and (pdElement["attr"]["key"] == expectedName))
|
||||
then
|
||||
local result = json.decode(pdElement[1]);
|
||||
--module:log("debug", "deserializeDataElement() = '"..dumpTable(result).."'");
|
||||
return result;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
|
||||
local function contains(list, x)
|
||||
if (list) then
|
||||
for _, v in pairs(list) do
|
||||
if (v == x) then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
local function trim(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("^%s*(.-)%s*$", "%1"))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function collapseSpaces(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("%s+", " "))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function prettyUsername(userName)
|
||||
if not userName then
|
||||
userName = "";
|
||||
end
|
||||
userName = string.upper(string.sub(userName, 1, 1))..string.sub(userName, 2, -1) -- upcase first char in any case
|
||||
if string.find(userName, "%.") -- contains dot
|
||||
then
|
||||
local i = 1
|
||||
while true do
|
||||
local j = string.find(userName, "%.", i)
|
||||
if j == nil or #userName == i then break end
|
||||
-- replace dot with space and upcase next char
|
||||
userName = string.sub(userName, 1, j-1).." "..string.upper(string.sub(userName, j+1, j+1))..string.sub(userName, j+2, -1)
|
||||
if #userName <= j then break end
|
||||
i = j
|
||||
end
|
||||
end
|
||||
return trim(collapseSpaces(userName));
|
||||
end
|
||||
|
||||
|
||||
local function getDisplayName(userName)
|
||||
-- module:log("info", "getDisplayName user: %s", userName);
|
||||
local vCard, err = vcard_storage:get(userName);
|
||||
if vCard then
|
||||
vCard = st.deserialize(vCard);
|
||||
end
|
||||
local name = nil;
|
||||
if not vCard or err then
|
||||
module:log("debug", "Unable to get vCard for %s : %s ", userName, tostring(err));
|
||||
else
|
||||
name = vCard:find("{vcard-temp}FN#");
|
||||
end
|
||||
if not name then name = prettyUsername(userName) or userName end
|
||||
-- module:log("info", "name for %s : %s", userName, name)
|
||||
return name;
|
||||
end
|
||||
|
||||
local function getPostCallback(_fcmId, _userName)
|
||||
return function(body, code, response)
|
||||
local shortKey = string.sub(_fcmId, 1, 6)
|
||||
if code ~= 200 then
|
||||
module:log("info", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||||
-- if body then module:log("error", body); end
|
||||
else
|
||||
module:log("debug", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||||
end
|
||||
return false -- close request
|
||||
--return true -- keep request open
|
||||
end
|
||||
end
|
||||
|
||||
local function postFCM(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom, _isMessageCorrection, _enableSoundOpt)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
local domainname = "";
|
||||
local v_attachment = "";
|
||||
domainname = module:get_option_string("storage_host");
|
||||
|
||||
if (_attachment_type ~= "") and (_attachment_type ~=nil) then
|
||||
v_attachment = _attachment_type;
|
||||
end
|
||||
|
||||
local lsource = _source;
|
||||
local ltype = _type;
|
||||
local signal = nil;
|
||||
local nfrom = _source;
|
||||
local nJitsiURL = "";
|
||||
local nJitsiRoom = "";
|
||||
local replaceMsgId = "";
|
||||
|
||||
if (_isMessageCorrection ~= nil) then
|
||||
replaceMsgId = _isMessageCorrection;
|
||||
end
|
||||
|
||||
|
||||
if (_vncTalkConferenceId ~= nil) then
|
||||
if string.match(_vncTalkConferenceId, "@") then
|
||||
lsource = _vncTalkConferenceId;
|
||||
end
|
||||
end
|
||||
|
||||
if (_vncTalkConferenceEtype ~=nil) then
|
||||
signal = "1";
|
||||
ltype = _vncTalkConferenceEtype;
|
||||
end
|
||||
|
||||
if (_nFromJid ~= nil) then
|
||||
nfrom = _nFromJid;
|
||||
end
|
||||
|
||||
if (_jitsiURL ~= nil) then
|
||||
nJitsiURL = _jitsiURL;
|
||||
end
|
||||
|
||||
if (_jitsiRoom ~= nil) then
|
||||
nJitsiRoom = _jitsiRoom;
|
||||
end
|
||||
|
||||
local stamp = os.time();
|
||||
|
||||
module:log("info", "postFCM username %s", _userName);
|
||||
local uname = "";
|
||||
if string.match(_userName, "@") then
|
||||
uname = _userName;
|
||||
else
|
||||
uname = _userName.."@"..domainname;
|
||||
end
|
||||
|
||||
local fcmRequest = {
|
||||
to = _fcmId,
|
||||
enableSoundOpt = _enableSoundOpt,
|
||||
content_available = true,
|
||||
priority = "high",
|
||||
data = {
|
||||
nType = "local_notification", -- notification type
|
||||
eType = ltype, -- event type
|
||||
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
|
||||
conferenceId = _vncTalkConferenceId,
|
||||
nfrom = nfrom,
|
||||
nto = uname,
|
||||
name = _senderName, -- group chat nick name or single chat peer vCard name",
|
||||
gt = _groupTopic, -- topic of room iff groupchat
|
||||
aft = v_attachment,
|
||||
callSignal = signal,
|
||||
jitsiURL = nJitsiURL,
|
||||
jitsiRoom = nJitsiRoom,
|
||||
t = stamp,
|
||||
lang = _lang,
|
||||
msgid = _msgid,
|
||||
replaceid = replaceMsgId,
|
||||
body = _body -- notification body/text content
|
||||
}
|
||||
};
|
||||
|
||||
fcmRequest["android"] = { priority = "high", ttl = 345600 };
|
||||
fcmRequest["webpush"] = { headers = { Urgency = "high", TTL = 345600 } };
|
||||
|
||||
local data = json.encode(fcmRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Authorization"] = "key="..fcmAPIKey,
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
-- module:log("info", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body);
|
||||
-- ssl does not seem to work with net.http, so we proxy it
|
||||
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
|
||||
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||||
end
|
||||
|
||||
local function postFCMIOS(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom, _isMessageCorrection, _enableSoundOpt)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
local domainname = "";
|
||||
local ntitle = "";
|
||||
local nbody = "";
|
||||
local v_attachment = "";
|
||||
local nJitsiURL = "";
|
||||
local nJitsiRoom = "";
|
||||
local replaceMsgId = "";
|
||||
|
||||
if (_isMessageCorrection ~= nil) then
|
||||
replaceMsgId = _isMessageCorrection;
|
||||
end
|
||||
|
||||
domainname = module:get_option_string("storage_host");
|
||||
if (_groupTopic ~= "") and (_groupTopic ~= nil) then
|
||||
ntitle = _groupTopic;
|
||||
else
|
||||
ntitle = jid_split(_source);
|
||||
end
|
||||
|
||||
if (_attachment_type ~= "") and (_attachment_type ~=nil) then
|
||||
v_attachment = _attachment_type;
|
||||
end
|
||||
|
||||
local _click_action = _type:upper();
|
||||
|
||||
if (_click_action == "NORMAL") then
|
||||
_click_action = "CHAT";
|
||||
end
|
||||
|
||||
local lsource = _source;
|
||||
local ltype = _type;
|
||||
local signal = nil;
|
||||
local nfrom = _source;
|
||||
|
||||
if (_vncTalkConferenceId ~= nil) then
|
||||
if string.match(_vncTalkConferenceId, "@") then
|
||||
lsource = _vncTalkConferenceId;
|
||||
end
|
||||
end
|
||||
|
||||
if (_vncTalkConferenceEtype ~=nil) then
|
||||
signal = "1";
|
||||
ltype = _vncTalkConferenceEtype;
|
||||
end
|
||||
|
||||
if (_nFromJid ~= nil) then
|
||||
nfrom = _nFromJid;
|
||||
end
|
||||
if (_jitsiURL ~= nil) then
|
||||
nJitsiURL = _jitsiURL;
|
||||
end
|
||||
|
||||
if (_jitsiRoom ~= nil) then
|
||||
nJitsiRoom = _jitsiRoom;
|
||||
end
|
||||
|
||||
local uname = "";
|
||||
if string.match(_userName, "@") then
|
||||
uname = _userName;
|
||||
else
|
||||
uname = _userName.."@"..domainname;
|
||||
end
|
||||
|
||||
|
||||
local fcmRequest = {
|
||||
to = _fcmId,
|
||||
priority = "high",
|
||||
enableSoundOpt = _enableSoundOpt,
|
||||
notification = {
|
||||
title = ntitle,
|
||||
body = _body,
|
||||
mutable_content = true,
|
||||
sound = "incoming-call-loop.caff",
|
||||
thread_id = lsource,
|
||||
click_action = _click_action
|
||||
},
|
||||
data = {
|
||||
nType = "local_notification", -- notification type
|
||||
eType = ltype, -- event type
|
||||
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
|
||||
conferenceId = _vncTalkConferenceId,
|
||||
nfrom = nfrom,
|
||||
nto = uname,
|
||||
name = _senderName, -- group chat nick name or single chat peer vCard name",
|
||||
gt = ntitle, -- topic of room iff groupchat
|
||||
aft = v_attachment,
|
||||
callSignal = signal,
|
||||
jitsiURL = nJitsiURL,
|
||||
jitsiRoom = nJitsiRoom,
|
||||
lang = _lang,
|
||||
msgid = _msgid,
|
||||
replaceid = replaceMsgId,
|
||||
body = _body -- notification body/text content
|
||||
}
|
||||
};
|
||||
|
||||
local data = json.encode(fcmRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Authorization"] = "key="..fcmAPIKey,
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
-- module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, string.sub(_body, 20).."...");
|
||||
module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body.."...");
|
||||
-- ssl does not seem to work with net.http, so we proxy it
|
||||
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
|
||||
module:log("debug", "calling http.request -- fcmAPIURL: %s", fcmAPIURL);
|
||||
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||||
end
|
||||
|
||||
|
||||
-- Given a stanza, compute if it qualifies as important (notifiable)
|
||||
-- return true for message stanzas with non-empty body
|
||||
-- Should probably use something similar to muc-message-is-historic event
|
||||
local function is_important(stanza)
|
||||
local body = stanza:find("body#")
|
||||
return body and #body
|
||||
end
|
||||
|
||||
|
||||
-- get notify options for local users
|
||||
local function getNotifyOptionsForUser(userName)
|
||||
--module:log("debug", "getFCMtokenForUser("..userName..")");
|
||||
local pd, pfFetchError = private_storage:get(userName);
|
||||
if (pfFetchError) then
|
||||
module:log(
|
||||
"error",
|
||||
"Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError)
|
||||
);
|
||||
return 0,{};
|
||||
else
|
||||
local tokens;
|
||||
local nType;
|
||||
local isGlobalMute = false;
|
||||
local nLang = "en";
|
||||
local enableSound;
|
||||
if (pd) then
|
||||
for key,value in pairs(pd)
|
||||
do
|
||||
-- module:log("info", "PD("..userName..") : "..tostring(key).." => "..tostring(value));
|
||||
module:log("debug", "PD( %s) : %s => %s ", userName,tostring(key),tostring(value));
|
||||
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
|
||||
--module:log("debug", "PD("..userName..") : "..json.encode(value));
|
||||
-- module:log("debug", "PD( %s ) : %s ", userName ,json.encode(value));
|
||||
for k2,v2 in pairs(value) do
|
||||
if (not tokens) then
|
||||
local fcm = deserializeDataElement(v2, "fcm");
|
||||
if (type(fcm) == "table") then
|
||||
tokens = {};
|
||||
for token,x in pairs(fcm) do
|
||||
-- module:log("debug", "token: %s - x[os]: %s", token, x["os"]);
|
||||
if (x["os"] ~= nil) then
|
||||
table.insert(tokens, token.."@"..x["os"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if (not nType) then
|
||||
local options = deserializeDataElement(v2, "notifyOptions");
|
||||
if (options and (type(options) == "table") and options["type"]) then
|
||||
nType = tonumber(options["type"]);
|
||||
end
|
||||
end
|
||||
if (not nType) then
|
||||
local options = deserializeDataElement(v2, "notification");
|
||||
-- module:log("info", "got new NTYPE option %s", options);
|
||||
if (options) then
|
||||
nType = tonumber(options);
|
||||
-- module:log("info", "got nType: %s", nType);
|
||||
end
|
||||
end
|
||||
if (enableSound == nil) then
|
||||
local enableSoundOpt = deserializeDataElement(v2, "enabledSound");
|
||||
-- module:log("info", "got new SOUND option %s", enableSoundOpt);
|
||||
enableSound = enableSoundOpt;
|
||||
end;
|
||||
|
||||
local pGlobalMute = deserializeDataElement(v2, "globalMute");
|
||||
if (pGlobalMute) then
|
||||
-- module:log("info", "got pGlobalMute: %s", pGlobalMute);
|
||||
nType = 0;
|
||||
isGlobalMute = true;
|
||||
end
|
||||
if (not nLang) then
|
||||
local options = deserializeDataElement(v2, "lang");
|
||||
-- module:log("debug", "PDLang: %s => %s", userName, json.encode(options));
|
||||
if (options) and (type(options) == "string") then
|
||||
nLang = options;
|
||||
end;
|
||||
-- module:log("debug", "type of options: %s", type(options));
|
||||
end
|
||||
end
|
||||
break; -- we can stop after the documents:stanza:io:json element
|
||||
end
|
||||
end
|
||||
end
|
||||
--module:log("debug", "getNotifyOptionsForUser("..userName..") = "..tostring(tokens).." => "..tostring(nType));
|
||||
|
||||
local map_res = fcm_token_store:get(userName);
|
||||
|
||||
-- module:log("debug", "result from map store for %s is: %s", userName, dumpTable(map_res));
|
||||
|
||||
if (map_res) then
|
||||
for _device, _tkr in pairs(map_res) do
|
||||
-- module:log("debug", "got for %s device %s => %s", userName, _device, dumpTable(_tkr));
|
||||
-- module:log("debug", "got for %s device %s => token: %s - os: %s", userName, _device, _tkr.token, _tkr.os);
|
||||
local sessions = prosody.bare_sessions;
|
||||
local ubjid = userName.."@"..storage_host;
|
||||
local u_sessions = sessions[ubjid] or nil;
|
||||
local deviceonline = false;
|
||||
if (u_sessions) then
|
||||
local u_sessions_device = u_sessions["sessions"] or nil;
|
||||
if (u_sessions_device) then
|
||||
-- module:log("info", "devise is %s", _device);
|
||||
-- module:log("debug", "u_sessions_device %s", dumpTable(u_sessions_device));
|
||||
local dsession = u_sessions_device[_device] or nil;
|
||||
-- module:log("debug", "dsession %s", dumpTable(dsession));
|
||||
if (dsession) then
|
||||
local hibernated = dsession["hibernated"] or nil;
|
||||
local hibernating = dsession["hibernating"] or nil;
|
||||
local awaiting_ack = dsession["awaiting_ack"] or nil;
|
||||
-- module:log("info", "found session for _device %s - hibernating: %s, hibernated %s", _device, hibernating, hibernated);
|
||||
if not ((hibernated) or (hibernating)) then
|
||||
local dev_in_background = inactive_devices[_device] or nil;
|
||||
if dev_in_background then
|
||||
if awaiting_ack then
|
||||
module:log("debug", "device %s is still connected, but inactive and awaiting_ack", _device);
|
||||
-- module:log("info", "handling1: %s", _tkr.token.."@".._tkr.os);
|
||||
else
|
||||
module:log("debug", "device %s is still connected, but inactive", _device);
|
||||
-- module:log("info", "handling2: %s", _tkr.token.."@".._tkr.os);
|
||||
if (_tkr.os ~= "ios") then
|
||||
deviceonline = true;
|
||||
end
|
||||
end
|
||||
else
|
||||
if awaiting_ack then
|
||||
module:log("debug", "device %s is connected and active, but awaiting_ack", _device);
|
||||
-- module:log("info", "handling3: %s", _tkr.token.."@".._tkr.os);
|
||||
else
|
||||
module:log("debug", "device %s is connected and active", _device);
|
||||
-- module:log("info", "handling4: %s", _tkr.token.."@".._tkr.os);
|
||||
if (_tkr.os ~= "ios") then
|
||||
deviceonline = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not(deviceonline) then
|
||||
local already_added = contains(tokens, _tkr.token.."@".._tkr.os);
|
||||
module:log("debug","already_added: %s", _tkr.token.."@".._tkr.os);
|
||||
if not(tokens) then
|
||||
tokens = {};
|
||||
end
|
||||
if not(already_added) then
|
||||
table.insert(tokens, _tkr.token.."@".._tkr.os);
|
||||
end
|
||||
end
|
||||
-- module:log("debug","tokens table is now: %s", dumpTable(tokens));
|
||||
end
|
||||
end
|
||||
|
||||
if (not tokens) then tokens = {}; end;
|
||||
if (not nType) then nType = 2; end;
|
||||
local sessions = prosody.bare_sessions;
|
||||
local ubjid = userName.."@"..ThisDomain;
|
||||
for _user, _resdetails in pairs(sessions) do
|
||||
if (_user == ubjid) then
|
||||
for _res, _detail1 in pairs(_resdetails["sessions"]) do
|
||||
local hibernated = _detail1["hibernated"] or _detail1["hibernating"] or nil;
|
||||
if (hibernated) then
|
||||
module:log("debug", "for _user: %s and _res: %s got hibernated since: %s", _user, _res, hibernated);
|
||||
else
|
||||
module:log("debug", "for _user: %s and _res: %s got active session?", _user, _res);
|
||||
--module:log("debug", "for _user: %s and _res: %s got detail: %s", _user, _res, dumpTable(_detail1));
|
||||
end
|
||||
end
|
||||
-- module:log("debug", "session for _res %s is: %s", _res, dumpTable(_resdetails));
|
||||
end
|
||||
end
|
||||
if (isGlobalMute) then
|
||||
tokens = {};
|
||||
end
|
||||
-- module:log("debug", "sessions: %s", dumpTable(sessions[ubjid]));
|
||||
return nType, tokens, nLang, enableSound;
|
||||
end
|
||||
end
|
||||
|
||||
local function fcm_notify(userName, title, body, type, _senderName, _groupTopic, vnc_attachment_type, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom, _isMessageCorrection)
|
||||
if (not moduleIsActive) then module:log("info", "module not active - bailung out"); return end;
|
||||
-- module:log("info", "getting notify opts for %s", userName);
|
||||
local uname, uhost = jid_split(userName);
|
||||
if (uhost ~= storage_host) then return end;
|
||||
local nType, tokens, lang, soundOpt = getNotifyOptionsForUser(uname);
|
||||
if (tokens and (#tokens) > 0) and ((nType == 1) or (nType == 2))
|
||||
then
|
||||
-- module:log(
|
||||
-- "info", "in fcm_notify(%s, %s, %s) => type=%s, %s tokens",
|
||||
-- userName, title, body, tostring(nType), tostring((#tokens))
|
||||
--);
|
||||
local sName = _senderName;
|
||||
local gTopic = _groupTopic;
|
||||
if (nType == 1) then
|
||||
sName = "new message";
|
||||
-- gTopic = "new message";
|
||||
body = "";
|
||||
end
|
||||
for i,token in pairs(tokens) do
|
||||
if (token)
|
||||
then
|
||||
local rtoken, ros = jid_split(token);
|
||||
-- module:log("debug","got rtoken: %s - os: %s ", rtoken, ros);
|
||||
if (ros == "ios") then
|
||||
-- module:log("debug", "postFCMIOS with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang);
|
||||
postFCMIOS(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom, _isMessageCorrection, soundOpt);
|
||||
else
|
||||
-- module:log("info", "postFCM with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang, _nFrom);
|
||||
postFCM(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom, _isMessageCorrection, soundOpt);
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
module:log("debug", "fcm_notify(%s, %s, %s) => type=%s, no tokens", userName, title, body, tostring(nType));
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function handle_muc_message(event)
|
||||
-- event.room and event.stanza are available
|
||||
local room = event.room
|
||||
local stanza = event.stanza
|
||||
-- module:log("info", "handling stanza %s", stanza);
|
||||
-- module:log("info", "room: %s", dumpTable(room));
|
||||
|
||||
local message_id = event.stanza.attr.id;
|
||||
-- module:log("info","message id: %s", event.stanza.attr.id);
|
||||
local type = stanza.attr.type or "normal";
|
||||
local from = jid_bare(stanza.attr.from);
|
||||
local to = jid_bare(stanza.attr.to) or from;
|
||||
|
||||
local vncTalkConferenceEtype = stanza:find("{xmpp:vnctalk}vncTalkConference/eventType#") or nil;
|
||||
|
||||
-- module:log("info", "handling message from=%s, to=%s", stanza.attr.from, to);
|
||||
|
||||
|
||||
local content = trim(stanza:find("body#"));
|
||||
local delayedDelivery = stanza:get_child("delay", "urn:xmpp:delay");
|
||||
|
||||
-- if we have a vncTalk incoming or whiteboard element, it overrides the type
|
||||
local vncTalkIncomingType = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceType#");
|
||||
local vncTalkConferenceId = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceId#") or nil;
|
||||
local jitsiURL = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiURL#") or nil;
|
||||
local jitsiRoom = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiRoom#") or nil;
|
||||
local vncTalkWhiteboard = stanza:find("{xmpp:vnctalk}whiteboard");
|
||||
local isSentCarbonMessage = stanza:get_child("sent", "urn:xmpp:carbons:2");
|
||||
local vnc_attachment_type = stanza:find("{xmpp:vnctalk}attachment/fileType#");
|
||||
|
||||
local vnctalk_broadcast = stanza:get_child("vncTalkBroadcast", "xmpp:vnctalk");
|
||||
local vnctalk_broadcast_childid = nil;
|
||||
if vnctalk_broadcast then
|
||||
if starts_with(to, "broadcast") then
|
||||
-- module:log("message is to broadcast - bailing out");
|
||||
return nil;
|
||||
end
|
||||
vnctalk_broadcast_childid = vnctalk_broadcast.attr.id;
|
||||
local vnctalk_broadcast_title = vnctalk_broadcast.attr.title;
|
||||
local vnctalk_broadcast_target = vnctalk_broadcast.attr.origtarget;
|
||||
local body = stanza:get_child("body");
|
||||
-- module:log("debug", "broadcast body: %s", content);
|
||||
-- module:log("debug", "jid_split(to): %s", jid_split(to));
|
||||
-- fcm_notify(jid_split(to), vnctalk_broadcast_target, content, type, getDisplayName(jid_split(from)), "Broadcast: "..vnctalk_broadcast_title, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local isMessageCorrection = stanza:find("{urn:xmpp:message-correct:0}replace@id") or nil;
|
||||
if (isMessageCorrection) then
|
||||
content = " ";
|
||||
type = "CORRECTION";
|
||||
end
|
||||
|
||||
|
||||
local auxType = vncTalkIncomingType or (vncTalkWhiteboard and "whiteboard");
|
||||
|
||||
if (auxType)
|
||||
then
|
||||
content = auxType;
|
||||
type = auxType;
|
||||
end;
|
||||
|
||||
local doNotify = content and (content ~= " ") and (content ~= "") and (delayedDelivery == nil) and (isSentCarbonMessage == nil) and (
|
||||
(type == "chat") or (type == "groupchat") or (type == "audio") or (type == "video") or (type == "whiteboard") or (type == "screen") or (type == "CORRECTION")
|
||||
);
|
||||
|
||||
if (doNotify) then
|
||||
local sender = "";
|
||||
local f2,h2,n2 = jid.split(stanza.attr.from);
|
||||
if (room._affiliations[n2] ~= nil) then sender = n2; end
|
||||
-- module:log("info","sender is %s", sender);
|
||||
local roomTopic = room._data.subject or nil;
|
||||
for jid, aff in pairs(room._affiliations) do
|
||||
if aff ~= "outcast" then
|
||||
-- module:log("info", "found jid: %s as aff: %s", jid, aff);
|
||||
local roomJid = to;
|
||||
if (sender ~= jid) then
|
||||
module:log("info", "from is: %s", from);
|
||||
fcm_notify(jid, roomJid, content, type, getDisplayName(jid_split(sender)), roomTopic, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, sender, jitsiURL, jitsiRoom, isMessageCorrection);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module:hook("muc-broadcast-message", handle_muc_message, 5)
|
||||
|
||||
module:hook("host-activated", function(host)
|
||||
module:log("info", "got activated event for host %s", host);
|
||||
end);
|
||||
|
||||
module:log("debug", "Module loaded")
|
||||
@@ -0,0 +1,654 @@
|
||||
-- mod_muc_notifications
|
||||
--
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
--
|
||||
-- A module to notify non-present members of messages in a group chat
|
||||
--
|
||||
|
||||
local http = require "net.http";
|
||||
local json = require "util.json";
|
||||
-- local ltn12 = require "ltn12"
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local timer = require "util.timer";
|
||||
local id = require"util.id"
|
||||
local st = require"util.stanza"
|
||||
local storage_host = module:get_option_string("storage_host");
|
||||
|
||||
module:log("info","founs storage_host: %s", storage_host);
|
||||
|
||||
|
||||
local fcmAPIKey = module:get_option_string("fcm_api_key");
|
||||
local fcmAPIURL = module:get_option_string("fcm_api_url");
|
||||
|
||||
local moduleIsActive = fcmAPIURL and fcmAPIKey;
|
||||
|
||||
local inactive_devices = {};
|
||||
|
||||
local ThisDomain = module.host;
|
||||
|
||||
local xmlns_sm2 = "urn:xmpp:sm:2";
|
||||
local xmlns_sm3 = "urn:xmpp:sm:3";
|
||||
|
||||
|
||||
local fcm_token_store = module:open_store("fcmtoken");
|
||||
local fcm_token_map_store = module:open_store("fcmtoken", "map");
|
||||
local vcard_storage = module:open_store("vcard");
|
||||
local private_storage = module:open_store("private");
|
||||
|
||||
timer.add_task(3, function ()
|
||||
private_storage = module:open_host_store(storage_host, "private");
|
||||
vcard_storage = module:open_host_store(storage_host, "vcard");
|
||||
fcm_token_store = module:open_host_store(storage_host, "fcmtoken");
|
||||
fcm_token_map_store = module:open_host_store(storage_host, "fcmtoken", "map");
|
||||
if not private_storage or not vcard_storage then
|
||||
module:log("info", "private/vcard_store %s/%s not found? - will try again", private_storage, vcard_storage);
|
||||
return 3;
|
||||
else
|
||||
module:log("info", "private/vcard_store are now %s/%s", private_storage, vcard_storage);
|
||||
end
|
||||
end
|
||||
);
|
||||
|
||||
|
||||
-- some helper functions
|
||||
local function deserializeDataElement(pdElement, expectedName)
|
||||
--module:log("debug", "deserializeDataElement("..expectedName..", "..dumpTable(pdElement)..")");
|
||||
if (not pdElement) then return nil end
|
||||
if (type(pdElement) ~= "table") then return nil end
|
||||
|
||||
if (pdElement[1] and pdElement["attr"] and pdElement["attr"]["key"] and (pdElement["attr"]["key"] == expectedName))
|
||||
then
|
||||
local result = json.decode(pdElement[1]);
|
||||
--module:log("debug", "deserializeDataElement() = '"..dumpTable(result).."'");
|
||||
return result;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
|
||||
local function contains(list, x)
|
||||
if (list) then
|
||||
for _, v in pairs(list) do
|
||||
if (v == x) then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
local function trim(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("^%s*(.-)%s*$", "%1"))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function collapseSpaces(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("%s+", " "))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function prettyUsername(userName)
|
||||
userName = string.upper(string.sub(userName, 1, 1))..string.sub(userName, 2, -1) -- upcase first char in any case
|
||||
if string.find(userName, "%.") -- contains dot
|
||||
then
|
||||
local i = 1
|
||||
while true do
|
||||
local j = string.find(userName, "%.", i)
|
||||
if j == nil or #userName == i then break end
|
||||
-- replace dot with space and upcase next char
|
||||
userName = string.sub(userName, 1, j-1).." "..string.upper(string.sub(userName, j+1, j+1))..string.sub(userName, j+2, -1)
|
||||
if #userName <= j then break end
|
||||
i = j
|
||||
end
|
||||
end
|
||||
return trim(collapseSpaces(userName));
|
||||
end
|
||||
|
||||
|
||||
local function getDisplayName(userName)
|
||||
-- module:log("info", "getDisplayName user: %s", userName);
|
||||
local vCard, err = vcard_storage:get(userName);
|
||||
if vCard then
|
||||
vCard = st.deserialize(vCard);
|
||||
end
|
||||
local name = nil;
|
||||
if not vCard or err then
|
||||
module:log("debug", "Unable to get vCard for %s : %s ", userName, tostring(err));
|
||||
else
|
||||
name = vCard:find("{vcard-temp}FN#");
|
||||
end
|
||||
if not name then name = prettyUsername(userName) or userName end
|
||||
-- module:log("info", "name for %s : %s", userName, name)
|
||||
return name;
|
||||
end
|
||||
|
||||
local function getPostCallback(_fcmId, _userName)
|
||||
return function(body, code, response)
|
||||
local shortKey = string.sub(_fcmId, 1, 6)
|
||||
if code ~= 200 then
|
||||
module:log("info", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||||
-- if body then module:log("error", body); end
|
||||
else
|
||||
module:log("debug", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||||
end
|
||||
return false -- close request
|
||||
--return true -- keep request open
|
||||
end
|
||||
end
|
||||
|
||||
local function postFCM(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
local domainname = "";
|
||||
local v_attachment = "";
|
||||
domainname = module:get_option_string("storage_host");
|
||||
|
||||
if (_attachment_type ~= "") and (_attachment_type ~=nil) then
|
||||
v_attachment = _attachment_type;
|
||||
end
|
||||
|
||||
local lsource = _source;
|
||||
local ltype = _type;
|
||||
local signal = nil;
|
||||
local nfrom = _source;
|
||||
local nJitsiURL = "";
|
||||
local nJitsiRoom = "";
|
||||
|
||||
if (_vncTalkConferenceId ~= nil) then
|
||||
if string.match(_vncTalkConferenceId, "@") then
|
||||
lsource = _vncTalkConferenceId;
|
||||
end
|
||||
end
|
||||
|
||||
if (_vncTalkConferenceEtype ~=nil) then
|
||||
signal = "1";
|
||||
ltype = _vncTalkConferenceEtype;
|
||||
end
|
||||
|
||||
if (_nFromJid ~= nil) then
|
||||
nfrom = _nFromJid;
|
||||
end
|
||||
|
||||
if (_jitsiURL ~= nil) then
|
||||
nJitsiURL = _jitsiURL;
|
||||
end
|
||||
|
||||
if (_jitsiRoom ~= nil) then
|
||||
nJitsiRoom = _jitsiRoom;
|
||||
end
|
||||
|
||||
local stamp = os.time();
|
||||
|
||||
module:log("info", "postFCM username %s", _userName);
|
||||
local uname = "";
|
||||
if string.match(_userName, "@") then
|
||||
uname = _userName;
|
||||
else
|
||||
uname = _userName.."@"..domainname;
|
||||
end
|
||||
|
||||
local fcmRequest = {
|
||||
to = _fcmId,
|
||||
content_available = true,
|
||||
priority = "high",
|
||||
data = {
|
||||
nType = "local_notification", -- notification type
|
||||
eType = ltype, -- event type
|
||||
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
|
||||
nfrom = nfrom,
|
||||
nto = uname,
|
||||
name = _senderName, -- group chat nick name or single chat peer vCard name",
|
||||
gt = _groupTopic, -- topic of room iff groupchat
|
||||
aft = v_attachment,
|
||||
callSignal = signal,
|
||||
jitsiURL = nJitsiURL,
|
||||
jitsiRoom = nJitsiRoom,
|
||||
t = stamp,
|
||||
lang = _lang,
|
||||
msgid = _msgid,
|
||||
body = _body -- notification body/text content
|
||||
}
|
||||
};
|
||||
|
||||
fcmRequest["android"] = { priority = "high", ttl = 345600 };
|
||||
fcmRequest["webpush"] = { headers = { Urgency = "high", TTL = 345600 } };
|
||||
|
||||
local data = json.encode(fcmRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Authorization"] = "key="..fcmAPIKey,
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
-- module:log("info", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body);
|
||||
-- ssl does not seem to work with net.http, so we proxy it
|
||||
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
|
||||
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||||
end
|
||||
|
||||
local function postFCMIOS(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom)
|
||||
|
||||
if (not moduleIsActive) then return end;
|
||||
local domainname = "";
|
||||
local ntitle = "";
|
||||
local nbody = "";
|
||||
local v_attachment = "";
|
||||
local nJitsiURL = "";
|
||||
local nJitsiRoom = "";
|
||||
|
||||
domainname = module:get_option_string("storage_host");
|
||||
if (_groupTopic ~= "") and (_groupTopic ~= nil) then
|
||||
ntitle = _groupTopic;
|
||||
else
|
||||
ntitle = jid_split(_source);
|
||||
end
|
||||
|
||||
if (_attachment_type ~= "") and (_attachment_type ~=nil) then
|
||||
v_attachment = _attachment_type;
|
||||
end
|
||||
|
||||
local _click_action = _type:upper();
|
||||
|
||||
if (_click_action == "NORMAL") then
|
||||
_click_action = "CHAT";
|
||||
end
|
||||
|
||||
local lsource = _source;
|
||||
local ltype = _type;
|
||||
local signal = nil;
|
||||
local nfrom = _source;
|
||||
|
||||
if (_vncTalkConferenceId ~= nil) then
|
||||
if string.match(_vncTalkConferenceId, "@") then
|
||||
lsource = _vncTalkConferenceId;
|
||||
end
|
||||
end
|
||||
|
||||
if (_vncTalkConferenceEtype ~=nil) then
|
||||
signal = "1";
|
||||
ltype = _vncTalkConferenceEtype;
|
||||
end
|
||||
|
||||
if (_nFromJid ~= nil) then
|
||||
nfrom = _nFromJid;
|
||||
end
|
||||
if (_jitsiURL ~= nil) then
|
||||
nJitsiURL = _jitsiURL;
|
||||
end
|
||||
|
||||
if (_jitsiRoom ~= nil) then
|
||||
nJitsiRoom = _jitsiRoom;
|
||||
end
|
||||
|
||||
local fcmRequest = {
|
||||
to = _fcmId,
|
||||
priority = "high",
|
||||
notification = {
|
||||
title = ntitle,
|
||||
body = _body,
|
||||
mutable_content = true,
|
||||
sound = "incoming-call-loop.caff",
|
||||
thread_id = lsource,
|
||||
click_action = _click_action
|
||||
},
|
||||
data = {
|
||||
nType = "local_notification", -- notification type
|
||||
eType = ltype, -- event type
|
||||
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
|
||||
nfrom = nfrom,
|
||||
nto = _userName.."@"..domainname,
|
||||
name = _senderName, -- group chat nick name or single chat peer vCard name",
|
||||
gt = ntitle, -- topic of room iff groupchat
|
||||
aft = v_attachment,
|
||||
callSignal = signal,
|
||||
jitsiURL = nJitsiURL,
|
||||
jitsiRoom = nJitsiRoom,
|
||||
lang = _lang,
|
||||
msgid = _msgid,
|
||||
body = _body -- notification body/text content
|
||||
}
|
||||
};
|
||||
|
||||
local data = json.encode(fcmRequest);
|
||||
|
||||
local httpRequestOptions = {
|
||||
method = "POST",
|
||||
body = data,
|
||||
headers = {
|
||||
["Connection"] = 'close',
|
||||
["Authorization"] = "key="..fcmAPIKey,
|
||||
["Content-Type"] = "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
-- module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, string.sub(_body, 20).."...");
|
||||
module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body.."...");
|
||||
-- ssl does not seem to work with net.http, so we proxy it
|
||||
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
|
||||
module:log("debug", "calling http.request -- fcmAPIURL: %s", fcmAPIURL);
|
||||
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||||
end
|
||||
|
||||
|
||||
-- Given a stanza, compute if it qualifies as important (notifiable)
|
||||
-- return true for message stanzas with non-empty body
|
||||
-- Should probably use something similar to muc-message-is-historic event
|
||||
local function is_important(stanza)
|
||||
local body = stanza:find("body#")
|
||||
return body and #body
|
||||
end
|
||||
|
||||
|
||||
-- get notify options for local users
|
||||
local function getNotifyOptionsForUser(userName)
|
||||
--module:log("debug", "getFCMtokenForUser("..userName..")");
|
||||
local pd, pfFetchError = private_storage:get(userName);
|
||||
if (pfFetchError) then
|
||||
module:log(
|
||||
"error",
|
||||
"Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError)
|
||||
);
|
||||
return 0,{};
|
||||
elseif (pd) then
|
||||
local tokens;
|
||||
local nType;
|
||||
local nLang;
|
||||
for key,value in pairs(pd)
|
||||
do
|
||||
-- module:log("info", "PD("..userName..") : "..tostring(key).." => "..tostring(value));
|
||||
module:log("debug", "PD( %s) : %s => %s ", userName,tostring(key),tostring(value));
|
||||
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
|
||||
--module:log("debug", "PD("..userName..") : "..json.encode(value));
|
||||
-- module:log("debug", "PD( %s ) : %s ", userName ,json.encode(value));
|
||||
for k2,v2 in pairs(value) do
|
||||
if (not tokens) then
|
||||
local fcm = deserializeDataElement(v2, "fcm");
|
||||
if (type(fcm) == "table") then
|
||||
tokens = {};
|
||||
for token,x in pairs(fcm) do
|
||||
-- module:log("debug", "token: %s - x[os]: %s", token, x["os"]);
|
||||
if (x["os"] ~= nil) then
|
||||
table.insert(tokens, token.."@"..x["os"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if (not nType) then
|
||||
local options = deserializeDataElement(v2, "notifyOptions");
|
||||
if (options and (type(options) == "table") and options["type"]) then
|
||||
nType = tonumber(options["type"]);
|
||||
end
|
||||
end
|
||||
if (not nLang) then
|
||||
local options = deserializeDataElement(v2, "lang");
|
||||
-- module:log("debug", "PDLang: %s => %s", userName, json.encode(options));
|
||||
if (options) and (type(options) == "string") then
|
||||
nLang = options;
|
||||
end;
|
||||
-- module:log("debug", "type of options: %s", type(options));
|
||||
end
|
||||
end
|
||||
break; -- we can stop after the documents:stanza:io:json element
|
||||
end
|
||||
end
|
||||
--module:log("debug", "getNotifyOptionsForUser("..userName..") = "..tostring(tokens).." => "..tostring(nType));
|
||||
|
||||
local map_res = fcm_token_store:get(userName);
|
||||
|
||||
-- module:log("debug", "result from map store for %s is: %s", userName, dumpTable(map_res));
|
||||
|
||||
if (map_res) then
|
||||
for _device, _tkr in pairs(map_res) do
|
||||
-- module:log("debug", "got for %s device %s => %s", userName, _device, dumpTable(_tkr));
|
||||
-- module:log("debug", "got for %s device %s => token: %s - os: %s", userName, _device, _tkr.token, _tkr.os);
|
||||
local sessions = prosody.bare_sessions;
|
||||
local ubjid = userName.."@"..storage_host;
|
||||
local u_sessions = sessions[ubjid] or nil;
|
||||
local deviceonline = false;
|
||||
if (u_sessions) then
|
||||
local u_sessions_device = u_sessions["sessions"] or nil;
|
||||
if (u_sessions_device) then
|
||||
-- module:log("info", "devise is %s", _device);
|
||||
-- module:log("debug", "u_sessions_device %s", dumpTable(u_sessions_device));
|
||||
local dsession = u_sessions_device[_device] or nil;
|
||||
-- module:log("debug", "dsession %s", dumpTable(dsession));
|
||||
if (dsession) then
|
||||
local hibernated = dsession["hibernated"] or nil;
|
||||
local hibernating = dsession["hibernating"] or nil;
|
||||
local awaiting_ack = dsession["awaiting_ack"] or nil;
|
||||
module:log("info", "found session for _device %s - hibernating: %s, hibernated %s", _device, hibernating, hibernated);
|
||||
if not ((hibernated) or (hibernating)) then
|
||||
local dev_in_background = inactive_devices[_device] or nil;
|
||||
if dev_in_background then
|
||||
if awaiting_ack then
|
||||
module:log("debug", "device %s is still connected, but inactive and awaiting_ack", _device);
|
||||
-- module:log("info", "handling1: %s", _tkr.token.."@".._tkr.os);
|
||||
else
|
||||
module:log("debug", "device %s is still connected, but inactive", _device);
|
||||
-- module:log("info", "handling2: %s", _tkr.token.."@".._tkr.os);
|
||||
if (_tkr.os ~= "ios") then
|
||||
deviceonline = true;
|
||||
end
|
||||
end
|
||||
else
|
||||
if awaiting_ack then
|
||||
module:log("debug", "device %s is connected and active, but awaiting_ack", _device);
|
||||
-- module:log("info", "handling3: %s", _tkr.token.."@".._tkr.os);
|
||||
else
|
||||
module:log("debug", "device %s is connected and active", _device);
|
||||
-- module:log("info", "handling4: %s", _tkr.token.."@".._tkr.os);
|
||||
if (_tkr.os ~= "ios") then
|
||||
deviceonline = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not(deviceonline) then
|
||||
local already_added = contains(tokens, _tkr.token.."@".._tkr.os);
|
||||
module:log("debug","already_added: %s", _tkr.token.."@".._tkr.os);
|
||||
if not(tokens) then
|
||||
tokens = {};
|
||||
end
|
||||
if not(already_added) then
|
||||
table.insert(tokens, _tkr.token.."@".._tkr.os);
|
||||
end
|
||||
end
|
||||
-- module:log("debug","tokens table is now: %s", dumpTable(tokens));
|
||||
end
|
||||
end
|
||||
|
||||
if (not tokens) then tokens = {}; end;
|
||||
if (not nType) then nType = 0; end;
|
||||
local sessions = prosody.bare_sessions;
|
||||
local ubjid = userName.."@"..ThisDomain;
|
||||
for _user, _resdetails in pairs(sessions) do
|
||||
if (_user == ubjid) then
|
||||
for _res, _detail1 in pairs(_resdetails["sessions"]) do
|
||||
local hibernated = _detail1["hibernated"] or _detail1["hibernating"] or nil;
|
||||
if (hibernated) then
|
||||
module:log("info", "for _user: %s and _res: %s got hibernated since: %s", _user, _res, hibernated);
|
||||
else
|
||||
module:log("info", "for _user: %s and _res: %s got active session?", _user, _res);
|
||||
--module:log("debug", "for _user: %s and _res: %s got detail: %s", _user, _res, dumpTable(_detail1));
|
||||
end
|
||||
end
|
||||
-- module:log("debug", "session for _res %s is: %s", _res, dumpTable(_resdetails));
|
||||
end
|
||||
end
|
||||
-- module:log("debug", "sessions: %s", dumpTable(sessions[ubjid]));
|
||||
return nType, tokens, nLang;
|
||||
end
|
||||
end
|
||||
|
||||
local function fcm_notify(userName, title, body, type, _senderName, _groupTopic, vnc_attachment_type, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom)
|
||||
if (not moduleIsActive) then module:log("info", "module not active - bailung out"); return end;
|
||||
module:log("info", "getting notify opts for %s", userName);
|
||||
local uname, uhost = jid_split(userName);
|
||||
if (uhost ~= storage_host) then return end;
|
||||
local nType, tokens, lang = getNotifyOptionsForUser(uname);
|
||||
if (tokens and (#tokens) > 0) and ((nType == 1) or (nType == 2))
|
||||
then
|
||||
-- module:log(
|
||||
-- "info", "in fcm_notify(%s, %s, %s) => type=%s, %s tokens",
|
||||
-- userName, title, body, tostring(nType), tostring((#tokens))
|
||||
--);
|
||||
local sName = _senderName;
|
||||
local gTopic = _groupTopic;
|
||||
sName = "Neue Nachricht";
|
||||
gTopic = "Neue Nachricht";
|
||||
|
||||
if (lang == "en") then
|
||||
sName = "new message";
|
||||
gTopic = "new message";
|
||||
end;
|
||||
|
||||
if (lang == "fr") then
|
||||
sName = "Nouveau message";
|
||||
gTopic = "Nouveau message";
|
||||
end;
|
||||
|
||||
body = "";
|
||||
|
||||
for i,token in pairs(tokens) do
|
||||
if (token)
|
||||
then
|
||||
local rtoken, ros = jid_split(token);
|
||||
-- module:log("debug","got rtoken: %s - os: %s ", rtoken, ros);
|
||||
if (ros == "ios") then
|
||||
-- module:log("debug", "postFCMIOS with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang);
|
||||
postFCMIOS(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom);
|
||||
else
|
||||
-- module:log("info", "postFCM with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang, _nFrom);
|
||||
postFCM(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom);
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
module:log("debug", "fcm_notify(%s, %s, %s) => type=%s, no tokens", userName, title, body, tostring(nType));
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function handle_muc_message(event)
|
||||
-- event.room and event.stanza are available
|
||||
local room = event.room
|
||||
local stanza = event.stanza
|
||||
-- module:log("info", "handling stanza %s", stanza);
|
||||
-- module:log("info", "room: %s", dumpTable(room));
|
||||
|
||||
local message_id = event.stanza.attr.id;
|
||||
-- module:log("info","message id: %s", event.stanza.attr.id);
|
||||
local type = stanza.attr.type or "normal";
|
||||
local from = jid_bare(stanza.attr.from);
|
||||
local to = jid_bare(stanza.attr.to) or from;
|
||||
|
||||
local vncTalkConferenceEtype = stanza:find("{xmpp:vnctalk}vncTalkConference/eventType#") or nil;
|
||||
|
||||
-- module:log("info", "handling message from=%s, to=%s", stanza.attr.from, to);
|
||||
|
||||
|
||||
local content = trim(stanza:find("body#"));
|
||||
local delayedDelivery = stanza:get_child("delay", "urn:xmpp:delay");
|
||||
|
||||
-- if we have a vncTalk incoming or whiteboard element, it overrides the type
|
||||
local vncTalkIncomingType = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceType#");
|
||||
local vncTalkConferenceId = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceId#") or nil;
|
||||
local jitsiURL = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiURL#") or nil;
|
||||
local jitsiRoom = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiRoom#") or nil;
|
||||
local vncTalkWhiteboard = stanza:find("{xmpp:vnctalk}whiteboard");
|
||||
local isSentCarbonMessage = stanza:get_child("sent", "urn:xmpp:carbons:2");
|
||||
local vnc_attachment_type = stanza:find("{xmpp:vnctalk}attachment/fileType#");
|
||||
|
||||
local vnctalk_broadcast = stanza:get_child("vncTalkBroadcast", "xmpp:vnctalk");
|
||||
local vnctalk_broadcast_childid = nil;
|
||||
if vnctalk_broadcast then
|
||||
if starts_with(to, "broadcast") then
|
||||
-- module:log("message is to broadcast - bailing out");
|
||||
return nil;
|
||||
end
|
||||
vnctalk_broadcast_childid = vnctalk_broadcast.attr.id;
|
||||
local vnctalk_broadcast_title = vnctalk_broadcast.attr.title;
|
||||
local vnctalk_broadcast_target = vnctalk_broadcast.attr.origtarget;
|
||||
local body = stanza:get_child("body");
|
||||
-- module:log("debug", "broadcast body: %s", content);
|
||||
-- module:log("debug", "jid_split(to): %s", jid_split(to));
|
||||
-- fcm_notify(jid_split(to), vnctalk_broadcast_target, content, type, getDisplayName(jid_split(from)), "Broadcast: "..vnctalk_broadcast_title, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local auxType = vncTalkIncomingType or (vncTalkWhiteboard and "whiteboard");
|
||||
|
||||
if (auxType)
|
||||
then
|
||||
content = auxType;
|
||||
type = auxType;
|
||||
end;
|
||||
|
||||
local doNotify = content and (content ~= " ") and (content ~= "") and (delayedDelivery == nil) and (isSentCarbonMessage == nil) and (
|
||||
(type == "chat") or (type == "groupchat") or (type == "audio") or (type == "video") or (type == "whiteboard") or (type == "screen")
|
||||
);
|
||||
|
||||
if (doNotify) then
|
||||
local sender = "";
|
||||
for _, occupant in pairs(room._occupants) do
|
||||
if (stanza.attr.from == occupant.nick) then sender=occupant.bare_jid; end;
|
||||
end
|
||||
-- module:log("info","sender is %s", sender);
|
||||
local roomTopic = room._data.subject or nil;
|
||||
for jid, aff in pairs(room._affiliations) do
|
||||
if aff ~= "outcast" then
|
||||
-- module:log("info", "found jid: %s as aff: %s", jid, aff);
|
||||
local roomJid = to;
|
||||
if (sender ~= jid) then
|
||||
module:log("info", "from is: %s", from);
|
||||
fcm_notify(jid, roomJid, content, type, getDisplayName(jid_split(sender)), roomTopic, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, from, jitsiURL, jitsiRoom);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module:hook("muc-broadcast-message", handle_muc_message)
|
||||
|
||||
module:hook("host-activated", function(host)
|
||||
module:log("info", "got activated event for host %s", host);
|
||||
end);
|
||||
|
||||
module:log("debug", "Module loaded")
|
||||
@@ -0,0 +1,129 @@
|
||||
-- mod_muc_notifications
|
||||
--
|
||||
-- Copyright (C) 2019 Marcos de Vera Piquero <marcos.devera@quobis.com>
|
||||
-- enhanced by Stefan Sänger <stefan.saenger@vnc.biz>
|
||||
-- This file is MIT/X11 licensed.
|
||||
--
|
||||
-- A module to notify non-present members of messages in a group chat
|
||||
--
|
||||
|
||||
local id = require"util.id"
|
||||
local st = require"util.stanza"
|
||||
|
||||
local use_invite = module:get_option_boolean("muc_notification_invite", false)
|
||||
|
||||
local function dumpTable1(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..tostring(value).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
-- Given a stanza, compute if it qualifies as important (notifiable)
|
||||
-- return true for message stanzas with non-empty body
|
||||
-- Should probably use something similar to muc-message-is-historic event
|
||||
local function is_important(stanza)
|
||||
local body = stanza:find("body#")
|
||||
return body and #body
|
||||
end
|
||||
|
||||
local function handle_muc_message(event)
|
||||
-- event.room and event.stanza are available
|
||||
local room = event.room
|
||||
-- module:log("info","room: %s", dumpTable(room._occupants));
|
||||
local stanza = event.stanza
|
||||
local aff_jids = {};
|
||||
for jid, aff in pairs(room._affiliations) do
|
||||
if aff ~= "outcast" then
|
||||
local is_occupant = false
|
||||
local sessions = prosody.bare_sessions;
|
||||
local user_online = sessions[jid] or nil;
|
||||
local jid_resources = {};
|
||||
if (user_online ~= nil) then
|
||||
local u_sessions = user_online["sessions"] or nil;
|
||||
for k,v in pairs(u_sessions) do
|
||||
jid_resources[jid.."/"..tostring(k)] = true;
|
||||
end
|
||||
-- module:log("info", "room user %s sessions: \n %s", jid, dumpTable(jid_resources));
|
||||
end
|
||||
|
||||
for _, occupant in pairs(room._occupants) do
|
||||
if occupant.bare_jid == jid then
|
||||
for k,v in pairs(occupant.sessions) do
|
||||
-- module:log("info", "occupant k %s", k);
|
||||
jid_resources[k] = nil;
|
||||
end
|
||||
-- module:log("info", "remaining resources: ", dumpTable(jid_resources));
|
||||
-- module:log("info", "remaining resources#: %s", #jid_resources);
|
||||
if next(jid_resources) == nil then
|
||||
-- module:log("info", "user %s is occupant with all sessions", jid);
|
||||
is_occupant = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- module:log("info", "got jid: %s", jid);
|
||||
-- local sessions = prosody.bare_sessions;
|
||||
-- local user_online_sessions = sessions[jid] or nil;
|
||||
|
||||
if not is_occupant and is_important(stanza) and (user_online ~= nil) then
|
||||
-- send notification to jid
|
||||
local attrs = {
|
||||
to = jid,
|
||||
id = id.short(),
|
||||
from = room.jid,
|
||||
}
|
||||
local not_attrs = {
|
||||
xmlns = "http://vnc.biz/xmpp/muc#hook",
|
||||
jid = room.jid,
|
||||
}
|
||||
local reason = "You have messages in group chat "..(room:get_name() or room.jid)
|
||||
local notification = st.message(attrs)
|
||||
:body(reason):up()
|
||||
:tag("notification", not_attrs):up()
|
||||
:tag("no-store", {xmlns = "urn:xmpp:hints"})
|
||||
local invite = st.message(attrs):tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
|
||||
:tag("invite", {from = stanza.attr.from})
|
||||
:tag("reason"):text(reason):up():up():up()
|
||||
:tag("notification", not_attrs):up()
|
||||
:tag("no-store", {xmlns = "urn:xmpp:hints"})
|
||||
module:log("debug", "notifying with %s", tostring(use_invite and invite or notification))
|
||||
module:send(use_invite and invite or notification)
|
||||
module:log("debug", "sent notification of MUC message %s", use_invite and invite or notification)
|
||||
else
|
||||
module:log("debug", "user %s is not online or already occupant - not sending invite", jid);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("muc-broadcast-message", handle_muc_message)
|
||||
|
||||
module:log("debug", "Module loaded")
|
||||
@@ -0,0 +1,59 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2017 Matthew Wild
|
||||
-- Copyright (C) 2008-2017 Waqas Hussain
|
||||
-- Copyright (C) 2011-2017 Kim Alvefur
|
||||
-- Copyright (C) 2011-2017 Stefan Sänger
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- store receipts based on mod_mam
|
||||
--
|
||||
|
||||
local um = require "core.usermanager";
|
||||
local st = require "util.stanza";
|
||||
local rsm = require "util.rsm";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prepped_split = require "util.jid".prepped_split;
|
||||
local host = module.host;
|
||||
|
||||
local is_stanza = st.is_stanza;
|
||||
local tostring = tostring;
|
||||
local time_now = os.time;
|
||||
local m_min = math.min;
|
||||
local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
|
||||
|
||||
local receipt_store = module:open_store("receipts", "archive");
|
||||
|
||||
|
||||
-- Handle messages
|
||||
local function message_handler(event, c2s)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local log = c2s and origin.log or module._log;
|
||||
local orig_type = stanza.attr.type or "normal";
|
||||
local orig_from = stanza.attr.from;
|
||||
local orig_to = stanza.attr.to or orig_from;
|
||||
-- Stanza without 'to' are treated as if it was to their own bare jid
|
||||
local received_child = stanza:get_child("received", "urn:xmpp:receipts");
|
||||
if received_child then
|
||||
local received_test=tostring(received_child);
|
||||
local received_id = received_child.attr.id;
|
||||
local receipt_user = jid_split(orig_to);
|
||||
local receipt_with = jid_bare(orig_from);
|
||||
local receiptxml = "<received id=\""..received_id.."\" />";
|
||||
if jid_bare(orig_from) ~= jid_bare(orig_to) then
|
||||
-- local receiptok = receipt_store:append(receipt_user, nil, received_id, time_now(), receipt_with);
|
||||
local receiptok = receipt_store:append(receipt_user, nil, receiptxml, time_now(), receipt_with);
|
||||
-- log("info", "receipt for id: %s from %s to %s", received_id, jid_bare(orig_from), jid_bare(orig_to));
|
||||
else
|
||||
log("debug", "not storing receipt for id: %s from %s to %s", received_id, jid_bare(orig_from), jid_bare(orig_to));
|
||||
end
|
||||
else
|
||||
log("debug", "not stored receipt - no receipt-id in message from %s to %s", jid_bare(orig_from), jid_bare(orig_to));
|
||||
end
|
||||
end
|
||||
|
||||
-- Stanzas to local clients
|
||||
module:hook("message/bare", message_handler, 0);
|
||||
module:hook("message/full", message_handler, 0);
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2017 Matthew Wild
|
||||
-- Copyright (C) 2008-2017 Waqas Hussain
|
||||
-- Copyright (C) 2011-2017 Kim Alvefur
|
||||
-- Copyright (C) 2011-2017 Stefan Sänger
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- store groupchat messages from / to remote hosts based on mod_mam
|
||||
--
|
||||
|
||||
local um = require "core.usermanager";
|
||||
local st = require "util.stanza";
|
||||
local rsm = require "util.rsm";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prepped_split = require "util.jid".prepped_split;
|
||||
local host = module.host;
|
||||
|
||||
module:log("running on host: %s", host);
|
||||
|
||||
local archive_store = "muc_remote_inv";
|
||||
local archive = module:open_store(archive_store, "archive");
|
||||
|
||||
|
||||
|
||||
local time_now = os.time;
|
||||
local m_min = math.min;
|
||||
local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function handle_vnc_invite(event)
|
||||
-- module:log("info", "seen hooked invite from %s to %s", _to, _invitee);
|
||||
|
||||
-- module:log("info", "hooked event: %s ", dumpTable(event));
|
||||
local muc_child = (event:get_child("x", "http://jabber.org/protocol/muc#user") or nil);
|
||||
local room = event.attr.from or nil;
|
||||
local to = event.attr.to or nil;
|
||||
if (to) then
|
||||
local to_node, to_host = jid_split(to);
|
||||
local room_node, room_host = jid_split(room);
|
||||
if not (prosody.hosts[to_host]) then
|
||||
-- inviting remote user
|
||||
module:log("info", "hooked room: %s => to: %s", room, to);
|
||||
local with = to_node.."@"..to_host;
|
||||
archive:append(room_node, nil, event, time_now(), with);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- Stanzas to local clients
|
||||
module:hook("vnc-muc-invitation", handle_vnc_invite, 10);
|
||||
@@ -0,0 +1,166 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2017 Matthew Wild
|
||||
-- Copyright (C) 2008-2017 Waqas Hussain
|
||||
-- Copyright (C) 2011-2017 Kim Alvefur
|
||||
-- Copyright (C) 2011-2017 Stefan Sänger
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- store groupchat messages from / to remote hosts based on mod_mam
|
||||
--
|
||||
|
||||
local um = require "core.usermanager";
|
||||
local st = require "util.stanza";
|
||||
local rsm = require "util.rsm";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prepped_split = require "util.jid".prepped_split;
|
||||
local host = module.host;
|
||||
|
||||
module:log("running on host: %s", host);
|
||||
|
||||
local archive_store = "muc_remote";
|
||||
local archive = module:open_store(archive_store, "archive");
|
||||
local mr_store = module:open_store("muc_remote", "map");
|
||||
|
||||
|
||||
local time_now = os.time;
|
||||
local m_min = math.min;
|
||||
local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
local function presence_handler(event,c2s)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local muc_child = (stanza:get_child("x", "http://jabber.org/protocol/muc#user") or nil);
|
||||
local item_child = nil;
|
||||
local presence_type = (stanza.attr.type or nil);
|
||||
local from_node, from_host, from_res = jid_split(stanza.attr.from);
|
||||
local to_node, to_host = jid_split(stanza.attr.to);
|
||||
if not (prosody.hosts[from_host]) then
|
||||
if (muc_child and not(presence_type)) then
|
||||
item_child = muc_child:get_child("item");
|
||||
if (item_child) then
|
||||
-- module:log("info", "got muc presence from: %s - to: %s, jid: %s || raw: %s", stanza.attr.from, stanza.attr.to, jid_bare(item_child.attr.jid), tostring(stanza));
|
||||
if (item_child.attr.jid) then
|
||||
local fjid = stanza.attr.from;
|
||||
if (stanza.attr.from == jid_bare(stanza.attr.from)) then
|
||||
fjid = stanza.attr.from .. "/" .. jid_bare(item_child.attr.jid);
|
||||
end
|
||||
-- module:log("info", "got item... from= %s item.attr.jid= %s", fjid, jid_bare(item_child.attr.jid));
|
||||
mr_store:set(to_node, fjid, jid_bare(item_child.attr.jid));
|
||||
end
|
||||
if (from_res) then
|
||||
mr_store:set(to_node,stanza.attr.from, from_res);
|
||||
-- module:log("info", "got muc presence from_res: %s - to: %s, jid: %s || raw: %s", stanza.attr.from, stanza.attr.to, from_res, tostring(stanza));
|
||||
end
|
||||
end
|
||||
-- mr_store:set(to_node,stanza.attr.from, );
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle messages
|
||||
local function message_handler(event, c2s)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.attr.type == "groupchat" then
|
||||
local from_node, from_host, from_res = jid_split(stanza.attr.from);
|
||||
local remote_muc = from_node.."@"..from_host;
|
||||
local to_node, to_host, to_res = jid_split(stanza.attr.to);
|
||||
-- module:log("info", "seen from_host: %s", from_host);
|
||||
-- module:log("info", "seen to_res: %s", to_res);
|
||||
if prosody.hosts[from_host] then
|
||||
-- message is from a local host, no further action required
|
||||
module:log("debug", "seen stanza from local host - from: %s", stanza.attr.from);
|
||||
-- return nil;
|
||||
else
|
||||
-- message is from remote host, going to handle...
|
||||
-- module:log("info", "seen stanza with muc %s@%s - raw: %s - to %s with type %s with id %s", from_node, from_host, stanza.attr.from, stanza.attr.to, stanza.attr.type, stanza.attr.id);
|
||||
local mkey = stanza.attr.id;
|
||||
local muc_with = from_node.."@"..from_host;
|
||||
local content = (stanza and stanza:get_child("body") and stanza:get_child("body")[1] or nil)
|
||||
-- module:log("info","got content: %s", content);
|
||||
local delayedDelivery = stanza:get_child("delay", "urn:xmpp:delay");
|
||||
if (delayedDelivery) then
|
||||
-- module:log("info","got delay: %s", delayedDelivery);
|
||||
end
|
||||
local muc_subject = (stanza:get_child_text("subject") or nil);
|
||||
if (muc_subject) then
|
||||
-- module:log("info","subject for %s: %s", stanza.attr.from, muc_subject);
|
||||
mr_store:set(to_node, remote_muc, muc_subject);
|
||||
end
|
||||
local nick = (stanza:get_child_text("nick", "http://jabber.org/protocol/nick") or nil);
|
||||
local muc_child = (stanza:get_child("x", "http://jabber.org/protocol/muc#user") or nil);
|
||||
local item_child = nil;
|
||||
if (muc_child) then
|
||||
-- module:log("info","got muc_child: %s", muc_child);
|
||||
item_child = muc_child:get_child("item");
|
||||
if (item_child and nick) then
|
||||
-- module:log("info","got sender jid nick: %s: %s => %s", stanza.attr.from, nick, item_child.attr.jid);
|
||||
mr_store:set(to_node,stanza.attr.from, item_child.attr.jid);
|
||||
end
|
||||
end
|
||||
|
||||
if (mkey and content and not(delayedDelivery)) then
|
||||
-- module:log("info", "seen stanza with muc %s@%s - raw: %s - to %s with type %s with id %s", from_node, from_host, stanza.attr.from, stanza.attr.to, stanza.attr.type, stanza.attr.id);
|
||||
-- module:log("info", "going to store - to: %s, conten: %s", to_node, content);
|
||||
local storeok = archive:append(to_node, mkey, stanza, time_now(), muc_with);
|
||||
-- prevent further processing for messages to offline user (to bare jid)
|
||||
if (to_res == nil) then
|
||||
-- module:log("info", "going prevent stanza processing to bare from muc - to: %s, conten: %s", to_node, content);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- module:log("info","seen stanza.attr.type: %s", stanza.attr.type);
|
||||
local from_node, from_host, from_res = jid_split(stanza.attr.from);
|
||||
local to_node, to_host, to_res = jid_split(stanza.attr.to);
|
||||
local muc_child = (stanza:get_child("x", "http://jabber.org/protocol/muc#user") or nil);
|
||||
local invite_child = nil;
|
||||
local invite_from = nil;
|
||||
if (muc_child) then
|
||||
invite_child = (muc_child:get_child("invite") or nil);
|
||||
if (invite_child) then
|
||||
module:log("info", "got invite_child: %s", dumpTable(invite_child));
|
||||
invite_from = (invite_child.attr.from or nil);
|
||||
if (invite_from) then
|
||||
local invite_from_node, invite_from_host = jid_split(invite_from);
|
||||
module:log("info","got invite from %s to muc %s for %s", invite_from_node.."@"..invite_from_host, stanza.attr.from, to_node.."@"..to_host );
|
||||
local store_key = stanza.attr.from.."/"..invite_from_node.."@"..invite_from_host;
|
||||
local store_value = invite_from_node.."@"..invite_from_host;
|
||||
-- module:log("debug","storing: %s -> %s -> %s", to_node, stanza.attr.from.."/"..invite_from_node.."@"..invite_from_host, invite_from_node.."@"..invite_from_host);
|
||||
mr_store:set(to_node, store_key, store_value);
|
||||
else
|
||||
module:log("info","found empty part - check %s", invite_from);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Stanzas to local clients
|
||||
module:hook("message/bare", message_handler, 0);
|
||||
module:hook("message/full", message_handler, 0);
|
||||
module:hook("presence/full", presence_handler, 10);
|
||||
module:hook("presence/bare", presence_handler, 10);
|
||||
@@ -0,0 +1,70 @@
|
||||
-- vnc timestamp module
|
||||
-- based on https://modules.prosody.im/mod_delay.html
|
||||
-- Copyright (C) 2016-2017 Thilo Molitor
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local add_filter = require "util.filters".add_filter;
|
||||
local remove_filter = require "util.filters".remove_filter;
|
||||
local datetime = require "util.datetime";
|
||||
local st = require "util.stanza";
|
||||
local uuid = require "util.uuid";
|
||||
|
||||
local xmlns_timestamp = "xmpp:vnctalk:stamp";
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
-- Raise an error if the modules has been loaded as a component in prosody's config
|
||||
if module:get_host_type() == "component" then
|
||||
error(module.name.." should NOT be loaded as a component, check out http://prosody.im/doc/components", 0);
|
||||
end
|
||||
|
||||
local add_delay = function(stanza, session)
|
||||
if stanza and stanza.name == "message" and stanza:get_child("stamp", xmlns_timestamp) == nil then
|
||||
-- only add delay tag to chat or groupchat messages (should we add a delay to anything else, too???)
|
||||
if stanza.attr.type == "chat" or stanza.attr.type == "groupchat" then
|
||||
if stanza:get_child("body") then
|
||||
-- session.log("debug", "adding delay to message %s", tostring(stanza));
|
||||
local stamp = os.time();
|
||||
if (stanza.attr.from and stanza.attr.id) then
|
||||
-- module:log("info", "vnc-stamp added to stanza %s", dumpTable(stanza));
|
||||
local iqid = uuid.generate();
|
||||
module:send(st.iq({ id=iqid, to=stanza.attr.from, from=module.host, type="result", t = tostring(stamp) , f=stanza.attr.id}));
|
||||
-- session:send(st.stanza("t", { xmlns =xmlns_timestamp, t = stamp }));
|
||||
end
|
||||
stanza = stanza:tag("stamp", { xmlns = xmlns_timestamp, from = session.host, stamp = tostring(stamp) });
|
||||
end;
|
||||
end
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
module:hook("resource-bind", function(event)
|
||||
add_filter(event.session, "stanzas/in", add_delay, 1);
|
||||
end);
|
||||
module:hook("smacks-hibernation-end", function(event)
|
||||
-- older smacks module versions send only the "intermediate" session in event.session and no session.resumed one
|
||||
if event.resumed then
|
||||
add_filter(event.resumed, "stanzas/in", add_delay, 1);
|
||||
end
|
||||
end);
|
||||
module:hook("pre-resource-unbind", function (event)
|
||||
remove_filter(event.session, "stanzas/in", add_delay);
|
||||
end);
|
||||
@@ -0,0 +1,40 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2017 Matthew Wild
|
||||
-- Copyright (C) 2008-2017 Waqas Hussain
|
||||
-- Copyright (C) 2011-2017 Kim Alvefur
|
||||
-- Copyright (C) 2011-2017 Stefan Sänger
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- store kicks based on mod_mam
|
||||
--
|
||||
|
||||
local um = require "core.usermanager";
|
||||
local st = require "util.stanza";
|
||||
local rsm = require "util.rsm";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prepped_split = require "util.jid".prepped_split;
|
||||
local host = module.host;
|
||||
|
||||
local is_stanza = st.is_stanza;
|
||||
local tostring = tostring;
|
||||
local time_now = os.time;
|
||||
local m_min = math.min;
|
||||
local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
|
||||
|
||||
local kick_store = module:open_store("kick", "archive");
|
||||
|
||||
module:hook("vnc-muc-kick", function (event)
|
||||
-- module:log("info", "vnc-muc-kick room: %s", event.room);
|
||||
-- module:log("info", "vnc-muc-kick who: %s", event.who);
|
||||
local room_node = jid_split(event.room);
|
||||
local kicktime = tostring(os.time());
|
||||
local id, err = kick_store:append(room_node, nil, "<status>kicked or left</status>", time_now(), event.who);
|
||||
if id then
|
||||
module:log("debug", "stored as id: %s", id);
|
||||
else
|
||||
module:log("info", "failed to store: %s", err);
|
||||
end
|
||||
end);
|
||||
@@ -0,0 +1,117 @@
|
||||
--
|
||||
|
||||
local st = require "util.stanza"
|
||||
local jid = require "util.jid";
|
||||
local base64 = require"util.encodings".base64;
|
||||
local sha1 = require"util.hashes".sha1;
|
||||
local http = require "net.http";
|
||||
local mm = require "core.modulemanager";
|
||||
|
||||
local my_host = module:get_host();
|
||||
local my_host_type = module:get_host_type();
|
||||
|
||||
local avatar_upload_url = module:get_option("avatar_upload_url", "");
|
||||
local avatar_upload_user = module:get_option("avatar_upload_user", "");
|
||||
local avatar_upload_pass = module:get_option("avatar_upload_pass", "");
|
||||
|
||||
local avatar_update_store = module:open_store("avatarupdate", "map");
|
||||
|
||||
|
||||
local function dumpTable(t, depth)
|
||||
local result = "";
|
||||
if (not depth) then depth = 0 end;
|
||||
|
||||
local prefix = string.rep(" ", depth);
|
||||
|
||||
if (type(t) ~= "table") then
|
||||
result = result.." "..tostring(t);
|
||||
else
|
||||
for key,value in pairs(t) do
|
||||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||||
end
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function putGlobalAvatar(_userName, _host, _photo)
|
||||
local to_url = avatar_upload_url.._userName.."@".._host;
|
||||
module:log("info", "upload target URL: %s", to_url);
|
||||
local httpRequestOptions = {
|
||||
method = "PUT",
|
||||
body = _photo,
|
||||
headers = {
|
||||
["Content-Type"] = "image/png"
|
||||
}
|
||||
}
|
||||
if avatar_upload_user ~= "" then
|
||||
httpRequestOptions = {
|
||||
method = "PUT",
|
||||
body = _photo,
|
||||
headers = {
|
||||
["Content-Type"] = "image/png",
|
||||
["Authorization"] = "Basic "..base64.encode(avatar_upload_user..":"..avatar_upload_pass)
|
||||
}
|
||||
}
|
||||
end
|
||||
local function cb(content_, code_, request_, response_)
|
||||
content, code, request, response = content_, code_, request_, response_;
|
||||
module:log("info", "response code is: %s", code);
|
||||
--done();
|
||||
end
|
||||
|
||||
http.request(to_url, httpRequestOptions, cb);
|
||||
|
||||
module:log("info", "avatar upload for %s", _userName);
|
||||
end
|
||||
|
||||
module:depends"vcard";
|
||||
local vcard_storage = module:open_store("vcard");
|
||||
|
||||
local function get_vcard(username)
|
||||
local vcard, err = vcard_storage:get(username);
|
||||
if vcard then
|
||||
vcard = st.deserialize(vcard);
|
||||
end
|
||||
if not vcard then
|
||||
vcard = st.stanza("vCard", { xmlns = "vcard-temp" });
|
||||
end
|
||||
return vcard, err;
|
||||
end
|
||||
|
||||
|
||||
-- vCard -> upload
|
||||
local function update_avt(session, vcard)
|
||||
if not vcard then return end
|
||||
local nickname = vcard:get_child_text("NICKNAME");
|
||||
|
||||
local photo = vcard:get_child("PHOTO");
|
||||
if photo then
|
||||
local photo_type = photo:get_child_text("TYPE");
|
||||
local photo_b64 = photo:get_child_text("BINVAL");
|
||||
local photo_raw = photo_b64 and base64.decode(photo_b64);
|
||||
if photo_raw and photo_type then -- Else invalid data or encoding
|
||||
|
||||
module:log("info", "avatar update for session.username %s", session.username);
|
||||
module:log("info", "avatar update for session.host %s", session.host);
|
||||
local s = os.time();
|
||||
avatar_update_store:set(session.username, "updated", s);
|
||||
if avatar_upload_url ~= "" then
|
||||
putGlobalAvatar(session.username, session.host, photo_raw);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_vcard(event)
|
||||
module.log("info","handle_vcard called 1 - handling update from ");
|
||||
local session, stanza = event.origin, event.stanza;
|
||||
if not stanza.attr.to and stanza.attr.type == "set" then
|
||||
module.log("info","handle_vcard called 2 - handling update from ");
|
||||
return update_avt(session, stanza:get_child("vCard", "vcard-temp"));
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("iq/bare/vcard-temp:vCard", handle_vcard, 1);
|
||||
@@ -0,0 +1,106 @@
|
||||
local datamanager = require "util.datamanager";
|
||||
local usermanager = require "core.usermanager";
|
||||
local st = require "util.stanza";
|
||||
local host = module.host;
|
||||
local jid_split = require "util.jid".split;
|
||||
|
||||
local orgname = module:get_option_string("default_vcard_orgname");
|
||||
local orgmail = module:get_option_boolean("default_vcard_orgmail");
|
||||
|
||||
local function trim(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("^%s*(.-)%s*$", "%1"))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function collapseSpaces(s)
|
||||
if s and (type(s) == "string") then
|
||||
return (s:gsub("%s+", " "))
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
local function prettyUsername(userName)
|
||||
userName = string.upper(string.sub(userName, 1, 1))..string.sub(userName, 2, -1) -- upcase first char in any case
|
||||
if string.find(userName, "%.") -- contains dot
|
||||
then
|
||||
local i = 1
|
||||
while true do
|
||||
local j = string.find(userName, "%.", i)
|
||||
if j == nil or #userName == i then break end
|
||||
-- replace dot with space and upcase next char
|
||||
userName = string.sub(userName, 1, j-1).." "..string.upper(string.sub(userName, j+1, j+1))..string.sub(userName, j+2, -1)
|
||||
if #userName <= j then break end
|
||||
i = j
|
||||
end
|
||||
end
|
||||
return trim(collapseSpaces(userName));
|
||||
end
|
||||
|
||||
|
||||
|
||||
module:hook("iq/bare/vcard-temp:vCard", function(event)
|
||||
local session, stanza = event.origin, event.stanza;
|
||||
local to = stanza.attr.to;
|
||||
local from = stanza.attr.from;
|
||||
|
||||
local username = "";
|
||||
if (stanza.attr.to == nil) then
|
||||
-- module:log("info", "query for vcard from stanza.attr.from: %s", stanza.attr.from);
|
||||
username = jid_split(from);
|
||||
else
|
||||
-- module:log("info", "query for vcard for stanza.attr.to: %s", stanza.attr.to);
|
||||
username = jid_split(to);
|
||||
end
|
||||
-- return
|
||||
if not username then return end
|
||||
|
||||
local vcard = datamanager.load(username, host, "vcard");
|
||||
local exists = usermanager.user_exists(username, host);
|
||||
-- module:log("info", "query for vcard for user: %s host %s", username, host);
|
||||
module:log("debug", "has %s: %s", "vcard", tostring(vcard));
|
||||
module:log("debug", "has %s: %s", "exists", tostring(exists));
|
||||
-- data = data or {};
|
||||
if vcard then
|
||||
module:log("debug", "found existing vcard for %s", username);
|
||||
end
|
||||
|
||||
local firstraw = "";
|
||||
local lastraw = "";
|
||||
local dotpos = string.find(username, "%.");
|
||||
if dotpos ~= nil then
|
||||
firstraw = prettyUsername(string.sub(username, 0, dotpos-1));
|
||||
lastraw = prettyUsername(string.sub(username, dotpos+1));
|
||||
module:log("info","found raw firstname / lastname from username: %s %s", firstraw, lastraw);
|
||||
else
|
||||
firstraw = prettyUsername(username);
|
||||
module:log("info", "found raw firstname from username: %s", firstraw);
|
||||
end
|
||||
|
||||
local nickname = prettyUsername(username);
|
||||
|
||||
if not(vcard) and exists then
|
||||
-- MAYBE
|
||||
-- first .. " " .. last
|
||||
-- first, last = name:match("^(%w+) (%w+)$")
|
||||
local vcard = st.reply(stanza):tag("vCard", { xmlns = "vcard-temp" })
|
||||
:tag("VERSION"):text("3.0"):up()
|
||||
:tag("N")
|
||||
:tag("FAMILY"):text(lastraw):up()
|
||||
:tag("GIVEN"):text(firstraw):up()
|
||||
:up()
|
||||
:tag("FN"):text(prettyUsername(username) or ""):up()
|
||||
:tag("NICKNAME"):text(prettyUsername(username)):up()
|
||||
-- :tag("JABBERID"):text(username.."@"..host):up();
|
||||
if orgname then
|
||||
vcard:tag("ORG"):tag("ORGNAME"):text(orgname):up():up();
|
||||
end
|
||||
session.send(vcard);
|
||||
return true;
|
||||
else
|
||||
module:log("debug", "not modifying vcard");
|
||||
end
|
||||
end, 1);
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Stable'
|
||||
summary: Display your online status in web pages
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
Quite often you may want to publish your Jabber status to your blog or
|
||||
website. mod\_webpresence allows you to do exactly this.
|
||||
|
||||
Details
|
||||
=======
|
||||
|
||||
This module uses Prosody's built-in HTTP server (it does not depend on
|
||||
mod\_httpserver). It supplies a status icon representative of a user's
|
||||
online state.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Simply copy mod\_webpresence.lua to your modules directory, the image
|
||||
files are embedded within it. Then add "webpresence" to your
|
||||
modules\_enabled list.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Once loaded you can embed the icon into a page using a simple `<img>`
|
||||
tag, as follows:
|
||||
|
||||
<img src="http://prosody.example.com:5280/status/john.smith" />
|
||||
|
||||
Alternatively, it can be used to get status name as plaint text, status
|
||||
message as plain text or html-code for embedding on web-pages.
|
||||
|
||||
To get status name in plain text you can use something like that link:
|
||||
`http://prosody.example.com:5280/status/john.smith/text`
|
||||
|
||||
To get status message as plain text you can use something like following
|
||||
link: `http://prosody.example.com:5280/status/john.smith/message`
|
||||
|
||||
To get html code, containig status name, status image and status message
|
||||
(if set): `http://prosody.example.com:5280/status/john.smith/html`
|
||||
|
||||
All other
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
----- -------
|
||||
0.9 Works
|
||||
0.8 Works
|
||||
0.7 Works
|
||||
0.6 Works
|
||||
----- -------
|
||||
|
||||
Todo
|
||||
====
|
||||
|
||||
- JSON?
|
||||
- Display PEP information (maybe a new plugin?)
|
||||
- More (free) iconsets
|
||||
- Internal/external image generator (GD, ImageMagick)
|
||||
@@ -0,0 +1,43 @@
|
||||
#summary Display your online status in web pages
|
||||
#labels Stage-Stable
|
||||
|
||||
= Introduction =
|
||||
|
||||
Quite often you may want to publish your Jabber status to your blog or website. mod_webpresence allows you to do exactly this.
|
||||
|
||||
= Details =
|
||||
|
||||
This module uses Prosody's built-in HTTP server (it does not depend on mod_httpserver). It supplies a status icon representative of a user's online state.
|
||||
|
||||
= Installation =
|
||||
|
||||
Simply copy mod_webpresence.lua to your modules directory, the image files are embedded within it. Then add "webpresence" to your modules_enabled list.
|
||||
|
||||
= Usage =
|
||||
|
||||
Once loaded you can embed the icon into a page using a simple `<img>` tag, as follows:
|
||||
|
||||
{{{<img src="http://prosody.example.com:5280/status/john.smith" />}}}
|
||||
|
||||
Alternatively, it can be used to get status name as plaint text, status message as plain text or html-code for embedding on web-pages.
|
||||
|
||||
To get status name in plain text you can use something like that link: {{{http://prosody.example.com:5280/status/john.smith/text}}}
|
||||
|
||||
To get status message as plain text you can use something like following link: {{{http://prosody.example.com:5280/status/john.smith/message}}}
|
||||
|
||||
To get html code, containig status name, status image and status message (if set):
|
||||
{{{http://prosody.example.com:5280/status/john.smith/html}}}
|
||||
|
||||
All other
|
||||
= Compatibility =
|
||||
||0.9||Works||
|
||||
||0.8||Works||
|
||||
||0.7||Works||
|
||||
||0.6||Works||
|
||||
|
||||
= Todo =
|
||||
|
||||
* JSON?
|
||||
* Display PEP information (maybe a new plugin?)
|
||||
* More (free) iconsets
|
||||
* Internal/external image generator (GD, ImageMagick)
|
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 948 B |
|
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 920 B |
|
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 905 B After Width: | Height: | Size: 905 B |
|
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 920 B |
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 954 B |
@@ -0,0 +1,118 @@
|
||||
module:depends("http");
|
||||
|
||||
local jid_split = require "util.jid".prepped_split;
|
||||
local b64 = require "util.encodings".base64.encode;
|
||||
local sha1 = require "util.hashes".sha1;
|
||||
local stanza = require "util.stanza".stanza;
|
||||
local json = require "util.json".encode_ordered;
|
||||
|
||||
local function require_resource(name)
|
||||
local icon_path = module:get_option_string("presence_icons", "icons");
|
||||
local f, err = module:load_resource(icon_path.."/"..name);
|
||||
if f then
|
||||
return f:read("*a");
|
||||
end
|
||||
module:log("warn", "Failed to open image file %s", icon_path..name);
|
||||
return "";
|
||||
end
|
||||
|
||||
local statuses = { online = {}, away = {}, xa = {}, dnd = {}, chat = {}, offline = {} };
|
||||
|
||||
local function handle_request(event, path)
|
||||
local status, message;
|
||||
local jid, type = path:match("([^/]+)/?(.*)$");
|
||||
if jid then
|
||||
local user, host = jid_split(jid);
|
||||
if host and not user then
|
||||
user, host = host, event.request.headers.host;
|
||||
if host then host = host:gsub(":%d+$", ""); end
|
||||
end
|
||||
if user and host then
|
||||
local user_sessions = hosts[host] and hosts[host].sessions[user];
|
||||
if user_sessions then
|
||||
status = user_sessions.top_resources[1];
|
||||
if status and status.presence then
|
||||
message = status.presence:child_with_name("status");
|
||||
status = status.presence:child_with_name("show");
|
||||
if not status then
|
||||
status = "online";
|
||||
else
|
||||
status = status:get_text();
|
||||
end
|
||||
if message then
|
||||
message = message:get_text();
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
status = status or "offline";
|
||||
|
||||
statuses[status].image = function()
|
||||
return { status_code = 200, headers = { content_type = "image/png" },
|
||||
body = require_resource("status_"..status..".png")
|
||||
};
|
||||
end;
|
||||
statuses[status].html = function()
|
||||
local jid_hash = sha1(jid, true);
|
||||
return { status_code = 200, headers = { content_type = "text/html" },
|
||||
body = [[<!DOCTYPE html>]]..
|
||||
tostring(
|
||||
stanza("html")
|
||||
:tag("head")
|
||||
:tag("title"):text("XMPP Status Page for "..jid):up():up()
|
||||
:tag("body")
|
||||
:tag("div", { id = jid_hash.."_status", class = "xmpp_status" })
|
||||
:tag("img", { id = jid_hash.."_img", class = "xmpp_status_image xmpp_status_"..status,
|
||||
src = "data:image/png;base64,"..b64(require_resource("status_"..status..".png")) }):up()
|
||||
:tag("span", { id = jid_hash.."_status_name", class = "xmpp_status_name" })
|
||||
:text("\194\160"..status):up()
|
||||
:tag("span", { id = jid_hash.."_status_message", class = "xmpp_status_message" })
|
||||
:text(message and "\194\160"..message.."" or "")
|
||||
)
|
||||
};
|
||||
end;
|
||||
statuses[status].text = function()
|
||||
return { status_code = 200, headers = { content_type = "text/plain" },
|
||||
body = status
|
||||
};
|
||||
end;
|
||||
statuses[status].message = function()
|
||||
return { status_code = 200, headers = { content_type = "text/plain" },
|
||||
body = (message and message or "")
|
||||
};
|
||||
end;
|
||||
statuses[status].json = function()
|
||||
return { status_code = 200, headers = { content_type = "application/json" },
|
||||
body = json({
|
||||
jid = jid,
|
||||
show = status,
|
||||
status = (message and message or "null")
|
||||
})
|
||||
};
|
||||
end;
|
||||
statuses[status].xml = function()
|
||||
return { status_code = 200, headers = { content_type = "application/xml" },
|
||||
body = [[<?xml version="1.0" encoding="utf-8"?>]]..
|
||||
tostring(
|
||||
stanza("result")
|
||||
:tag("jid"):text(jid):up()
|
||||
:tag("show"):text(status):up()
|
||||
:tag("status"):text(message)
|
||||
)
|
||||
};
|
||||
end
|
||||
|
||||
if ((type == "") or (not statuses[status][type])) then
|
||||
type = "image"
|
||||
end;
|
||||
|
||||
return statuses[status][type]();
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/status";
|
||||
route = {
|
||||
["GET /*"] = handle_request;
|
||||
};
|
||||
});
|
||||