fix: initial dockerbuild of current prosody for alpine

This commit is contained in:
2022-09-05 06:50:25 +02:00
parent f0d24ead8a
commit 7601143bad
76 changed files with 13681 additions and 90 deletions
+88
View File
@@ -0,0 +1,88 @@
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: eu.gcr.io/vnc-development/docker-vnc-runner:latest
stage: scan
script:
- apt -y update
- apt -y install wget apt-transport-https gnupg lsb-release
- wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -
- echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" >> /etc/apt/sources.list.d/trivy.list
- apt -y update
- apt -y install trivy
- 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-.*/
+37
View File
@@ -0,0 +1,37 @@
FROM alpine:3.13
RUN apk update
RUN apk upgrade
RUN 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 libstdc++6 libc-dev openssl openssl-dev
RUN wget https://prosody.im/downloads/source/prosody-0.11.6.tar.gz
RUN 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
RUN mkdir -p /var/run/prosody/
RUN addgroup prosody
RUN adduser -D -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
RUN cp /vnc/patches/mod_mam.lua /usr/local/lib/prosody/modules/mod_mam/mod_mam.lua
RUN cp /vnc/patches/mod_muc.lua /usr/local/lib/prosody/modules/muc/mod_muc.lua
RUN cp /vnc/patches/mod_muc_unique.lua /usr/local/lib/prosody/modules/mod_muc_unique.lua
RUN cp /vnc/patches/moduleapi.lua /usr/local/lib/prosody/core/moduleapi.lua
RUN cp /vnc/patches/muc.lib.lua /usr/local/lib/prosody/modules/muc/muc.lib.lua
RUN cp /vnc/patches/register.lib.lua /usr/local/lib/prosody/modules/muc/register.lib.lua
RUN cp /vnc/patches/mod_admin_telnet.lua /usr/local/lib/prosody/modules/mod_admin_telnet.lua
RUN 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
CMD ["/vnc/config/startup.sh"]
+1 -90
View File
@@ -1,92 +1,3 @@
# Vnctalk Prosody # Vnctalk Prosody
dockerized build for vnctalk prosody
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://gitlab.vnc.biz/uxf/vnctalk-prosody.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+291
View File
@@ -0,0 +1,291 @@
--------------------------------------------------------------------------------
---- VNCtalk Prosody/XMPP Settings
--------------------------------------------------------------------------------
daemonize = false;
pidfile = "/var/run/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";
-- VNCtalk modules
"vnc_receipts";
"vnc_remotemucstore";
}
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" ;
}
--------------------------------------------------------------------------------
---- 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 = 10; --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-ssl.key";
certificate = "/etc/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
--------------------------------------------------------------------------------
---- 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 = 0;
smack_max_ack_delay = 60;
smacks_max_hibernated_sessions = 10;
smacks_max_old = 10;
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
---- 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 = "http://10.0.201.48:9544/"
http_auth_url = "${hybridaAuthUrl}"
fcm_api_key = "${fcmApiKey}"
fcm_api_url = "${fcmApiUrl}"
del_api_url = "${fcmDelUrl}"
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-ssl.key";
certificate = "/etc/prosody-ssl.pem";
}
disco_items = {
-- Do not display/publicy any disco item to avoid information leaking due to public reachable chatroom service.
}
Component "conference.${prosodyDomain}" "muc"
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
-- 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 = "${fcmApiUrl}"
del_api_url = "${fcmDelUrl}"
avatar_upload_url = "${avatarUploadUrl}"
avatar_upload_user = "${avatarUploadUser}"
avatar_upload_pass = "${avatarUploadPass}"
storage_host = "${prosodyDomain}";
Component "broadcast@${prosodyDomain}" "vnc_broadcast"
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
echo "running startup"
mkdir -p /etc/prosody/certs
defaultProsodySSLkey="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRRHdqakRJL2hiWERXZTQKRlB0RGtCbG9wUjBJSTQ2U25tY001R3ZQSzlXVkx6dXM0YlBsTmF5Y2dVVFhGTUR2OThmTTdCa3pYaEFuRGorTAo4VTQ1dnp6bW1STHQ3aDBHdStud0t1dXg2Z2MvTW9XVjJYd3JlY1U0bUp2Z0IvTW9QZ05ReGNVVlhHYTF6VFJLCm5VMDJDa28zUEZaNmxWNkFEN3dISGR0cXhTbGQxdTVxMnZMa3VHM2psellqZFJmc2tTdjNLS1BJS3FKdUszM0sKUnRDb1J1TzBvZFNzOHN2MmhmYVdEangvZEEyQ29ubXh3RTlIa3VCUFhIWnQzM2s3ajdTWkh6ZnZxZ1BibVRXNwpuTmhTWVRCa05LRXpHU051a0gvQTcrS0VUWXJoVFcwTUNVSkNIbE5oSlY5MmYxaFBjeGVHMGhJV0J0bEoyS1RuClNHQUI3S0FaQWdNQkFBRUNnZ0VBQ3g4bjFrTVpIenc1YldOYThwYTQ5Z09jUWdJUzEyaUhieXNvYU4ybjlFdkkKd0lkMjM4UUpoUHJkQ2V5bHR2S0h4RXV1SnBmVXUxbUhTcHNQZU5BRXFzeXMrVjhtN0diSHZCamY2TXZmSmViYQpFa0dKNGlDeEk1cTZxakx6NVFwbzhOVVQ5WmJlRU0yNSt6RW1ucmIvRTJxd3ZONG5EbGlyNHRCWWtxa203bldvCmdjbFk4RHpWSy9qcHZpSThSVmkzckFBTGlPbU50NWgwUHJGN0dFYVBRQ3Z4eVVEa1VZVGRaUjB1bTFMalZmZjUKcEMvVlRhUzFsdkNCd3JTb0FRdUlnQVRjQ0xGTDRpS0FIQjFHdnEwK2FsQXBCaWFpY1Z0WGQ5c0tBalBsS3R2dgpOamJFc3BBUzJFSVBBRHRZNmg4Yzd3R016NGxCenEzSm5oaVppUFJkZVFLQmdRRDVqMGtpbmFORitXR3dFbVVYCjBLWkk2dnM3R0YwelVQaW1FaFBPRmxrYVczcTZYUXFpN3ArbVpJUUhreEpmWlVMcEdtUm1rL3JsMlBHUnZzTzkKWHAyamVTbmQyRnlGdnRkYUpTcXMyOTNVRC8vZmtuQTQ5U0E2NW5tSkYrdUFkMlRBQW5JOFNmTVZHV2hLNGltbQo0aU1BNzRKM2RvUG1XdzFHWm9JZzZ6NzVCd0tCZ1FEMncyc09md2lJVlNvT3JqWitNMzJrUytmVWFjRUxxSmNQCk4wc0Z6Um1YY0RPY1VyamlCcDNwOXRzcmxqNzc4TkpWUkVSTG1PVHRleVFJWjVpTVBLQW15cTNleHBhY2FFSHIKN1M3WUtVcXBRRGZhbkNCVkdYcFdmcW84bDZZeEM3SVhKSFY5SjVZV05HWWVzWW5QWVh0dEdPQVlRQWlxUnpRYwpHdHpycHRMMTN3S0JnUURzNmVIOFFycWx5MEc1N1R5Ymh6RFZPSDZZYzJESFBxOU91UXFrTXdNVHlwU1FKNVBaCmFiK1dMd1pJU282K2tWc2NBRG9nUjR1QWVKdWNTUVY1MlVDOTJHQ3VncUFUT0VFcEN1K2dlbm4wdjl1aFcxdSsKTnJjS001SmRXNS91cldtdi9xS0VLWS9qRCtPNmMwb25hN2JmUklFTytDdnZtN0RXaktvc3BpZDZNUUtCZ1FEWgpIcGpUNkhKWmdwVysxUXFkZ0RETW52MGNxUEJ5a2FSWWZ3NUR6SEw0RElGY3lteGljKy8rTEhZcm84ZEhabDJyCktLWHUzbGlQbkFuWVcwNmVGYm5VYUFFVVF5ZkI5dm9KM0dlYlNybW1zaHB5LzJ4emREZHdZNExtVFlpdEh0MVQKLzRCZzNyUHdWYXFFMXpHcWh0dFM0dEJLaGcwWnhnelRyYzgwaVZTVmdRS0JnUUNCZ1pYaVEwY3FpWkpHdWkrcwoxRXhWdnNBdjh4NnJLMmEzc0ZvOXE1WEc4NnZuVEdrVElNNmZCWXo2Qm1lMTdiTDgrYmVKcjRmeVc4WWlENGVWCjFrQWxuaUdlYnZDcTNrSlh4U0VhdjQxM0dLQzhkMFZqWVpFbUVaN1hkU1dxcWVWa0xpUGNNVFdnaC9DdmpLU3YKVnpnOGFyN3VZaUh3WGdPQXNFVWJpWllNaUE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg=="
defaultProsodySSLcert="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ5ekNDQXQrZ0F3SUJBZ0lVZSsyVmpVM3psWDVzUHVqWGFZNnY0UWZOK2VRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2JqRUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQW9NQ2xCeWIzTnZaSGtnU1UweExEQXFCZ05WQkFzTQpJMmgwZEhCek9pOHZjSEp2YzI5a2VTNXBiUzlrYjJNdlkyVnlkR2xtYVdOaGRHVnpNUnd3R2dZRFZRUUREQk5GCmVHRnRjR3hsSUdObGNuUnBabWxqWVhSbE1CNFhEVEl5TURrd01qRXlNVGt3T1ZvWERUSXpNRGt3TWpFeU1Ua3cKT1Zvd2JqRUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQW9NQ2xCeWIzTnZaSGtnU1UweExEQXFCZ05WQkFzTQpJMmgwZEhCek9pOHZjSEp2YzI5a2VTNXBiUzlrYjJNdlkyVnlkR2xtYVdOaGRHVnpNUnd3R2dZRFZRUUREQk5GCmVHRnRjR3hsSUdObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0MKQVFFQThJNHd5UDRXMXcxbnVCVDdRNUFaYUtVZENDT09rcDVuRE9Scnp5dlZsUzg3ck9HejVUV3NuSUZFMXhUQQo3L2ZIek93Wk0xNFFKdzQvaS9GT09iODg1cGtTN2U0ZEJydnA4Q3Jyc2VvSFB6S0ZsZGw4SzNuRk9KaWI0QWZ6CktENERVTVhGRlZ4bXRjMDBTcDFOTmdwS056eFdlcFZlZ0ErOEJ4M2Jhc1VwWGRidWF0cnk1TGh0NDVjMkkzVVgKN0pFcjl5aWp5Q3FpYml0OXlrYlFxRWJqdEtIVXJQTEw5b1gybGc0OGYzUU5ncUo1c2NCUFI1TGdUMXgyYmQ5NQpPNCswbVI4Mzc2b0QyNWsxdTV6WVVtRXdaRFNoTXhramJwQi93Ty9paEUySzRVMXREQWxDUWg1VFlTVmZkbjlZClQzTVhodElTRmdiWlNkaWs1MGhnQWV5Z0dRSURBUUFCbzRHTU1JR0pNQXdHQTFVZEV3UUZNQU1CQWY4d2VRWUQKVlIwUkJISXdjSUlKYkc5allXeG9iM04wb0NRR0NDc0dBUVVGQndnSG9CZ1dGbDk0YlhCd0xXTnNhV1Z1ZEM1cwpiMk5oYkdodmMzU2dKQVlJS3dZQkJRVUhDQWVnR0JZV1gzaHRjSEF0YzJWeWRtVnlMbXh2WTJGc2FHOXpkS0FYCkJnZ3JCZ0VGQlFjSUJhQUxEQWxzYjJOaGJHaHZjM1F3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUpGK0dBZ28KSW8vWlBqVWM4YWJQbXZCMHZNTlhRVURYVGlzdkxRdDg5MGJmNzVDWkM2SFVGekw5ZjljamQ1NU1ZTG80QllxZQpVcjZOL3R3ZGRHWVNwb3JBY1dQWmtIb3F5bmQ5QXhIR0VpQUtvV2s5VXB0U2crcWhRd1JLQ1owQnBKemcrOENqCmtUdHBiWmdMa0s5RWhmSWlXbW1LSG41cHovYkhaK0FHaU9ESzcrYmsxZk1IZ0dDemFjVWVsU2lwSS9WOE1jY3IKbitwaGRRc2pjN1Vpd05zNlFEL2dxSS9GME1pV2FCd3ZiSjg0dWk3YkF4OFJqZ29OZC9ya3htUGhyQzBtL2VYYgpjUGFTOXpEMGFuUFN3ZUQ5VEQzUXBvNXZaNlJMempzNjZRR09VK0Y0a1dIU2NMTmVhaW9mOUV1aEMxQ0VrMDk4CmxQZ0F2SEQydTlKcURLdz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
if [ -n "$prosodySSLkey" ]; then
echo "$prosodySSLkey" | base64 -d > /etc/prosody-ssl.key
else
echo "$defaultProsodySSLkey" | base64 -d > /etc/prosody-ssl.key
fi
if [ -n "$prosodySSLcert" ]; then
echo "$prosodySSLcert" | base64 -d > /etc/prosody-ssl.pem
else
echo "$defaultProsodySSLcert" | base64 -d > /etc/prosody-ssl.pem
fi
chmod 444 /etc/prosody-ssl.pem
chmod 444 /etc/prosody-ssl.key
mkdir -p /etc/prosody
chmod 755 /etc/prosody
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
/usr/local/bin/prosody -F
File diff suppressed because it is too large Load Diff
+116
View File
@@ -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);
+416
View File
@@ -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);
+535
View File
@@ -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
+37
View File
@@ -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);
+440
View File
@@ -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;
+1604
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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
+244
View File
@@ -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;
};
+229
View File
@@ -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;
}
View File
Executable
+16
View File
@@ -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"
+44
View File
@@ -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);
+51
View File
@@ -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,126 @@
-- 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
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("debug", "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;
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 auth provider confirmed valid password");
return true;
else
module:log("debug", "HTTP 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("debug", "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
+44
View File
@@ -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
----- -------------
+32
View File
@@ -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||
+25
View File
@@ -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
----- -------
+18
View File
@@ -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;
};
});
+54
View File
@@ -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>
+58
View File
@@ -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 = "/";
});
+29
View File
@@ -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>'
+46
View File
@@ -0,0 +1,46 @@
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","got error: %s", err);
return 415;
end
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);
+119
View File
@@ -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;
+24
View File
@@ -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
------- -------
+13
View File
@@ -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||
+31
View File
@@ -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);
+165
View File
@@ -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
+63
View File
@@ -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
+31
View File
@@ -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
+659
View File
@@ -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);
+586
View File
@@ -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.");
+174
View File
@@ -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)
+106
View File
@@ -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);
+123
View File
@@ -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
+54
View File
@@ -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);
+982
View File
@@ -0,0 +1,982 @@
--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, _isMessageCorrection, _enableSoundOpt)
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 = "";
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();
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 = _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,
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)
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, _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
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
module:log("info", "sending IOS sound %s", _enableSoundOpt);
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 = _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,
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)
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
end
local function getNotifyOptionsForUser(userName)
-- module:log("info", "in getNotifyOptionsForUser %s", 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 empty;
local nType;
local nLang = "en";
local globalMute = false;
local enableSound;
if (pd) then
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"]);
module:log("info", "got nType: %s", nType);
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;
tokens = {};
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));
end
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 = 2; 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, 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 return end;
local nType, tokens, lang, soundOpt = 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;
if (nType == 1) then
sName = "new message";
gTopic = "new message";
body = "";
end
if (_isMessageCorrection ~= nil) then
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("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, _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
-- 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, fromLocal=%s", from, to, fromLocal);
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") or nil;
if (isMessageCorrection) then
content = " ";
type = "CORRECTION";
end
-- debug from here
module:log("debug","continue to process message with body: %s", content);
module:log("info","isMessageCorrection: %s", dumpTable(isMessageCorrection));
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") or (type == "CORRECTION")
);
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("info", "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);
local fromuser2, fromdomain2 = jid_split(stanza.attr.from);
local touser2, todomain2 = jid_split(stanza.attr.to);
module:log("info", "fromdomain2: %s <=> ThisDomain: %s", fromdomain2, ThisDomain);
module:log("info", "todomain2: %s <=> ThisDomain: %s", todomain2, ThisDomain);
if (fromLocal and (fromdomain2 == ThisDomain) and ((todomain2 == ThisDomain) or (todomain2 == nil))) and (type ~= "groupchat") then
module:log("info", "checkssa1");
fcm_notify(jid_split(to), from, content, type, getDisplayName(jid_split(from)), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom, isMessageCorrection);
else
module:log("info", "checkssa2");
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, isMessageCorrection);
else
-- notify when not from a local domain
if not (prosody.hosts[fromdomain2]) then
module:log("info", "I do not know about %s", fromdomain2);
end
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, isMessageCorrection);
end
else
module:log("debug", " WTF 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, isMessageCorrection);
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("vnc-rest-message", 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
+932
View File
@@ -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
+370
View File
@@ -0,0 +1,370 @@
-- 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 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 origin, stanza = event.origin, event.stanza;
local typ = stanza.attr.type;
-- module:log("info", "Cache hook, got %s from a %s", stanza:top_tag(), origin.type);
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_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;
if ((#remote_act_sessions[contact_bare]) == 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", contact_bare);
remote_act_cache[contact_bare] = 0;
local contact_sessions = remote_act_sessions[contact_bare] or {};
contact_sessions[contact_full] = 1;
remote_act_sessions[contact_bare] = contact_sessions;
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;
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_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
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
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
+32
View File
@@ -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);
+92
View File
@@ -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);
+695
View File
@@ -0,0 +1,695 @@
-- 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)
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 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 = _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,
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("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
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")
+654
View File
@@ -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")
+129
View File
@@ -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")
+59
View File
@@ -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);
+72
View File
@@ -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);
+166
View File
@@ -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);
+70
View File
@@ -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);
+40
View File
@@ -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("info", "stored as id: %s", id);
else
module:log("info", "failed to store: %s", err);
end
end);
+117
View File
@@ -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);
+106
View File
@@ -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);
+65
View File
@@ -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)
+43
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

+118
View File
@@ -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;
};
});