]> granicus.if.org Git - ejabberd/commitdiff
Support for Elixir configuration file #1208
authorgabrielgatu <gabriel.dny@gmail.com>
Thu, 8 Sep 2016 09:34:42 +0000 (11:34 +0200)
committerMickael Remond <mremond@process-one.net>
Thu, 8 Sep 2016 09:37:14 +0000 (11:37 +0200)
Contribution for Google Summer of code 2016 by Gabriel Gatu

28 files changed:
config/config.exs
config/ejabberd.exs [new file with mode: 0644]
config/ejabberd.yml [new file with mode: 0644]
lib/ejabberd/config/attr.ex [new file with mode: 0644]
lib/ejabberd/config/config.ex [new file with mode: 0644]
lib/ejabberd/config/ejabberd_hook.ex [new file with mode: 0644]
lib/ejabberd/config/ejabberd_module.ex [new file with mode: 0644]
lib/ejabberd/config/logger/ejabberd_logger.ex [new file with mode: 0644]
lib/ejabberd/config/opts_formatter.ex [new file with mode: 0644]
lib/ejabberd/config/store.ex [new file with mode: 0644]
lib/ejabberd/config/validator/validation.ex [new file with mode: 0644]
lib/ejabberd/config/validator/validator_attrs.ex [new file with mode: 0644]
lib/ejabberd/config/validator/validator_dependencies.ex [new file with mode: 0644]
lib/ejabberd/config/validator/validator_utility.ex [new file with mode: 0644]
lib/ejabberd/config_util.ex [new file with mode: 0644]
lib/ejabberd/module.ex [new file with mode: 0644]
lib/mix/tasks/deps.tree.ex [new file with mode: 0644]
lib/mod_presence_demo.ex
src/ejabberd_app.erl
src/ejabberd_config.erl
src/ejabberd_http.erl
test/elixir-config/attr_test.exs [new file with mode: 0644]
test/elixir-config/config_test.exs [new file with mode: 0644]
test/elixir-config/ejabberd_logger.exs [new file with mode: 0644]
test/elixir-config/shared/ejabberd.exs [new file with mode: 0644]
test/elixir-config/shared/ejabberd_different_from_default.exs [new file with mode: 0644]
test/elixir-config/shared/ejabberd_for_validation.exs [new file with mode: 0644]
test/elixir-config/validation_test.exs [new file with mode: 0644]

index 4d378348059252bc8cf95f9510b201229c75797f..0d1a3c720ce6d051838254495a4007db8ea20874 100644 (file)
@@ -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 (file)
index 0000000..05c2b5d
--- /dev/null
@@ -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 (file)
index 0000000..80fc3c6
--- /dev/null
@@ -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 (file)
index 0000000..9d17b15
--- /dev/null
@@ -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 (file)
index 0000000..4d1270b
--- /dev/null
@@ -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 (file)
index 0000000..8b7858d
--- /dev/null
@@ -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 (file)
index 0000000..4de9a30
--- /dev/null
@@ -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 (file)
index 0000000..270fbfa
--- /dev/null
@@ -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 (file)
index 0000000..b7010dd
--- /dev/null
@@ -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 (file)
index 0000000..72beea6
--- /dev/null
@@ -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 (file)
index 0000000..2fe0036
--- /dev/null
@@ -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 (file)
index 0000000..94117ab
--- /dev/null
@@ -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 (file)
index 0000000..d44c8a1
--- /dev/null
@@ -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 (file)
index 0000000..17805f7
--- /dev/null
@@ -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 (file)
index 0000000..6592104
--- /dev/null
@@ -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 (file)
index 0000000..9fb3f04
--- /dev/null
@@ -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 (file)
index 0000000..94cb85a
--- /dev/null
@@ -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
index ba5abe90eaaa06d355920e87bc2ac1f0390ec49a..09bf584057a1cfd24f11b14bbd2bd32c6e459061 100644 (file)
@@ -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
index 3b333b3b5d44327ff45fe9859bf67234b119b9fd..9d127e748fb8cd7eb4c8ea79ecf147dc6eee72a3 100644 (file)
@@ -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;
index b75883fb2c480662ae27a13de2b138f1e72d0f6e..7d5dfbc0c2e40bcfee146084c96fa7d31b295f08 100644 (file)
@@ -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
index a79f26305e9deec3004c460dc5411dff8280e710..31f80be788e9e2cb99dbf00aca81d2c54d8f6ac8 100644 (file)
@@ -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 (file)
index 0000000..c5cab5b
--- /dev/null
@@ -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 (file)
index 0000000..c359c49
--- /dev/null
@@ -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 (file)
index 0000000..d13f79a
--- /dev/null
@@ -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 (file)
index 0000000..5d0243b
--- /dev/null
@@ -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 (file)
index 0000000..a394096
--- /dev/null
@@ -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 (file)
index 0000000..8c0196c
--- /dev/null
@@ -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 (file)
index 0000000..1df7759
--- /dev/null
@@ -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