summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgabrielgatu <gabriel.dny@gmail.com>2016-09-08 11:34:42 +0200
committerMickael Remond <mremond@process-one.net>2016-09-08 11:37:14 +0200
commit803270fc6b8ed3ba718f7e231b149caef70aa1ae (patch)
treecc4508758cbcec7a74568834888f3208d876a953
parentSupport for publishing to hex.pm with latest Elixir mix (diff)
Support for Elixir configuration file #1208
Contribution for Google Summer of code 2016 by Gabriel Gatu
Diffstat (limited to '')
-rw-r--r--config/config.exs2
-rw-r--r--config/ejabberd.exs169
-rw-r--r--config/ejabberd.yml667
-rw-r--r--lib/ejabberd/config/attr.ex119
-rw-r--r--lib/ejabberd/config/config.ex145
-rw-r--r--lib/ejabberd/config/ejabberd_hook.ex23
-rw-r--r--lib/ejabberd/config/ejabberd_module.ex70
-rw-r--r--lib/ejabberd/config/logger/ejabberd_logger.ex32
-rw-r--r--lib/ejabberd/config/opts_formatter.ex46
-rw-r--r--lib/ejabberd/config/store.ex55
-rw-r--r--lib/ejabberd/config/validator/validation.ex40
-rw-r--r--lib/ejabberd/config/validator/validator_attrs.ex28
-rw-r--r--lib/ejabberd/config/validator/validator_dependencies.ex30
-rw-r--r--lib/ejabberd/config/validator/validator_utility.ex30
-rw-r--r--lib/ejabberd/config_util.ex18
-rw-r--r--lib/ejabberd/module.ex19
-rw-r--r--lib/mix/tasks/deps.tree.ex94
-rw-r--r--lib/mod_presence_demo.ex12
-rw-r--r--src/ejabberd_app.erl14
-rw-r--r--src/ejabberd_config.erl13
-rw-r--r--src/ejabberd_http.erl7
-rw-r--r--test/elixir-config/attr_test.exs87
-rw-r--r--test/elixir-config/config_test.exs65
-rw-r--r--test/elixir-config/ejabberd_logger.exs49
-rw-r--r--test/elixir-config/shared/ejabberd.exs31
-rw-r--r--test/elixir-config/shared/ejabberd_different_from_default.exs9
-rw-r--r--test/elixir-config/shared/ejabberd_for_validation.exs20
-rw-r--r--test/elixir-config/validation_test.exs32
28 files changed, 1914 insertions, 12 deletions
diff --git a/config/config.exs b/config/config.exs
index 4d378348..0d1a3c72 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -4,7 +4,7 @@ use Mix.Config
config :ejabberd,
file: "config/ejabberd.yml",
log_path: 'log/ejabberd.log'
-
+
# Customize Mnesia directory:
config :mnesia,
dir: 'mnesiadb/'
diff --git a/config/ejabberd.exs b/config/ejabberd.exs
new file mode 100644
index 00000000..05c2b5d8
--- /dev/null
+++ b/config/ejabberd.exs
@@ -0,0 +1,169 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ log_rotate_size: 10485760,
+ log_rotate_date: "",
+ log_rotate_count: 1,
+ log_rate_limit: 100,
+ auth_method: :internal,
+ max_fsm_queue: 1000,
+ language: "en",
+ allow_contrib_modules: true,
+ hosts: ["localhost"],
+ shaper: shaper,
+ acl: acl,
+ access: access]
+ end
+
+ defp shaper do
+ [normal: 1000,
+ fast: 50000,
+ max_fsm_queue: 1000]
+ end
+
+ defp acl do
+ [local:
+ [user_regexp: "", loopback: [ip: "127.0.0.0/8"]]]
+ end
+
+ defp access do
+ [max_user_sessions: [all: 10],
+ max_user_offline_messages: [admin: 5000, all: 100],
+ local: [local: :allow],
+ c2s: [blocked: :deny, all: :allow],
+ c2s_shaper: [admin: :none, all: :normal],
+ s2s_shaper: [all: :fast],
+ announce: [admin: :allow],
+ configure: [admin: :allow],
+ muc_admin: [admin: :allow],
+ muc_create: [local: :allow],
+ muc: [all: :allow],
+ pubsub_createnode: [local: :allow],
+ register: [all: :allow],
+ trusted_network: [loopback: :allow]]
+ end
+
+ listen :ejabberd_c2s do
+ @opts [
+ port: 5222,
+ max_stanza_size: 65536,
+ shaper: :c2s_shaper,
+ access: :c2s]
+ end
+
+ listen :ejabberd_s2s_in do
+ @opts [port: 5269]
+ end
+
+ listen :ejabberd_http do
+ @opts [
+ port: 5280,
+ web_admin: true,
+ http_poll: true,
+ http_bind: true,
+ captcha: true]
+ end
+
+ module :mod_adhoc do
+ end
+
+ module :mod_announce do
+ @opts [access: :announce]
+ end
+
+ module :mod_blocking do
+ end
+
+ module :mod_caps do
+ end
+
+ module :mod_carboncopy do
+ end
+
+ module :mod_client_state do
+ @opts [
+ drop_chat_states: true,
+ queue_presence: false]
+ end
+
+ module :mod_configure do
+ end
+
+ module :mod_disco do
+ end
+
+ module :mod_irc do
+ end
+
+ module :mod_http_bind do
+ end
+
+ module :mod_last do
+ end
+
+ module :mod_muc do
+ @opts [
+ access: :muc,
+ access_create: :muc_create,
+ access_persistent: :muc_create,
+ access_admin: :muc_admin]
+ end
+
+ module :mod_offline do
+ @opts [access_max_user_messages: :max_user_offline_messages]
+ end
+
+ module :mod_ping do
+ end
+
+ module :mod_privacy do
+ end
+
+ module :mod_private do
+ end
+
+ module :mod_pubsub do
+ @opts [
+ access_createnode: :pubsub_createnode,
+ ignore_pep_from_offline: true,
+ last_item_cache: true,
+ plugins: ["flat", "hometree", "pep"]]
+ end
+
+ module :mod_register do
+ @opts [welcome_message: [
+ subject: "Welcome!",
+ body: "Hi.\nWelcome to this XMPP Server",
+ ip_access: :trusted_network,
+ access: :register]]
+ end
+
+ module :mod_roster do
+ end
+
+ module :mod_shared_roster do
+ end
+
+ module :mod_stats do
+ end
+
+ module :mod_time do
+ end
+
+ module :mod_version do
+ end
+
+ # Example of how to define a hook, called when the event
+ # specified is triggered.
+ #
+ # @event: Name of the event
+ # @opts: Params are optional. Available: :host and :priority.
+ # If missing, defaults are used. (host: :global | priority: 50)
+ # @callback Could be an anonymous function or a callback from a module,
+ # use the &ModuleName.function/arity format for that.
+ hook :register_user, [host: "localhost"], fn(user, server) ->
+ info("User registered: #{user} on #{server}")
+ end
+end
diff --git a/config/ejabberd.yml b/config/ejabberd.yml
new file mode 100644
index 00000000..80fc3c62
--- /dev/null
+++ b/config/ejabberd.yml
@@ -0,0 +1,667 @@
+###
+### 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: 4
+
+##
+## rotation: Describe how to rotate logs. Either size and/or date can trigger
+## log rotation. Setting count to N keeps N rotated logs. Setting count to 0
+## does not disable rotation, it instead rotates the file and keeps no previous
+## versions around. Setting size to X rotate log when it reaches X bytes.
+## To disable rotation set the size to 0 and the date to ""
+## Date syntax is taken from the syntax newsyslog uses in newsyslog.conf.
+## Some examples:
+## $D0 rotate every night at midnight
+## $D23 rotate every day at 23:00 hr
+## $W0D23 rotate every week on Sunday at 23:00 hr
+## $W5D16 rotate every week on Friday at 16:00 hr
+## $M1D0 rotate on the first day of every month at midnight
+## $M5D6 rotate on every 5th day of the month at 6:00 hr
+##
+log_rotate_size: 10485760
+log_rotate_date: ""
+log_rotate_count: 1
+
+##
+## overload protection: If you want to limit the number of messages per second
+## allowed from error_logger, which is a good idea if you want to avoid a flood
+## of messages when system is overloaded, you can set a limit.
+## 100 is ejabberd's default.
+log_rate_limit: 100
+
+##
+## watchdog_admins: Only useful for developers: if an ejabberd process
+## consumes a lot of memory, send live notifications to these XMPP
+## accounts.
+##
+## watchdog_admins:
+## - "bob@example.com"
+
+
+### ================
+### SERVED HOSTNAMES
+
+##
+## hosts: Domains served by ejabberd.
+## You can define one or several, for example:
+## hosts:
+## - "example.net"
+## - "example.com"
+## - "example.org"
+##
+hosts:
+ - "localhost"
+
+##
+## 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
+
+##
+## listen: The ports ejabberd will listen on, which service each is handled
+## by and what options to start it with.
+##
+listen:
+ -
+ port: 5222
+ module: ejabberd_c2s
+ ##
+ ## If TLS is compiled in and you installed a SSL
+ ## certificate, specify the full path to the
+ ## file and uncomment these lines:
+ ##
+ ## certfile: "/path/to/ssl.pem"
+ ## starttls: true
+ ##
+ ## To enforce TLS encryption for client connections,
+ ## use this instead of the "starttls" option:
+ ##
+ ## starttls_required: true
+ ##
+ ## Custom OpenSSL options
+ ##
+ ## protocol_options:
+ ## - "no_sslv3"
+ ## - "no_tlsv1"
+ max_stanza_size: 65536
+ shaper: c2s_shaper
+ access: c2s
+ -
+ port: 5269
+ module: ejabberd_s2s_in
+ ##
+ ## ejabberd_service: Interact with external components (transports, ...)
+ ##
+ ## -
+ ## port: 8888
+ ## module: ejabberd_service
+ ## access: all
+ ## shaper_rule: fast
+ ## ip: "127.0.0.1"
+ ## hosts:
+ ## "icq.example.org":
+ ## password: "secret"
+ ## "sms.example.org":
+ ## password: "secret"
+
+ ##
+ ## ejabberd_stun: Handles STUN Binding requests
+ ##
+ ## -
+ ## port: 3478
+ ## transport: udp
+ ## module: ejabberd_stun
+
+ ##
+ ## To handle XML-RPC requests that provide admin credentials:
+ ##
+ ## -
+ ## port: 4560
+ ## module: ejabberd_xmlrpc
+ -
+ port: 5280
+ module: ejabberd_http
+ ## request_handlers:
+ ## "/pub/archive": mod_http_fileserver
+ web_admin: true
+ http_poll: true
+ http_bind: true
+ ## register: true
+ captcha: true
+
+##
+## s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections.
+## Allowed values are: false optional required required_trusted
+## You must specify a certificate file.
+##
+## s2s_use_starttls: optional
+
+##
+## s2s_certfile: Specify a certificate file.
+##
+## s2s_certfile: "/path/to/ssl.pem"
+
+## Custom OpenSSL options
+##
+## s2s_protocol_options:
+## - "no_sslv3"
+## - "no_tlsv1"
+
+##
+## domain_certfile: Specify a different certificate for each served hostname.
+##
+## host_config:
+## "example.org":
+## domain_certfile: "/path/to/example_org.pem"
+## "example.com":
+## domain_certfile: "/path/to/example_com.pem"
+
+##
+## S2S whitelist or blacklist
+##
+## Default s2s policy for undefined hosts.
+##
+## s2s_access: s2s
+
+##
+## Outgoing S2S options
+##
+## Preferred address families (which to try first) and connect timeout
+## in milliseconds.
+##
+## outgoing_s2s_families:
+## - ipv4
+## - ipv6
+## outgoing_s2s_timeout: 10000
+
+### ==============
+### AUTHENTICATION
+
+##
+## auth_method: Method used to authenticate the users.
+## The default method is the internal.
+## If you want to use a different method,
+## comment this line and enable the correct ones.
+##
+auth_method: internal
+
+##
+## Store the plain passwords or hashed for SCRAM:
+## auth_password_format: plain
+## auth_password_format: scram
+##
+## Define the FQDN if ejabberd doesn't detect it:
+## fqdn: "server3.example.com"
+
+##
+## Authentication using external script
+## Make sure the script is executable by ejabberd.
+##
+## auth_method: external
+## extauth_program: "/path/to/authentication/script"
+
+##
+## Authentication using ODBC
+## Remember to setup a database in the next section.
+##
+## auth_method: odbc
+
+##
+## Authentication using PAM
+##
+## auth_method: pam
+## pam_service: "pamservicename"
+
+##
+## Authentication using LDAP
+##
+## auth_method: ldap
+##
+## List of LDAP servers:
+## ldap_servers:
+## - "localhost"
+##
+## Encryption of connection to LDAP servers:
+## ldap_encrypt: none
+## ldap_encrypt: tls
+##
+## Port to connect to on LDAP servers:
+## ldap_port: 389
+## ldap_port: 636
+##
+## LDAP manager:
+## ldap_rootdn: "dc=example,dc=com"
+##
+## Password of LDAP manager:
+## ldap_password: "******"
+##
+## Search base of LDAP directory:
+## ldap_base: "dc=example,dc=com"
+##
+## LDAP attribute that holds user ID:
+## ldap_uids:
+## - "mail": "%u@mail.example.org"
+##
+## LDAP filter:
+## ldap_filter: "(objectClass=shadowAccount)"
+
+##
+## Anonymous login support:
+## auth_method: anonymous
+## anonymous_protocol: sasl_anon | login_anon | both
+## allow_multiple_connections: true | false
+##
+## host_config:
+## "public.example.org":
+## auth_method: anonymous
+## allow_multiple_connections: false
+## anonymous_protocol: sasl_anon
+##
+## To use both anonymous and internal authentication:
+##
+## host_config:
+## "public.example.org":
+## auth_method:
+## - internal
+## - anonymous
+
+### ==============
+### DATABASE SETUP
+
+## ejabberd by default uses the internal Mnesia database,
+## so you do not necessarily need this section.
+## This section provides configuration examples in case
+## you want to use other database backends.
+## Please consult the ejabberd Guide for details on database creation.
+
+##
+## MySQL server:
+##
+## odbc_type: mysql
+## odbc_server: "server"
+## odbc_database: "database"
+## odbc_username: "username"
+## odbc_password: "password"
+##
+## If you want to specify the port:
+## odbc_port: 1234
+
+##
+## PostgreSQL server:
+##
+## odbc_type: pgsql
+## odbc_server: "server"
+## odbc_database: "database"
+## odbc_username: "username"
+## odbc_password: "password"
+##
+## If you want to specify the port:
+## odbc_port: 1234
+##
+## If you use PostgreSQL, have a large database, and need a
+## faster but inexact replacement for "select count(*) from users"
+##
+## pgsql_users_number_estimate: true
+
+##
+## ODBC compatible or MSSQL server:
+##
+## odbc_type: odbc
+## odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd"
+
+##
+## Number of connections to open to the database for each virtual host
+##
+## odbc_pool_size: 10
+
+##
+## Interval to make a dummy SQL request to keep the connections to the
+## database alive. Specify in seconds: for example 28800 means 8 hours
+##
+## odbc_keepalive_interval: undefined
+
+### ===============
+### TRAFFIC SHAPERS
+
+shaper:
+ ##
+ ## The "normal" shaper limits traffic speed to 1000 B/s
+ ##
+ normal: 1000
+
+ ##
+ ## The "fast" shaper limits traffic speed to 50000 B/s
+ ##
+ fast: 50000
+
+##
+## This option specifies the maximum number of elements in the queue
+## of the FSM. Refer to the documentation for details.
+##
+max_fsm_queue: 1000
+
+###. ====================
+###' ACCESS CONTROL LISTS
+acl:
+ ##
+ ## The 'admin' ACL grants administrative privileges to XMPP accounts.
+ ## You can put here as many accounts as you want.
+ ##
+ ## admin:
+ ## user:
+ ## - "aleksey": "localhost"
+ ## - "ermine": "example.org"
+ ##
+ ## Blocked users
+ ##
+ ## blocked:
+ ## user:
+ ## - "baduser": "example.org"
+ ## - "test"
+
+ ## Local users: don't modify this.
+ ##
+ local:
+ 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"
+
+ ##
+ ## 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"
+
+### ============
+### ACCESS RULES
+access:
+ ## Maximum number of simultaneous sessions allowed for a single user:
+ max_user_sessions:
+ all: 10
+ ## Maximum number of offline messages that users can have:
+ max_user_offline_messages:
+ admin: 5000
+ all: 100
+ ## This rule allows access only for local users:
+ local:
+ local: allow
+ ## Only non-blocked users can use c2s connections:
+ c2s:
+ blocked: deny
+ all: allow
+ ## For C2S connections, all users except admins use the "normal" shaper
+ c2s_shaper:
+ admin: none
+ all: normal
+ ## All S2S connections use the "fast" shaper
+ s2s_shaper:
+ all: fast
+ ## Only admins can send announcement messages:
+ announce:
+ admin: allow
+ ## Only admins can use the configuration interface:
+ configure:
+ admin: allow
+ ## Admins of this server are also admins of the MUC service:
+ muc_admin:
+ admin: allow
+ ## Only accounts of the local ejabberd server can create rooms:
+ muc_create:
+ local: allow
+ ## All users are allowed to use the MUC service:
+ muc:
+ all: allow
+ ## Only accounts on the local ejabberd server can create Pubsub nodes:
+ pubsub_createnode:
+ local: allow
+ ## In-band registration allows registration of any possible username.
+ ## To disable in-band registration, replace 'allow' with 'deny'.
+ register:
+ all: allow
+ ## Only allow to register from localhost
+ trusted_network:
+ loopback: allow
+ ## Do not establish S2S connections with bad servers
+ ## s2s:
+ ## bad_servers: deny
+ ## all: allow
+
+## By default the frequency of account registrations from the same IP
+## is limited to 1 account every 10 minutes. To disable, specify: infinity
+## registration_timeout: 600
+
+##
+## Define specific Access Rules in a virtual host.
+##
+## host_config:
+## "localhost":
+## access:
+## c2s:
+## admin: allow
+## all: deny
+## register:
+## all: deny
+
+### ================
+### DEFAULT LANGUAGE
+
+##
+## language: Default language used for server messages.
+##
+language: "en"
+
+##
+## Set a different default language in a virtual host.
+##
+## host_config:
+## "localhost":
+## language: "ru"
+
+### =======
+### CAPTCHA
+
+##
+## Full path to a script that generates the image.
+##
+## captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh"
+
+##
+## Host for the URL and port where ejabberd listens for CAPTCHA requests.
+##
+## captcha_host: "example.org:5280"
+
+##
+## Limit CAPTCHA calls per minute for JID/IP to avoid DoS.
+##
+## captcha_limit: 5
+
+### =======
+### MODULES
+
+##
+## Modules enabled in all ejabberd virtual hosts.
+##
+modules:
+ mod_adhoc: {}
+ ## mod_admin_extra: {}
+ mod_announce: # recommends mod_adhoc
+ access: announce
+ mod_blocking: {} # requires mod_privacy
+ mod_caps: {}
+ mod_carboncopy: {}
+ mod_client_state:
+ drop_chat_states: true
+ queue_presence: false
+ mod_configure: {} # requires mod_adhoc
+ mod_disco: {}
+ ## mod_echo: {}
+ mod_irc: {}
+ mod_http_bind: {}
+ ## mod_http_fileserver:
+ ## docroot: "/var/www"
+ ## accesslog: "/var/log/ejabberd/access.log"
+ mod_last: {}
+ mod_muc:
+ ## host: "conference.@HOST@"
+ access: muc
+ access_create: muc_create
+ access_persistent: muc_create
+ access_admin: muc_admin
+ ## mod_muc_log: {}
+ 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_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: {}
+ mod_shared_roster: {}
+ mod_stats: {}
+ mod_time: {}
+ mod_vcard: {}
+ mod_version: {}
+
+##
+## 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
diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex
new file mode 100644
index 00000000..9d17b157
--- /dev/null
+++ b/lib/ejabberd/config/attr.ex
@@ -0,0 +1,119 @@
+defmodule Ejabberd.Config.Attr do
+ @moduledoc """
+ Module used to work with the attributes parsed from
+ an elixir block (do...end).
+
+ Contains functions for extracting attrs from a block
+ and validation.
+ """
+
+ @type attr :: {atom(), any()}
+
+ @attr_supported [
+ active:
+ [type: :boolean, default: true],
+ git:
+ [type: :string, default: ""],
+ name:
+ [type: :string, default: ""],
+ opts:
+ [type: :list, default: []],
+ dependency:
+ [type: :list, default: []]
+ ]
+
+ @doc """
+ Takes a block with annotations and extracts the list
+ of attributes.
+ """
+ @spec extract_attrs_from_block_with_defaults(any()) :: [attr]
+ def extract_attrs_from_block_with_defaults(block) do
+ block
+ |> extract_attrs_from_block
+ |> put_into_list_if_not_already
+ |> insert_default_attrs_if_missing
+ end
+
+ @doc """
+ Takes an attribute or a list of attrs and validate them.
+
+ Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes.
+ """
+ @spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}]
+ def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1)
+ def validate(attr), do: validate([attr]) |> List.first
+
+ @doc """
+ Returns the type of an attribute, given its name.
+ """
+ @spec get_type_for_attr(atom()) :: atom()
+ def get_type_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:type)
+ end
+
+ @doc """
+ Returns the default value for an attribute, given its name.
+ """
+ @spec get_default_for_attr(atom()) :: any()
+ def get_default_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:default)
+ end
+
+ # Private API
+
+ # Given an elixir block (do...end) returns a list with the annotations
+ # or a single annotation.
+ @spec extract_attrs_from_block(any()) :: [attr] | attr
+ defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1)
+ defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs)
+ defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value}
+ defp extract_attrs_from_block(nil), do: []
+
+ # In case extract_attrs_from_block returns a single attribute,
+ # then put it into a list. (Ensures attrs are always into a list).
+ @spec put_into_list_if_not_already([attr] | attr) :: [attr]
+ defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs
+ defp put_into_list_if_not_already(attr), do: [attr]
+
+ # Given a list of attributes, it inserts the missing attribute with their
+ # default value.
+ @spec insert_default_attrs_if_missing([attr]) :: [attr]
+ defp insert_default_attrs_if_missing(attrs) do
+ Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) ->
+ case Keyword.has_key?(acc, attr_name) do
+ true -> acc
+ false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name))
+ end
+ end
+ end
+
+ # Given an attribute, validates it and return a tuple with
+ # {:ok, attr} or {:error, attr, cause}
+ @spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()}
+ defp valid_attr?({attr_name, param} = attr) do
+ case Keyword.get(@attr_supported, attr_name) do
+ nil -> {:error, attr, :attr_not_supported}
+ [{:type, param_type} | _] -> case is_of_type?(param, param_type) do
+ true -> {:ok, attr}
+ false -> {:error, attr, :type_not_supported}
+ end
+ end
+ end
+
+ # Given an attribute value and a type, it returns a true
+ # if the value its of the type specified, false otherwise.
+
+ # Usefoul for checking if an attr value respects the type
+ # specified for the annotation.
+ @spec is_of_type?(any(), atom()) :: boolean()
+ defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true
+ defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true
+ defp is_of_type?(param, type) when type == :list and is_list(param), do: true
+ defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true
+ defp is_of_type?(_param, type) when type == :any, do: true
+ defp is_of_type?(_, _), do: false
+end
diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex
new file mode 100644
index 00000000..4d1270bc
--- /dev/null
+++ b/lib/ejabberd/config/config.ex
@@ -0,0 +1,145 @@
+defmodule Ejabberd.Config do
+ @moduledoc """
+ Base module for configuration file.
+
+ Imports macros for the config DSL and contains functions
+ for working/starting the configuration parsed.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.EjabberdLogger
+
+ defmacro __using__(_opts) do
+ quote do
+ import Ejabberd.Config, only: :macros
+ import Ejabberd.Logger
+
+ @before_compile Ejabberd.Config
+ end
+ end
+
+ # Validate the modules parsed and log validation errors at compile time.
+ # Could be also possible to interrupt the compilation&execution by throwing
+ # an exception if necessary.
+ def __before_compile__(_env) do
+ get_modules_parsed_in_order
+ |> EjabberdModule.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ @doc """
+ Given the path of the config file, it evaluates it.
+ """
+ def init(file_path, force \\ false) do
+ init_already_executed = Ejabberd.Config.Store.get(:module_name) != []
+
+ case force do
+ true ->
+ Ejabberd.Config.Store.stop
+ Ejabberd.Config.Store.start_link
+ do_init(file_path)
+ false ->
+ if not init_already_executed, do: do_init(file_path)
+ end
+ end
+
+ @doc """
+ Returns a list with all the opts, formatted for ejabberd.
+ """
+ def get_ejabberd_opts do
+ get_general_opts
+ |> Dict.put(:modules, get_modules_parsed_in_order())
+ |> Dict.put(:listeners, get_listeners_parsed_in_order())
+ |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd
+ end
+
+ @doc """
+ Register the hooks defined inside the elixir config file.
+ """
+ def start_hooks do
+ get_hooks_parsed_in_order()
+ |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1)
+ end
+
+ ###
+ ### MACROS
+ ###
+
+ defmacro listen(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:listeners, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro module(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:modules, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro hook(hook_name, opts, fun) do
+ quote do
+ Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{
+ hook: unquote(hook_name),
+ opts: unquote(opts),
+ fun: unquote(fun)
+ })
+ end
+ end
+
+ # Private API
+
+ defp do_init(file_path) do
+ # File evaluation
+ Code.eval_file(file_path) |> extract_and_store_module_name()
+
+ # Getting start/0 config
+ Ejabberd.Config.Store.get(:module_name)
+ |> case do
+ nil -> IO.puts "[ ERR ] Configuration module not found."
+ [module] -> call_start_func_and_store_data(module)
+ end
+
+ # Fetching git modules and install them
+ get_modules_parsed_in_order()
+ |> EjabberdModule.fetch_git_repos
+ end
+
+ # Returns the modules from the store
+ defp get_modules_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse
+
+ # Returns the listeners from the store
+ defp get_listeners_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse
+
+ defp get_hooks_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse
+
+ # Returns the general config options
+ defp get_general_opts,
+ do: Ejabberd.Config.Store.get(:general) |> List.first
+
+ # Gets the general ejabberd options calling
+ # the start/0 function and stores them.
+ defp call_start_func_and_store_data(module) do
+ opts = apply(module, :start, [])
+ Ejabberd.Config.Store.put(:general, opts)
+ end
+
+ # Stores the configuration module name
+ defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do
+ Ejabberd.Config.Store.put(:module_name, mod)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex
new file mode 100644
index 00000000..8b7858d2
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_hook.ex
@@ -0,0 +1,23 @@
+defmodule Ejabberd.Config.EjabberdHook do
+ @moduledoc """
+ Module containing functions for manipulating
+ ejabberd hooks.
+ """
+
+ defstruct hook: nil, opts: [], fun: nil
+
+ alias Ejabberd.Config.EjabberdHook
+
+ @type t :: %EjabberdHook{}
+
+ @doc """
+ Register a hook to ejabberd.
+ """
+ @spec start(EjabberdHook.t) :: none
+ def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do
+ host = Keyword.get(opts, :host, :global)
+ priority = Keyword.get(opts, :priority, 50)
+
+ :ejabberd_hooks.add(hook, host, fun, priority)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex
new file mode 100644
index 00000000..4de9a302
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_module.ex
@@ -0,0 +1,70 @@
+defmodule Ejabberd.Config.EjabberdModule do
+ @moduledoc """
+ Module representing a module block in the configuration file.
+ It offers functions for validation and for starting the modules.
+
+ Warning: The name is EjabberdModule to not collide with
+ the already existing Elixir.Module.
+ """
+
+ @type t :: %{module: atom, attrs: [Attr.t]}
+
+ defstruct [:module, :attrs]
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.Validation
+
+ @doc """
+ Given a list of modules / single module
+ it runs different validators on them.
+
+ For each module, returns a {:ok, mod} or {:error, mod, errors}
+ """
+ def validate(modules) do
+ Validation.validate(modules)
+ end
+
+ @doc """
+ Given a list of modules, it takes only the ones with
+ a git attribute and tries to fetch the repo,
+ then, it install them through :ext_mod.install/1
+ """
+ @spec fetch_git_repos([EjabberdModule.t]) :: none()
+ def fetch_git_repos(modules) do
+ modules
+ |> Enum.filter(&is_git_module?/1)
+ |> Enum.each(&fetch_and_install_git_module/1)
+ end
+
+ # Private API
+
+ defp is_git_module?(%EjabberdModule{attrs: attrs}) do
+ case Keyword.get(attrs, :git) do
+ "" -> false
+ repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/)
+ end
+ end
+
+ defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do
+ repo = Keyword.get(attrs, :git)
+ mod_name = case Keyword.get(attrs, :name) do
+ "" -> infer_mod_name_from_git_url(repo)
+ name -> name
+ end
+
+ path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}"
+ fetch_and_store_repo_source_if_not_exists(path, repo)
+ :ext_mod.install(mod_name) # Have to check if overwrites an already present mod
+ end
+
+ defp fetch_and_store_repo_source_if_not_exists(path, repo) do
+ unless File.exists?(path) do
+ IO.puts "[info] Fetching: #{repo}"
+ :os.cmd('git clone #{repo} #{path}')
+ end
+ end
+
+ defp infer_mod_name_from_git_url(repo),
+ do: String.split(repo, "/") |> List.last |> String.replace(".git", "")
+end
diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex
new file mode 100644
index 00000000..270fbfaa
--- /dev/null
+++ b/lib/ejabberd/config/logger/ejabberd_logger.ex
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.EjabberdLogger do
+ @moduledoc """
+ Module used to log validation errors given validated modules
+ given validated modules.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Given a list of modules validated, in the form of {:ok, mod} or
+ {:error, mod, errors}, it logs to the user the errors found.
+ """
+ @spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t]
+ def log_errors(modules_validated) when is_list(modules_validated) do
+ Enum.each modules_validated, &do_log_errors/1
+ modules_validated
+ end
+
+ defp do_log_errors({:ok, _mod}), do: nil
+ defp do_log_errors({:error, _mod, errors}), do: Enum.each errors, &do_log_errors/1
+ defp do_log_errors({:attribute, errors}), do: Enum.each errors, &log_attribute_error/1
+ defp do_log_errors({:dependency, errors}), do: Enum.each errors, &log_dependency_error/1
+
+ defp log_attribute_error({{attr_name, val}, :attr_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} is not supported."
+
+ defp log_attribute_error({{attr_name, val}, :type_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)."
+
+ defp log_dependency_error({module, :not_found}), do:
+ IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency."
+end
diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex
new file mode 100644
index 00000000..b7010ddf
--- /dev/null
+++ b/lib/ejabberd/config/opts_formatter.ex
@@ -0,0 +1,46 @@
+defmodule Ejabberd.Config.OptsFormatter do
+ @moduledoc """
+ Module for formatting options parsed into the format
+ ejabberd uses.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Takes a keyword list with keys corresponding to
+ the keys requested by the ejabberd config (ex: modules: mods)
+ and formats them to be correctly evaluated by ejabberd.
+
+ Look at how Config.get_ejabberd_opts/0 is constructed for
+ more informations.
+ """
+ @spec format_opts_for_ejabberd([{atom(), any()}]) :: list()
+ def format_opts_for_ejabberd(opts) do
+ opts
+ |> format_attrs_for_ejabberd
+ end
+
+ defp format_attrs_for_ejabberd(opts) when is_list(opts),
+ do: Enum.map opts, &format_attrs_for_ejabberd/1
+
+ defp format_attrs_for_ejabberd({:listeners, mods}),
+ do: {:listen, format_listeners_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({:modules, mods}),
+ do: {:modules, format_mods_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({key, opts}) when is_atom(key),
+ do: {key, opts}
+
+ defp format_mods_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ {mod, attrs[:opts]}
+ end
+ end
+
+ defp format_listeners_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ Keyword.put(attrs[:opts], :module, mod)
+ end
+ end
+end
diff --git a/lib/ejabberd/config/store.ex b/lib/ejabberd/config/store.ex
new file mode 100644
index 00000000..72beea64
--- /dev/null
+++ b/lib/ejabberd/config/store.ex
@@ -0,0 +1,55 @@
+defmodule Ejabberd.Config.Store do
+ @moduledoc """
+ Module used for storing the modules parsed from
+ the configuration file.
+
+ Example:
+ - Store.put(:modules, mod1)
+ - Store.put(:modules, mod2)
+
+ - Store.get(:modules) :: [mod1, mod2]
+
+ Be carefoul: when retrieving data you get them
+ in the order inserted into the store, which normally
+ is the reversed order of how the modules are specified
+ inside the configuration file. To resolve this just use
+ a Enum.reverse/1.
+ """
+
+ @name __MODULE__
+
+ def start_link do
+ Agent.start_link(fn -> %{} end, name: @name)
+ end
+
+ @doc """
+ Stores a value based on the key. If the key already exists,
+ then it inserts the new element, maintaining all the others.
+ It uses a list for this.
+ """
+ @spec put(atom, any) :: :ok
+ def put(key, val) do
+ Agent.update @name, &Map.update(&1, key, [val], fn coll ->
+ [val | coll]
+ end)
+ end
+
+ @doc """
+ Gets a value based on the key passed.
+ Returns always a list.
+ """
+ @spec get(atom) :: [any]
+ def get(key) do
+ Agent.get @name, &Map.get(&1, key, [])
+ end
+
+ @doc """
+ Stops the store.
+ It uses Agent.stop underneath, so be aware that exit
+ could be called.
+ """
+ @spec stop() :: :ok
+ def stop do
+ Agent.stop @name
+ end
+end
diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex
new file mode 100644
index 00000000..2fe00361
--- /dev/null
+++ b/lib/ejabberd/config/validator/validation.ex
@@ -0,0 +1,40 @@
+defmodule Ejabberd.Config.Validation do
+ @moduledoc """
+ Module used to validate a list of modules.
+ """
+
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map}
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.Validator
+ alias Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module or a list of modules it runs validators on them
+ and returns {:ok, mod} or {:error, mod, errors}, for each
+ of them.
+ """
+ @spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result]
+ def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1))
+ def validate(module), do: validate([module])
+
+ # Private API
+
+ @spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result
+ defp do_validate(modules, mod) do
+ {modules, mod, %{}}
+ |> Validator.Attrs.validate
+ |> Validator.Dependencies.validate
+ |> resolve_validation_result
+ end
+
+ @spec resolve_validation_result(mod_validation) :: mod_validation_result
+ defp resolve_validation_result({_modules, mod, errors}) do
+ case errors do
+ err when err == %{} -> {:ok, mod}
+ err -> {:error, mod, err}
+ end
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex
new file mode 100644
index 00000000..94117ab2
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_attrs.ex
@@ -0,0 +1,28 @@
+defmodule Ejabberd.Config.Validator.Attrs do
+ @moduledoc """
+ Validator module used to validate attributes.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+
+ import Ejabberd.Config.ValidatorUtility
+ alias Ejabberd.Config.Attr
+
+ @doc """
+ Given a module (with the form used for validation)
+ it runs Attr.validate/1 on each attribute and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ errors = Enum.reduce mod.attrs, errors, fn(attr, err) ->
+ case Attr.validate(attr) do
+ {:ok, attr} -> err
+ {:error, attr, cause} -> put_error(err, :attribute, {attr, cause})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex
new file mode 100644
index 00000000..d44c8a13
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_dependencies.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.Validator.Dependencies do
+ @moduledoc """
+ Validator module used to validate dependencies specified
+ with the @dependency annotation.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ import Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module (with the form used for validation)
+ it checks if the @dependency annotation is respected and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ module_names = extract_module_names(modules)
+ dependencies = mod.attrs[:dependency]
+
+ errors = Enum.reduce dependencies, errors, fn(req_module, err) ->
+ case req_module in module_names do
+ true -> err
+ false -> put_error(err, :dependency, {req_module, :not_found})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex
new file mode 100644
index 00000000..17805f74
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_utility.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.ValidatorUtility do
+ @moduledoc """
+ Module used as a base validator for validation modules.
+ Imports utility functions for working with validation structures.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Inserts an error inside the errors collection, for the given key.
+ If the key doesn't exists then it creates an empty collection
+ and inserts the value passed.
+ """
+ @spec put_error(map, atom, any) :: map
+ def put_error(errors, key, val) do
+ Map.update errors, key, [val], fn coll ->
+ [val | coll]
+ end
+ end
+
+ @doc """
+ Given a list of modules it extracts and returns a list
+ of the module names (which are Elixir.Module).
+ """
+ @spec extract_module_names(EjabberdModule.t) :: [atom]
+ def extract_module_names(modules) when is_list(modules) do
+ modules
+ |> Enum.map(&Map.get(&1, :module))
+ end
+end
diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex
new file mode 100644
index 00000000..6592104a
--- /dev/null
+++ b/lib/ejabberd/config_util.ex
@@ -0,0 +1,18 @@
+defmodule Ejabberd.ConfigUtil do
+ @moduledoc """
+ Module containing utility functions for
+ the config file.
+ """
+
+ @doc """
+ Returns true when the config file is based on elixir.
+ """
+ @spec is_elixir_config(list) :: boolean
+ def is_elixir_config(filename) when is_list(filename) do
+ is_elixir_config(to_string(filename))
+ end
+
+ def is_elixir_config(filename) do
+ String.ends_with?(filename, "exs")
+ end
+end
diff --git a/lib/ejabberd/module.ex b/lib/ejabberd/module.ex
new file mode 100644
index 00000000..9fb3f040
--- /dev/null
+++ b/lib/ejabberd/module.ex
@@ -0,0 +1,19 @@
+defmodule Ejabberd.Module do
+
+ defmacro __using__(opts) do
+ logger_enabled = Keyword.get(opts, :logger, true)
+
+ quote do
+ @behaviour :gen_mod
+ import Ejabberd.Module
+
+ unquote(if logger_enabled do
+ quote do: import Ejabberd.Logger
+ end)
+ end
+ end
+
+ # gen_mod callbacks
+ def depends(_host, _opts), do: []
+ def mod_opt_type(_), do: []
+end
diff --git a/lib/mix/tasks/deps.tree.ex b/lib/mix/tasks/deps.tree.ex
new file mode 100644
index 00000000..94cb85a5
--- /dev/null
+++ b/lib/mix/tasks/deps.tree.ex
@@ -0,0 +1,94 @@
+defmodule Mix.Tasks.Ejabberd.Deps.Tree do
+ use Mix.Task
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @shortdoc "Lists all ejabberd modules and their dependencies"
+
+ @moduledoc """
+ Lists all ejabberd modules and their dependencies.
+
+ The project must have ejabberd as a dependency.
+ """
+
+ def run(_argv) do
+ # First we need to start manually the store to be available
+ # during the compilation of the config file.
+ Ejabberd.Config.Store.start_link
+ Ejabberd.Config.init(:ejabberd_config.get_ejabberd_config_path())
+
+ Mix.shell.info "ejabberd modules"
+
+ Ejabberd.Config.Store.get(:modules)
+ |> Enum.reverse # Because of how mods are stored inside the store
+ |> format_mods
+ |> Mix.shell.info
+ end
+
+ defp format_mods(mods) when is_list(mods) do
+ deps_tree = build_dependency_tree(mods)
+ mods_used_as_dependency = get_mods_used_as_dependency(deps_tree)
+
+ keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency)
+ |> format_mods_into_string
+ end
+
+ defp build_dependency_tree(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ deps = attrs[:dependency]
+ build_dependency_tree(mods, mod, deps)
+ end
+ end
+
+ defp build_dependency_tree(mods, mod, []), do: %{module: mod, dependency: []}
+ defp build_dependency_tree(mods, mod, deps) when is_list(deps) do
+ dependencies = Enum.map deps, fn dep ->
+ dep_deps = get_dependencies_of_mod(mods, dep)
+ build_dependency_tree(mods, dep, dep_deps)
+ end
+
+ %{module: mod, dependency: dependencies}
+ end
+
+ defp get_mods_used_as_dependency(mods) when is_list(mods) do
+ Enum.reduce mods, [], fn(mod, acc) ->
+ case mod do
+ %{dependency: []} -> acc
+ %{dependency: deps} -> get_mod_names(deps) ++ acc
+ end
+ end
+ end
+
+ defp get_mod_names([]), do: []
+ defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten
+ defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)]
+
+ defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do
+ Enum.filter mods, fn %{module: mod} ->
+ not mod in mods_used_as_dep
+ end
+ end
+
+ defp get_dependencies_of_mod(deps, mod_name) do
+ Enum.find(deps, &(Map.get(&1, :module) == mod_name))
+ |> Map.get(:attrs)
+ |> Keyword.get(:dependency)
+ end
+
+ defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0)
+ defp format_mods_into_string([], _indentation), do: ""
+ defp format_mods_into_string(mods, indentation) when is_list(mods) do
+ Enum.reduce mods, "", fn(mod, acc) ->
+ acc <> format_mods_into_string(mod, indentation)
+ end
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do
+ "\n├── #{mod}" <> format_mods_into_string(deps, 2)
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do
+ spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end
+ "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4)
+ end
+end
diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex
index ba5abe90..09bf5840 100644
--- a/lib/mod_presence_demo.ex
+++ b/lib/mod_presence_demo.ex
@@ -1,16 +1,15 @@
defmodule ModPresenceDemo do
- import Ejabberd.Logger # this allow using info, error, etc for logging
- @behaviour :gen_mod
+ use Ejabberd.Module
def start(host, _opts) do
info('Starting ejabberd module Presence Demo')
- Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
def stop(host) do
info('Stopping ejabberd module Presence Demo')
- Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
@@ -18,9 +17,4 @@ defmodule ModPresenceDemo do
info('Receive presence for #{user}')
:none
end
-
- # gen_mod callbacks
- def depends(_host, _opts), do: []
- def mod_opt_type(_), do: []
-
end
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl
index 3b333b3b..9d127e74 100644
--- a/src/ejabberd_app.erl
+++ b/src/ejabberd_app.erl
@@ -56,6 +56,7 @@ start(normal, _Args) ->
ejabberd_admin:start(),
gen_mod:start(),
ext_mod:start(),
+ setup_if_elixir_conf_used(),
ejabberd_config:start(),
set_settings_from_config(),
acl:start(),
@@ -76,6 +77,7 @@ start(normal, _Args) ->
gen_mod:start_modules(),
ejabberd_listener:start_listeners(),
ejabberd_service:start(),
+ register_elixir_config_hooks(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),
Sup;
start(_, _) ->
@@ -240,6 +242,18 @@ opt_type(modules) ->
end;
opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime].
+setup_if_elixir_conf_used() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config.Store':start_link();
+ false -> ok
+ end.
+
+register_elixir_config_hooks() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config':start_hooks();
+ false -> ok
+ end.
+
start_elixir_application() ->
case application:ensure_started(elixir) of
ok -> ok;
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index b75883fb..7d5dfbc0 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -33,6 +33,7 @@
get_option/2, get_option/3, add_option/2, has_option/1,
get_vh_by_auth_method/1, is_file_readable/1,
get_version/0, get_myhosts/0, get_mylang/0,
+ get_ejabberd_config_path/0, is_using_elixir_config/0,
prepare_opt_val/4, convert_table_to_binary/5,
transform_options/1, collect_options/1, default_db/2,
convert_to_yaml/1, convert_to_yaml/2, v_db/2,
@@ -147,7 +148,13 @@ read_file(File) ->
{include_modules_configs, true}]).
read_file(File, Opts) ->
- Terms1 = get_plain_terms_file(File, Opts),
+ Terms1 = case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of
+ true ->
+ 'Elixir.Ejabberd.Config':init(File),
+ 'Elixir.Ejabberd.Config':get_ejabberd_opts();
+ false ->
+ get_plain_terms_file(File, Opts)
+ end,
Terms_macros = case proplists:get_bool(replace_macros, Opts) of
true -> replace_macros(Terms1);
false -> Terms1
@@ -1042,6 +1049,10 @@ replace_modules(Modules) ->
%% Elixir module naming
%% ====================
+is_using_elixir_config() ->
+ Config = get_ejabberd_config_path(),
+ 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config).
+
%% If module name start with uppercase letter, this is an Elixir module:
is_elixir_module(Module) ->
case atom_to_list(Module) of
diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl
index a79f2630..31f80be7 100644
--- a/src/ejabberd_http.erl
+++ b/src/ejabberd_http.erl
@@ -145,9 +145,14 @@ init({SockMod, Socket}, Opts) ->
DefinedHandlers = gen_mod:get_opt(
request_handlers, Opts,
fun(Hs) ->
+ Hs1 = lists:map(fun
+ ({Mod, Path}) when is_atom(Mod) -> {Path, Mod};
+ ({Path, Mod}) -> {Path, Mod}
+ end, Hs),
+
[{str:tokens(
iolist_to_binary(Path), <<"/">>),
- Mod} || {Path, Mod} <- Hs]
+ Mod} || {Path, Mod} <- Hs1]
end, []),
RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++
Admin ++ Bind ++ XMLRPC,
diff --git a/test/elixir-config/attr_test.exs b/test/elixir-config/attr_test.exs
new file mode 100644
index 00000000..c5cab5bd
--- /dev/null
+++ b/test/elixir-config/attr_test.exs
@@ -0,0 +1,87 @@
+defmodule Ejabberd.Config.AttrTest do
+ use ExUnit.Case, async: true
+
+ alias Ejabberd.Config.Attr
+
+ test "extract attrs from single line block" do
+ block = quote do
+ @active false
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+ assert {:active, false} in block_res
+ end
+
+ test "extract attrs from multi line block" do
+ block = quote do
+ @active false
+ @opts [http: true]
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+ assert {:active, false} in block_res
+ assert {:opts, [http: true]} in block_res
+ end
+
+ test "inserts correctly defaults attr when missing in block" do
+ block = quote do
+ @active false
+ @opts [http: true]
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+
+ assert {:active, false} in block_res
+ assert {:git, ""} in block_res
+ assert {:name, ""} in block_res
+ assert {:opts, [http: true]} in block_res
+ assert {:dependency, []} in block_res
+ end
+
+ test "inserts all defaults attr when passed an empty block" do
+ block = quote do
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+
+ assert {:active, true} in block_res
+ assert {:git, ""} in block_res
+ assert {:name, ""} in block_res
+ assert {:opts, []} in block_res
+ assert {:dependency, []} in block_res
+ end
+
+ test "validates attrs and returns errors, if any" do
+ block = quote do
+ @not_supported_attr true
+ @active "false"
+ @opts [http: true]
+ end
+
+ block_res =
+ block
+ |> Attr.extract_attrs_from_block_with_defaults
+ |> Attr.validate
+
+ assert {:ok, {:opts, [http: true]}} in block_res
+ assert {:ok, {:git, ""}} in block_res
+ assert {:error, {:not_supported_attr, true}, :attr_not_supported} in block_res
+ assert {:error, {:active, "false"}, :type_not_supported} in block_res
+ end
+
+ test "returns the correct type for an attribute" do
+ assert :boolean == Attr.get_type_for_attr(:active)
+ assert :string == Attr.get_type_for_attr(:git)
+ assert :string == Attr.get_type_for_attr(:name)
+ assert :list == Attr.get_type_for_attr(:opts)
+ assert :list == Attr.get_type_for_attr(:dependency)
+ end
+
+ test "returns the correct default for an attribute" do
+ assert true == Attr.get_default_for_attr(:active)
+ assert "" == Attr.get_default_for_attr(:git)
+ assert "" == Attr.get_default_for_attr(:name)
+ assert [] == Attr.get_default_for_attr(:opts)
+ assert [] == Attr.get_default_for_attr(:dependency)
+ end
+end
diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs
new file mode 100644
index 00000000..c359c49c
--- /dev/null
+++ b/test/elixir-config/config_test.exs
@@ -0,0 +1,65 @@
+defmodule Ejabberd.ConfigTest do
+ use ExUnit.Case
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "extracts successfully the module name from config file" do
+ assert [Ejabberd.ConfigFile] == Store.get(:module_name)
+ end
+
+ test "extracts successfully general opts from config file" do
+ [general] = Store.get(:general)
+ shaper = [normal: 1000, fast: 50000, max_fsm_queue: 1000]
+ assert [loglevel: 4, language: "en", hosts: ["localhost"], shaper: shaper] == general
+ end
+
+ test "extracts successfully listeners from config file" do
+ [listen] = Store.get(:listeners)
+ assert :ejabberd_c2s == listen.module
+ assert [port: 5222, max_stanza_size: 65536, shaper: :c2s_shaper, access: :c2s] == listen.attrs[:opts]
+ end
+
+ test "extracts successfully modules from config file" do
+ [module] = Store.get(:modules)
+ assert :mod_adhoc == module.module
+ assert [] == module.attrs[:opts]
+ end
+
+ test "extracts successfully hooks from config file" do
+ [register_hook] = Store.get(:hooks)
+
+ assert :register_user == register_hook.hook
+ assert [host: "localhost"] == register_hook.opts
+ assert is_function(register_hook.fun)
+ end
+
+ # TODO: When enalbed, this test causes the evaluation of a different config file, so
+ # the other tests, that uses the store, are compromised because the data is different.
+ # So, until a good way is found, this test should remain disabed.
+ #
+ # test "init/2 with force:true re-initializes the config store with new data" do
+ # config_file_path = File.cwd! <> "/ejabberd_different_from_default.exs"
+ # Config.init(config_file_path, true)
+ #
+ # assert [Ejabberd.ConfigFile] == Store.get(:module_name)
+ # assert [[loglevel: 4, language: "en", hosts: ["localhost"]]] == Store.get(:general)
+ # assert [] == Store.get(:modules)
+ # assert [] == Store.get(:listeners)
+ #
+ # Store.stop
+ # end
+end
diff --git a/test/elixir-config/ejabberd_logger.exs b/test/elixir-config/ejabberd_logger.exs
new file mode 100644
index 00000000..d13f79aa
--- /dev/null
+++ b/test/elixir-config/ejabberd_logger.exs
@@ -0,0 +1,49 @@
+defmodule Ejabberd.Config.EjabberdLoggerTest do
+ use ExUnit.Case
+
+ import ExUnit.CaptureIO
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+ alias Ejabberd.Config.Validation
+ alias Ejabberd.Config.EjabberdLogger
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd_for_validation.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "outputs correctly when attr is not supported" do
+ error_msg = "[ WARN ] Annotation @attr_not_supported is not supported.\n"
+
+ [_mod_irc, _mod_configure, mod_time] = Store.get(:modules)
+ fun = fn ->
+ mod_time
+ |> Validation.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ assert capture_io(fun) == error_msg
+ end
+
+ test "outputs correctly when dependency is not found" do
+ error_msg = "[ WARN ] Module :mod_adhoc was not found, but is required as a dependency.\n"
+
+ [_mod_irc, mod_configure, _mod_time] = Store.get(:modules)
+ fun = fn ->
+ mod_configure
+ |> Validation.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ assert capture_io(fun) == error_msg
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd.exs b/test/elixir-config/shared/ejabberd.exs
new file mode 100644
index 00000000..5d0243bb
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd.exs
@@ -0,0 +1,31 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"],
+ shaper: shaper]
+ end
+
+ defp shaper do
+ [normal: 1000,
+ fast: 50000,
+ max_fsm_queue: 1000]
+ end
+
+ listen :ejabberd_c2s do
+ @opts [
+ port: 5222,
+ max_stanza_size: 65536,
+ shaper: :c2s_shaper,
+ access: :c2s]
+ end
+
+ module :mod_adhoc do
+ end
+
+ hook :register_user, [host: "localhost"], fn(user, server) ->
+ info("User registered: #{user} on #{server}")
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd_different_from_default.exs b/test/elixir-config/shared/ejabberd_different_from_default.exs
new file mode 100644
index 00000000..a3940968
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd_different_from_default.exs
@@ -0,0 +1,9 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"]]
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd_for_validation.exs b/test/elixir-config/shared/ejabberd_for_validation.exs
new file mode 100644
index 00000000..8c0196c7
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd_for_validation.exs
@@ -0,0 +1,20 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"]]
+ end
+
+ module :mod_time do
+ @attr_not_supported true
+ end
+
+ module :mod_configure do
+ @dependency [:mod_adhoc]
+ end
+
+ module :mod_irc do
+ end
+end
diff --git a/test/elixir-config/validation_test.exs b/test/elixir-config/validation_test.exs
new file mode 100644
index 00000000..1df77596
--- /dev/null
+++ b/test/elixir-config/validation_test.exs
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.ValidationTest do
+ use ExUnit.Case
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+ alias Ejabberd.Config.Validation
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd_for_validation.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "validates correctly the modules" do
+ [mod_irc, mod_configure, mod_time] = Store.get(:modules)
+
+ [{:error, _mod, errors}] = Validation.validate(mod_configure)
+ assert %{dependency: [mod_adhoc: :not_found]} == errors
+
+ [{:error, _mod, errors}] = Validation.validate(mod_time)
+ assert %{attribute: [{{:attr_not_supported, true}, :attr_not_supported}]} == errors
+
+ [{:ok, _mod}] = Validation.validate(mod_irc)
+ end
+end