diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a5aa269 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = true + +[*.json] +indent_style = space +indent_size = 2 + +[*.yml] +indent_style = space +indent_size = 2 + +[Makefile] +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +[*.sh] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a08d153 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.travis/ejabberd* +ebin +tmp +log +*.tar.gz diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..13008d6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,63 @@ +language: erlang +otp_release: 20.2 +sudo: true +dist: trusty + +cache: + directories: + - .travis/ejabberd + - tests/node_modules + +stages: + # We want to build and test each and every branch. + - name: build + - name: test + # We just run the release stage, when we have a tag build. + - name: release + if: tag =~ .* AND type IN (push, api) + +jobs: + include: + - stage: build + install: skip + script: + # Reown the build workspace to the travis user (due to Docker, + # and caching) + - sudo chown travis:travis -R $PWD/.. + # Build the ejabberd module + - make -C .travis build + + - stage: test + install: + # Install docker-compose version 1.22.0 + - sudo rm /usr/local/bin/docker-compose + - curl -L http://bit.ly/2B4msDT > docker-compose + - chmod +x docker-compose + - sudo mv docker-compose /usr/local/bin + # Fix some travis/2000 common/1000 user id mapping issues + - source .travis/exe/docker-glue + # Install the test suite dependencies + - make install + script: + - docker --version + - docker-compose --version + - START=background make start reload test + + - stage: release + install: skip + script: + # Reown the build workspace to the travis user (due to Docker, + # and caching) + - sudo chown travis:travis -R $PWD/.. + # Setup the module version environment variable for the release + - export MOD_VERSION=${TRAVIS_TAG} + - \[ -n "${MOD_VERSION}" \] || export MOD_VERSION=latest + # Build and package the ejabberd module + - make -C .travis build package + deploy: + provider: releases + api_key: ${GITHUB_AUTH_TOKEN} + file: .travis/ejabberd-mam2sidekiq-${MOD_VERSION}.tar.gz + skip_cleanup: true + on: + tags: true diff --git a/.travis/Makefile b/.travis/Makefile new file mode 100644 index 0000000..30a93f3 --- /dev/null +++ b/.travis/Makefile @@ -0,0 +1,87 @@ +MAKEFLAGS += --warn-undefined-variables -j1 +SHELL := bash +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := all +.DELETE_ON_ERROR: +.SUFFIXES: +.PHONY: package + +# Environment switches +MODULE ?= ejabberd-mam2sidekiq +VERSION ?= 18.01 +MOD_VERSION ?= latest +SOURCE_URL ?= https://github.com/processone/ejabberd/archive/$(VERSION).tar.gz + +# Directories +INCLUDE_DIRS ?= ../include +EBIN_DIRS ?= ../ebin +SRC_DIR ?= ../src + +# Host binaries +APTGET ?= apt-get +CD ?= cd +CP ?= cp +ERLC ?= erlc +FIND ?= find +MKDIR ?= mkdir +PWD ?= pwd +SED ?= sed +SUDO ?= sudo +TAR ?= tar +TEST ?= test +WGET ?= wget + +.download-ejabberd-sources: + # Download the ejabberd $(VERSION) sources + @$(TEST) -f ejabberd/autogen.sh || ( \ + $(WGET) -O ejabberd.tar.gz $(SOURCE_URL) && \ + $(MKDIR) -p ejabberd && \ + $(TAR) xf ejabberd.tar.gz -C ejabberd --strip-components=1 \ + ) + +.install-ejabberd-build-deps: + # Install the ejabberd $(VERSION) build dependencies + @$(SUDO) $(APTGET) update -y + @$(SUDO) $(APTGET) build-dep -y ejabberd + @$(SUDO) $(APTGET) install -y libssl-dev libyaml-dev libgd-dev libwebp-dev + +.build-ejabberd: + # Build ejabberd $(VERSION) from source + @cd ejabberd && ./autogen.sh && ./configure --enable-redis \ + && ./rebar get-deps && $(MAKE) + +.find-deps: + # Find all build dependencies + $(eval INCLUDES = $(addprefix -I ,\ + $(shell $(FIND) `$(PWD)` -type d -name include) $(INCLUDE_DIRS))) + $(eval EBINS = $(addprefix -pa ,\ + $(shell $(FIND) `$(PWD)` -type d -name ebin) $(EBIN_DIRS))) + +install: \ + .download-ejabberd-sources \ + .install-ejabberd-build-deps \ + .build-ejabberd \ + .find-deps + +build: install + # Build $(MODULE) module from source + @$(MKDIR) -p $(EBIN_DIRS) + @$(ERLC) \ + -o $(EBIN_DIRS) \ + $(INCLUDES) \ + $(EBINS) \ + -DLAGER \ + -DNO_EXT_LIB \ + $(SRC_DIR)/*.erl + +package: + # Create a new release package ($(MODULE)-$(MOD_VERSION).tar.gz) + @$(MKDIR) -p package package/conf + @$(CP) -r $(EBIN_DIRS) package/ + @$(CP) ../LICENSE ../README.md ../INSTALL.md \ + ../mod_mam2sidekiq.spec ../CHANGELOG.md \ + package/ + @$(CP) ../config/mod_mam2sidekiq.yml package/conf/ + @$(CD) package && \ + $(TAR) cfvz ../$(MODULE)-$(MOD_VERSION).tar.gz --owner=0 --group=0 . + @$(RM) -rf package diff --git a/.travis/exe/docker-glue b/.travis/exe/docker-glue new file mode 100755 index 0000000..c505a35 --- /dev/null +++ b/.travis/exe/docker-glue @@ -0,0 +1,18 @@ +#!/bin/bash +# +# Add a new glue user with the uid 1000 and add him to the docker and travis +# group. Then add the travis user to the glue group and reload the glue group +# on the current shell session. Then re-own the whole build directory to the +# glue user and group. +# +# @author Hermann Mayer + +# Setup the glue user +sudo useradd -m -u 1000 -G travis,docker glue +sudo usermod -aG glue travis + +# Reown the build workspace to the glue user +sudo chown glue:travis -R $PWD/.. + +# Ensure correct permissions on the SSH files +sudo chmod 600 ~/.ssh/config diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d1a9f60 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Implemented the basic MAM to Sidekiq bridge diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e05b70b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at hermann.mayer92@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/COPYING b/COPYING new file mode 120000 index 0000000..7a694c9 --- /dev/null +++ b/COPYING @@ -0,0 +1 @@ +LICENSE \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1aac861 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +FROM hausgold/ejabberd:18.01 +MAINTAINER Hermann Mayer + +# Install custom supervisord units +COPY config/supervisor/* /etc/supervisor/conf.d/ + +# Install system packages and the ruby bundles +RUN rm -rf /var/lib/apt/lists/* && \ + sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list && \ + apt-get update -yqqq && \ + apt-get install -y \ + build-essential libicu-dev locales sudo curl wget \ + vim bash-completion inotify-tools git libexpat1-dev \ + fakeroot dpkg-dev libssl-dev libyaml-dev libgd-dev libwebp-dev \ + erlang-redis-client && \ + echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && /usr/sbin/locale-gen + +# Install nodejs 10 +RUN rm -rf /var/lib/apt/lists/* && \ + curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ + apt-get install -y nodejs + +# Setup additional build dependencies for ejabberd/erlang +RUN cd /tmp && \ + apt-get source ejabberd && \ + apt-get build-dep -y ejabberd + +# Setup the runtime directories for ejabberd +RUN mkdir /run/ejabberd && chmod ugo+rwx /run/ejabberd + +# Setup a contrib modules directory +RUN mkdir -p /opt/modules.d/sources && \ + chmod ugo+rwx /opt/modules.d + +# Add new app user +RUN mkdir /app && \ + adduser app --home /home/app --shell /bin/bash \ + --disabled-password --gecos "" +COPY config/docker/shell/* /home/app/ +COPY config/docker/shell/* /root/ +RUN chown app:app -R /app /home/app && \ + mkdir -p /home/app/.ssh + +# Set the root password and grant root access to app +RUN echo 'root:root' | chpasswd +RUN echo 'app ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers + +WORKDIR /app diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..86cb1df --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,36 @@ +# mod_mam2sidekiq Installation + +- [Common notes](#common-notes) +- [Manual installation](#manual-installation) +- [Manual uninstall](#manual-uninstall) +- [Automatic install on Ubuntu/Debian](#automatic-install-on-ubuntudebian) + +## Common notes + +Take care of the mod_mam2sidekiq module configuration on you ejabberd config, +otherwise the module won't be started on your instance and you cannot use the +features. + +## Manual installation + +The ejabberd project is able to compile and load contribution modules at +runtime. You just need to download the source of this module into +`~/.ejabberd-modules` directory or the one defined by the +`CONTRIB_MODULES_PATH` setting in `ejabberdctl.cfg`. There you create a +directory named `mod_mam2sidekiq`. + +Then run `ejabberdctl module_install mod_mam2sidekiq` while the ejabberd +server is running and you should see a logged info about the message bridge +module was started. Then you are able to use it. + +## Manual uninstall + +Just run `ejabberdctl module_uninstall mod_mam2sidekiq` while the ejabberd +server is running and delete the `mod_mam2sidekiq` directory from your +contribution modules directory (`~/.ejabberd-modules`). + +## Automatic install on Ubuntu/Debian + +Be sure that the `ejabberd` package is installed correctly via `apt`. Then you +can use the following curl-pipe command to automatically install the module to +the ejabberd server. The server MUST be restarted afterwards. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6783cbd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 HAUSGOLD + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..beebb46 --- /dev/null +++ b/Makefile @@ -0,0 +1,335 @@ +MAKEFLAGS += --warn-undefined-variables -j1 +SHELL := bash +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := all +.DELETE_ON_ERROR: +.SUFFIXES: +.PHONY: + +# Environment switches +MAKE_ENV ?= docker +IMAGE_VENDOR ?= hausgold +PROJECT_NAME ?= jabberreadmarkers +START ?= foreground +START_CONTAINERS ?= jabber +BUNDLE_FLAGS ?= +COMPOSE_RUN_COMMAND ?= run +COMPOSE_RUN_SHELL_FLAGS ?= --rm +BASH_RUN_SHELL_FLAGS ?= +BASH_RUN_SHELL_USER ?= app +BASH_RUN_SHELL_CONTAINER ?= jabber +MODULE ?= mod_mam2sidekiq +DOMAIN ?= jabber.local +DATABASE ?= jabber + +# Directories +APP_DIR ?= /app +LOG_DIR ?= log +TMP_DIR ?= tmp +VENDOR_DIR ?= vendor/bundle +VENDOR_CACHE_DIR ?= vendor/cache + +# Host binaries +AWK ?= awk +BASH ?= bash +CHMOD ?= chmod +COMPOSE ?= docker-compose +CUT ?= cut +CP ?= cp +DOCKER ?= docker +ECHO ?= echo +FIND ?= find +GREP ?= grep +HEAD ?= head +INOTIFYWAIT ?= inotifywait +LS ?= ls +MKDIR ?= mkdir +MV ?= mv +NODE ?= node +NPM ?= npm +NPROC ?= nproc +PRINTF ?= printf +RM ?= rm +SED ?= sed +SLEEP ?= sleep +TAIL ?= tail +TEE ?= tee +TEST ?= test +TOUCH ?= touch +WC ?= wc +XARGS ?= xargs + +# Container binaries +EJABBERDCTL ?= ejabberdctl +REDIS_CLI ?= redis-cli +WAITFORSTART ?= config/docker/wait-for-start + +ifeq ($(MAKE_ENV),docker) +# Check also the docker binaries +CHECK_BINS += COMPOSE DOCKER +else ifeq ($(MAKE_ENV),baremetal) +# Nothing to do here - just a env check +else +$(error MAKE_ENV got an invalid value. Use `docker` or `baremetal`) +endif + +all: + # Jabber Message Bridge + # + # install Install the application + # start Start the application + # stop Stop all running containers + # + # logs Monitor the started application + # relevant-logs Show only relevant logs (with [RM] prefix) + # + # shell Attach an interactive shell session (jabber) + # + # reload Uninstall, check and build, install at once + # uninstall-module Uninstall the $(MODULE) module + # build Check and build the $(MODULE) module + # install-module Install the $(MODULE) module + # + # watch Watch for file changes and reload the module and + # run the test suite against it + # + # test Run the test suite + # + # clean Clean all temporary application files + # clean-containers Clean the Docker containers (also database data) + # distclean Same as clean and cleans Docker images + +# Define a generic shell run wrapper +# $1 - The command to run +ifeq ($(MAKE_ENV),docker) +define run-shell + $(COMPOSE) $(COMPOSE_RUN_COMMAND) $(COMPOSE_RUN_SHELL_FLAGS) \ + -e LANG=en_US.UTF-8 -e LANGUAGE=en_US.UTF-8 -e LC_ALL=en_US.UTF-8 \ + -u $(BASH_RUN_SHELL_USER) $(BASH_RUN_SHELL_CONTAINER) \ + bash $(BASH_RUN_SHELL_FLAGS) -c 'sleep 0.1; echo; $(1)' +endef +else ifeq ($(MAKE_ENV),baremetal) +define run-shell + $(1) +endef +endif + +# Define a retry helper +# $1 - The command to run +define retry + if eval "$(call run-shell,$(1))"; then exit 0; fi; \ + for i in 1; do sleep 10s; echo "Retrying $$i..."; \ + if eval "$(call run-shell,$(1))"; then exit 0; fi; \ + done; \ + exit 1 +endef + +COMPOSE := $(COMPOSE) -p $(PROJECT_NAME) + +.start: install clean-tmpfiles + @$(eval BASH_RUN_SHELL_FLAGS = --login) + +.jabber: + @$(eval BASH_RUN_SHELL_CONTAINER = jabber) + @$(eval COMPOSE_RUN_COMMAND = exec) + @$(eval BASH_RUN_SHELL_USER = root) + @$(eval COMPOSE_RUN_SHELL_FLAGS = ) + +.redis: + @$(eval BASH_RUN_SHELL_CONTAINER = redis) + @$(eval COMPOSE_RUN_COMMAND = exec) + @$(eval BASH_RUN_SHELL_USER = root) + @$(eval COMPOSE_RUN_SHELL_FLAGS = ) + +.test: + @$(eval BASH_RUN_SHELL_CONTAINER = jabber) + @$(eval COMPOSE_RUN_COMMAND = exec) + @$(eval BASH_RUN_SHELL_USER = app) + @$(eval COMPOSE_RUN_SHELL_FLAGS = ) + +.e2e: + @$(eval BASH_RUN_SHELL_CONTAINER = e2e) + @$(eval COMPOSE_RUN_COMMAND = run) + @$(eval BASH_RUN_SHELL_USER = root) + @$(eval COMPOSE_RUN_SHELL_FLAGS = ) + +.disable-module-conf: + # Disable $(MODULE) configuration + @$(CP) config/ejabberd.yml config/ejabberd.yml.old + @$(SED) 's/^\(\s*\)\(mod_mam2sidekiq:.*\)/\1# \2/g' \ + config/ejabberd.yml.old > config/ejabberd.yml + @$(RM) config/ejabberd.yml.old + +.enable-module-conf: + # Enable $(MODULE) configuration + @$(CP) config/ejabberd.yml config/ejabberd.yml.old + @$(SED) 's/^\(\s*\)# \(mod_mam2sidekiq:.*\)/\1\2/g' \ + config/ejabberd.yml.old > config/ejabberd.yml + @$(RM) config/ejabberd.yml.old + +install: .test + # Install the application + @$(eval COMPOSE_RUN_COMMAND = run) + @$(call retry,cd tests && $(NPM) install) + +.wait-for-start: + # Monitor the started application until it is booted + @COMPOSE='$(COMPOSE)' $(WAITFORSTART) + +start: clean-tmpfiles clean-logs .disable-module-conf + # Start the application +ifeq ($(START),foreground) + @$(COMPOSE) up $(START_CONTAINERS) +else ifeq ($(START),background) + @$(COMPOSE) up -d $(START_CONTAINERS) + @$(MAKE) .wait-for-start +else + $(error START got an invalid value. Use `foreground` or `background`) +endif + +uninstall-module: .jabber + # Uninstall the $(MODULE) module + @-$(call run-shell,$(EJABBERDCTL) module_uninstall $(MODULE)) + +build: .jabber uninstall-module + # Check and build the $(MODULE) module + @$(call run-shell,$(EJABBERDCTL) module_check $(MODULE)) + +.update-build-number: + @$(eval VERSION = $(shell \ + $(GREP) -oP '\d+\.\d+\.\d+-\d+' include/mod_mam2sidekiq.hrl)) + @$(eval BUILD = $(lastword $(subst -, ,$(VERSION)))) + @$(eval NEXT_BUILD = $(shell $(ECHO) $$(($(BUILD)+1)))) + @$(eval NEXT_VERSION = $(firstword $(subst -, ,$(VERSION)))-$(NEXT_BUILD)) + # Update build number ($(VERSION) -> $(NEXT_VERSION)) + @$(SED) -i 's/$(VERSION)/$(NEXT_VERSION)/g' include/mod_mam2sidekiq.hrl + +install-module: .jabber .update-build-number .enable-module-conf + # Install the $(MODULE) module + @$(call run-shell,\ + $(MKDIR) -p ebin && $(CHMOD) 777 ebin && \ + $(EJABBERDCTL) module_install $(MODULE)) + +reload: .update-build-number .enable-module-conf \ + uninstall-module install-module + @$(SLEEP) 1 + # Reloaded the $(MODULE) module + +restart-module: .jabber + # Reload the module code and restart the module (0 means success) + @$(call run-shell,$(EJABBERDCTL) restart_module $(DOMAIN) $(MODULE)) + +watch: .jabber + # Watch for file changes and reload the $(MODULE) module + @while true; do \ + $(INOTIFYWAIT) --quiet -r `pwd` -e close_write --format "%e -> %w%f"; \ + $(SHELL) -c "reset; \ + $(MAKE) --no-print-directory reload test || true"; $(ECHO); done + +test: \ + test-specs \ + test-e2e + +test-specs: .test + # Run test specs for the $(MODULE) module + @$(MAKE) clean-database + @$(call run-shell,$(NODE) tests/index.js) + +test-e2e: + # Run end-to-end tests for the $(MODULE) module + @$(MAKE) clean-database + @$(MAKE) .test-e2e-produce .test-e2e-consume + +.test-e2e-produce: .test + # Produce Sidekiq jobs + @$(call run-shell,$(NODE) tests/index-e2e.js) + +.test-e2e-consume: .e2e + # Consume Sidekiq jobs + @$(call run-shell,$(MAKE) -C /app start) + +restart: + # Restart the application + @$(MAKE) stop start + +logs: + # Monitor the started application + @$(COMPOSE) logs -f --tail='all' + +relevant-logs: + # Monitor all relevant logs + @$(COMPOSE) logs -f --tail='0' | $(GREP) --line-buffered '\[M2S\]' + +stop: clean-containers +stop-containers: + # Stop all running containers + @$(COMPOSE) stop -t 5 || true + @$(DOCKER) ps -a | $(GREP) $(PROJECT_NAME)_ | $(CUT) -d ' ' -f1 \ + | $(XARGS) -rn10 $(DOCKER) stop -t 5 || true + +shell: .test + # Start an interactive shell session + @$(eval BASH_RUN_SHELL_USER = app) + @$(call run-shell,$(BASH) -i) + +shell-redis: .redis + # Start an interactive database session + @$(call run-shell,$(REDIS_CLI)) + +shell-e2e: .e2e + # Start an interactive e2e shell session + @$(call run-shell,$(BASH) -i) + +clean-database: clean-redis + +clean-redis: .redis + # Clean the Redis database + @$(call run-shell,$(REDIS_CLI) FLUSHALL >/dev/null 2>&1) + +clean-vendors: + # Clean vendors + @$(RM) -rf $(VENDOR_DIR) || true + @$(RM) -rf .bundle + +clean-logs: + # Clean logs + @$(MKDIR) -p $(LOG_DIR) + @$(FIND) $(LOG_DIR) -type f -name *.log \ + | $(XARGS) -rn1 -I{} $(BASH) -c '$(PRINTF) "\n" > {}' + @$(TOUCH) $(LOG_DIR)/ejabberd.log + +clean-tmpfiles: + # Clean temporary files + @$(MKDIR) -p $(TMP_DIR) + @$(RM) -rf $(TMP_DIR)/build || true + @$(FIND) $(TMP_DIR) -type f \ + | $(XARGS) -rn1 -I{} $(BASH) -c "$(RM) '{}'" + @$(RM) -rf ebin + +clean-containers: stop-containers + # Stop and kill all containers + @$(COMPOSE) rm -vf || true + @$(DOCKER) ps -a | $(GREP) $(PROJECT_NAME)_ | $(CUT) -d ' ' -f1 \ + | $(XARGS) -rn10 $(DOCKER) rm -vf || true + +clean-images: clean-containers + # Remove all docker images + $(eval EMPTY = ) $(eval CLEAN_IMAGES = $(PROJECT_NAME)_) + $(eval CLEAN_IMAGES += $(IMAGE_VENDOR)/app:$(PROJECT_NAME)) + $(eval CLEAN_IMAGES += ) + @$(DOCKER) images -a --format '{{.ID}} {{.Repository}}:{{.Tag}}' \ + | $(GREP) -P "$(subst $(EMPTY) $(EMPTY),|,$(CLEAN_IMAGES))" \ + | $(AWK) '{print $$0}' \ + | $(XARGS) -rn1 $(DOCKER) rmi -f || true + +clean-test-results: + # Clean test results + @$(RM) -rf coverage || true + @$(RM) -rf snapshots || true + +clean-vendor-cache: + # Clean the vendor cache + @$(RM) -rf $(VENDOR_CACHE_DIR) || true + +clean: clean-vendors clean-logs clean-tmpfiles clean-containers +distclean: clean clean-vendor-cache clean-images diff --git a/README.md b/README.md new file mode 100644 index 0000000..b186e87 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +![ejabberd MAM/Sidekiq Bridge](doc/assets/project.svg) + +[![Build Status](https://api.travis-ci.com/hausgold/ejabberd-mam2sidekiq.svg?token=4XcyqxxmkyBSSV3wWRt7&branch=master)](https://travis-ci.com/hausgold/ejabberd-mam2sidekiq) + +This is a custom [ejabberd](https://www.ejabberd.im/) module which allows to +bridge all archived messages (from [Message Archive +Management/XEP-0313](https://xmpp.org/extensions/xep-0313.html)) to actual +[Sidekiq](https://sidekiq.org/) +[jobs](https://github.com/mperham/sidekiq/wiki/Job-Format) on a +[Redis](https://redis.io/) database. This enables third party applications to +work and react on messages without implementing a XMPP presence application +which must subscribe to all multi user chats. Furthermore this module allows +direct messages to be processed the same way as multi user chat messages on +third party applications. This module requires an activated [ejabberd +mod_mam](https://docs.ejabberd.im/admin/configuration/#mod-mam) to work, +because we listen for the storage hooks. They do not suffer from message +dupplication. (Copies, changing sender/receiver side) + +- [Requirements](#requirements) + - [Runtime](#runtime) + - [Build and development](#build-and-development) +- [Installation](#installation) +- [Configuration](#configuration) + - [Database](#database) +- [Development](#development) + - [Getting started](#getting-started) + - [mDNS host configuration](#mdns-host-configuration) + - [Test suite](#test-suite) +- [Additional readings](#additional-readings) + +## Requirements + +### Runtime + +* [ejabberd](https://www.ejabberd.im/) (=18.01) + * Compiled Redis support (`--enable-redis` or [erlang-redis-client package](https://packages.ubuntu.com/bionic/erlang-redis-client) on [hausgold/ejabberd](https://hub.docker.com/r/hausgold/ejabberd) image) +* [Redis](https://redis.io/) (>=3.2) + +### Build and development + +* [GNU Make](https://www.gnu.org/software/make/) (>=4.2.1) +* [Docker](https://www.docker.com/get-docker) (>=17.09.0-ce) +* [Docker Compose](https://docs.docker.com/compose/install/) (>=1.22.0) + +## Installation + +See the [detailed installation instructions](./INSTALL.md) to get the ejabberd +module up and running. When you are using Debian/Ubuntu, you can use an +automatic curl pipe script which simplifies the installation process for you. + +## Configuration + +We make use of the global database settings of ejabberd, but you can also +specify a different database type by setting it explicitly. + +```yaml +# Global Redis config +# See: https://docs.ejabberd.im/admin/configuration/#redis +redis_server: "redis.server.com" +redis_port: 6379 +redis_db: 1 + +modules: + mod_mam2sidekiq: + sidekiq_queue: "default" + sidekiq_class: "SomeWorker" +``` + +## Development + +### Getting started + +The project bootstrapping is straightforward. We just assume you took already +care of the requirements and you have your favorite terminal emulator pointed +on the project directory. Follow the instructions below and then relaxen and +watchen das blinkenlichten. + +```bash +# Installs and starts the ejabberd server and it's database +$ make start + +# (The jabber server should already running now on its Docker container) + +# Open a new terminal on the project path, +# install the custom module and run the test suite +$ make reload test +``` + +When your host mDNS Stack is fine, you can also inspect the [ejabberd admin +webconsole](http://jabber.local/admin) with +`admin@jabber.local` as username and `defaultpw` as password. In the +case you want to shut this thing down use `make stop`. + +#### mDNS host configuration + +If you running Ubuntu, everything should be in place out of the box. When +you however find yourself unable to resolve the domains, read on. + +**Heads up:** This is the Arch Linux way. (package and service names may +differ, config is the same) Install the `nss-mdns` and `avahi` packages, enable +and start the `avahi-daemon.service`. Then, edit the file /etc/nsswitch.conf +and change the hosts line like this: + +```bash +hosts: ... mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns ... +``` + +**Further readings** +* Archlinux howto: https://wiki.archlinux.org/index.php/avahi +* Ubuntu/Debian howto: https://wiki.ubuntuusers.de/Avahi/ + +### Test suite + +The test suite sets up a simple environment with 3 independent users. (admin, +alice and bob). A new test room is created by the admin user, as well as alice +and bob were made members by setting their affiliations on the room. (This is +the same procedure we use on production for lead/user/agent integrations on the +Jabber service) The suite sends then multiple text messagess. The Redis +database/queue contains then a job for each sent message. + +The test suite was written in JavaScript and is executed by Node.js inside a +Docker container. We picked JavaScript here due to the easy and good featured +[stanza.io](http://stanza.io) client library for XMPP. It got all the things +which were needed to fulfil the job. + +## Additional readings + +* [mod_mam MUC IQ integration](http://bit.ly/2M2cSWl) +* [mod_mam MUC message integration](http://bit.ly/2Kx69iF) +* [mod_muc implementation](http://bit.ly/2AJTSYq) +* [mod_muc_room implementation](http://bit.ly/2LX6As4) +* [mod_muc_room IQ implementation](http://bit.ly/2LWgXfI) +* [muc_filter_message hook example](http://bit.ly/2Oey9K0) +* [MUC message definition](http://bit.ly/2MavaVo) +* [MUCState definition](http://bit.ly/2AM4CWi) +* [XMPP codec API docs](http://bit.ly/2LXQ235) +* [XMPP codec guide](http://bit.ly/2LHKFoq) +* [XMPP codec script example](http://bit.ly/2M8sgNM) diff --git a/README.txt b/README.txt new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/config/docker/shell/.bash_profile b/config/docker/shell/.bash_profile new file mode 100644 index 0000000..45cb87e --- /dev/null +++ b/config/docker/shell/.bash_profile @@ -0,0 +1,3 @@ +if [ -f ~/.bashrc ]; then + . ~/.bashrc +fi diff --git a/config/docker/shell/.bashrc b/config/docker/shell/.bashrc new file mode 100644 index 0000000..b364623 --- /dev/null +++ b/config/docker/shell/.bashrc @@ -0,0 +1,193 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. +# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) +# for examples + +export LANG=en_US.UTF-8 +export LANGUAGE=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 + +_GEM_PATHS=$(ls -d1 ${HOME}/.gem/ruby/*/bin 2>/dev/null | paste -sd ':') +_APP_PATHS=$(ls -d1 /app/vendor/bundle/ruby/*/bin 2>/dev/null | paste -sd ':') + +export PATH="${_GEM_PATHS}:${_APP_PATHS}:${PATH}" +export PATH="/app/node_modules/.bin:${HOME}/.bin:/app/bin:${PATH}" + +if [ "${MDNS_STACK}" = 'true' ]; then + # Disable the autostart of all supervisord units + sudo sed -i 's/autostart=.*/autostart=false/g' /etc/supervisor/conf.d/* + + # Start the supervisord (empty, no units) + sudo supervisord >/dev/null 2>&1 & + + # Wait for supervisord + while ! supervisorctl status >/dev/null 2>&1; do sleep 1; done + + # Boot the mDNS stack + echo '# Start the mDNS stack' + sudo supervisorctl start dbus avahi + echo +fi + +if [ "${SSH_STACK}" = 'true' ]; then + # Start the ssh-agent + echo '# Start the SSH agent' + eval "$(ssh-agent -s)" >/dev/null + + # Run a user script for adding the relevant ssh keys + if [ -f ~/.ssh/add-all ]; then + . ~/.ssh/add-all + fi +fi + +# If not running interactively, don't do anything +case $- in + *i*) ;; + *) return;; +esac + +# Clear the color for the first time +echo -e "\e[0m" + +export HISTCONTROL="ignoreboth:erasedups" +export HISTSIZE=1000000 + +# Enable less mouse scrolling +export LESS=-r + +# Default Editor +export EDITOR=vim + +# set variable identifying the chroot you work in (used in the prompt below) +if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then + debian_chroot=$(cat /etc/debian_chroot) +fi + +# If this is an xterm set the title to user@host:dir +case "$TERM" in +xterm*|rxvt*) + PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" + ;; +*) + ;; +esac + +# enable color support of ls and also add handy aliases +if [ -x /usr/bin/dircolors ]; then + test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" \ + || eval "$(dircolors -b)" +fi + +if [ -f ~/.bash_aliases ]; then + . ~/.bash_aliases +fi + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi + +export COLOR_OPTIONS='--color=auto' + +alias ..="cd .." +alias ...="cd ../.." +alias ....="cd ../../.." +alias .....="cd ../../../.." +alias ls='ls $COLOR_OPTIONS --group-directories-first --time-style="+%F, %T "' +alias ll='ls $COLOR_OPTIONS -lh' +alias l='ls $COLOR_OPTIONS -lAh' +alias grep='grep $COLOR_OPTIONS' +alias egrep='egrep $COLOR_OPTIONS' +alias g='git' +alias p='pwd' +alias mkdir='mkdir -p -v' +alias less='less -R' +alias x='exit' + +# Bash won't get SIGWINCH if another process is in the foreground. +# Enable checkwinsize so that bash will check the terminal size when +# it regains control. #65623 +# http://cnswww.cns.cwru.edu/~chet/bash/FAQ (E11) +shopt -s checkwinsize + +# Enable history appending instead of overwriting. +shopt -s histappend + +# Enable extended globbing +shopt -s extglob + +# Enable globbing for dotfiles +shopt -s dotglob + +# Enable globstars for recursive globbing +shopt -s globstar + +# Auto "cd" when entering just a path +shopt -s autocd + +# Disable XOFF (interrupt data flow) +stty -ixoff + +# Disable XON (interrupt data flow) +stty -ixon + +bind "set completion-ignore-case on" # note: bind used instead of sticking these in .inputrc +bind "set bell-style none" # no bell +bind "set show-all-if-ambiguous On" # show list automatically, without double tab + +# use ctl keys to move forward and back in words +bind '"\e[1;5C": forward-word' +bind '"\e[1;5D": backward-word' +bind '"\e[5C": forward-word' +bind '"\e[5D": backward-word' +bind '"\e\e[C": forward-word' +bind '"\e\e[D": backward-word' + +# use arrow keys to fast search +bind '"\e[A": history-search-backward' +bind '"\e[B": history-search-forward' + +# Enable colors for ls, etc. Prefer ~/.dir_colors #64489 +if type -P dircolors >/dev/null ; then + if [[ -f ~/.dir_colors ]] ; then + eval $(dircolors -b ~/.dir_colors) + elif [[ -f /etc/DIR_COLORS ]] ; then + eval $(dircolors -b /etc/DIR_COLORS) + fi +fi + +function watch-run() +{ + while [ 1 ]; do + inotifywait --quiet -r `pwd` -e close_write --format '%e -> %w%f' + bash -c "$@" + done +} + +PROMPT_COMMAND='RET=$?;' +RET_OUT='$(if [[ $RET = 0 ]]; then echo -ne "\[\e[0;32m\][G]"; else echo -ne "\[\e[0;31m\][Err: $RET]"; fi;)' +RET_OUT="\n$RET_OUT" + +HOST="${MDNS_HOSTNAME}" +if [ -z "${HOST}" ]; then + HOST="\h" +fi + +_TIME='\t' +_FILES="\$(ls -a1 | grep -vE '\.$' | wc -l)" +_SIZE="\$(ls -lah | head -n1 | cut -d ' ' -f2)" +_META="${_TIME} | Files: ${_FILES} | Size: ${_SIZE} | \[\e[0;36m\]\w" +META=" \[\e[0;31m\][\[\e[1;37m\]${_META}\[\e[0;31m\]]\[\e[0;32m\]\033]2;\w\007" + +PSL1=${RET_OUT}${META} +PSL2="\n\[\e[0;31m\][\u\[\e[0;33m\]@\[\e[0;37m\]${HOST}\[\e[0;31m\]] \[\e[0;31m\]$\[\e[0;32m\] " + +export PS1=${PSL1}${PSL2} + +# Rebind enter key to insert newline before command output +trap 'echo -e "\e[0m"' DEBUG diff --git a/config/docker/shell/.gemrc b/config/docker/shell/.gemrc new file mode 100644 index 0000000..22fd901 --- /dev/null +++ b/config/docker/shell/.gemrc @@ -0,0 +1,2 @@ +install: --no-ri --no-rdoc +update: --no-ri --no-rdoc diff --git a/config/docker/shell/.inputrc b/config/docker/shell/.inputrc new file mode 100644 index 0000000..4414089 --- /dev/null +++ b/config/docker/shell/.inputrc @@ -0,0 +1,17 @@ +# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving +"\e[1;5C": forward-word +"\e[1;5D": backward-word +"\e[5C": forward-word +"\e[5D": backward-word +"\e\e[C": forward-word +"\e\e[D": backward-word + +# handle common Home/End escape codes +"\e[1~": beginning-of-line +"\e[4~": end-of-line +"\e[7~": beginning-of-line +"\e[8~": end-of-line +"\eOH": beginning-of-line +"\eOF": end-of-line +"\e[H": beginning-of-line +"\e[F": end-of-line diff --git a/config/docker/wait-for-start b/config/docker/wait-for-start new file mode 100755 index 0000000..f311b78 --- /dev/null +++ b/config/docker/wait-for-start @@ -0,0 +1,36 @@ +#!/bin/bash +# +# @author Hermann Mayer + +if [ -z "${COMPOSE}" ]; then + COMPOSE='docker-compose' +fi + +# We wait and print the logs until we find this regex on the output +STOP_TRIGGER='Executing command ejabberd_admin:register' + +# Save the current pid for bad times +CURRENT_PID=$$ + +# Read every line form the logs we follow +${COMPOSE} logs -f --tail="all" | while read -t 30 LINE; do + # Proxy every line from the log to stdout + echo -e "${LINE}" + # Search for the stop trigger + if [ -n "`echo "${LINE}" | grep "${STOP_TRIGGER}"`" ]; then + echo + echo "# Start completed." + # We found our stop trigger, now we need to kill the + # logs we are following. This is hairy because of the + # BSD/GNU compatibility, so we drop every comfort and + # be POSIX only. + ps xao ppid,pid,command \ + | sed 's/^\s\+//g' \ + | grep "^${CURRENT_PID} " \ + | grep "${COMPOSE} logs -f --tail" \ + | awk '{print $2}' \ + | xargs -n1 kill -9 + # For safety reasons we die, too + exit 0 + fi +done diff --git a/config/ejabberd.yml b/config/ejabberd.yml new file mode 100644 index 0000000..7345ff6 --- /dev/null +++ b/config/ejabberd.yml @@ -0,0 +1,803 @@ +### +###' ejabberd configuration file +### +### + +### The parameters used in this configuration file are explained in more detail +### in the ejabberd Installation and Operation Guide. +### Please consult the Guide in case of doubts, it is included with +### your copy of ejabberd, and is also available online at +### http://www.process-one.net/en/ejabberd/docs/ + +### The configuration file is written in YAML. +### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. +### However, ejabberd treats different literals as different types: +### +### - unquoted or single-quoted strings. They are called "atoms". +### Example: dog, 'Jupiter', '3.14159', YELLOW +### +### - numeric literals. Example: 3, -45.0, .0 +### +### - quoted or folded strings. +### Examples of quoted string: "Lizzard", "orange". +### Example of folded string: +### > Art thou not Romeo, +### and a Montague? +--- +###. ======= +###' LOGGING + +## +## loglevel: Verbosity of log files generated by ejabberd. +## 0: No ejabberd log at all (not recommended) +## 1: Critical +## 2: Error +## 3: Warning +## 4: Info +## 5: Debug +## +loglevel: 5 + +## +## rotation: Disable ejabberd's internal log rotation, as the Debian package +## uses logrotate(8). +log_rotate_size: 0 +log_rotate_date: "" + +## +## overload protection: If you want to limit the number of messages per second +## allowed from error_logger, which is a good idea if you want to avoid a flood +## of messages when system is overloaded, you can set a limit. +## 100 is ejabberd's default. +log_rate_limit: 1000 + +## +## watchdog_admins: Only useful for developers: if an ejabberd process +## consumes a lot of memory, send live notifications to these XMPP +## accounts. +## +## watchdog_admins: +## - "bob@example.com" + +###. =============== +###' NODE PARAMETERS + +## +## net_ticktime: Specifies net_kernel tick time in seconds. This options must have +## identical value on all nodes, and in most cases shouldn't be changed at all from +## default value. +## +## net_ticktime: 60 + +###. ================ +###' SERVED HOSTNAMES + +## +## hosts: Domains served by ejabberd. +## You can define one or several, for example: +## hosts: +## - "example.net" +## - "example.com" +## - "example.org" +## +hosts: + # - "{[MDNS_HOSTNAME]}" + - "jabber.local" + +## +## route_subdomains: Delegate subdomains to other XMPP servers. +## For example, if this ejabberd serves example.org and you want +## to allow communication with an XMPP server called im.example.org. +## +## route_subdomains: s2s + +###. =============== +###' LISTENING PORTS + +## Define common macros used by listeners +define_macro: + 'CERTFILE': "/etc/ejabberd/ejabberd.pem" +## 'CIPHERS': "ECDH:DH:!3DES:!aNULL:!eNULL:!MEDIUM@STRENGTH" + 'TLSOPTS': + - "no_sslv3" + - "no_tlsv1" + - "cipher_server_preference" + - "no_compression" +## 'DHFILE': "/path/to/dhparams.pem" # generated with: openssl dhparam -out dhparams.pem 2048 + +## +## listen: The ports ejabberd will listen on, which service each is handled +## by and what options to start it with. +## +listen: + - + port: 5222 + ip: "::" + module: ejabberd_c2s + starttls_required: true + certfile: 'CERTFILE' + protocol_options: 'TLSOPTS' + ## dhfile: 'DHFILE' + ## ciphers: 'CIPHERS' + max_stanza_size: 65536 + shaper: c2s_shaper + access: c2s + resend_on_timeout: if_offline + - + port: 5269 + ip: "::" + module: ejabberd_s2s_in + - + port: 5280 + ip: "::" + module: ejabberd_http + request_handlers: + "/ws": ejabberd_http_ws + "/bosh": mod_bosh + "/api": mod_http_api + ## "/pub/archive": mod_http_fileserver + web_admin: true + ## register: true + ## captcha: true + tls: false + certfile: 'CERTFILE' + protocol_options: 'TLSOPTS' + ## + ## ejabberd_service: Interact with external components (transports, ...) + ## + ## - + ## port: 8888 + ## ip: "::" + ## module: ejabberd_service + ## access: all + ## shaper_rule: fast + ## ip: "127.0.0.1" + ## privilege_access: + ## roster: "both" + ## message: "outgoing" + ## presence: "roster" + ## delegations: + ## "urn:xmpp:mam:1": + ## filtering: ["node"] + ## "http://jabber.org/protocol/pubsub": + ## filtering: [] + ## hosts: + ## "icq.example.org": + ## password: "secret" + ## "sms.example.org": + ## password: "secret" + + ## + ## ejabberd_stun: Handles STUN Binding requests + ## + ## - + ## port: 3478 + ## transport: udp + ## module: ejabberd_stun + + ## + ## To handle XML-RPC requests that provide admin credentials: + ## + ## - + ## port: 4560 + ## ip: "::" + ## module: ejabberd_xmlrpc + ## access_commands: {} + + ## + ## To enable secure http upload + ## + ## - + ## port: 5444 + ## ip: "::" + ## module: ejabberd_http + ## request_handlers: + ## "": mod_http_upload + ## tls: true + ## certfile: 'CERTFILE' + ## protocol_options: 'TLSOPTS' + ## dhfile: 'DHFILE' + ## ciphers: 'CIPHERS' + +## Disabling digest-md5 SASL authentication. digest-md5 requires plain-text +## password storage (see auth_password_format option). +disable_sasl_mechanisms: "digest-md5" + +###. ================== +###' S2S GLOBAL OPTIONS + +## +## s2s_use_starttls: Enable STARTTLS for S2S connections. +## Allowed values are: false optional required required_trusted +## You must specify a certificate file. +## +s2s_use_starttls: required + +## +## s2s_certfile: Specify a certificate file. +## +# s2s_certfile: 'CERTFILE' + +## Custom OpenSSL options +## +s2s_protocol_options: 'TLSOPTS' + +## +## domain_certfile: Specify a different certificate for each served hostname. +## +## host_config: +## "example.org": +## domain_certfile: "/path/to/example_org.pem" +## "example.com": +## domain_certfile: "/path/to/example_com.pem" + +## +## S2S whitelist or blacklist +## +## Default s2s policy for undefined hosts. +## +## s2s_access: s2s + +## +## Outgoing S2S options +## +## Preferred address families (which to try first) and connect timeout +## in seconds. +## +## outgoing_s2s_families: +## - ipv4 +## - ipv6 +## outgoing_s2s_timeout: 190 + +###. ============== +###' AUTHENTICATION + +## +## auth_method: Method used to authenticate the users. +## The default method is the internal. +## If you want to use a different method, +## comment this line and enable the correct ones. +## +# auth_method: +# - internal +# - external +# auth_use_cache: false + +## +## Store the plain passwords or hashed for SCRAM: +## auth_password_format: plain +auth_password_format: scram +## +## Define the FQDN if ejabberd doesn't detect it: +## fqdn: "server3.example.com" + +## +## Authentication using external script +## Make sure the script is executable by ejabberd. +## +## auth_method: external +## extauth_program: "/path/to/authentication/script" + +## +## Authentication using SQL +## Remember to setup a database in the next section. +## +auth_method: internal + +## +## Authentication using PAM +## +## auth_method: pam +## pam_service: "pamservicename" + +## +## Authentication using LDAP +## +## auth_method: ldap +## +## List of LDAP servers: +## ldap_servers: +## - "localhost" +## +## Encryption of connection to LDAP servers: +## ldap_encrypt: none +## ldap_encrypt: tls +## +## Port to connect to on LDAP servers: +## ldap_port: 389 +## ldap_port: 636 +## +## LDAP manager: +## ldap_rootdn: "dc=example,dc=com" +## +## Password of LDAP manager: +## ldap_password: "******" +## +## Search base of LDAP directory: +## ldap_base: "dc=example,dc=com" +## +## LDAP attribute that holds user ID: +## ldap_uids: +## - "mail": "%u@mail.example.org" +## +## LDAP filter: +## ldap_filter: "(objectClass=shadowAccount)" + +## +## Anonymous login support: +## auth_method: anonymous +## anonymous_protocol: sasl_anon | login_anon | both +## allow_multiple_connections: true | false +## +## host_config: +## "public.example.org": +## auth_method: anonymous +## allow_multiple_connections: false +## anonymous_protocol: sasl_anon +## +## To use both anonymous and internal authentication: +## +## host_config: +## "public.example.org": +## auth_method: +## - internal +## - anonymous + +###. ============== +###' DATABASE SETUP + +# However, if you want to use MySQL for all modules that support MySQL as +# db_type, you can simply use global option default_db: sql: +default_db: mnesia + +# Global Redis config +redis_server: "redis" +redis_port: 6379 +redis_db: 5 + +## ejabberd by default uses the internal Mnesia database, +## so you do not necessarily need this section. +## This section provides configuration examples in case +## you want to use other database backends. +## Please consult the ejabberd Guide for details on database creation. + +## +## MySQL server: +## +## sql_type: mysql +## sql_server: "server" +## sql_database: "database" +## sql_username: "username" +## sql_password: "password" +## +## If you want to specify the port: +## sql_port: 1234 + +## +## PostgreSQL server: +## +# sql_type: pgsql +# sql_server: "db" +# sql_database: "jabber" +# sql_username: "postgres" +# sql_password: "postgres" +## +## If you want to specify the port: +## sql_port: 1234 +## +## If you use PostgreSQL, have a large database, and need a +## faster but inexact replacement for "select count(*) from users" +## +## pgsql_users_number_estimate: true + +## +## SQLite: +## +## sql_type: sqlite +## sql_database: "/path/to/database.db" + +## +## ODBC compatible or MSSQL server: +## +## sql_type: odbc +## sql_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd" + +## +## Number of connections to open to the database for each virtual host +## +## sql_pool_size: 10 + +## +## Interval to make a dummy SQL request to keep the connections to the +## database alive. Specify in seconds: for example 28800 means 8 hours +## +## sql_keepalive_interval: undefined + +###. =============== +###' TRAFFIC SHAPERS + +shaper: + ## + ## The "normal" shaper limits traffic speed to 1000 B/s + ## + normal: 1000 + + ## + ## The "fast" shaper limits traffic speed to 50000 B/s + ## + fast: 50000 + +## +## This option specifies the maximum number of elements in the queue +## of the FSM. Refer to the documentation for details. +## +max_fsm_queue: 1000 + +###. ==================== +###' ACCESS CONTROL LISTS +acl: + ## + ## The 'admin' ACL grants administrative privileges to XMPP accounts. + ## You can put here as many accounts as you want. + ## + admin: + user: + # - "admin@{[MDNS_HOSTNAME]}" + - "admin@jabber.local" + + ## + ## Blocked users + ## + ## blocked: + ## user: + ## - "baduser@example.org" + ## - "test" + + ## Local users: don't modify this. + ## + local: + user_regexp: "" + + ## + ## More examples of ACLs + ## + ## jabberorg: + ## server: + ## - "jabber.org" + ## aleksey: + ## user: + ## - "aleksey@jabber.ru" + ## test: + ## user_regexp: "^test" + ## user_glob: "test*" + + ## + ## Loopback network + ## + loopback: + ip: + - "127.0.0.0/8" + - "::1/128" + - "::FFFF:127.0.0.1/128" + + ## + ## Bad XMPP servers + ## + ## bad_servers: + ## server: + ## - "xmpp.zombie.org" + ## - "xmpp.spam.com" + +## +## Define specific ACLs in a virtual host. +## +## host_config: +## "localhost": +## acl: +## admin: +## user: +## - "bob-local@localhost" + +###. ============ +###' SHAPER RULES + +shaper_rules: + ## Maximum number of simultaneous sessions allowed for a single user: + max_user_sessions: 10 + ## Maximum number of offline messages that users can have: + max_user_offline_messages: + - 5000: admin + - 100 + ## For C2S connections, all users except admins use the "normal" shaper + c2s_shaper: + - none: admin + - normal + ## All S2S connections use the "fast" shaper + s2s_shaper: fast + +###. ============ +###' ACCESS RULES +access_rules: + ## This rule allows access only for local users: + local: + - allow: local + ## Only non-blocked users can use c2s connections: + c2s: + - deny: blocked + - allow + ## Only admins can send announcement messages: + announce: + - allow: admin + ## Only admins can use the configuration interface: + configure: + - allow: admin + ## Only admin accounts can create rooms: + muc_admin: + - allow: admin + ## Only accounts on the local ejabberd server can create Pubsub nodes: + pubsub_createnode: + - allow: local + ## In-band registration allows registration of any possible username. + ## To disable in-band registration, replace 'allow' with 'deny'. + register: + - deny + ## Only allow to register from localhost + trusted_network: + - allow: loopback + ## Do not establish S2S connections with bad servers + ## If you enable this you also have to uncomment "s2s_access: s2s" + ## s2s: + ## - deny: + ## - ip: "XXX.XXX.XXX.XXX/32" + ## - deny: + ## - ip: "XXX.XXX.XXX.XXX/32" + ## - allow + +## =============== +## API PERMISSIONS +## =============== +## +## This section allows you to define who and using what method +## can execute commands offered by ejabberd. +## +## By default "console commands" section allow executing all commands +## issued using ejabberdctl command, and "admin access" section allows +## users in admin acl that connect from 127.0.0.1 to execute all +## commands except start and stop with any available access method +## (ejabberdctl, http-api, xmlrpc depending what is enabled on server). +## +## If you remove "console commands" there will be one added by +## default allowing executing all commands, but if you just change +## permissions in it, version from config file will be used instead +## of default one. +## +api_permissions: + "console commands": + from: + - ejabberd_ctl + who: all + what: "*" + "admin access": + who: + - access: + - allow: + - acl: admin + - oauth: + - scope: "ejabberd:admin" + - access: + - allow: + - acl: admin + what: + - "*" + - "!stop" + - "!start" + "public commands": + who: + - ip: "127.0.0.1/8" + what: + - "status" + +## By default the frequency of account registrations from the same IP +## is limited to 1 account every 10 minutes. To disable, specify: infinity +## registration_timeout: 600 + +## +## Define specific Access Rules in a virtual host. +## +## host_config: +## "localhost": +## access: +## c2s: +## - allow: admin +## - deny +## register: +## - deny + +###. ================ +###' DEFAULT LANGUAGE + +## +## language: Default language used for server messages. +## +language: "en" + +## +## Set a different default language in a virtual host. +## +## host_config: +## "localhost": +## language: "ru" + +###. ======= +###' CAPTCHA + +## +## Full path to a script that generates the image. +## +## captcha_cmd: "/usr/share/ejabberd/captcha.sh" + +## +## Host for the URL and port where ejabberd listens for CAPTCHA requests. +## +## captcha_host: "example.org:5280" + +## +## Limit CAPTCHA calls per minute for JID/IP to avoid DoS. +## +## captcha_limit: 5 + +###. ======= +###' MODULES + +## +## Modules enabled in all ejabberd virtual hosts. +## +modules: + mod_mam2sidekiq: { sidekiq_queue: "default", sidekiq_class: "SomeWorker" } + mod_adhoc: {} + mod_admin_extra: {} + mod_announce: # recommends mod_adhoc + access: announce + mod_blocking: {} # requires mod_privacy + mod_caps: {} + mod_carboncopy: {} + mod_client_state: {} + mod_configure: {} # requires mod_adhoc + ## mod_delegation: {} # for xep0356 + mod_disco: {} + mod_echo: {} + mod_irc: {} + mod_bosh: { } + ## mod_http_fileserver: + ## docroot: "/var/www" + ## accesslog: "/var/log/ejabberd/access.log" + ## mod_http_upload: + ## # docroot: "@HOME@/upload" + ## put_url: "https://@HOST@:5444" + ## thumbnail: false # otherwise needs the identify command from ImageMagick installed + ## mod_http_upload_quota: + ## max_days: 30 + mod_last: {} + ## XEP-0313: Message Archive Management + ## You might want to setup a SQL backend for MAM because the mnesia database is + ## limited to 2GB which might be exceeded on large servers + ## mod_mam: {} # for xep0313, mnesia is limited to 2GB, better use an SQL backend + mod_mam: + request_activates_archiving: false + iqdisc: one_queue + default: always + db_type: mnesia + mod_muc: + ## host: "conference.@HOST@" + access: local + access_admin: muc_admin + access_create: muc_admin + access_persistent: muc_admin + ## Rooms should be persistent by default + default_room_options: + allow_subscription: true + persistent: true + mam: true + anonymous: false + mod_muc_admin: {} + ## mod_muc_log: {} + ## mod_multicast: {} + mod_offline: + access_max_user_messages: max_user_offline_messages + mod_ping: {} + ## mod_pres_counter: + ## count: 5 + ## interval: 60 + mod_privacy: {} + mod_private: {} + ## mod_proxy65: {} + mod_pubsub: + access_createnode: pubsub_createnode + ## reduces resource comsumption, but XEP incompliant + ignore_pep_from_offline: true + ## XEP compliant, but increases resource comsumption + ## ignore_pep_from_offline: false + last_item_cache: false + plugins: + - "flat" + - "hometree" + - "pep" # pep requires mod_caps + mod_push: {} + mod_push_keepalive: {} + ## mod_register: + ## + ## Protect In-Band account registrations with CAPTCHA. + ## + ## captcha_protected: true + ## + ## Set the minimum informational entropy for passwords. + ## + ## password_strength: 32 + ## + ## After successful registration, the user receives + ## a message with this subject and body. + ## + ## welcome_message: + ## subject: "Welcome!" + ## body: |- + ## Hi. + ## Welcome to this XMPP server. + ## + ## When a user registers, send a notification to + ## these XMPP accounts. + ## + ## registration_watchers: + ## - "admin1@example.org" + ## + ## Only clients in the server machine can register accounts + ## + ## ip_access: trusted_network + ## + ## Local c2s or remote s2s users cannot register accounts + ## + ## access_from: deny + ## access: register + mod_roster: + versioning: true + mod_shared_roster: {} + mod_stats: {} + mod_time: {} + mod_vcard: + search: false + mod_version: {} + mod_stream_mgmt: {} + ## Non-SASL Authentication (XEP-0078) is now disabled by default + ## because it's obsoleted and is used mostly by abandoned + ## client software + ## mod_legacy_auth: {} + ## The module for S2S dialback (XEP-0220). Please note that you cannot + ## rely solely on dialback if you want to federate with other servers, + ## because a lot of servers have dialback disabled and instead rely on + ## PKIX authentication. Make sure you have proper certificates installed + ## and check your accessibility at https://xmpp.net/ + mod_s2s_dialback: {} + mod_http_api: {} + +## +## Enable modules with custom options in a specific virtual host +## +## host_config: +## "localhost": +## modules: +## mod_echo: +## host: "mirror.localhost" + +## +## Enable modules management via ejabberdctl for installation and +## uninstallation of public/private contributed modules +## (enabled by default) +## + +allow_contrib_modules: true + +###. +###' +### Local Variables: +### mode: yaml +### End: +### vim: set filetype=yaml tabstop=8 foldmarker=###',###. foldmethod=marker: diff --git a/config/ejabberdctl.cfg b/config/ejabberdctl.cfg new file mode 100644 index 0000000..0ab4aa2 --- /dev/null +++ b/config/ejabberdctl.cfg @@ -0,0 +1,187 @@ +# +# In this file you can configure options that are passed by ejabberdctl +# to the erlang runtime system when starting ejabberd +# + +#' POLL: Kernel polling ([true|false]) +# +# The kernel polling option requires support in the kernel. +# Additionally, you need to enable this feature while compiling Erlang. +# +# Default: true +# +#POLL=true + +#. +#' SMP: SMP support ([enable|auto|disable]) +# +# Explanation in Erlang/OTP documentation: +# enable: starts the Erlang runtime system with SMP support enabled. +# This may fail if no runtime system with SMP support is available. +# auto: starts the Erlang runtime system with SMP support enabled if it +# is available and more than one logical processor are detected. +# disable: starts a runtime system without SMP support. +# +# Default: auto +# +#SMP=auto + +#. +#' ERL_MAX_PORTS: Maximum number of simultaneously open Erlang ports +# +# ejabberd consumes two or three ports for every connection, either +# from a client or from another Jabber server. So take this into +# account when setting this limit. +# +# Default: 32000 +# Maximum: 268435456 +# +#ERL_MAX_PORTS=32000 + +#. +#' FIREWALL_WINDOW: Range of allowed ports to pass through a firewall +# +# If Ejabberd is configured to run in cluster, and a firewall is blocking ports, +# it's possible to make Erlang use a defined range of port (instead of dynamic +# ports) for node communication. +# +# Default: not defined +# Example: 4200-4210 +# +#FIREWALL_WINDOW= + +#. +#' INET_DIST_INTERFACE: IP address where this Erlang node listens other nodes +# +# This communication is used by ejabberdctl command line tool, +# and in a cluster of several ejabberd nodes. +# +# Default: 0.0.0.0 +# +#INET_DIST_INTERFACE=127.0.0.1 + +#. +#' ERL_EPMD_ADDRESS: IP addresses where epmd listens for connections +# +# IMPORTANT: This option works only in Erlang/OTP R14B03 and newer. +# +# This environment variable may be set to a comma-separated +# list of IP addresses, in which case the epmd daemon +# will listen only on the specified address(es) and on the +# loopback address (which is implicitly added to the list if it +# has not been specified). The default behaviour is to listen on +# all available IP addresses. +# +# Default: 0.0.0.0 +# +#ERL_EPMD_ADDRESS=127.0.0.1 + +#. +#' ERL_PROCESSES: Maximum number of Erlang processes +# +# Erlang consumes a lot of lightweight processes. If there is a lot of activity +# on ejabberd so that the maximum number of processes is reached, people will +# experience greater latency times. As these processes are implemented in +# Erlang, and therefore not related to the operating system processes, you do +# not have to worry about allowing a huge number of them. +# +# Default: 250000 +# Maximum: 268435456 +# +#ERL_PROCESSES=250000 + +#. +#' ERL_MAX_ETS_TABLES: Maximum number of ETS and Mnesia tables +# +# The number of concurrent ETS and Mnesia tables is limited. When the limit is +# reached, errors will appear in the logs: +# ** Too many db tables ** +# You can safely increase this limit when starting ejabberd. It impacts memory +# consumption but the difference will be quite small. +# +# Default: 1400 +# +#ERL_MAX_ETS_TABLES=1400 + +#. +#' ERL_OPTIONS: Additional Erlang options +# +# The next variable allows to specify additional options passed to erlang while +# starting ejabberd. Some useful options are -noshell, -detached, -heart. When +# ejabberd is started from an init.d script options -noshell and -detached are +# added implicitly. See erl(1) for more info. +# +# It might be useful to add "-pa /usr/local/lib/ejabberd/ebin" if you +# want to add local modules in this path. +# +# Default: "-env ERL_CRASH_DUMP_BYTES 0" +# +ERL_OPTIONS="-env ERL_CRASH_DUMP_BYTES 0" + +#. +#' ERLANG_NODE: Erlang node name +# +# The next variable allows to explicitly specify erlang node for ejabberd +# It can be given in different formats: +# ERLANG_NODE=ejabberd +# Lets erlang add hostname to the node (ejabberd uses short name in this case) +# ERLANG_NODE=ejabberd@hostname +# Erlang uses node name as is (so make sure that hostname is a real +# machine hostname or you'll not be able to control ejabberd) +# ERLANG_NODE=ejabberd@hostname.domainname +# The same as previous, but erlang will use long hostname +# (see erl (1) manual for details) +# +# Default: ejabberd@localhost +# +ERLANG_NODE=ejabberd@jabber.local + +#. +#' EJABBERD_PID_PATH: ejabberd PID file +# +# Indicate the full path to the ejabberd Process identifier (PID) file. +# If this variable is defined, ejabberd writes the PID file when starts, +# and deletes it when stops. +# Remember to create the directory and grant write permission to ejabberd. +# +# Default: don't write PID file +# +EJABBERD_PID_PATH=/run/ejabberd/ejabberd.pid + +#. +#' EJABBERD_CONFIG_PATH: ejabberd configuration file +# +# Specify the full path to the ejabberd configuration file. If the file name has +# yml or yaml extension, it is parsed as a YAML file; otherwise, Erlang syntax is +# expected. +# +# Default: $ETC_DIR/ejabberd.yml +# +EJABBERD_CONFIG_PATH=/etc/ejabberd/ejabberd.yml + +#. +#' CONTRIB_MODULES_PATH: contributed ejabberd modules path +# +# Specify the full path to the contributed ejabberd modules. If the path is not +# defined, ejabberd will use ~/.ejabberd-modules in home of user running ejabberd. +# Note: this is not needed for the ejabberd-mod-* packages +# +# Default: $HOME/.ejabberd-modules +# +CONTRIB_MODULES_PATH=/opt/modules.d + +#. +#' CONTRIB_MODULES_CONF_DIR: configuration directory for contributed modules +# +# Specify the full path to the configuration directory for contributed ejabberd +# modules. In order to configure a module named mod_foo, a mod_foo.yml file can +# be created in this directory. This file will then be used instead of the +# default configuration file provided with the module. +# +# Default: $CONTRIB_MODULES_PATH/conf +# +# CONTRIB_MODULES_CONF_DIR=/app/modules-conf.d + +#. +#' +# vim: foldmarker=#',#. foldmethod=marker: diff --git a/config/mod_mam2sidekiq.yml b/config/mod_mam2sidekiq.yml new file mode 100644 index 0000000..95f992e --- /dev/null +++ b/config/mod_mam2sidekiq.yml @@ -0,0 +1,10 @@ +# Global Redis config +# See: https://docs.ejabberd.im/admin/configuration/#redis +redis_server: "redis.server.com" +redis_port: 6379 +redis_db: 1 + +modules: + mod_mam2sidekiq: + sidekiq_queue: "default" + sidekiq_class: "SomeWorker" diff --git a/config/supervisor/ejabberd-admin.conf b/config/supervisor/ejabberd-admin.conf new file mode 100644 index 0000000..69216a4 --- /dev/null +++ b/config/supervisor/ejabberd-admin.conf @@ -0,0 +1,15 @@ +[program:ejabberd-admin] +priority=10 +startretries=15 +directory=/tmp +command=/bin/sh -c "ejabberdctl started && ejabberdctl register admin ${MDNS_HOSTNAME} defaultpw && tail -F /dev/null" +user=root +autostart=true +autorestart=unexpected +exitcodes=0,1 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stopsignal=KILL +stopwaitsecs=1 diff --git a/config/supervisor/ejabberd.conf b/config/supervisor/ejabberd.conf new file mode 100644 index 0000000..dbddbeb --- /dev/null +++ b/config/supervisor/ejabberd.conf @@ -0,0 +1,15 @@ +[program:ejabberd] +priority=10 +startretries=20 +directory=/tmp +environment=LANG="en_US.UTF-8",LANGUAGE="en_US.UTF-8",LC_ALL="en_US.UTF-8" +command=ejabberdctl foreground +user=root +autostart=true +autorestart=true +stdout_logfile=/app/log/ejabberd.log +stdout_logfile_maxbytes=0 +stderr_logfile=/app/log/ejabberd.log +stderr_logfile_maxbytes=0 +stopsignal=KILL +stopwaitsecs=1 diff --git a/config/supervisor/logs.conf b/config/supervisor/logs.conf new file mode 100644 index 0000000..ab34db9 --- /dev/null +++ b/config/supervisor/logs.conf @@ -0,0 +1,13 @@ +[program:logs] +priority=10 +directory=/app +command=tail -F + /app/log/ejabberd.log +user=root +autostart=true +autorestart=true +startretries=0 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/doc/assets/project.svg b/doc/assets/project.svg new file mode 100644 index 0000000..8bc0ee3 --- /dev/null +++ b/doc/assets/project.svg @@ -0,0 +1,68 @@ + +image/svg+xml + + + + +ejabberd MAM/Sidekiq Bridge +Bridge all archived messages to Sidekiq for third party processing + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7512ec9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3" +services: + redis: + image: redis:3.2 + network_mode: bridge + + jabber: + build: . + network_mode: bridge + extra_hosts: + - jabber.local:127.0.0.1 + volumes: + - ./:/app + - ./:/opt/modules.d/sources/mod_mam2sidekiq + - ./config/ejabberd.yml:/etc/ejabberd/ejabberd.yml + - ./config/ejabberdctl.cfg:/etc/ejabberd/ejabberdctl.cfg + links: + - redis + environment: + MDNS_HOSTNAME: jabber.local + + e2e: + image: ruby:2.6 + network_mode: bridge + volumes: + - ./tests/e2e:/app + links: + - redis diff --git a/include/mod_mam2sidekiq.hrl b/include/mod_mam2sidekiq.hrl new file mode 100644 index 0000000..fce46d8 --- /dev/null +++ b/include/mod_mam2sidekiq.hrl @@ -0,0 +1,52 @@ +-define(MODULE_VERSION, <<"0.1.0-325">>). +-define(NS_MAM_SIDEKIQ, <<"xmpp:mam:hausgold:sidekiq">>). + +%% A macro to convert a record to tuple{[tuples]} for jiffy (JSON) encoding +-define(record_to_tuple(Rec, Ref), + {lists:zip(record_info(fields, Rec), tl(tuple_to_list(Ref)))}). + +-record(job, {class = 'undefined' :: binary(), + jid = 'undefined' :: binary(), + args = [] :: nonempty_list(string()), + created_at = 0 :: non_neg_integer(), + enqueued_at = 0 :: non_neg_integer()}). + +%% +%% +%% +%% +%% Try to copy the THX protocol, maybe it will +%% calculate the auxiliary matrix! +%% +%% +%% + +%% +%% +%% +%% +%% Try to copy the THX protocol, maybe it will +%% calculate the auxiliary matrix! +%% +%% +%% diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9b43a02 --- /dev/null +++ b/install.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# +# This script will perform the installation of the mod_mam2sidekiq ejabberd +# module. It has the same requirements as described on the readme file. We +# download the module and install it to your systems ejabberd files. +# +# This script was tested on Ubuntu Bionic (18), and works just on +# Ubuntu/Debian. +# +# This script should be called like this: +# +# $ curl -L 'http://bit.ly/2IVdGJf' | bash +# +# Used Ubuntu packages: wget +# +# @author Hermann Mayer + +# Fail on any errors +set -eE + +# Specify the module/ejabberd version +MOD_VERSION=0.1.0 +SUPPORTED_EJABBERD_VERSION=18.01 + +# Check for Debian/Ubuntu, otherwise die +if ! grep -P 'Ubuntu|Debian' /etc/issue >/dev/null 2>&1; then + echo 'Looks like you are not running Debian/Ubuntu.' + echo 'This installer is only working for them.' + echo 'Sorry.' + exit 1 +fi + +# Discover the installed ejabberd version +EJABBERD_VERSION=$(dpkg -l ejabberd | grep '^ii' \ + | awk '{print $3}' | cut -d- -f1) + +# Check for the ejabberd ebin repository, otherwise die +if [ -z "${EJABBERD_VERSION}" ]; then + echo 'ejabberd is currently not installed via apt.' + echo 'Suggestion: sudo apt-get install ejabberd' + exit 1 +fi + +# Check for the correct ejabberd version is available +if [ "${EJABBERD_VERSION}" != "${SUPPORTED_EJABBERD_VERSION}" ]; then + echo "The installed ejabberd version (${EJABBERD_VERSION}) is not supported." + echo "We just support ejabberd ${SUPPORTED_EJABBERD_VERSION}." + echo 'Sorry.' + exit 1 +fi + +# Discover the ejabberd ebin repository on the system +EBINS_PATH=$(dirname $(dpkg -L ejabberd \ + | grep 'ejabberd.*/ebin/.*\.beam$' | head -n1)) + +# Check for the ejabberd ebin repository, otherwise die +if [ ! -d "${EBINS_PATH}" ]; then + echo 'No ejabberd ebin repository path was found.' + echo 'Sorry.' + exit 1 +fi + +# Install the apt Redis dependency +sudo apt-get update -yqqq +sudo apt-get install -y erlang-redis-client + +# Download the module binary distribution and install it +URL="https://github.com/hausgold/ejabberd-mam2sidekiq/releases/" +URL+="download/${MOD_VERSION}/ejabberd-mam2sidekiq-${MOD_VERSION}.tar.gz" + +cd /tmp +rm -rf ejabberd-mam2sidekiq ejabberd-mam2sidekiq.tar.gz + +mkdir ejabberd-mam2sidekiq +wget -O ejabberd-mam2sidekiq.tar.gz "${URL}" +tar xf ejabberd-mam2sidekiq.tar.gz \ + --no-same-owner --no-same-permissions -C ejabberd-mam2sidekiq + +echo "Install ejabberd-mam2sidekiq to ${EBINS_PATH} .." +sudo chown root:root ejabberd-mam2sidekiq/ebin/* +sudo chmod 0644 ejabberd-mam2sidekiq/ebin/* +sudo cp -far ejabberd-mam2sidekiq/ebin/* "${EBINS_PATH}" +rm -rf ejabberd-mam2sidekiq ejabberd-mam2sidekiq.tar.gz + +echo -e "\n\n" +echo -n 'Take care of the configuration of mod_mam2sidekiq on ' +echo '/etc/ejabberd/ejabberd.yml' +echo 'Restart the ejabberd server afterwards.' +echo +echo 'Done.' diff --git a/mod_mam2sidekiq.spec b/mod_mam2sidekiq.spec new file mode 100644 index 0000000..5538081 --- /dev/null +++ b/mod_mam2sidekiq.spec @@ -0,0 +1,5 @@ +author: "Hermann Mayer " +category: "admin" +summary: "Bridge all messages to Sidekiq" +home: "https://github.com/hausgold/ejabberd-mam2sidekiq" +url: "git@github.com:hausgold/ejabberd-mam2sidekiq.git" diff --git a/src/mod_mam2sidekiq.erl b/src/mod_mam2sidekiq.erl new file mode 100644 index 0000000..72b34ac --- /dev/null +++ b/src/mod_mam2sidekiq.erl @@ -0,0 +1,173 @@ +-module(mod_mam2sidekiq). +-author("hermann.mayer92@gmail.com"). +-behaviour(gen_mod). +-export([%% ejabberd module API + start/2, stop/1, reload/3, depends/2, mod_opt_type/1, + %% Hooks + on_store_mam_message/6 + ]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("xmpp.hrl"). +-include("mod_mam.hrl"). +-include("mod_mam2sidekiq.hrl"). + +-callback store(#job{}) -> ok. + +%% Start the module by implementing the +gen_mod+ behaviour. Here we register +%% the hooks to listen to, for the custom MAM bridging functionality. +-spec start(binary(), gen_mod:opts()) -> ok. +start(Host, _Opts) -> + %% Register hooks + %% Run the MUC IQ hook after mod_mam (101) + ejabberd_hooks:add(store_mam_message, + Host, ?MODULE, on_store_mam_message, 101), + %% Log the boot up of the module + ?INFO_MSG("[M2S] Start MAM bridge (v~s) for ~s", [?MODULE_VERSION, Host]), + ok. + +%% Stop the module, and deregister all hooks. +-spec stop(binary()) -> any(). +stop(Host) -> + %% Deregister the custom XMPP codec + xmpp:unregister_codec(hg_read_markers), + %% Deregister all the hooks + ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE, on_muc_iq, 101), + %% Signalize we are done with stopping the module + ?INFO_MSG("[M2S] Stop MAM/Sidekiq bridge", []), + ok. + +%% Inline reload the module in case of external triggered +ejabberdctl+ reloads. +-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. +reload(_Host, _NewOpts, _OldOpts) -> ok. + +%% Hook on all MAM (message archive management) storage requests to grab the +%% stanza packet and write it out to Redis in a Sidekiq compatible format. This +%% is the core of this module and takes care of the message bridging for third +%% party applications. +-spec on_store_mam_message(message() | drop, binary(), binary(), jid(), + chat | groupchat, recv | send) -> message(). +on_store_mam_message(#message{} = Packet, _LUser, _LServer, _Peer, + Type, recv) -> + %% Write the Sidekiq job to the Redis queue (list) + store(create_job(create_event(Packet, Type))), + %% Pass through the input message packet + Packet; +on_store_mam_message(Packet, _LUser, _LServer, _Peer, _Type, _Dir) -> Packet. + +%% Convert the given +#job+ instance to its binary JSON representation and push +%% it to the Redis queue (list) in order to be picked up by Sidekiq as a +%% regular job. +-spec store(#job{}) -> ok. +store(#job{} = Job) -> + %% Encode the given job to JSON + Json = encode_job(Job), + %% Push the new job to the Redis list + case ejabberd_redis:q(["LPUSH", queue(), Json]) of + {ok, _} -> ok; + Errs -> ?ERROR_MSG("[M2S] Failed to add job to Redis list. (~s)", [Json]) + end, + ok. + +%% Encode the given +#job+ record to its binary JSON representation. +-spec encode_job(#job{}) -> binary(). +encode_job(#job{} = Job) -> + case catch jiffy:encode(?record_to_tuple(job, Job)) of + {'EXIT', Reason} -> + ?ERROR_MSG("[M2S] Failed to encode job to JSON:~n" + "** Content = ~p~n" + "** Err = ~p", + [Job, Reason]), + <<>>; + Encoded -> Encoded + end. + +%% Put all the job data together and create a new +#job+ record instance. This +%% data structure will be passed to the Redis backend where it is encoded to +%% JSON and put on the configured Sidekiq queue (list). +-spec create_job(binary()) -> #job{}. +create_job(Event) -> + %% Fetch the current UNIX time (epoch in seconds) + Now = get_current_unix_time(), + %% Assemble the job record which can be passed to our Redis behaviour + #job{class = gen_mod:get_module_opt(global, ?MODULE, sidekiq_class, <<"">>), + jid = get_job_id(), + args = [Event, <<"xmpp-mam">>], + created_at = Now, + enqueued_at = Now}. + +%% Assemble a wrapping XML event element with meta data. This serves as first +%% argument to the Sidekiq job and contains the actual MAM message and relevant +%% meta data. +-spec create_event(message(), chat | groupchat) -> binary(). +create_event(#message{} = Packet, Type) -> + %% Assemble the meta data XML blob for the Sidekiq job event/message wrapper + Meta = #xmlel{name = "meta", + attrs = [{"type", misc:atom_to_binary(Type)}, + {"id", get_stanza_id(Packet)}, + {"from", nickname_jid(Packet, from, Type)}, + {"to", nickname_jid(Packet, to, Type)}]}, + %% Build the wrapping event + fxml:element_to_binary(#xmlel{name = "event", + attrs = [{"xmlns", ?NS_MAM_SIDEKIQ}], + children = [Meta, xmpp:encode(Packet)]}). + +%% Fetch the JID with nickname resource for the given message packet. +-spec nickname_jid(message(), from | to, chat | groupchat) -> string(). +nickname_jid(#message{} = Packet, Dir, chat) -> + Jid = get_jid(Packet, Dir), + %% @TODO: Fetch the nickname from the Roster. (sql: rosterusers#jid -> #nick) + %% jid:replace_resource(Jid, Nick) + jid:encode(Jid); +nickname_jid(#message{} = Packet, Dir, groupchat) -> + Jid = get_jid(Packet, Dir), + %% @TODO: Fetch the nickname for the target MUC/(sender/receiver). + %% jid:replace_resource(Jid, Nick) + jid:encode(Jid); +nickname_jid(_Packet, _Dir, _Type) -> <<"unknown">>. + +%% Calculate the current UNIX time stamp. +-spec get_current_unix_time() -> integer(). +get_current_unix_time() -> + {MegaSecs, Secs, _MicroSecs} = erlang:timestamp(), + MegaSecs * 1000000 + Secs. + +%% Generate a new random job id for Sidekiq. +%% (12-byte random number as 24 char hex string) +-spec get_job_id() -> binary(). +get_job_id() -> + Chrs = list_to_tuple("abcdef0123456789"), + ChrsSize = size(Chrs), + F = fun(_, R) -> [element(rand:uniform(ChrsSize), Chrs) | R] end, + list_to_binary(lists:foldl(F, "", lists:seq(1, 24))). + +%% Access the to/from JID of a given message dynamically. +-spec get_jid(message(), from | to) -> jid(). +get_jid(#message{from = From} = _Packet, from) -> jid:remove_resource(From); +get_jid(#message{to = To} = _Packet, to) -> jid:remove_resource(To); +get_jid(_Packet, _Dir) -> ok. + +%% Extract the stanza id from a message packet and convert it to a string. +-spec get_stanza_id(stanza()) -> string(). +get_stanza_id(#message{meta = #{stanza_id := ID}}) -> + integer_to_list(ID). + +%% Assemble the Redis key for the list to write to, which will be picked up by +%% Sidekiq. +-spec queue() -> binary(). +queue() -> + Queue = gen_mod:get_module_opt(global, ?MODULE, sidekiq_queue, <<"">>), + <<"queue:", Queue/binary>>. + +%% Some ejabberd custom module API fullfilments +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. +depends(_Host, _Opts) -> [{mod_mam, hard}]. + +%% Parse or handle our configuration inputs +-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. +mod_opt_type(O) + when O == sidekiq_queue; O == sidekiq_class -> + fun iolist_to_binary/1; +mod_opt_type(_) -> + [ram_db_type, sidekiq_queue, sidekiq_class]. diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/tests/config.json b/tests/config.json new file mode 100644 index 0000000..e79d9cb --- /dev/null +++ b/tests/config.json @@ -0,0 +1,17 @@ +{ + "hostname": "jabber.local", + "jid": "admin@jabber.local", + "password": "defaultpw", + "transport": "websocket", + "wsURL": "ws://jabber.local/ws", + "room": "test@conference.jabber.local", + "users": ["bob", "alice"], + "match": "mam2sidekiq", + "redis": { + "host": "redis", + "port": 6379, + "db": 5 + }, + "skipUnrelated": true, + "debug": false +} diff --git a/tests/e2e/Gemfile b/tests/e2e/Gemfile new file mode 100644 index 0000000..f252eb8 --- /dev/null +++ b/tests/e2e/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# Simple, efficient background processing for Ruby. +gem 'sidekiq', '~> 5.1', '>= 5.1.1' +# Extends String class or add a ColorizedString with methods to set text color, +# background color and text effects. +gem 'colorize', '~> 0.8.1' diff --git a/tests/e2e/Gemfile.lock b/tests/e2e/Gemfile.lock new file mode 100644 index 0000000..a613332 --- /dev/null +++ b/tests/e2e/Gemfile.lock @@ -0,0 +1,24 @@ +GEM + remote: https://rubygems.org/ + specs: + colorize (0.8.1) + connection_pool (2.2.2) + rack (2.0.7) + rack-protection (2.0.5) + rack + redis (4.1.2) + sidekiq (5.2.7) + connection_pool (~> 2.2, >= 2.2.2) + rack (>= 1.5.0) + rack-protection (>= 1.5.0) + redis (>= 3.3.5, < 5) + +PLATFORMS + ruby + +DEPENDENCIES + colorize (~> 0.8.1) + sidekiq (~> 5.1, >= 5.1.1) + +BUNDLED WITH + 1.17.2 diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile new file mode 100644 index 0000000..471ebcb --- /dev/null +++ b/tests/e2e/Makefile @@ -0,0 +1,9 @@ +all: install start + +install: + @bundle install + +start: install + @bundle exec sidekiq \ + -C ./config/sidekiq.yml \ + -r ./config/app.rb || true diff --git a/tests/e2e/app/some_worker.rb b/tests/e2e/app/some_worker.rb new file mode 100644 index 0000000..a824dce --- /dev/null +++ b/tests/e2e/app/some_worker.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class SomeWorker + include Sidekiq::Worker + + def perform(message, source) + # Count the messages to stop the test + $messages += 1 + puts "\n> " + "Process a new XMPP message (##{$messages})\n".blue + + # When the given source is not as expected, rasise + raise ArgumentError, "Wrong source (#{source})" unless source == 'xmpp-mam' + + # Format the XML in a neat way + out = StringIO.new + doc = REXML::Document.new(message) + formatter = REXML::Formatters::Pretty.new + formatter.compact = true + formatter.write(doc, out) + out = out.string.lines.map { |line| ' ' * 2 + line }.join + puts "#{out}\n\n" + + # Stop the test when we received at least two jobs + if $messages >= 2 + sleep 2 + puts "> end-to-end test finished.\n\n".bold.green + + # We need kill our parent process here, because Sidekiq is not designed + # to be closed from within a job + pid = `ps -a`.lines.grep(/bundle/).first.strip.split(' ').first + system("kill -9 #{pid}") + sleep 1 + system("kill -9 #{pid}") + end + end +end diff --git a/tests/e2e/config/app.rb b/tests/e2e/config/app.rb new file mode 100755 index 0000000..e94dca6 --- /dev/null +++ b/tests/e2e/config/app.rb @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'rexml/document' +require 'colorized_string' + +$stdout.sync = true +$messages = 0 + +require 'bundler/setup' # Set up gems listed in the Gemfile. +Bundler.require(:default) +require_relative '../app/some_worker' + +url = 'redis://redis/5' + +Sidekiq.configure_server do |conf| + conf.redis = { url: url } +end + +Sidekiq.configure_client do |conf| + conf.redis = { url: url } +end diff --git a/tests/e2e/config/sidekiq.yml b/tests/e2e/config/sidekiq.yml new file mode 100644 index 0000000..fab5d4b --- /dev/null +++ b/tests/e2e/config/sidekiq.yml @@ -0,0 +1,3 @@ +:concurrency: 1 +:queues: + - default diff --git a/tests/index-e2e.js b/tests/index-e2e.js new file mode 100755 index 0000000..867bca2 --- /dev/null +++ b/tests/index-e2e.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +const async = require('async'); + +// Setup a new client and run the test suite +require('./src/client')(require('./config'), (client, utils) => { + + // Get the seeds and test cases available + const seeds = require('./src/seeds')(client); + const test = require('./src/testcases')(client, utils); + + // Run each test case, sequentially + async.waterfall([ + seeds, + + // Send a message to a MUC + test.message, + + // Send second message to a direct chat + test.directMessage('alice'), + ], utils.exit); +}); diff --git a/tests/index.js b/tests/index.js new file mode 100755 index 0000000..54b0152 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +const async = require('async'); + +// Setup a new client and run the test suite +require('./src/client')(require('./config'), (client, utils) => { + + // Get the seeds and test cases available + const seeds = require('./src/seeds')(client); + const test = require('./src/testcases')(client, utils); + + // Run each test case, sequentially + async.waterfall([ + seeds, + + // Initial state + test.jobs('default', { amount: 0 }), + + // After a message the initial state changes + test.message, + test.jobs('default', { amount: 1 }), + + // A second message increases the counter + test.message, + test.jobs('default', { amount: 2 }), + test.job('default', { class: 'SomeWorker' }), + test.job('default', { source: 'xmpp-mam' }), + test.job('default', { body: /type=['"]groupchat['"]/ }), + test.job('default', { body: /to=['"]test@conference[^'"]+['"]/ }), + test.job('default', { body: /from=['"]admin@[^'"]+['"]/ }), + test.job('default', { body: /id=['"]{{stanza-id}}['"]/ }), + test.job('default', { body: /.*{{message}}/ }), + + // After a direct message increases the counter + test.directMessage('bob'), + test.jobs('default', { amount: 3 }), + test.job('default', { class: 'SomeWorker' }), + test.job('default', { source: 'xmpp-mam' }), + test.job('default', { body: /type=['"]chat['"]/ }), + test.job('default', { body: /to=['"]bob@[^'"]+['"]/ }), + test.job('default', { body: /from=['"]admin@[^'"]+['"]/ }), + test.job('default', { body: /.*{{message}}/ }), + + // After a direct message increases the counter + test.directMessage('alice'), + test.jobs('default', { amount: 4 }), + test.job('default', { body: /alice@/ }), + ], utils.exit); +}); diff --git a/tests/lib/hljs-console.js b/tests/lib/hljs-console.js new file mode 100644 index 0000000..3677108 --- /dev/null +++ b/tests/lib/hljs-console.js @@ -0,0 +1,96 @@ +const hljs = require('highlight.js') +const h2j = require('html2json'); +const css2json = require('css2json'); +const chalk = require('chalk') +const fs = require('fs'); +const path = require('path'); + +const readStylesheet = function(name) { + var styleRaw = fs.readFileSync(path.join(__dirname, '..', 'node_modules', + 'highlight.js', 'styles', + name + '.css')); + return css2json(styleRaw.toString()); +}; + +const stylize = function(name, text, styleData) { + var currentStyle = styleData['.'+name]; + if (currentStyle !== undefined) { + // Handle foreground color + if (currentStyle.color !== undefined) { + if (currentStyle.color.startsWith('#')) { + if (currentStyle.color.length === 4) { + var expandColor = '#'; + var char = currentStyle.color.substring(1,2); + expandColor = expandColor + char + char; + char = currentStyle.color.substring(2,3); + expandColor = expandColor + char + char; + char = currentStyle.color.substring(3,4); + expandColor = expandColor + char + char; + text = chalk.hex(expandColor)(text); + } else { + text = chalk.hex(currentStyle.color)(text); + } + } else { + text = chalk.keyword(currentStyle.color)(text); + } + } + + // Handle bold/italics/underline + if (currentStyle["text-decoration"] !== undefined && + currentStyle["text-decoration"].toLowerCase() === "underline") { + text = chalk.underline(text); + } + + if (currentStyle["font-weight"] !== undefined && + currentStyle["font-weight"].toLowerCase() === "bold") { + text = chalk.bold(text); + } + + if (currentStyle["font-style"] !== undefined && + currentStyle["font-style"].toLowerCase() === "italics") { + text = chalk.italics(text); + } + } + return text; +}; + +const deentitize = function(str) { + str = str.replace(/>/g, '>'); + str = str.replace(/</g, '<'); + str = str.replace(/"/g, '"'); + str = str.replace(/'/g, "'"); + str = str.replace(/&/g, '&'); + return str; +}; + +const replaceSpan = function(obj, styleData) { + // If there are child objects, convert on each child first + if (obj.child) { + for (var i = 0; i < obj.child.length; i++) { + obj.child[i] = replaceSpan(obj.child[i], styleData); + } + } + + if (obj.node === "element") { + return stylize(obj.attr.class, obj.child.join(''), styleData); + } else if (obj.node === "text") { + return obj.text; + } else if (obj.node === "root") { + return obj.child.join(''); + } else { + console.error("Found a node type of " + obj.node + " that I can't handle!"); + } +}; + +const convertHLJS = function(hljsHTML, styleName) { + var styleData = readStylesheet(styleName); + var json = h2j.html2json(hljsHTML); + var text = replaceSpan(json, styleData); + text = stylize('hljs', text, styleData); + text = deentitize(text); + return text; +} + +exports.convert = function(hljsHTML, styleName) { + return convertHLJS(hljsHTML, styleName); +}; diff --git a/tests/lib/rooms.js b/tests/lib/rooms.js new file mode 100644 index 0000000..67e35cd --- /dev/null +++ b/tests/lib/rooms.js @@ -0,0 +1,69 @@ +const async = require('async'); +const colors = require('colors'); +const xmpp = require('stanza.io'); +const config = require('../config'); +const utils = require('./utils')(config); + +/** + * Create a brand new MUC. + * + * @param {String} name The name of the room + * @param {Object} client The XMPP client + * @param {Function} callback The function to call on finish + */ +var createRoom = function(name, client, callback) +{ + utils.log(`Create room ${name.magenta}`, false, 1); + client.joinRoom(name, 'admin'); + callback && callback(); +}; + +/** + * Invite the given list of users to the given room. + * + * @param {String} name The name of the user + * @param {Array} users The users to invite + * @param {Object} client The XMPP client + * @param {Function} callback The function to call on finish + */ +var inviteUsers = function(name, users, client, callback) +{ + async.each(users, function(user, callback) { + utils.log(`Make ${user.blue} member of ${name.magenta}`, false, 1); + client.setRoomAffiliation(name, user, 'member', null, function(err) { + callback && callback(err); + }); + }, function(err) { + // client.getRoomMembers(name, { + // items: [{ affiliation: 'member' }] + // }, function() { + // callback && callback(); + // utils.log(arguments[1].mucAdmin.items); + // }); + + callback && callback(err); + }); +}; + +/** + * Create all configured MUCs and invite all configured users to it. + * + * @param {String} room The name of the room + * @param {Array} users The users to invite + * @param {Object} client The XMPP client + * @param {Function} callback The function to call on finish + */ +module.exports = function(room, users, client, callback) +{ + async.waterfall([ + function(callback) { + createRoom(room, client, callback); + }, + + function(callback) { + inviteUsers(room, users, client, callback); + } + ], function(err) { + callback && callback(err); + }); +}; diff --git a/tests/lib/users.js b/tests/lib/users.js new file mode 100644 index 0000000..be6153d --- /dev/null +++ b/tests/lib/users.js @@ -0,0 +1,60 @@ +const request = require('request'); +const async = require('async'); +const config = require('../config'); +const utils = require('./utils')(config); + +/** + * Create a new ejabberd user account. + * + * @param {String} name The name of the user + * @param {Function} callback The function to call on finish + */ +var createUser = function(name, callback) +{ + request.post( + `http://${config.hostname}/admin/server/${config.hostname}/users/`, + { + auth: { + user: config.jid, + pass: config.password + }, + form: { + newusername: name, + newuserpassword: name, + addnewuser: 'add' + } + }, + function(err, res, body) { + if (!err && res.statusCode === 200) { + let jid = `${name}@${config.hostname}`; + utils.log(`Create user ${jid.blue}`, false, 1); + return callback && callback(null, { + user: name, + password: name, + jid: jid + }); + } + + utils.log(`User creation failed. (${name})`, false, 1); + utils.log(`Error: ${err.message}`, false, 1); + callback && callback(new Error()); + }); +}; + +/** + * Create all given users and pass them back as an array of + * user objects. + * + * @param {Array} users An array of user names + * @param {Function} callback The function to call on finish + */ +module.exports = function(users, callback) +{ + async.map(users, createUser, function(err, users) { + if (err) { + return callback && callback(err); + } + + callback && callback(null, users); + }); +}; diff --git a/tests/lib/utils.js b/tests/lib/utils.js new file mode 100644 index 0000000..5fd4a97 --- /dev/null +++ b/tests/lib/utils.js @@ -0,0 +1,189 @@ +const moment = require('moment'); +const colors = require('colors'); +const format = require('./xml-format'); +const startAt = new moment(); + +module.exports = (config) => { + // Presave some defaults + const origMatch = `${config.match}`; + var stanzas = {}; + var matchers = 0; + + // Setup some defaults + config.errors = []; + config.matchCallback = null; + + const utils = { + isRelevant: (xml, direction) => { + // if (direction == 'response') console.log(xml); + + // In case config says log all, everything is related + if (!config.skipUnrelated) { return true; } + + // Stop further stanzas when we already had this one before + if (stanzas[config.match]) { return false; } + + // Try multiple matches based on the given data type + match = false; + if (typeof config.match == 'string' && ~xml.indexOf(config.match)) { + match = true; + } + if (config.match instanceof RegExp && config.match.test(xml)) { + match = true; + } + + // We have a match, so we save it + if (match) { + // Except we are on the default matcher again + if (config.match !== origMatch) { + stanzas[config.match] = true; + } + + // In case we have a matching stanza, save it's id + utils.saveId(xml); + + // Run hooks if there are any + if (config.matchCallback) { + config.matchCallback(xml, direction); + } + + return true; + } + + return false; + }, + + saveId: (xml) => { + let match = xml.match(/ config, + + restoreMatcher: () => { + config.match = origMatch; + config.matchCallback = null; + matchers++; + }, + + setMatcher: (match, callback = null) => { + stanzas = {}; + config.match = match; + config.matchCallback = callback; + matchers++; + }, + + setMatcherFake: () => { + matchers++; + }, + + setMatcherCallback: (callback) => { + config.matchCallback = callback; + }, + + log: (str, multiple = true, level = 2) => { + if (!config.debug) { + multiple = false; + level = 1; + } + + let pre = Array(level + 1).join('#'); + + if (multiple === true) { + console.log(`${pre}\n${pre} ${str}\n${pre}`); + } else { + console.log(`${pre} ${str}`); + } + }, + + logError: (message, xml, meta = null) => { + config.errors.push({ + message: message, + xml: xml, + meta: meta + }); + utils.log('> ' + `${message} (#${config.errors.length})`.red); + }, + + errors: () => { + if (!config.errors.length) { return; } + + console.log('#'); + utils.log('Error details'.red); + config.errors.forEach((err, idx) => { + console.log('#'); + utils.log(`#${++idx} ${err.message}`.red); + if (err.xml) { + console.log('#'); + console.log(format(err.xml, '# ')); + console.log('#'); + } + if (err.meta) { + utils.log(` ${err.meta}`); + console.log('#'); + } + }); + }, + + stats: () => { + const endAt = new moment(); + const duration = moment.duration(endAt.diff(startAt)); + const seconds = new String(duration.as('seconds')); + const bad = config.errors.length; + var good = matchers - config.errors.length; + + if (good < 0) { good = 0; } + + utils.log([ + 'Statistics: ' + + `${matchers} test cases`.magenta, + `${good} successful`.green, + `${bad} failed`.red + ].join(', ')); + utils.log('Finished in ' + `${seconds}s`.green); + }, + + isoMinute: () => { + return moment().toISOString().split(':').slice(0, 2).join(':'); + }, + + isoHour: () => { + return moment().toISOString().split(':').slice(0, 1).join(':'); + }, + + exit: () => { + utils.errors(); + utils.stats(); + setTimeout(() => { + let code = (!config.errors.length) ? 0 : 1; + process.exit(code); + }, 200); + }, + + escapeXml: (xml) => { + let entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }; + + return String(xml).replace(/[&<>"'`=\/]/g, function (s) { + return entityMap[s]; + }); + } + }; + + return utils; +}; diff --git a/tests/lib/xml-format.js b/tests/lib/xml-format.js new file mode 100644 index 0000000..389672e --- /dev/null +++ b/tests/lib/xml-format.js @@ -0,0 +1,43 @@ +const format = require('xml-formatter'); +const hljs = require('highlight.js'); +const h2c = require('./hljs-console'); + +module.exports = (xml, indentation = '') => { + let formatted = format(xml, { indentation: ' ' }); + let level = 0; + + // Break all attributes on a new line + formatted = formatted.replace(/(=['"][^'"]+)(['"])/g, "$1$2\n"); + + // Fix the attribute indentation level + formatted = formatted.split("\n").map((line, idx) => { + // Tag line + if (/^\s*, />) + formatted = formatted.replace(/(['"])\s*>/g, '$1>'); + formatted = formatted.replace(/(['"])\s*\/>/g, '$1/>'); + formatted = formatted.replace(/\/>/g, ' />'); + + // Highlight the formatted XML + let highlighted = hljs.highlightAuto(formatted, ['xml']); + + // Good styles: androidstudio hybrid obsidian solarized-dark + highlighted = h2c.convert(highlighted.value, 'androidstudio'); + + return highlighted.split("\n").map((line, idx) => { + return indentation + line; + }).join("\n"); +}; diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 0000000..aae92a8 --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,1200 @@ +{ + "name": "test", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "alt-sasl-digest-md5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alt-sasl-digest-md5/-/alt-sasl-digest-md5-1.0.2.tgz", + "integrity": "sha1-JIEdahFj9I/xkLUOUsmASHzMgAo=", + "requires": { + "create-hash": "^1.1.0", + "randombytes": "^2.0.1" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "babel-runtime": { + "version": "5.8.38", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz", + "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=", + "requires": { + "core-js": "^1.0.0" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=", + "optional": true + }, + "bitwise-xor": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/bitwise-xor/-/bitwise-xor-0.0.0.tgz", + "integrity": "sha1-BAqBcrW7jMVisLcRnyMLKhp4Dj0=" + }, + "browserify-versionify": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/browserify-versionify/-/browserify-versionify-1.0.6.tgz", + "integrity": "sha1-qy3GHWoRnmJ77Eh1mNGYO3/bJ14=", + "requires": { + "find-root": "^0.1.1", + "through2": "0.6.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" + }, + "colors": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.1.tgz", + "integrity": "sha512-jg/vxRmv430jixZrC+La5kMbUWqIg32/JsYNZb94+JEmzceYbWKTsv1OuTp+7EaqiaWRR2tPcykibwCRgclIsw==" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "css2json": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css2json/-/css2json-0.0.4.tgz", + "integrity": "sha1-t5gpaBM3CFWZqdsQXVVxGBglQ/c=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "dom-walk": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", + "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz", + "integrity": "sha1-QlFPhAFdE1bK9Rh5ad+yvBvaCCM=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "filetransfer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/filetransfer/-/filetransfer-2.0.5.tgz", + "integrity": "sha1-iHyARv5UbsiugVwzAaXDE1+js10=", + "requires": { + "async": "^0.9.0", + "iana-hashes": "^1.0.0", + "wildemitter": "1.x" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + } + } + }, + "find-root": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-0.1.2.tgz", + "integrity": "sha1-mNImfP8ZFsyvJ0OzoO6oHXnX3NE=" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "global": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", + "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", + "requires": { + "min-document": "^2.19.0", + "process": "~0.5.1" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "highlight.js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", + "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" + }, + "hostmeta": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hostmeta/-/hostmeta-2.0.2.tgz", + "integrity": "sha512-VpTNg8KQ4ALgoGrMGBYqHLBFBrmZ5ANiQC1mLb/uroilTmuzjymChvDmDe3K/LRd1ZbszSkvKq65AW1VBykDJQ==", + "requires": { + "async": "^2.5.0", + "jxt": "^3.0.1", + "request": "^2.53.0", + "xhr": "^2.0.1" + } + }, + "html2json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html2json/-/html2json-1.0.2.tgz", + "integrity": "sha1-ydbSAvplQCOGwgKzRc9RvOgO0e8=" + }, + "http-parser-js": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iana-hashes": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/iana-hashes/-/iana-hashes-1.0.3.tgz", + "integrity": "sha1-vqpvIOpIPw8631/+b1lLtQySjKo=", + "requires": { + "create-hash": "^1.1.0", + "create-hmac": "^1.1.3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "intersect": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/intersect/-/intersect-0.1.0.tgz", + "integrity": "sha1-AaZRNFvVWnvlCuLw9yV/i6EULMs=" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + }, + "is-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", + "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jingle": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jingle/-/jingle-3.0.3.tgz", + "integrity": "sha512-/aLl0GuggF9E4GjDTmIS93/m+FXE9Ukdz2hKOyOYvE9BnfOfpyp+6QaLU0fBdQm6lbuvd7/Z32t3FNpjrKIeHg==", + "requires": { + "extend-object": "^1.0.0", + "intersect": "^0.1.0", + "jingle-filetransfer-session": "^2.0.0", + "jingle-media-session": "^2.0.0", + "jingle-session": "^2.0.0", + "wildemitter": "^1.0.1" + } + }, + "jingle-filetransfer-session": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jingle-filetransfer-session/-/jingle-filetransfer-session-2.0.2.tgz", + "integrity": "sha512-go+xcXj9pwXGhhSvqGrn9cB+FizW0ryif4OK8oAQzoxlH6jXR/Hczcb6h9pGU/rgojTrV0CiXRlncRKyoJWIJg==", + "requires": { + "extend-object": "^1.0.0", + "filetransfer": "^2.0.4", + "jingle-session": "^2.0.0", + "rtcpeerconnection": "^8.0.0" + } + }, + "jingle-media-session": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jingle-media-session/-/jingle-media-session-2.3.1.tgz", + "integrity": "sha512-5QnBSHamP33hWm5/sLCQd+7IWrN9Qsg1VevAwMo0uLBAX/OqGQXI7f21S/KhZ+GuB7M1Gw3EfSyWd12Q3LyEgA==", + "requires": { + "extend-object": "^1.0.0", + "jingle-session": "^2.0.0", + "rtcpeerconnection": "^8.3.1" + } + }, + "jingle-session": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jingle-session/-/jingle-session-2.0.3.tgz", + "integrity": "sha512-Nv4GTjI+mqVbaAKy0J03UUIAG/7dunOWvFAjQ83seyzu1Wfxn0iiQCZMCphWNa04SYWiVzQVqkeCxsA0OAylMw==", + "requires": { + "async": "^2.5.0", + "extend-object": "^1.0.0", + "uuid": "^3.1.0", + "wildemitter": "^1.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jxt": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jxt/-/jxt-3.1.0.tgz", + "integrity": "sha512-rKWfE6BflsT1pDQCbyyN2pSHmVxZPjPwibzVUwlI3n4iOHgALVc08wDDB3ZuJ/lolTKdDQvWNCaGz3lLW4yoog==", + "requires": { + "lodash.assign": "^3.0.0", + "ltx": "^2.2.0", + "uuid": "^3.0.0" + } + }, + "jxt-xmpp": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jxt-xmpp/-/jxt-xmpp-3.2.2.tgz", + "integrity": "sha512-cK1+0+xaXniTXO1lHB8J3Y7TFIXZNMO6f4yegq3IFkXlkIvcNTPt9n/S3xU+33XfT8+M3+Qxe/UsOiGaMbqm5Q==", + "requires": { + "babel-runtime": "^5.6.15", + "lodash.foreach": "^3.0.3", + "xmpp-constants": "^2.3.0", + "xmpp-jid": "^1.1.1" + } + }, + "jxt-xmpp-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jxt-xmpp-types/-/jxt-xmpp-types-3.0.0.tgz", + "integrity": "sha1-wBW6458Mlry9T0yp64GXdDr+bgg=", + "requires": { + "jxt": "^3.0.1", + "xmpp-constants": "^2.0.0", + "xmpp-jid": "^1.0.2" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=" + }, + "lodash._arrayfilter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayfilter/-/lodash._arrayfilter-3.0.0.tgz", + "integrity": "sha1-LevhHuxp5dzG9LhhNxKKSPFSQjc=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecallback": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz", + "integrity": "sha1-t7K7Q9whYEJKIczybFfkQ3cqjic=", + "requires": { + "lodash._baseisequal": "^3.0.0", + "lodash._bindcallback": "^3.0.0", + "lodash.isarray": "^3.0.0", + "lodash.pairs": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" + }, + "lodash._baseeach": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", + "integrity": "sha1-z4cGVyyhROjZ11InyZDamC+TKvM=", + "requires": { + "lodash.keys": "^3.0.0" + } + }, + "lodash._basefilter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefilter/-/lodash._basefilter-3.0.0.tgz", + "integrity": "sha1-S3ZAPfDihtA9Xg9yle00QeEB0SE=", + "requires": { + "lodash._baseeach": "^3.0.0" + } + }, + "lodash._baseindexof": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz", + "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=" + }, + "lodash._baseisequal": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", + "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", + "requires": { + "lodash.isarray": "^3.0.0", + "lodash.istypedarray": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._baseuniq": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-3.0.3.tgz", + "integrity": "sha1-ISP6DbLWnCjVvrHB821hUip0AjQ=", + "requires": { + "lodash._baseindexof": "^3.0.0", + "lodash._cacheindexof": "^3.0.0", + "lodash._createcache": "^3.0.0" + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + }, + "lodash._cacheindexof": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz", + "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=" + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "requires": { + "lodash._bindcallback": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash.restparam": "^3.0.0" + } + }, + "lodash._createcache": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash._createcache/-/lodash._createcache-3.1.2.tgz", + "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", + "requires": { + "lodash._getnative": "^3.0.0" + } + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" + }, + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._createassigner": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "lodash.filter": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-3.1.1.tgz", + "integrity": "sha1-m7HUUzctnPkpEnw1Tfcj9Hri3D8=", + "requires": { + "lodash._arrayfilter": "^3.0.0", + "lodash._basecallback": "^3.0.0", + "lodash._basefilter": "^3.0.0", + "lodash.isarray": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash.foreach": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-3.0.3.tgz", + "integrity": "sha1-b9fvt5aRrs1n/erCdhyY5wHWw5o=", + "requires": { + "lodash._arrayeach": "^3.0.0", + "lodash._baseeach": "^3.0.0", + "lodash._bindcallback": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "lodash.istypedarray": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", + "integrity": "sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=" + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.pairs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.pairs/-/lodash.pairs-3.0.1.tgz", + "integrity": "sha1-u+CNV4bu6qCaFckevw3LfSvjJqk=", + "requires": { + "lodash.keys": "^3.0.0" + } + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + }, + "lodash.uniq": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-3.2.2.tgz", + "integrity": "sha1-FGw28l510ZUBukAuiLoUk39jzYs=", + "requires": { + "lodash._basecallback": "^3.0.0", + "lodash._baseuniq": "^3.0.0", + "lodash._getnative": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "ltx": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/ltx/-/ltx-2.7.1.tgz", + "integrity": "sha1-Dly9y1vxeM+ngx6kHcMj2XQiMVo=", + "requires": { + "inherits": "^2.0.1" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "requires": { + "mime-db": "~1.35.0" + } + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "requires": { + "dom-walk": "^0.1.0" + } + }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + }, + "nan": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz", + "integrity": "sha1-gioNwmYpDOTNOhIoLKPn42Rmigg=", + "optional": true + }, + "node-stringprep": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/node-stringprep/-/node-stringprep-0.8.0.tgz", + "integrity": "sha1-5KOeSOpEhuxFS8CNylHdGqRoZBc=", + "optional": true, + "requires": { + "bindings": "~1.2.1", + "debug": "~2.2.0", + "nan": "~2.3.3" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "parse-headers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.1.tgz", + "integrity": "sha1-aug6eqJanZtwCswoaYzR8e1+lTY=", + "requires": { + "for-each": "^0.3.2", + "trim": "0.0.1" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.6.0" + } + }, + "redis-commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", + "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rtcpeerconnection": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/rtcpeerconnection/-/rtcpeerconnection-8.4.0.tgz", + "integrity": "sha512-HgntXWv+7DRufcGUroOSSNTpAIFRwLWCiLyutwfyVfrmPI7E5n7xP4JlwFWC6XNJV/LBILNru9bYa/FWcvyUuA==", + "requires": { + "lodash.clonedeep": "^4.3.2", + "sdp-jingle-json": "^3.0.0", + "wildemitter": "1.x" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sasl-anonymous": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sasl-anonymous/-/sasl-anonymous-0.1.0.tgz", + "integrity": "sha1-9UTH6CTfKkDZrUczgpVyzI2e1aU=" + }, + "sasl-external": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sasl-external/-/sasl-external-0.1.0.tgz", + "integrity": "sha1-n2vL6ccZKxyFzkhMu8QJlAkYbGI=" + }, + "sasl-plain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sasl-plain/-/sasl-plain-0.1.0.tgz", + "integrity": "sha1-zxRefAIiK2TWDAgG2c0q5TgEJsw=" + }, + "sasl-scram-sha-1": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sasl-scram-sha-1/-/sasl-scram-sha-1-1.2.1.tgz", + "integrity": "sha1-2I1R/qoP8yDY6x1vx1ZXZT+dzUs=", + "requires": { + "bitwise-xor": "0.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.3", + "randombytes": "^2.0.1" + } + }, + "sasl-x-oauth2": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sasl-x-oauth2/-/sasl-x-oauth2-0.1.0.tgz", + "integrity": "sha1-NcREC8JG9bUuW+RXTOP29P2CKpk=" + }, + "saslmechanisms": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/saslmechanisms/-/saslmechanisms-0.1.1.tgz", + "integrity": "sha1-R4vhQpUA/PqngL6IszQ87X0qkYI=" + }, + "sdp-jingle-json": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sdp-jingle-json/-/sdp-jingle-json-3.0.3.tgz", + "integrity": "sha512-MoRCqjk8bUVHhNm86yiCfOmOCMnXCrbAyhrPCv9qUkK6Ye0NpxU7S6la8791+Rta/IQxf/+801rjmv7IWzl2zQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stanza.io": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/stanza.io/-/stanza.io-9.1.0.tgz", + "integrity": "sha512-+iSiG4x+qeMkXmSQ1PeX+aHfJc0L+VNBKe9BQW5x+ugNPZm5Eu2P6vlz5kRNKBvIvA14JGnWKyFmPqvmrba3ig==", + "requires": { + "alt-sasl-digest-md5": "^1.0.0", + "async": "^2.5.0", + "browserify-versionify": "^1.0.4", + "faye-websocket": "^0.11.0", + "hostmeta": "^2.0.0", + "iana-hashes": "^1.0.2", + "jingle": "^3.0.0", + "jxt": "^3.1.0", + "jxt-xmpp": "^3.1.0", + "jxt-xmpp-types": "^3.0.0", + "lodash.assign": "^3.0.0", + "lodash.filter": "^3.1.0", + "lodash.foreach": "^3.0.2", + "lodash.isarray": "^3.0.1", + "lodash.uniq": "^3.1.0", + "request": "^2.53.0", + "sasl-anonymous": "^0.1.0", + "sasl-external": "^0.1.0", + "sasl-plain": "^0.1.0", + "sasl-scram-sha-1": "^1.1.0", + "sasl-x-oauth2": "^0.1.0", + "saslmechanisms": "^0.1.1", + "uuid": "^3.0.1", + "wildemitter": "^1.0.1", + "xhr": "^2.0.1", + "xmpp-jid": "^1.0.0" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "through2": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.3.tgz", + "integrity": "sha1-eVKS/enyVMKjaLOPnMXRvUZjr7Y=", + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vkbeautify": { + "version": "0.99.3", + "resolved": "https://registry.npmjs.org/vkbeautify/-/vkbeautify-0.99.3.tgz", + "integrity": "sha512-2ozZEFfmVvQcHWoHLNuiKlUfDKlhh4KGsy54U0UrlLMR1SO+XKAIDqBxtBwHgNrekurlJwE8A9K6L49T78ZQ9Q==" + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "requires": { + "http-parser-js": ">=0.4.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" + }, + "wildemitter": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.0.tgz", + "integrity": "sha1-Kd06ctaZw+J53QIcPNIVC4LJohE=" + }, + "xhr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", + "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", + "requires": { + "global": "~4.3.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "xml-formatter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-1.0.1.tgz", + "integrity": "sha1-OAgz3dC86iwJht7+u6cfhDhPNT0=", + "requires": { + "xml-parser-xo": "^2.1.1" + } + }, + "xml-parser-xo": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-2.1.3.tgz", + "integrity": "sha1-TqjrhW36TddcSrVLJRVY3grcHGE=", + "requires": { + "debug": "^2.2.0" + } + }, + "xmpp-constants": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/xmpp-constants/-/xmpp-constants-2.4.0.tgz", + "integrity": "sha1-uP5bgAWrBdtnNlhtfFpllq96yOE=" + }, + "xmpp-jid": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/xmpp-jid/-/xmpp-jid-1.2.3.tgz", + "integrity": "sha1-pgo+mr+p4CeRAgFdZJQWgc+z8yA=", + "requires": { + "node-stringprep": "^0.8.0", + "punycode": "^1.3.0" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..5206951 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,26 @@ +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "async": "^2.6.1", + "chalk": "^2.4.1", + "colors": "^1.3.1", + "css2json": "0.0.4", + "faker": "^4.1.0", + "highlight.js": "^9.12.0", + "html2json": "^1.0.2", + "moment": "^2.22.2", + "redis": "^2.8.0", + "request": "^2.87.0", + "stanza.io": "^9.1.0", + "vkbeautify": "^0.99.3", + "xml-formatter": "^1.0.1" + } +} diff --git a/tests/src/client.js b/tests/src/client.js new file mode 100644 index 0000000..0017fc4 --- /dev/null +++ b/tests/src/client.js @@ -0,0 +1,26 @@ +const xmpp = require('stanza.io'); +const colors = require('colors'); + +const installMiddleware = require('./stanza-middleware'); +const setupUtils = require('../lib/utils'); + +module.exports = (config, callback) => { + // Setup a new XMPP client + const client = xmpp.createClient(config); + const utils = setupUtils(client.config); + + // Install all the nifty handlers to that thing + installMiddleware(client); + + // Call the user given function when we have a connection + client.on('session:started', () => { + client.sendPresence(); + client.enableCarbons(); + setTimeout(() => { + callback && callback(client, utils); + }, 500); + }); + + // Connect the new client + client.connect(); +}; diff --git a/tests/src/seeds.js b/tests/src/seeds.js new file mode 100644 index 0000000..d650f8a --- /dev/null +++ b/tests/src/seeds.js @@ -0,0 +1,17 @@ +const async = require('async'); +const createUsers = require('../lib/users'); +const createRoom = require('../lib/rooms'); + +module.exports = (client) => { + return (callback) => { + async.waterfall([ + function(callback) { createUsers(client.config.users, callback); }, + function(users, callback) { + createRoom(client.config.room, + users.map((user) => user.jid), + client, + callback); + } + ], () => callback()); + }; +}; diff --git a/tests/src/stanza-middleware.js b/tests/src/stanza-middleware.js new file mode 100644 index 0000000..85fbc73 --- /dev/null +++ b/tests/src/stanza-middleware.js @@ -0,0 +1,28 @@ +const colors = require('colors'); +const format = require('../lib/xml-format'); + +module.exports = (client) => { + const utils = require('../lib/utils')(client.config); + + client.on('raw:outgoing', (xml) => { + // In case we ignore non-relevant stanzas + if (!utils.isRelevant(xml, 'request')) { return; } + + // Format and log the stanza + if (client.config.debug) { + console.log("###\n### Request\n###".magenta); + console.log(format(xml, '>>> '.magenta)); + } + }); + + client.on('raw:incoming', (xml) => { + // In case we ignore non-relevant stanzas + if (!utils.isRelevant(xml, 'response')) { return; } + + // Format and log the stanza + if (client.config.debug) { + console.log("###\n### Response\n###".blue); + console.log(format(xml, '<<< '.blue)); + } + }); +}; diff --git a/tests/src/stanza-validator.js b/tests/src/stanza-validator.js new file mode 100644 index 0000000..484bd55 --- /dev/null +++ b/tests/src/stanza-validator.js @@ -0,0 +1,82 @@ +module.exports = (utils) => { + const json = (obj) => JSON.stringify(obj); + + const match = (xml, regex, message) => { + if (regex.constructor !== RegExp) { + regex = new RegExp(regex); + } + + if (!regex.test(xml)) { + utils.logError(message, xml, `Match failed for ${regex}`); + } + }; + + const matchMissing = (xml, regex, message) => { + if (regex.constructor !== RegExp) { + regex = new RegExp(regex); + } + + if (regex.test(xml)) { + utils.logError(message, xml, `Match for ${regex}`); + } + }; + + return { + jobsEqual: (actual, expected) => { + if (actual != expected) { + utils.logError( + `${json(actual)} jobs found, expected to find ${json(expected)}` + ); + } + }, + jobsAtLeast: (actual, expected) => { + if (actual < expected) { + utils.logError( + `${json(actual)} jobs found, ` + + `expected to find at least ${json(expected)}` + ); + } + }, + jobClass: (actual, expected) => { + if (actual != expected) { + utils.logError( + `Job class is ${json(actual)}, should be ${json(expected)}` + ); + } + }, + sourceEqual: (actual, expected) => { + if (actual != expected) { + utils.logError( + `Source argument is ${json(actual)}, should be ${json(expected)}` + ); + } + }, + bodyContains: (actual, expected) => { + var good = false; + + if (expected.constructor === RegExp && expected.test(actual)) { + good = true; + } else if (actual == expected) { + good = true; + } + + if (!good) { + utils.logError(`Body match failed for ${expected}`, actual); + } + }, + contains: match, + missing: matchMissing, + message: (room) => { + return (xml, direction) => { + if (direction !== 'response') { return; } + const contains = (regex, message) => match(xml, regex, message); + const missing = (regex, message) => matchMissing(xml, regex, message); + + contains( + `.*`, + `Message response for ${room} failed.` + ); + }; + } + }; +}; diff --git a/tests/src/testcases.js b/tests/src/testcases.js new file mode 100644 index 0000000..969f367 --- /dev/null +++ b/tests/src/testcases.js @@ -0,0 +1,149 @@ +const faker = require('faker'); +const setupClient = require('./client'); + +module.exports = (client, utils) => { + // Setup a reference shortcut + var config = client.config; + + // Default message matcher + const msgMatcher = (msg) => new RegExp(` { + // Wait for the XMPP client + setTimeout(() => { + client.joinRoom(config.room, 'admin'); + + setTimeout(() => { + utils.log('Send a new message to ' + config.room.blue); + let message = faker.hacker.phrase(); + let callbacked = false; + let safeMessage = utils.escapeXml(message); + + utils.config().lastMessage = safeMessage; + utils.setMatcher(msgMatcher(safeMessage), (xml, direction) => { + if (callbacked) { return; } + callbacked = true; + + // Check the input message + validator.message(config.room)(xml, direction); + // Continue + setTimeout(() => callback(), 200); + }); + + setTimeout(() => { + client.sendMessage({ + to: config.room, + body: message, + type: 'groupchat' + }); + }, 200); + }, 200); + }, 200); + }, + + // Send a direct chat message + directMessage: (to) => { + return (callback) => { + to = `${to}@${config.hostname}`; + utils.log('Send a new message to ' + to.blue); + + setTimeout(() => { + let message = faker.hacker.phrase(); + utils.config().lastMessage = utils.escapeXml(message); + client.sendMessage({ + to: to, + body: message + }); + setTimeout(callback, messageTimeout); + }, 200); + }; + }, + + // Ask for all jobs on the queue (count) + jobs: (queue, expected) => { + return (callback) => { + utils.setMatcherFake(); + utils.log('Ask for the number of jobs on the ' + + queue.blue + ' '.reset + 'queue'); + utils.log(' Check for ' + expected.amount.toString().blue + ' jobs'); + redis.llen(`queue:${queue}`, function(err, length) { + if (err) { return callback(err); } + + validator.jobsEqual(length, expected.amount); + callback(); + }); + }; + }, + + // Ask for the last Sidekiq job on the queue + job: (queue, expected) => { + return (callback) => { + utils.setMatcherFake(); + utils.log('Ask for the last job on the ' + + queue.blue + ' '.reset + 'queue'); + key = Object.keys(expected)[0]; + utils.log(' Check ' + key.blue + ' for ' + + expected[key].toString().blue); + + redis.lrange(`queue:${queue}`, 0, 0, function(err, jobs) { + if (err) { return callback(err); } + + validator.jobsAtLeast(jobs.length, 1); + let job = jobs[0]; + + // No job, nothing to check against + if (!job) { return callback(); } + + // Decode the job data + job = JSON.parse(job); + + if (expected.class) { + validator.jobsEqual(job.class, expected.class); + } + + if (expected.source) { + validator.sourceEqual(job.args[1], expected.source); + } + + if (expected.body) { + body = expected.body.toString().replace(/^\/|\/$/g, ''); + + if (~body.indexOf('{{message}}')) { + body = body.replace('{{message}}', utils.config().lastMessage); + expected.body = new RegExp(body); + } + + if (~body.indexOf('{{stanza-id}}')) { + body = body.replace('{{stanza-id}}', utils.config().lastStanzaId); + expected.body = new RegExp(body); + } + + validator.bodyContains(job.args[0], expected.body); + } + + // console.log(utils.config().lastMessage); + // console.log(utils.config().lastStanzaId); + // console.log(job); + + callback(); + }); + }; + } + }; +};