├── .envrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
└── config.exs
├── lib
├── mix
│ └── tasks
│ │ └── ejabberd.gen.config.ex
├── romeo.ex
└── romeo
│ ├── auth.ex
│ ├── connection.ex
│ ├── connection
│ └── features.ex
│ ├── error.ex
│ ├── jid.ex
│ ├── roster.ex
│ ├── roster
│ └── item.ex
│ ├── stanza.ex
│ ├── stanza
│ ├── iq.ex
│ ├── message.ex
│ ├── parser.ex
│ └── presence.ex
│ ├── transports
│ └── tcp.ex
│ ├── xml.ex
│ └── xmlns.ex
├── mix.exs
├── mix.lock
├── priv
├── ssl
│ └── ejabberd.pem
└── templates
│ └── ejabberd.yml.eex
└── test
├── romeo
├── connection
│ └── features_test.exs
├── connection_test.exs
├── jid_test.exs
├── roster_test.exs
├── stanza
│ └── parser_test.exs
├── stanza_test.exs
├── xml_test.exs
└── xmlns_test.exs
├── romeo_test.exs
├── test_helper.exs
└── user_helper.exs
/.envrc:
--------------------------------------------------------------------------------
1 | openssl=$(brew --prefix openssl)
2 | libyaml=$(brew --prefix libyaml)
3 | export CFLAGS="-I$openssl/include -I$libyaml/include"
4 | export LDFLAGS="-L$openssl/lib -L$libyaml/lib"
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build
2 | /cover
3 | /deps
4 | erl_crash.dump
5 | *.ez
6 | /mnesia
7 | /logs
8 | /config/ejabberd.yml
9 | /doc
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 |
3 | matrix:
4 | include:
5 | - otp_release: 18.3
6 | elixir: 1.3
7 | - otp_release: 19.3
8 | elixir: 1.3
9 | - otp_release: 18.3
10 | elixir: 1.4
11 | - otp_release: 19.3
12 | elixir: 1.4
13 | - otp_release: 20.2
14 | elixir: 1.4
15 | - otp_release: 19.3
16 | elixir: 1.5
17 | - otp_release: 20.2
18 | elixir: 1.5
19 | - otp_release: 20.2
20 | elixir: 1.6
21 |
22 | env:
23 | global:
24 | - MIX_ENV=test
25 | - TRAVIS=false
26 |
27 | sudo: false
28 |
29 | install:
30 | - mix local.hex --force
31 | - mix local.rebar --force
32 | - mix deps.get
33 |
34 | before_script:
35 | - mix compile
36 | - mix ejabberd.gen.config
37 |
38 | after_script:
39 | - mix coveralls.travis
40 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.4.0
4 |
5 | - Enhancements
6 | - [Romeo.Stanza.join/3] adds options for specifying MUC room password and
7 | history options.
8 |
9 | ## v0.3.0
10 |
11 | - Backwards incompatible changes
12 | - Removed `payload` key in favor of `xml` in the `Message`, `Presence`, and
13 | `IQ` stanzas. The full `xmlel` record is now stored in the `xml` key. This
14 | allows easy access via the functions in `Romeo.XML` module.
15 | - Messages generated with `Romeo.Stanza.message/3` no longer escape the body
16 | by default.
17 |
18 | ## v0.2.0
19 |
20 | - Enhancements
21 | - [Romeo.JID] Added `user/1` which returns the `user` portion of the JID.
22 | - [Romeo.JID] Added `server/1` which returns the `server` portion of the JID.
23 | - [Romeo.JID] Added `resource/1` which returns the `resource` portion of the JID.
24 | - [Romeo.JID] Added key `full` JID struct for convenient access to the full
25 | JID.
26 | - [Romeo.Stanza] Added a clause to `to_xml/1` for `%Romeo.Stanza.Message{}`.
27 | - [Romeo.XML] Added a clause to `encode!/1` to handle all stanza structs.
28 |
29 | - Backward incompatible changes
30 | - [Romeo.Connection] No longer sends stanza messages to the owner process
31 | until after the connection process has finished.
32 | - [Romeo.Connection] Stanzas sent to the owner process are now parsed into the
33 | matching stanza struct. The message tuple has changed from
34 | `{:stanza_received, stanza}` to `{:stanza, stanza}`.
35 |
36 | ## v0.1.0
37 |
38 | Initial release :tada:
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Sonny Scroggin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Romeo
2 |
3 | > XMPP Client
4 |
5 | [](https://travis-ci.org/scrogson/romeo)
6 | [](https://coveralls.io/github/scrogson/romeo?branch=master)
7 |
8 | ## Installation
9 |
10 | 1. Add romeo to your list of dependencies in `mix.exs`:
11 |
12 | ```elixir
13 | def deps do
14 | [{:romeo, "~> 0.7"}]
15 | end
16 | ```
17 |
18 | 2. Ensure romeo is started before your application:
19 |
20 | ```elixir
21 | def application do
22 | [applications: [:romeo]]
23 | end
24 | ```
25 |
26 | ## Usage
27 |
28 | ```elixir
29 | alias Romeo.Stanza
30 | alias Romeo.Connection, as: Conn
31 | alias Romeo.Roster
32 |
33 | # Minimum configuration options
34 | opts = [jid: "romeo@montague.lit", password: "iL0v3JuL137"]
35 |
36 | # Start the client
37 | {:ok, pid} = Conn.start_link(opts)
38 |
39 | # Send presence to the server
40 | :ok = Conn.send(pid, Stanza.presence)
41 |
42 | # Request your roster
43 | :ok = Conn.send(pid, Stanza.get_roster)
44 |
45 | # Join a chat room
46 | :ok = Conn.send(pid, Stanza.join("library@muc.montague.lit", "romeo"))
47 |
48 | # Send a message to the room
49 | msg = "See how she leans her cheek upon her hand! " <>
50 | "O that I were a glove upon that hand, that " <>
51 | "I might touch that cheek!"
52 | :ok = Conn.send(pid, Stanza.groupchat("library@muc.montague.lit", msg))
53 |
54 | # Get roster items as tuple of %Romeo.Roster.Items{} struct
55 | items = Roster.items(pid)
56 |
57 | # Add jid to roster
58 | :ok = Roster.add(pid, "juliet@capulet.lit")
59 |
60 | # Remove jid from roster
61 | :ok = Roster.remove(pid, "juliet@capulet.lit")
62 | ```
63 |
64 | ## Documentation
65 |
66 | Documentation is available on [hexdocs](http://hexdocs.pm/romeo/)
67 |
68 | ## Naming
69 |
70 | It follows the great tradition to use characters of William Shakespeare's Romeo
71 | and Juliet in the XMPP specifications.
72 |
73 | ## License
74 |
75 | The MIT License (MIT)
76 |
77 | Copyright (c) 2015 Sonny Scroggin
78 |
79 | Permission is hereby granted, free of charge, to any person obtaining a copy
80 | of this software and associated documentation files (the "Software"), to deal
81 | in the Software without restriction, including without limitation the rights
82 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
83 | copies of the Software, and to permit persons to whom the Software is
84 | furnished to do so, subject to the following conditions:
85 |
86 | The above copyright notice and this permission notice shall be included in all
87 | copies or substantial portions of the Software.
88 |
89 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
90 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
91 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
92 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
93 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
94 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
95 | SOFTWARE.
96 |
97 | ------------
98 |
99 | 
101 | **Image credit:** Henri-Pierre Picou [Public domain], via Wikimedia Commons
102 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :ex_unit, :assert_receive_timeout, 2000
4 |
5 | config :logger, level: :debug
6 |
7 | config :mnesia, dir: 'mnesia'
8 |
9 | config :sasl, sasl_error_logger: false
10 |
11 | config :ejabberd,
12 | file: "config/ejabberd.yml",
13 | log_path: "logs/ejabberd.log"
14 |
--------------------------------------------------------------------------------
/lib/mix/tasks/ejabberd.gen.config.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Ejabberd.Gen.Config do
2 | use Mix.Task
3 |
4 | @shortdoc "Generates an ejabberd.yml file"
5 |
6 | @moduledoc """
7 | Generates an ejabberd.yml file.
8 |
9 | mix ejabberd.gen.config
10 | """
11 |
12 | def run([]) do
13 | cwd = File.cwd!
14 | source = Path.join(cwd, "priv/templates/ejabberd.yml.eex")
15 | target = Path.join(cwd, "config/ejabberd.yml")
16 | contents = EEx.eval_file(source, cwd: cwd)
17 |
18 | Mix.Generator.create_file(target, contents)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/romeo.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo do
2 | use Application
3 |
4 | def start(_type, _args) do
5 | import Supervisor.Spec, warn: false
6 |
7 | children = [
8 | # worker(Romeo.Worker, [arg1, arg2, arg3]),
9 | ]
10 |
11 | opts = [strategy: :one_for_one, name: Romeo.Supervisor]
12 | Supervisor.start_link(children, opts)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/romeo/auth.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Auth do
2 | @moduledoc """
3 | Handles XMPP authentication mechanisms.
4 | """
5 |
6 | use Romeo.XML
7 | require Logger
8 |
9 | defmodule Mechanism do
10 | @doc "Authenticates using the supplied mechanism"
11 | @callback authenticate(String.t, Romeo.Connection.t) :: Romeo.Connection.t
12 | end
13 |
14 | defmodule Error do
15 | defexception [:message]
16 |
17 | def exception(mechanism) do
18 | msg = "Failed to authenticate using mechanism: #{inspect mechanism}"
19 | %Romeo.Auth.Error{message: msg}
20 | end
21 | end
22 |
23 | @doc """
24 | Authenticates the client using the configured preferred mechanism.
25 |
26 | If the preferred mechanism is not supported it will choose PLAIN.
27 | """
28 | def authenticate!(conn) do
29 | preferred = conn.preferred_auth_mechanisms
30 | mechanisms = conn.features.mechanisms
31 | preferred_mechanism(preferred, mechanisms) |> do_authenticate(conn)
32 | end
33 |
34 | def handshake!(%{transport: mod, password: password, stream_id: stream_id} = conn) do
35 | stanza =
36 | :crypto.hash(:sha, "#{stream_id}#{password}")
37 | |> Base.encode16(case: :lower)
38 | |> Stanza.handshake()
39 |
40 | conn
41 | |> mod.send(stanza)
42 | |> mod.recv(fn
43 | conn, xmlel(name: "handshake") ->
44 | conn
45 | _conn, xmlel(name: "stream:error") ->
46 | raise Romeo.Auth.Error, "handshake error"
47 | end)
48 | end
49 |
50 |
51 | defp do_authenticate(mechanism, conn) do
52 | {:ok, conn} =
53 | case mechanism do
54 | {name, mod} ->
55 | Logger.info fn -> "Authenticating with extension #{name} implemented by #{mod}" end
56 | mod.authenticate(name, conn)
57 | _ ->
58 | Logger.info fn -> "Authenticating with #{mechanism}" end
59 | authenticate_with(mechanism, conn)
60 | end
61 |
62 | case success?(conn) do
63 | {:ok, conn} -> conn
64 | {:error, _conn} -> raise Romeo.Auth.Error, mechanism
65 | end
66 | end
67 |
68 | defp authenticate_with("PLAIN", %{transport: mod} = conn) do
69 | [username, password] = get_client_credentials(conn)
70 | payload = <<0>> <> username <> <<0>> <> password
71 | mod.send(conn, Romeo.Stanza.auth("PLAIN", Romeo.Stanza.base64_cdata(payload)))
72 | end
73 |
74 |
75 | defp authenticate_with("ANONYMOUS", %{transport: mod} = conn) do
76 | conn |> mod.send(Romeo.Stanza.auth("ANONYMOUS"))
77 | end
78 |
79 | defp authenticate_with(mechanism_name, _conn) do
80 | raise """
81 | Romeo does not include an implementation for authentication mechanism #{inspect mechanism_name}.
82 | Please provide an implementation such as
83 |
84 | Romeo.Connection.start_link(preferred_auth_mechanisms: [{#{inspect mechanism_name}, SomeModule}])
85 |
86 | where `SomeModule` implements the Romeo.Auth.Mechanism behaviour.
87 | """
88 | end
89 |
90 | defp success?(%{transport: mod} = conn) do
91 | mod.recv(conn, fn conn, xmlel(name: name) ->
92 | case name do
93 | "success" ->
94 | Logger.info fn -> "Authenticated successfully" end
95 | {:ok, conn}
96 | "failure" ->
97 | {:error, conn}
98 | end
99 | end)
100 | end
101 |
102 | defp get_client_credentials(%{jid: jid, password: password}) do
103 | [Romeo.JID.parse(jid).user, password]
104 | end
105 |
106 | defp preferred_mechanism([], _), do: "PLAIN"
107 | defp preferred_mechanism([mechanism | tail], mechanisms) do
108 | case acceptable_mechanism?(mechanism, mechanisms) do
109 | true -> mechanism
110 | false -> preferred_mechanism(tail, mechanisms)
111 | end
112 | end
113 |
114 | defp acceptable_mechanism?({name, _mod}, mechanisms),
115 | do: acceptable_mechanism?(name, mechanisms)
116 | defp acceptable_mechanism?(mechanism, mechanisms),
117 | do: Enum.member?(mechanisms, mechanism)
118 | end
119 |
--------------------------------------------------------------------------------
/lib/romeo/connection.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Connection do
2 | @moduledoc false
3 |
4 | @timeout 5_000
5 | @default_transport Romeo.Transports.TCP
6 |
7 | alias Romeo.Connection.Features
8 |
9 | defstruct component: false,
10 | features: %Features{},
11 | host: nil,
12 | jid: nil,
13 | legacy_tls: false,
14 | nickname: "",
15 | owner: nil,
16 | parser: nil,
17 | password: nil,
18 | port: nil,
19 | preferred_auth_mechanisms: [],
20 | require_tls: false,
21 | resource: "",
22 | rooms: [],
23 | ssl_opts: [],
24 | socket: nil,
25 | socket_opts: [],
26 | stream_id: nil,
27 | timeout: nil,
28 | transport: nil
29 |
30 | use Connection
31 |
32 | require Logger
33 |
34 | ### PUBLIC API ###
35 |
36 | @doc """
37 | Start the connection process and connect to an XMPP server.
38 |
39 | ## Options
40 |
41 | * `:component` - Connect as an [XMPP Component][0] (default: `false`);
42 | * `:host` - Server hostname (default: inferred by the JID);
43 | * `:jid` - User jabber ID;
44 | * `:password` - User password;
45 | * `:port` - Server port (default: based on the transport);
46 | * `:require_tls` - Set to `false` if ssl should not be used (default: `true`);
47 | * `:ssl_opts` - A list of ssl options, see ssl docs;
48 | * `:socket_opts` - Options to be given to the underlying socket;
49 | * `:timeout` - Connect timeout in milliseconds (default: `#{@timeout}`);
50 | * `:transport` - Transport handles the protocol (default: `#{@default_transport}`);
51 |
52 | [0]: http://xmpp.org/extensions/xep-0114.html
53 | """
54 | def start_link(args, options \\ []) do
55 | args =
56 | args
57 | |> Keyword.put_new(:timeout, @timeout)
58 | |> Keyword.put_new(:transport, @default_transport)
59 | |> Keyword.put(:owner, self())
60 |
61 | Connection.start_link(__MODULE__, struct(__MODULE__, args), options)
62 | end
63 |
64 | @doc """
65 | Send a message via the underlying transport.
66 | """
67 | def send(pid, data) do
68 | Connection.call(pid, {:send, data})
69 | end
70 |
71 | @doc """
72 | Stop the process and disconnect.
73 |
74 | ## Options
75 |
76 | * `:timeout` - Call timeout (default: `#{@timeout}`)
77 | """
78 | @spec close(pid, Keyword.t) :: :ok
79 | def close(pid, opts \\ []) do
80 | Connection.call(pid, :close, opts[:timeout] || @timeout)
81 | end
82 |
83 | ## Connection callbacks
84 |
85 | def init(conn) do
86 | {:connect, :init, conn}
87 | end
88 |
89 | def connect(_, %{transport: transport, timeout: timeout} = conn) do
90 | case transport.connect(conn) do
91 | {:ok, conn} ->
92 | {:ok, conn}
93 | {:error, _} ->
94 | {:backoff, timeout, conn}
95 | end
96 | end
97 |
98 | def disconnect(info, %{socket: socket, transport: transport} = conn) do
99 | transport.disconnect(info, socket)
100 | {:connect, :reconnect, reset_connection(conn)}
101 | end
102 |
103 | defp reset_connection(conn) do
104 | %{conn | features: %Features{}, parser: nil, socket: nil}
105 | end
106 |
107 | def handle_call(_, _, %{socket: nil} = conn) do
108 | {:reply, {:error, :closed}, conn}
109 | end
110 | def handle_call({:send, data}, _, %{transport: transport} = conn) do
111 | case transport.send(conn, data) do
112 | {:ok, conn} ->
113 | {:reply, :ok, conn}
114 | {:error, _} = error ->
115 | {:disconnect, error, error, conn}
116 | end
117 | end
118 | def handle_call(:close, from, %{socket: socket, transport: transport} = conn) do
119 | transport.disconnect({:close, from}, socket)
120 | {:reply, :ok, conn}
121 | end
122 |
123 | def handle_info(info, %{owner: owner, transport: transport} = conn) do
124 | case transport.handle_message(info, conn) do
125 | {:ok, conn, :more} ->
126 | {:noreply, conn}
127 | {:ok, conn, stanza} ->
128 | stanza = Romeo.Stanza.Parser.parse(stanza)
129 | Kernel.send(owner, {:stanza, stanza})
130 | {:noreply, conn}
131 | {:error, _} = error ->
132 | {:disconnect, error, conn}
133 | :unknown ->
134 | Logger.debug fn ->
135 | [inspect(__MODULE__), ?\s, inspect(self()), " received message: " | inspect(info)]
136 | end
137 | {:noreply, conn}
138 | end
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/lib/romeo/connection/features.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Connection.Features do
2 | @moduledoc """
3 | Parses XMPP Stream features.
4 | """
5 |
6 | use Romeo.XML
7 |
8 | @type t :: %__MODULE__{}
9 | defstruct [
10 | amp?: false,
11 | compression?: false,
12 | registration?: false,
13 | stream_management?: false,
14 | tls?: false,
15 | mechanisms: []
16 | ]
17 |
18 | def parse_stream_features(features) do
19 | %__MODULE__{
20 | amp?: supports?(features, "amp"),
21 | compression?: supports?(features, "compression"),
22 | registration?: supports?(features, "register"),
23 | stream_management?: supports?(features, "sm"),
24 | tls?: supports?(features, "starttls"),
25 | mechanisms: supported_auth_mechanisms(features)
26 | }
27 | end
28 |
29 | def supported_auth_mechanisms(features) do
30 | case Romeo.XML.subelement(features, "mechanisms") do
31 | xml when Record.is_record(xml, :xmlel) ->
32 | mechanisms = xmlel(xml, :children)
33 | for mechanism <- mechanisms, into: [], do: Romeo.XML.cdata(mechanism)
34 | nil -> []
35 | end
36 | end
37 |
38 | def supports?(features, "compression") do
39 | case Romeo.XML.subelement(features, "compression") do
40 | xml when Record.is_record(xml, :xmlel) ->
41 | methods = xmlel(xml, :children)
42 | for method <- methods, into: [], do: Romeo.XML.cdata(method)
43 | _ -> false
44 | end
45 | end
46 | def supports?(features, feature) do
47 | case Romeo.XML.subelement(features, feature) do
48 | nil -> false
49 | _ -> true
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/romeo/error.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Error do
2 | defexception [:message]
3 |
4 | def exception(message) do
5 | %Romeo.Error{message: translate_message(message)}
6 | end
7 |
8 | defp translate_message({:timeout, ms, connection_step}) do
9 | step = translate_connection_step(connection_step)
10 | secs = ms / 1_000
11 | "Failed to #{step} after #{secs} seconds."
12 | end
13 | defp translate_message(message), do: inspect(message)
14 |
15 | defp translate_connection_step(atom) do
16 | Atom.to_string(atom) |> String.replace("_", " ")
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/romeo/jid.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.JID do
2 | @moduledoc """
3 | Jabber Identifiers (JIDs) uniquely identify individual entities in an XMPP
4 | network.
5 |
6 | A JID often resembles an email address with a user@host form, but there's
7 | a bit more to it. JIDs consist of three main parts:
8 |
9 | A JID can be composed of a local part, a server part, and a resource part.
10 | The server part is mandatory for all JIDs, and can even stand alone
11 | (e.g., as the address for a server).
12 |
13 | The combination of a local (user) part and a server is called a "bare JID",
14 | and it is used to identitfy a particular account on a server.
15 |
16 | A JID that includes a resource is called a "full JID", and it is used to
17 | identify a particular client connection (i.e., a specific connection for the
18 | associated "bare JID" account).
19 |
20 | This module implements the `to_string/1` for the `String.Chars` protocol for
21 | returning a binary string from the `JID` struct.
22 |
23 | Returns a string representation from a JID struct.
24 |
25 | ## Examples
26 | iex> to_string(%Romeo.JID{user: "romeo", server: "montague.lit", resource: "chamber"})
27 | "romeo@montague.lit/chamber"
28 |
29 | iex> to_string(%Romeo.JID{user: "romeo", server: "montague.lit"})
30 | "romeo@montague.lit"
31 |
32 | iex> to_string(%Romeo.JID{server: "montague.lit"})
33 | "montague.lit"
34 | """
35 |
36 | alias Romeo.JID
37 |
38 | @type t :: %__MODULE__{}
39 | defstruct user: "", server: "", resource: "", full: ""
40 |
41 | defimpl String.Chars, for: JID do
42 | def to_string(%JID{user: "", server: server, resource: ""}), do: server
43 | def to_string(%JID{user: user, server: server, resource: ""}) do
44 | user <> "@" <> server
45 | end
46 | def to_string(%JID{user: user, server: server, resource: resource}) do
47 | user <> "@" <> server <> "/" <> resource
48 | end
49 | end
50 |
51 | @doc """
52 | Returns a binary JID without a resource.
53 |
54 | ## Examples
55 | iex> Romeo.JID.bare(%Romeo.JID{user: "romeo", server: "montague.lit", resource: "chamber"})
56 | "romeo@montague.lit"
57 |
58 | iex> Romeo.JID.bare("romeo@montague.lit/chamber")
59 | "romeo@montague.lit"
60 | """
61 | @spec bare(jid :: binary | JID.t) :: binary
62 | def bare(jid) when is_binary(jid), do: parse(jid) |> bare
63 | def bare(%JID{} = jid), do: to_string(%JID{jid | resource: ""})
64 |
65 | @spec user(jid :: binary | JID.t) :: binary
66 | def user(jid) when is_binary(jid), do: parse(jid).user
67 | def user(%JID{user: user}), do: user
68 |
69 | @spec server(jid :: binary | JID.t) :: binary
70 | def server(jid) when is_binary(jid), do: parse(jid).server
71 | def server(%JID{server: server}), do: server
72 |
73 | @spec resource(jid :: binary | JID.t) :: binary
74 | def resource(jid) when is_binary(jid), do: parse(jid).resource
75 | def resource(%JID{resource: resource}), do: resource
76 |
77 | @doc """
78 | Parses a binary string JID into a JID struct.
79 |
80 | ## Examples
81 | iex> Romeo.JID.parse("romeo@montague.lit/chamber")
82 | %Romeo.JID{user: "romeo", server: "montague.lit", resource: "chamber", full: "romeo@montague.lit/chamber"}
83 |
84 | iex> Romeo.JID.parse("romeo@montague.lit")
85 | %Romeo.JID{user: "romeo", server: "montague.lit", resource: "", full: "romeo@montague.lit"}
86 | """
87 | @spec parse(jid :: binary) :: JID.t
88 | def parse(string) do
89 | case String.split(string, ["@", "/"], parts: 3) do
90 | [user, server, resource] ->
91 | %JID{user: user, server: server, resource: resource, full: string}
92 | [user, server] ->
93 | %JID{user: user, server: server, full: string}
94 | [server] ->
95 | %JID{server: server, full: string}
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/romeo/roster.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Roster do
2 | @moduledoc """
3 | Helper for work with roster
4 | """
5 | @timeout 5_000
6 |
7 | use Romeo.XML
8 | alias Romeo.Connection, as: Conn
9 | alias Romeo.Stanza
10 | require Logger
11 |
12 | @doc """
13 | Returns roster items
14 | """
15 | def items(pid) do
16 | Logger.info fn -> "Getting roster items" end
17 | stanza = Stanza.get_roster
18 | case send_stanza(pid, stanza) do
19 | {:ok, %IQ{type: "result"} = result} -> result.items
20 | _ -> :error
21 | end
22 | end
23 |
24 | @doc """
25 | Adds jid to roster
26 | """
27 | def add(pid, jid) do
28 | Logger.info fn -> "Adding #{jid} to roster items" end
29 | stanza = Stanza.set_roster_item(jid)
30 | case send_stanza(pid, stanza) do
31 | {:ok, _} -> :ok
32 | _ -> :error
33 | end
34 | end
35 | def add(pid, jid, name) do
36 | Logger.info fn -> "Adding #{jid} to roster items" end
37 | stanza = Stanza.set_roster_item(jid, "both", name)
38 | case send_stanza(pid, stanza) do
39 | {:ok, _} -> :ok
40 | _ -> :error
41 | end
42 | end
43 |
44 |
45 | @doc """
46 | Removes jid to roster
47 | """
48 | def remove(pid, jid) do
49 | Logger.info fn -> "Removing #{jid} from roster items" end
50 | stanza = Stanza.set_roster_item(jid, "remove")
51 | case send_stanza(pid, stanza) do
52 | {:ok, _} -> :ok
53 | _ -> :error
54 | end
55 | end
56 |
57 | defp send_stanza(pid, stanza) do
58 | id = Romeo.XML.attr(stanza, "id")
59 | :ok = Conn.send(pid, stanza)
60 |
61 | receive do
62 | {:stanza, %IQ{type: "result", id: ^id} = result} -> {:ok, result}
63 | {:stanza, %IQ{type: "error", id: ^id}} -> :error
64 | after @timeout ->
65 | {:error, :timeout}
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/romeo/roster/item.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Roster.Item do
2 | @type jid :: Romeo.JID.t
3 |
4 | @type t :: %__MODULE__{
5 | subscription: binary,
6 | name: binary,
7 | jid: jid,
8 | group: binary,
9 | xml: tuple
10 | }
11 |
12 | defstruct [
13 | subscription: nil,
14 | name: nil,
15 | jid: nil,
16 | group: nil,
17 | xml: nil
18 | ]
19 | end
20 |
--------------------------------------------------------------------------------
/lib/romeo/stanza.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Stanza do
2 | @moduledoc """
3 | Provides convenience functions for building XMPP stanzas.
4 | """
5 |
6 | use Romeo.XML
7 |
8 | @doc """
9 | Converts an `xml` record to an XML binary string.
10 | """
11 | def to_xml(record) when Record.is_record(record) do
12 | Romeo.XML.encode!(record)
13 | end
14 | def to_xml(record) when Record.is_record(record) do
15 | Romeo.XML.encode!(record)
16 | end
17 |
18 | def to_xml(%IQ{} = stanza) do
19 | xmlel(name: "iq",
20 | attrs: [
21 | {"to", to_string(stanza.to)},
22 | {"type", stanza.type},
23 | {"id", stanza.id}
24 | ]
25 | ) |> to_xml()
26 | end
27 |
28 | def to_xml(%Presence{} = stanza) do
29 | xmlel(name: "presence",
30 | attrs: [
31 | {"to", to_string(stanza.to)},
32 | {"type", stanza.type}
33 | ]
34 | ) |> to_xml()
35 | end
36 |
37 | def to_xml(%Message{to: to, type: type, body: body}) do
38 | message(to_string(to), type, body) |> to_xml()
39 | end
40 |
41 | @doc """
42 | Starts an XML stream.
43 |
44 | ## Example
45 |
46 | iex> stanza = Romeo.Stanza.start_stream("im.capulet.lit")
47 | {:xmlstreamstart, "stream:stream",
48 | [{"to", "im.capulet.lit"}, {"version", "1.0"}, {"xml:lang", "en"},
49 | {"xmlns", "jabber:client"},
50 | {"xmlns:stream", "http://etherx.jabber.org/streams"}]}
51 | iex> Romeo.Stanza.to_xml(stanza)
52 | ""
53 | """
54 | def start_stream(server, xmlns \\ ns_jabber_client()) do
55 | xmlstreamstart(name: "stream:stream",
56 | attrs: [
57 | {"to", server},
58 | {"version", "1.0"},
59 | {"xml:lang", "en"},
60 | {"xmlns", xmlns},
61 | {"xmlns:stream", ns_xmpp()}
62 | ])
63 | end
64 |
65 | @doc """
66 | Ends the XML stream
67 |
68 | ## Example
69 | iex> stanza = Romeo.Stanza.end_stream
70 | {:xmlstreamend, "stream:stream"}
71 | iex> Romeo.Stanza.to_xml(stanza)
72 | ""
73 | """
74 | def end_stream, do: xmlstreamend(name: "stream:stream")
75 |
76 | @doc """
77 | Generates the XML to start TLS.
78 |
79 | ## Example
80 | iex> stanza = Romeo.Stanza.start_tls
81 | {:xmlel, "starttls", [{"xmlns", "urn:ietf:params:xml:ns:xmpp-tls"}], []}
82 | iex> Romeo.Stanza.to_xml(stanza)
83 | ""
84 | """
85 | def start_tls do
86 | xmlel(name: "starttls",
87 | attrs: [
88 | {"xmlns", ns_tls()}
89 | ])
90 | end
91 |
92 | def compress(method) do
93 | xmlel(name: "compress",
94 | attrs: [
95 | {"xmlns", ns_compress()}
96 | ],
97 | children: [
98 | xmlel(name: "method", children: [cdata(method)])
99 | ])
100 | end
101 |
102 | def handshake(hash) do
103 | cdata = xmlcdata(content: hash)
104 | xmlel(name: "handshake", children: [cdata])
105 | end
106 |
107 | def auth(mechanism), do: auth(mechanism, [], [])
108 | def auth(mechanism, body) do
109 | auth(mechanism, body, [])
110 | end
111 | def auth(mechanism, [], []) do
112 | xmlel(name: "auth",
113 | attrs: [
114 | {"xmlns", ns_sasl},
115 | {"mechanism", mechanism},
116 | ],
117 | children: [])
118 | end
119 | def auth(mechanism, body, []) do
120 | xmlel(name: "auth",
121 | attrs: [
122 | {"xmlns", ns_sasl},
123 | {"mechanism", mechanism},
124 | ],
125 | children: [body])
126 | end
127 | def auth(mechanism, body, additional_attrs) do
128 | xmlel(name: "auth",
129 | attrs: [
130 | {"xmlns", ns_sasl()},
131 | {"mechanism", mechanism}
132 | | additional_attrs
133 | ],
134 | children: [body])
135 | end
136 |
137 | def bind(resource) do
138 | body = xmlel(name: "bind",
139 | attrs: [{"xmlns", ns_bind()}],
140 | children: [
141 | xmlel(name: "resource",
142 | children: [cdata(resource)])
143 | ])
144 | iq("set", body)
145 | end
146 |
147 | def session do
148 | iq("set", xmlel(name: "session", attrs: [{"xmlns", ns_session()}]))
149 | end
150 |
151 | def presence do
152 | xmlel(name: "presence")
153 | end
154 |
155 | def presence(type) do
156 | xmlel(name: "presence", attrs: [{"type", type}])
157 | end
158 |
159 | @doc """
160 | Returns a presence stanza to a given jid, of a given type.
161 | """
162 | def presence(to, type) do
163 | xmlel(name: "presence", attrs: [{"type", type}, {"to", to}])
164 | end
165 |
166 | def iq(type, body) do
167 | xmlel(name: "iq", attrs: [{"type", type}, {"id", id()}], children: [body])
168 | end
169 |
170 | def iq(to, type, body) do
171 | iq = iq(type, body)
172 | xmlel(iq, attrs: [{"to", to}|xmlel(iq, :attrs)])
173 | end
174 |
175 | def get_roster do
176 | iq("get", xmlel(name: "query", attrs: [{"xmlns", ns_roster()}]))
177 | end
178 |
179 | def set_roster_item(jid, subscription \\ "both", name \\ "", group \\ "") do
180 | name_to_set =
181 | case name do
182 | "" -> Romeo.JID.parse(jid).user
183 | _ -> name
184 | end
185 | group_xmlel =
186 | case group do
187 | "" -> []
188 | _ -> [xmlel(name: "group", children: [cdata(group)])]
189 | end
190 | iq("set", xmlel(
191 | name: "query",
192 | attrs: [{"xmlns", ns_roster()}],
193 | children: [
194 | xmlel(name: "item", attrs: [
195 | {"jid", jid},
196 | {"subscription", subscription},
197 | {"name", name_to_set}
198 | ], children: group_xmlel)
199 | ]
200 | ))
201 | end
202 |
203 | def get_inband_register do
204 | iq("get", xmlel(name: "query", attrs: [{"xmlns", ns_inband_register()}]))
205 | end
206 |
207 | def set_inband_register(username, password) do
208 | iq("set", xmlel(
209 | name: "query",
210 | attrs: [{"xmlns", ns_inband_register()}],
211 | children: [
212 | xmlel(name: "username", children: [cdata(username)]),
213 | xmlel(name: "password", children: [cdata(password)])
214 | ]
215 | ))
216 | end
217 |
218 | def get_vcard(to) do
219 | iq(to, "get", xmlel(name: "vCard", attrs: [{"xmlns", ns_vcard()}]))
220 | end
221 |
222 | def disco_info(to) do
223 | iq(to, "get", xmlel(name: "query", attrs: [{"xmlns", ns_disco_info()}]))
224 | end
225 |
226 | def disco_items(to) do
227 | iq(to, "get", xmlel(name: "query", attrs: [{"xmlns", ns_disco_items()}]))
228 | end
229 |
230 | @doc """
231 | Generates a stanza to join a pubsub node. (XEP-0060)
232 | """
233 | def subscribe(to, node, jid) do
234 | iq(to, "set", xmlel(
235 | name: "pubsub",
236 | attrs: [{"xmlns", ns_pubsub()}],
237 | children: [
238 | xmlel(name: "subscribe", attrs: [{"node", node}, {"jid", jid}])
239 | ]))
240 | end
241 |
242 | @doc """
243 | Generates a presence stanza to join a MUC room.
244 |
245 | ## Options
246 |
247 | * `password` - the password for a MUC room - if required.
248 | * `history` - used for specifying the amount of old messages to receive once
249 | joined. The value of the `:history` option should be a keyword list of one
250 | of the following:
251 | * `maxchars` - limit the total number of characters in the history.
252 | * `maxstanzas` - limit the total number of messages in the history.
253 | * `seconds` - send only the messages received in the last `n` seconds.
254 | * `since` - send only the messages received since the UTC datetime specified.
255 | See http://xmpp.org/extensions/xep-0045.html#enter-managehistory
256 | for details.
257 |
258 | ## Examples
259 | iex> Romeo.Stanza.join("lobby@muc.localhost", "hedwigbot")
260 | {:xmlel, "presence", [{"to", "lobby@muc.localhost/hedwigbot"}],
261 | [{:xmlel, "x", [{"xmlns", "http://jabber.org/protocol/muc"}],
262 | [{:xmlel, "history", [{"maxstanzas", "0"}], []}]}]}
263 | """
264 | def join(room, nickname, opts \\ []) do
265 | history = Keyword.get(opts, :history)
266 | password = Keyword.get(opts, :password)
267 |
268 | password = if password, do: [muc_password(password)], else: []
269 | history = if history, do: [history(history)], else: [history(maxstanzas: 0)]
270 |
271 | children = history ++ password
272 |
273 | xmlel(name: "presence",
274 | attrs: [
275 | {"to", "#{room}/#{nickname}"}
276 | ],
277 | children: [
278 | xmlel(name: "x",
279 | attrs: [{"xmlns", ns_muc()}],
280 | children: children)
281 | ])
282 | end
283 |
284 | defp history([{key, value}]) do
285 | xmlel(name: "history", attrs: [{to_string(key), to_string(value)}])
286 | end
287 |
288 | defp muc_password(password) do
289 | xmlel(name: "password", children: [xmlcdata(content: password)])
290 | end
291 |
292 | def chat(to, body), do: message(to, "chat", body)
293 | def normal(to, body), do: message(to, "normal", body)
294 | def groupchat(to, body), do: message(to, "groupchat", body)
295 |
296 | def message(msg) when is_map(msg) do
297 | message(msg["to"], msg["type"], msg["body"])
298 | end
299 | def message(to, type, message) do
300 | xmlel(name: "message",
301 | attrs: [
302 | {"to", to},
303 | {"type", type},
304 | {"id", id()},
305 | {"xml:lang", "en"}
306 | ],
307 | children: generate_body(message))
308 | end
309 |
310 | def generate_body(data) do
311 | cond do
312 | is_list(data) ->
313 | data
314 | is_tuple(data) ->
315 | [data]
316 | true ->
317 | [body(data)]
318 | end
319 | end
320 |
321 | def body(data) do
322 | xmlel(name: "body",
323 | children: [
324 | cdata(data)
325 | ])
326 | end
327 |
328 | def xhtml_im(data) when is_binary(data) do
329 | data
330 | |> :fxml_stream.parse_element
331 | |> xhtml_im()
332 | end
333 | def xhtml_im(data) do
334 | xmlel(name: "html",
335 | attrs: [
336 | {"xmlns", ns_xhtml_im()}
337 | ],
338 | children: [
339 | xmlel(name: "body",
340 | attrs: [
341 | {"xmlns", ns_xhtml()}
342 | ],
343 | children: [
344 | data
345 | ])
346 | ])
347 | end
348 |
349 | def cdata(payload) do
350 | xmlcdata(content: payload)
351 | end
352 |
353 | def base64_cdata(payload) do
354 | xmlcdata(content: Base.encode64(payload))
355 | end
356 |
357 | @doc """
358 | Generates a random hex string for use as an id for a stanza.
359 | """
360 | def id do
361 | :crypto.strong_rand_bytes(2) |> Base.encode16(case: :lower)
362 | end
363 | end
364 |
--------------------------------------------------------------------------------
/lib/romeo/stanza/iq.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Stanza.IQ do
2 | use Romeo.XML
3 |
4 | @type jid :: Romeo.JID.t
5 |
6 | @type t :: %__MODULE__{
7 | id: binary,
8 | to: jid,
9 | from: jid,
10 | type: binary,
11 | items: list | nil,
12 | xml: tuple
13 | }
14 |
15 | defstruct [
16 | id: nil,
17 | to: nil,
18 | from: nil,
19 | type: nil,
20 | items: nil,
21 | xml: nil
22 | ]
23 | end
24 |
--------------------------------------------------------------------------------
/lib/romeo/stanza/message.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Stanza.Message do
2 | use Romeo.XML
3 |
4 | @type jid :: Romeo.JID.t
5 |
6 | @type t :: %__MODULE__{
7 | id: binary,
8 | to: jid,
9 | from: jid,
10 | type: binary,
11 | body: binary | list,
12 | html: binary | list | nil,
13 | xml: tuple,
14 | delayed?: boolean
15 | }
16 |
17 | defstruct [
18 | id: "",
19 | to: nil,
20 | from: nil,
21 | type: "normal",
22 | body: "",
23 | html: nil,
24 | xml: nil,
25 | delayed?: false
26 | ]
27 | end
28 |
--------------------------------------------------------------------------------
/lib/romeo/stanza/parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Stanza.Parser do
2 | @moduledoc """
3 | Parses XML records into related structs.
4 | """
5 | use Romeo.XML
6 | import Romeo.XML
7 |
8 | alias Romeo.Roster.Item
9 |
10 | def parse(xmlel(name: "message", attrs: attrs) = stanza) do
11 | struct(Message, parse_attrs(attrs))
12 | |> struct([body: get_body(stanza)])
13 | |> struct([html: get_html(stanza)])
14 | |> struct([xml: stanza])
15 | |> struct([delayed?: delayed?(stanza)])
16 | end
17 |
18 | def parse(xmlel(name: "presence", attrs: attrs) = stanza) do
19 | struct(Presence, parse_attrs(attrs))
20 | |> struct([show: get_show(stanza)])
21 | |> struct([status: get_status(stanza)])
22 | |> struct([xml: stanza])
23 | end
24 |
25 | def parse(xmlel(name: "iq", attrs: attrs) = stanza) do
26 | case :fxml.get_path_s(stanza, [{:elem, "query"}, {:attr, "xmlns"}]) do
27 | "jabber:iq:roster" ->
28 | struct(IQ, parse_attrs(attrs))
29 | |> struct([items: (Romeo.XML.subelement(stanza, "query") |> parse)])
30 | |> struct([xml: stanza])
31 | _ -> struct(IQ, parse_attrs(attrs)) |> struct([xml: stanza])
32 | end
33 | end
34 |
35 | def parse(xmlel(name: "query") = stanza) do
36 | stanza |> Romeo.XML.subelements("item") |> Enum.map(&parse/1) |> Enum.reverse
37 | end
38 |
39 | def parse(xmlel(name: "item", attrs: attrs) = stanza) do
40 | struct(Item, parse_attrs(attrs))
41 | |> struct([group: get_group(stanza)])
42 | |> struct([xml: stanza])
43 | end
44 |
45 | def parse(xmlel(name: name, attrs: attrs) = stanza) do
46 | [name: name]
47 | |> Keyword.merge(parse_attrs(attrs))
48 | |> Keyword.merge([xml: stanza])
49 | |> Enum.into(%{})
50 | end
51 |
52 | def parse(xmlcdata(content: content)), do: content
53 |
54 | def parse(stanza), do: stanza
55 |
56 | defp parse_attrs([]), do: []
57 | defp parse_attrs(attrs) do
58 | parse_attrs(attrs, [])
59 | end
60 | defp parse_attrs([{k,v}|rest], acc) do
61 | parse_attrs(rest, [parse_attr({k,v})|acc])
62 | end
63 | defp parse_attrs([], acc), do: acc
64 |
65 | defp parse_attr({key, value}) when key in ["to", "from", "jid"] do
66 | {String.to_atom(key), Romeo.JID.parse(value)}
67 | end
68 | defp parse_attr({key, value}) do
69 | {String.to_atom(key), value}
70 | end
71 |
72 | defp get_body(stanza), do: subelement(stanza, "body") |> cdata
73 | defp get_html(stanza), do: subelement(stanza, "html")
74 |
75 | defp get_show(stanza), do: subelement(stanza, "show") |> cdata
76 | defp get_status(stanza), do: subelement(stanza, "status") |> cdata
77 |
78 | defp get_group(stanza), do: subelement(stanza, "group") |> cdata
79 |
80 | defp delayed?(xmlel(children: children)) do
81 | Enum.any? children, fn child ->
82 | elem(child, 1) == "delay" || elem(child, 1) == "x" &&
83 | attr(child, "xmlns") == "jabber:x:delay"
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/romeo/stanza/presence.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Stanza.Presence do
2 | use Romeo.XML
3 |
4 | @type jid :: Romeo.JID.t
5 |
6 | @type t :: %__MODULE__{
7 | id: binary,
8 | to: jid,
9 | from: jid,
10 | type: binary,
11 | show: binary | nil,
12 | status: binary | nil,
13 | xml: tuple
14 | }
15 |
16 | defstruct [
17 | id: nil,
18 | to: nil,
19 | from: nil,
20 | type: nil,
21 | show: nil,
22 | status: nil,
23 | xml: nil
24 | ]
25 | end
26 |
--------------------------------------------------------------------------------
/lib/romeo/transports/tcp.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Transports.TCP do
2 | @moduledoc false
3 |
4 | @default_port 5222
5 | @ssl_opts [reuse_sessions: true]
6 | @socket_opts [packet: :raw, mode: :binary, active: :once]
7 | @ns_jabber_client Romeo.XMLNS.ns_jabber_client
8 | @ns_component_accept Romeo.XMLNS.ns_component_accept
9 |
10 | @type state :: Romeo.Connection.t
11 |
12 | use Romeo.XML
13 |
14 | alias Romeo.Connection.Features
15 | alias Romeo.Connection, as: Conn
16 |
17 | require Logger
18 |
19 | import Kernel, except: [send: 2]
20 |
21 | @spec connect(Keyword.t) :: {:ok, state} | {:error, any}
22 | def connect(%Conn{host: host, port: port, socket_opts: socket_opts, legacy_tls: tls} = conn) do
23 | host = (host || host(conn.jid)) |> to_charlist()
24 | port = (port || @default_port)
25 |
26 | conn = %{conn | host: host, port: port, socket_opts: socket_opts}
27 |
28 | case :gen_tcp.connect(host, port, socket_opts ++ @socket_opts, conn.timeout) do
29 | {:ok, socket} ->
30 | Logger.info fn -> "Established connection to #{host}" end
31 | parser = :fxml_stream.new(self(), :infinity, [:no_gen_server])
32 | conn = %{conn | parser: parser, socket: {:gen_tcp, socket}}
33 | conn = if tls, do: upgrade_to_tls(conn), else: conn
34 | start_protocol(conn)
35 | {:error, _} = error ->
36 | error
37 | end
38 | end
39 |
40 | def disconnect(info, {mod, socket}) do
41 | :ok = mod.close(socket)
42 | case info do
43 | {:close, from} ->
44 | Connection.reply(from, :ok)
45 | {:error, :closed} ->
46 | :error_logger.format("Connection closed~n", [])
47 | {:error, reason} ->
48 | reason = :inet.format_error(reason)
49 | :error_logger.format("Connection error: ~s~n", [reason])
50 | end
51 | end
52 |
53 | defp start_protocol(%Conn{component: true} = conn) do
54 | conn
55 | |> start_stream(@ns_component_accept)
56 | |> handshake()
57 | |> ready()
58 | end
59 |
60 | defp start_protocol(%Conn{} = conn) do
61 | conn
62 | |> start_stream(@ns_jabber_client)
63 | |> negotiate_features()
64 | |> maybe_start_tls()
65 | |> authenticate()
66 | |> bind()
67 | |> session()
68 | |> ready()
69 | end
70 |
71 | defp start_stream(%Conn{jid: jid} = conn, xmlns \\ @ns_jabber_client) do
72 | conn
73 | |> send(jid |> host |> Romeo.Stanza.start_stream(xmlns))
74 | |> recv(fn conn, xmlstreamstart(attrs: attrs) ->
75 | {"id", id} = List.keyfind(attrs, "id", 0)
76 | %{conn | stream_id: id}
77 | end)
78 | end
79 |
80 | defp negotiate_features(%Conn{} = conn) do
81 | recv(conn, fn conn, xmlel(name: "stream:features") = packet ->
82 | %{conn | features: Features.parse_stream_features(packet)}
83 | end)
84 | end
85 |
86 | defp maybe_start_tls(%Conn{features: %Features{tls?: true}} = conn) do
87 | conn
88 | |> send(Stanza.start_tls)
89 | |> recv(fn conn, xmlel(name: "proceed") -> conn end)
90 | |> upgrade_to_tls
91 | |> start_stream
92 | |> negotiate_features
93 | end
94 | defp maybe_start_tls(%Conn{} = conn), do: conn
95 |
96 | defp upgrade_to_tls(%Conn{parser: parser, socket: {:gen_tcp, socket}} = conn) do
97 | Logger.info fn -> "Negotiating secure connection" end
98 |
99 | {:ok, socket} = :ssl.connect(socket, conn.ssl_opts ++ @ssl_opts)
100 | parser = :fxml_stream.reset(parser)
101 |
102 | Logger.info fn -> "Connection successfully secured" end
103 | %{conn | socket: {:ssl, socket}, parser: parser}
104 | end
105 |
106 | defp authenticate(%Conn{} = conn) do
107 | conn
108 | |> Romeo.Auth.authenticate!
109 | |> reset_parser
110 | |> start_stream
111 | |> negotiate_features
112 | end
113 |
114 | defp handshake(%Conn{} = conn) do
115 | Romeo.Auth.handshake!(conn)
116 | end
117 |
118 | defp bind(%Conn{owner: owner, resource: resource} = conn) do
119 | stanza = Romeo.Stanza.bind(resource)
120 | id = Romeo.XML.attr(stanza, "id")
121 |
122 | conn
123 | |> send(stanza)
124 | |> recv(fn conn, xmlel(name: "iq") = stanza ->
125 | "result" = Romeo.XML.attr(stanza, "type")
126 | ^id = Romeo.XML.attr(stanza, "id")
127 |
128 | %Romeo.JID{resource: resource} =
129 | stanza
130 | |> Romeo.XML.subelement("bind")
131 | |> Romeo.XML.subelement("jid")
132 | |> Romeo.XML.cdata
133 | |> Romeo.JID.parse
134 |
135 | Logger.info fn -> "Bound to resource: #{resource}" end
136 | Kernel.send(owner, {:resource_bound, resource})
137 | %{conn | resource: resource}
138 | end)
139 | end
140 |
141 | defp session(%Conn{} = conn) do
142 | stanza = Romeo.Stanza.session
143 | id = Romeo.XML.attr(stanza, "id")
144 |
145 | conn
146 | |> send(stanza)
147 | |> recv(fn conn, xmlel(name: "iq") = stanza ->
148 | "result" = Romeo.XML.attr(stanza, "type")
149 | ^id = Romeo.XML.attr(stanza, "id")
150 |
151 | Logger.info fn -> "Session established" end
152 | conn
153 | end)
154 | end
155 |
156 | defp ready(%Conn{owner: owner} = conn) do
157 | Kernel.send(owner, :connection_ready)
158 | {:ok, conn}
159 | end
160 |
161 | defp reset_parser(%Conn{parser: parser} = conn) do
162 | parser = :fxml_stream.reset(parser)
163 | %{conn | parser: parser}
164 | end
165 |
166 | defp parse_data(%Conn{jid: jid, parser: parser} = conn, data) do
167 | Logger.debug fn -> "[#{jid}][INCOMING] #{inspect data}" end
168 |
169 | parser = :fxml_stream.parse(parser, data)
170 |
171 | stanza =
172 | case receive_stanza() do
173 | :more -> :more
174 | stanza -> stanza
175 | end
176 |
177 | {:ok, %{conn | parser: parser}, stanza}
178 | end
179 |
180 | defp receive_stanza(timeout \\ 10) do
181 | receive do
182 | {:xmlstreamstart, _, _} = stanza -> stanza
183 | {:xmlstreamend, _} = stanza -> stanza
184 | {:xmlstreamraw, stanza} -> stanza
185 | {:xmlstreamcdata, stanza} -> stanza
186 | {:xmlstreamerror, _} = stanza -> stanza
187 | {:xmlstreamelement, stanza} -> stanza
188 | after timeout ->
189 | :more
190 | end
191 | end
192 |
193 | def send(%Conn{jid: jid, socket: {mod, socket}} = conn, stanza) do
194 | stanza = Romeo.XML.encode!(stanza)
195 | Logger.debug fn -> "[#{jid}][OUTGOING] #{inspect stanza}" end
196 | :ok = mod.send(socket, stanza)
197 | {:ok, conn}
198 | end
199 |
200 | def recv({:ok, conn}, fun), do: recv(conn, fun)
201 | def recv(%Conn{socket: {:gen_tcp, socket}, timeout: timeout} = conn, fun) do
202 | receive do
203 | {:xmlstreamelement, stanza} ->
204 | fun.(conn, stanza)
205 | {:tcp, ^socket, data} ->
206 | :ok = activate({:gen_tcp, socket})
207 | if whitespace_only?(data) do
208 | conn
209 | else
210 | {:ok, conn, stanza} = parse_data(conn, data)
211 | fun.(conn, stanza)
212 | end
213 | {:tcp_closed, ^socket} ->
214 | {:error, :closed}
215 | {:tcp_error, ^socket, reason} ->
216 | {:error, reason}
217 | after timeout ->
218 | Kernel.send(self(), {:error, :timeout})
219 | conn
220 | end
221 | end
222 | def recv(%Conn{socket: {:ssl, socket}, timeout: timeout} = conn, fun) do
223 | receive do
224 | {:xmlstreamelement, stanza} ->
225 | fun.(conn, stanza)
226 | {:ssl, ^socket, " "} ->
227 | :ok = activate({:ssl, socket})
228 | conn
229 | {:ssl, ^socket, data} ->
230 | :ok = activate({:ssl, socket})
231 |
232 | if whitespace_only?(data) do
233 | conn
234 | else
235 | {:ok, conn, stanza} = parse_data(conn, data)
236 | fun.(conn, stanza)
237 | end
238 | {:ssl_closed, ^socket} ->
239 | {:error, :closed}
240 | {:ssl_error, ^socket, reason} ->
241 | {:error, reason}
242 | after timeout ->
243 | Kernel.send(self(), {:error, :timeout})
244 | conn
245 | end
246 | end
247 |
248 | def handle_message({:tcp, socket, data}, %{socket: {:gen_tcp, socket}} = conn) do
249 | {:ok, _, _} = handle_data(data, conn)
250 | end
251 | def handle_message({:xmlstreamelement, stanza}, conn) do
252 | {:ok, conn, stanza}
253 | end
254 | def handle_message({:tcp_closed, socket}, %{socket: {:gen_tcp, socket}}) do
255 | {:error, :closed}
256 | end
257 | def handle_message({:tcp_error, socket, reason}, %{socket: {:gen_tcp, socket}}) do
258 | {:error, reason}
259 | end
260 | def handle_message({:ssl, socket, data}, %{socket: {:ssl, socket}} = conn) do
261 | {:ok, _, _} = handle_data(data, conn)
262 | end
263 | def handle_message({:ssl_closed, socket}, %{socket: {:ssl, socket}}) do
264 | {:error, :closed}
265 | end
266 | def handle_message({:ssl_error, socket, reason}, %{socket: {:ssl, socket}}) do
267 | {:error, reason}
268 | end
269 | def handle_message(_, _), do: :unknown
270 |
271 | defp handle_data(data, %{socket: socket} = conn) do
272 | :ok = activate(socket)
273 | {:ok, _conn, _stanza} = parse_data(conn, data)
274 | end
275 |
276 | defp whitespace_only?(data), do: Regex.match?(~r/^\s+$/, data)
277 |
278 | defp activate({:gen_tcp, socket}) do
279 | case :inet.setopts(socket, [active: :once]) do
280 | :ok ->
281 | :ok
282 | {:error, :closed} ->
283 | _ = Kernel.send(self(), {:tcp_closed, socket})
284 | :ok
285 | {:error, reason} ->
286 | _ = Kernel.send(self(), {:tcp_error, socket, reason})
287 | :ok
288 | end
289 | end
290 | defp activate({:ssl, socket}) do
291 | case :ssl.setopts(socket, [active: :once]) do
292 | :ok ->
293 | :ok
294 | {:error, :closed} ->
295 | _ = Kernel.send(self(), {:ssl_closed, socket})
296 | :ok
297 | {:error, reason} ->
298 | _ = Kernel.send(self(), {:ssl_error, socket, reason})
299 | :ok
300 | end
301 | end
302 |
303 | defp host(jid) do
304 | Romeo.JID.parse(jid).server
305 | end
306 | end
307 |
--------------------------------------------------------------------------------
/lib/romeo/xml.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.XML do
2 | @moduledoc """
3 | Provides functions for building XML stanzas with the `fast_xml` library.
4 | """
5 |
6 | require Record
7 |
8 | defmacro __using__(_opts) do
9 | quote do
10 | use Romeo.XMLNS
11 | require Record
12 | alias Romeo.Stanza
13 | alias Romeo.Stanza.IQ
14 | alias Romeo.Stanza.Message
15 | alias Romeo.Stanza.Presence
16 |
17 | Record.defrecordp :xmlel, name: "", attrs: [], children: []
18 | Record.defrecordp :xmlcdata, content: []
19 | Record.defrecordp :xmlstreamstart, name: "", attrs: []
20 | Record.defrecordp :xmlstreamend, name: ""
21 | end
22 | end
23 |
24 | def encode!({:xmlel, _, _, _} = xml), do:
25 | :fxml.element_to_binary(xml)
26 | def encode!({:xmlstreamstart, name, attrs}), do:
27 | encode!({:xmlel, name, attrs, []}) |> String.replace("/>", ">")
28 | def encode!({:xmlstreamend, name}), do:
29 | "#{name}>"
30 |
31 | def encode!(stanza), do:
32 | Romeo.Stanza.to_xml(stanza)
33 |
34 | @doc """
35 | Returns the given attribute value or default.
36 | """
37 | def attr(element, name, default \\ nil) do
38 | case :fxml.get_tag_attr_s(name, element) do
39 | "" -> default
40 | val -> val
41 | end
42 | end
43 |
44 | def subelement(element, name, default \\ nil) do
45 | case :fxml.get_subtag(element, name) do
46 | false -> default
47 | val -> val
48 | end
49 | end
50 |
51 | def subelements(element, name) do
52 | :fxml.get_subtags(element, name)
53 | end
54 |
55 | def cdata(nil), do: ""
56 | def cdata(element), do: :fxml.get_tag_cdata(element)
57 | end
58 |
--------------------------------------------------------------------------------
/lib/romeo/xmlns.ex:
--------------------------------------------------------------------------------
1 | defmodule Romeo.XMLNS do
2 | @moduledoc """
3 | XMPP XML Namespaces.
4 |
5 | This module provides functions for returning XML namespace strings for
6 | XMPP stanzas.
7 | """
8 |
9 | defmacro __using__([]) do
10 | quote do
11 | import unquote __MODULE__
12 | end
13 | end
14 |
15 | # Defined by XML.
16 | def ns_xml,
17 | do: "http://www.w3.org/XML/1998/namespace"
18 |
19 | # Defined by XMPP Core RFC 3920).
20 | def ns_xmpp,
21 | do: "http://etherx.jabber.org/streams"
22 |
23 | def ns_stream_errors,
24 | do: "urn:ietf:params:xml:ns:xmpp-streams"
25 |
26 | def ns_tls,
27 | do: "urn:ietf:params:xml:ns:xmpp-tls"
28 |
29 | def ns_sasl,
30 | do: "urn:ietf:params:xml:ns:xmpp-sasl"
31 |
32 | def ns_bind,
33 | do: "urn:ietf:params:xml:ns:xmpp-bind"
34 |
35 | def ns_stanza_errors,
36 | do: "urn:ietf:params:xml:ns:xmpp-stanzas"
37 |
38 | # Defined by XMPP-IM RFC 3921).
39 | def ns_jabber_client,
40 | do: "jabber:client"
41 |
42 | def ns_jabber_server,
43 | do: "jabber:server"
44 |
45 | def ns_session,
46 | do: "urn:ietf:params:xml:ns:xmpp-session"
47 |
48 | def ns_roster,
49 | do: "jabber:iq:roster"
50 |
51 | # Defined by End-to-End Signing and Object Encryption for XMPP RFC 3923).
52 | def ns_e2e,
53 | do: "urn:ietf:params:xml:ns:xmpp-e2e"
54 |
55 | # Defined by XEP-0003: Proxy Accept Socket Service PASS).
56 | def ns_pass,
57 | do: "jabber:iq:pass"
58 |
59 | # Defined by XEP-0004: Data Forms.
60 | def ns_data_forms,
61 | do: "jabber:x:data"
62 |
63 | # Defined by XEP-0009: Jabber-RPC.
64 | def ns_rpc,
65 | do: "jabber:iq:rpc"
66 |
67 | # Defined by XEP-0011: Jabber Browsing.
68 | def ns_browse,
69 | do: "jabber:iq:browse"
70 |
71 | # Defined by XEP-0012: Last Activity.
72 | def ns_last_activity,
73 | do: "jabber:iq:last"
74 |
75 | # Defined by XEP-0013: Flexible Offline Message Retrieval.
76 | def ns_offline,
77 | do: "http://jabber.org/protocol/offline"
78 |
79 | # Defined by XEP-0016: Privacy Lists.
80 | def ns_privacy,
81 | do: "jabber:iq:privacy"
82 |
83 | # Defined by XEP-0020: Feature Negotiation.
84 | def ns_feature_neg,
85 | do: "http://jabber.org/protocol/feature-neg"
86 |
87 | # Defined by XEP-0022: Message Events.
88 | def ns_message_event,
89 | do: "jabber:x:event"
90 |
91 | # Defined by XEP-0023: Message Expiration.
92 | def ns_message_expire,
93 | do: "jabber:x:expire"
94 |
95 | # Defined by XEP-0027: Current Jabber OpenPGP Usage.
96 | def ns_pgp_encrypted,
97 | do: "jabber:x:encrypted"
98 |
99 | def ns_pgp_signed,
100 | do: "jabber:x:signed"
101 |
102 | # Defined by XEP-0030: Service Discovery.
103 | def ns_disco_info,
104 | do: "http://jabber.org/protocol/disco#info"
105 |
106 | def ns_disco_items,
107 | do: "http://jabber.org/protocol/disco#items"
108 |
109 | # Defined by XEP-0033: Extended Stanza Addressing.
110 | def ns_address,
111 | do: "http://jabber.org/protocol/address"
112 |
113 | # Defined by XEP-0039: Statistics Gathering.
114 | def ns_stats,
115 | do: "http://jabber.org/protocol/stats"
116 |
117 | # Defined by XEP-0045: Multi-User Chat.
118 | def ns_muc,
119 | do: "http://jabber.org/protocol/muc"
120 |
121 | def ns_muc_admin,
122 | do: "http://jabber.org/protocol/muc#admin"
123 |
124 | def ns_muc_owner,
125 | do: "http://jabber.org/protocol/muc#owner"
126 |
127 | def ns_muc_unique,
128 | do: "http://jabber.org/protocol/muc#unique"
129 |
130 | def ns_muc_user,
131 | do: "http://jabber.org/protocol/muc#user"
132 |
133 | # Defined by XEP-0047: In-Band Bytestreams.
134 | def ns_ibb,
135 | do: "http://jabber.org/protocol/ibb"
136 |
137 | # Defined by XEP-0048: Bookmarks.
138 | def ns_bookmarks,
139 | do: "storage:bookmarks"
140 |
141 | # Defined by XEP-0049: Private XML Storage.
142 | def ns_private,
143 | do: "jabber:iq:private"
144 |
145 | # Defined by XEP-0050: Ad-Hoc Commands.
146 | def ns_adhoc,
147 | do: "http://jabber.org/protocol/commands"
148 |
149 | # Defined by XEP-0054: vcard-temp.
150 | def ns_vcard,
151 | do: "vcard-temp"
152 |
153 | # Defined by XEP-0055: Jabber Search.
154 | def ns_search,
155 | do: "jabber:iq:search"
156 |
157 | # Defined by XEP-0059: Result Set Management.
158 | def ns_rsm,
159 | do: "http://jabber.org/protocol/rsm"
160 |
161 | # Defined by XEP-0060: Publish-Subscribe.
162 | def ns_pubsub,
163 | do: "http://jabber.org/protocol/pubsub"
164 |
165 | def ns_pubsub_errors,
166 | do: "http://jabber.org/protocol/pubsub#errors"
167 |
168 | def ns_pubsub_event,
169 | do: "http://jabber.org/protocol/pubsub#event"
170 |
171 | def ns_pubsub_owner,
172 | do: "http://jabber.org/protocol/pubsub#owner"
173 |
174 | def ns_pubsub_subscribe_auth,
175 | do: "http://jabber.org/protocol/pubsub#subscribe_authorization"
176 |
177 | def ns_pubsub_subscribe_options,
178 | do: "http://jabber.org/protocol/pubsub#subscribe_options"
179 |
180 | def ns_pubsub_node_config,
181 | do: "http://jabber.org/protocol/pubsub#node_config"
182 |
183 | def ns_pubsub_access_auth,
184 | do: "http://jabber.org/protocol/pubsub#access-authorize"
185 |
186 | def ns_pubsub_access_open,
187 | do: "http://jabber.org/protocol/pubsub#access-open"
188 |
189 | def ns_pubsub_access_presence,
190 | do: "http://jabber.org/protocol/pubsub#access-presence"
191 |
192 | def ns_pubsub_access_roster,
193 | do: "http://jabber.org/protocol/pubsub#access-roster"
194 |
195 | def ns_pubsub_access_whitelist,
196 | do: "http://jabber.org/protocol/pubsub#access-whitelist"
197 |
198 | def ns_pubsub_auto_create,
199 | do: "http://jabber.org/protocol/pubsub#auto-create"
200 |
201 | def ns_pubsub_auto_subscribe,
202 | do: "http://jabber.org/protocol/pubsub#auto-subscribe"
203 |
204 | def ns_pubsub_collections,
205 | do: "http://jabber.org/protocol/pubsub#collections"
206 |
207 | def ns_pubsub_config_node,
208 | do: "http://jabber.org/protocol/pubsub#config-node"
209 |
210 | def ns_pubsub_create_configure,
211 | do: "http://jabber.org/protocol/pubsub#create-and-configure"
212 |
213 | def ns_pubsub_create_nodes,
214 | do: "http://jabber.org/protocol/pubsub#create-nodes"
215 |
216 | def ns_pubsub_delete_items,
217 | do: "http://jabber.org/protocol/pubsub#delete-items"
218 |
219 | def ns_pubsub_delete_nodes,
220 | do: "http://jabber.org/protocol/pubsub#delete-nodes"
221 |
222 | def ns_pubsub_filtered_notifications,
223 | do: "http://jabber.org/protocol/pubsub#filtered-notifications"
224 |
225 | def ns_pubsub_get_pending,
226 | do: "http://jabber.org/protocol/pubsub#get-pending"
227 |
228 | def ns_pubsub_instant_nodes,
229 | do: "http://jabber.org/protocol/pubsub#instant-nodes"
230 |
231 | def ns_pubsub_item_ids,
232 | do: "http://jabber.org/protocol/pubsub#item-ids"
233 |
234 | def ns_pubsub_last_published,
235 | do: "http://jabber.org/protocol/pubsub#last-published"
236 |
237 | def ns_pubsub_leased_subscription,
238 | do: "http://jabber.org/protocol/pubsub#leased-subscription"
239 |
240 | def ns_pubsub_manage_subscriptions,
241 | do: "http://jabber.org/protocol/pubsub#manage-subscriptions"
242 |
243 | def ns_pubsub_member_affiliation,
244 | do: "http://jabber.org/protocol/pubsub#member-affiliation"
245 |
246 | def ns_pubsub_meta_data,
247 | do: "http://jabber.org/protocol/pubsub#meta-data"
248 |
249 | def ns_pubsub_modify_affiliations,
250 | do: "http://jabber.org/protocol/pubsub#modify-affiliations"
251 |
252 | def ns_pubsub_multi_collection,
253 | do: "http://jabber.org/protocol/pubsub#multi-collection"
254 |
255 | def ns_pubsub_multi_subscribe,
256 | do: "http://jabber.org/protocol/pubsub#multi-subscribe"
257 |
258 | def ns_pubsub_outcast_affiliation,
259 | do: "http://jabber.org/protocol/pubsub#outcast-affiliation"
260 |
261 | def ns_pubsub_persistent_items,
262 | do: "http://jabber.org/protocol/pubsub#persistent-items"
263 |
264 | def ns_pubsub_presence_notifications,
265 | do: "http://jabber.org/protocol/pubsub#presence-notifications"
266 |
267 | def ns_pubsub_presence_subscribe,
268 | do: "http://jabber.org/protocol/pubsub#presence-subscribe"
269 |
270 | def ns_pubsub_publish,
271 | do: "http://jabber.org/protocol/pubsub#publish"
272 |
273 | def ns_pubsub_publish_options,
274 | do: "http://jabber.org/protocol/pubsub#publish-options"
275 |
276 | def ns_pubsub_publish_only_affiliation,
277 | do: "http://jabber.org/protocol/pubsub#publish-only-affiliation"
278 |
279 | def ns_pubsub_publisher_affiliation,
280 | do: "http://jabber.org/protocol/pubsub#publisher-affiliation"
281 |
282 | def ns_pubsub_purge_nodes,
283 | do: "http://jabber.org/protocol/pubsub#purge-nodes"
284 |
285 | def ns_pubsub_retract_items,
286 | do: "http://jabber.org/protocol/pubsub#retract-items"
287 |
288 | def ns_pubsub_retrieve_affiliations,
289 | do: "http://jabber.org/protocol/pubsub#retrieve-affiliations"
290 |
291 | def ns_pubsub_retrieve_default,
292 | do: "http://jabber.org/protocol/pubsub#retrieve-default"
293 |
294 | def ns_pubsub_retrieve_items,
295 | do: "http://jabber.org/protocol/pubsub#retrieve-items"
296 |
297 | def ns_pubsub_retrieve_subscriptions,
298 | do: "http://jabber.org/protocol/pubsub#retrieve-subscriptions"
299 |
300 | def ns_pubsub_subscribe,
301 | do: "http://jabber.org/protocol/pubsub#subscribe"
302 |
303 | def ns_pubsub_subscription_options,
304 | do: "http://jabber.org/protocol/pubsub#subscription-options"
305 |
306 | def ns_pubsub_subscription_notifications,
307 | do: "http://jabber.org/protocol/pubsub#subscription-notifications"
308 |
309 | # Defined by XEP-0065: SOCKS5 Bytestreams.
310 | def ns_bytestreams,
311 | do: "http://jabber.org/protocol/bytestreams"
312 |
313 | # Defined by XEP-0066: Out of Band Data.
314 | ## How about NS_OOB instead ?
315 | def ns_oobd_iq,
316 | do: "jabber:iq:oob"
317 |
318 | def ns_oobd_x,
319 | do: "jabber:x:oob"
320 |
321 | # Defined by XEP-0070: Verifying HTTP Requests via XMPP.
322 | def ns_http_auth,
323 | do: "http://jabber.org/protocol/http-auth"
324 |
325 | # Defined by XEP-0071: XHTML-IM.
326 | def ns_xhtml_im,
327 | do: "http://jabber.org/protocol/xhtml-im"
328 |
329 | # Defined by XEP-0072: SOAP Over XMPP.
330 | def ns_soap_fault,
331 | do: "http://jabber.org/protocol/soap#fault"
332 |
333 | # Defined by XEP-0077: In-Band Registration.
334 | def ns_inband_register,
335 | do: "jabber:iq:register"
336 |
337 | def ns_inband_register_feat,
338 | do: "http://jabber.org/features/iq-register"
339 |
340 | # Defined by XEP-0078: Non-SASL Authentication.
341 | def ns_legacy_auth,
342 | do: "jabber:iq:auth"
343 |
344 | def ns_legacy_auth_feat,
345 | do: "http://jabber.org/features/iq-aut"
346 |
347 | # Defined by XEP-0079: Advanced Message Processing.
348 | def ns_amp,
349 | do: "http://jabber.org/protocol/amp"
350 |
351 | def ns_amp_errors,
352 | do: "http://jabber.org/protocol/amp#error"
353 |
354 | def ns_amp_feat,
355 | do: "http://jabber.org/features/amp"
356 |
357 | # Defined by XEP-0080: User Location.
358 | def ns_geoloc,
359 | do: "http://jabber.org/protocol/geoloc"
360 |
361 | # Defined by XEP-0083: Nested Roster Groups.
362 | def ns_roster_delimiter,
363 | do: "roster:delimiter"
364 |
365 | # Defined by XEP-0084: User Avatar.
366 | def ns_user_avatar_data,
367 | do: "urn:xmpp:avatar:data"
368 |
369 | def ns_user_avatar_metadata,
370 | do: "urn:xmpp:avatar:metadata"
371 |
372 | # Defined by XEP-0085: Chat State Notifications
373 | def ns_chatstates,
374 | do: "http://jabber.org/protocol/chatstates"
375 |
376 | # Defined by XEP-0090: Entity Time.
377 | def ns_time_old,
378 | do: "jabber:iq:time"
379 |
380 | # Defined by XEP-0091: Delayed Delivery.
381 | def ns_delay_old,
382 | do: "jabber:x:delay"
383 |
384 | # Defined by XEP-0092: Software Version.
385 | def ns_soft_version,
386 | do: "jabber:iq:version"
387 |
388 | # Defined by XEP-0093: Roster Item Exchange.
389 | def ns_roster_exchange_old,
390 | do: "jabber:x:roster"
391 |
392 | # Defined by XEP-0095: Stream Initiation.
393 | def ns_si,
394 | do: "http://jabber.org/protocol/si"
395 |
396 | # Defined by XEP-0096: File Transfer.
397 | def ns_file_transfert,
398 | do: "http://jabber.org/protocol/si/profile/file-transfer"
399 |
400 | # Defined by XEP-0100: Gateway Interaction.
401 | def ns_gateway,
402 | do: "jabber:iq:gateway"
403 |
404 | # Defined by XEP-0107: User Mood.
405 | def ns_user_mood,
406 | do: "http://jabber.org/protocol/mood"
407 |
408 | # Defined by XEP-0108: User Activity.
409 | def ns_user_activity,
410 | do: "http://jabber.org/protocol/activity"
411 |
412 | # Defined by XEP-0112: User Physical Location Deferred).
413 | def ns_user_physloc,
414 | do: "http://jabber.org/protocol/physloc"
415 |
416 | # Defined by XEP-0114: Jabber Component Protocol.
417 | def ns_component_accept,
418 | do: "jabber:component:accept"
419 | def ns_component_connect,
420 | do: "jabber:component:connect"
421 |
422 | # Defined by XEP-0115: Entity Capabilities.
423 | def ns_caps,
424 | do: "http://jabber.org/protocol/caps"
425 |
426 | # Defined by XEP-0118: User Tune.
427 | def ns_user_tune,
428 | do: "http://jabber.org/protocol/tune"
429 |
430 | # Defined by XEP-0122: Data Forms Validation.
431 | def ns_data_forms_validate,
432 | do: "http://jabber.org/protocol/xdata-validate"
433 |
434 | # Defined by XEP-0124: Bidirectional-streams Over Synchronous HTTP.
435 | def ns_bosh,
436 | do: "urn:xmpp:xbosh"
437 |
438 | def ns_http_bind,
439 | do: "http://jabber.org/protocol/httpbind"
440 |
441 | # Defined by XEP-0130: Waiting Lists.
442 | def ns_waiting_list,
443 | do: "http://jabber.org/protocol/waitinglist"
444 |
445 | # Defined by XEP-0131: Stanza Headers and Internet Metadata SHIM).
446 | def ns_shim,
447 | do: "http://jabber.org/protocol/shim"
448 |
449 | # Defined by XEP-0133: Service Administration.
450 | def ns_admin,
451 | do: "http://jabber.org/protocol/admin"
452 |
453 | # Defined by XEP-0136: Message Archiving.
454 | def ns_archiving,
455 | do: "urn:xmpp:archive"
456 |
457 | # Defined by XEP-0137: Publishing Stream Initiation Requests.
458 | def ns_si_pub,
459 | do: "http://jabber.org/protocol/sipub"
460 |
461 | # Defined by XEP-0138: Stream Compression.
462 | def ns_compress,
463 | do: "http://jabber.org/protocol/compress"
464 |
465 | def ns_compress_feat,
466 | do: "http://jabber.org/features/compress"
467 |
468 | # Defined by XEP-0141: Data Forms Layout.
469 | def ns_data_forms_layout,
470 | do: "http://jabber.org/protocol/xdata-layout"
471 |
472 | # Defined by XEP-0144: Roster Item Exchange.
473 | def ns_roster_exchange,
474 | do: "http://jabber.org/protocol/rosterx"
475 |
476 | # Defined by XEP-0145: Annotations.
477 | def ns_roster_notes,
478 | do: "storage:rosternotes"
479 |
480 | # Defined by XEP-0153: vCard-Based Avatars.
481 | def ns_vcard_update,
482 | do: "vcard-temp:x:update"
483 |
484 | # Defined by XEP-0154: User Profile.
485 | def ns_user_profile,
486 | do: "urn:xmpp:tmp:profile"
487 |
488 | # Defined by XEP-0155: Stanza Session Negotiation.
489 | def ns_ssn,
490 | do: "urn:xmpp:ssn"
491 |
492 | # Defined by XEP-0157: Contact Addresses for XMPP Services.
493 | def ns_serverinfo,
494 | do: "http://jabber.org/network/serverinfo"
495 |
496 | # Defined by XEP-0158: CAPTCHA Forms.
497 | def ns_captcha,
498 | do: "urn:xmpp:captcha"
499 |
500 | ## Deferred : XEP-0158: Robot Challenges
501 | def ns_robot_challenge,
502 | do: "urn:xmpp:tmp:challenge"
503 |
504 | # Defined by XEP-0160: Best Practices for Handling Offline Messages.
505 | def ns_msgoffline,
506 | do: "msgoffline"
507 |
508 | # Defined by XEP-0161: Abuse Reporting.
509 | def ns_abuse_reporting,
510 | do: "urn:xmpp:tmp:abuse"
511 |
512 | # Defined by XEP-0166: Jingle.
513 | def ns_jingle,
514 | do: "urn:xmpp:tmp:jingle"
515 |
516 | def ns_jingle_errors,
517 | do: "urn:xmpp:tmp:jingle:errors"
518 |
519 | # Defined by XEP-0167: Jingle RTP Sessions.
520 | def ns_jingle_rpt,
521 | do: "urn:xmpp:tmp:jingle:apps:rtp"
522 |
523 | def ns_jingle_rpt_info,
524 | do: "urn:xmpp:tmp:jingle:apps:rtp:info"
525 |
526 | # Defined by XEP-0168: Resource Application Priority.
527 | def ns_rap,
528 | do: "http://www.xmpp.org/extensions/xep-0168.html#ns"
529 |
530 | def ns_rap_route,
531 | do: "http://www.xmpp.org/extensions/xep-0168.html#ns-route"
532 |
533 | # Defined by XEP-0171: Language Translation.
534 | def ns_lang_trans,
535 | do: "urn:xmpp:langtrans"
536 |
537 | def ns_lang_trans_items,
538 | do: "urn:xmpp:langtrans#items"
539 |
540 | # Defined by XEP-0172: User Nickname.
541 | def ns_user_nickname,
542 | do: "http://jabber.org/protocol/nick"
543 |
544 | # Defined by XEP-0176: Jingle ICE-UDP Transport Method.
545 | def ns_jingle_ice_udp,
546 | do: "urn:xmpp:tmp:jingle:transports:ice-udp"
547 |
548 | # Defined by XEP-0177: Jingle Raw UDP Transport Method.
549 | def ns_jingle_raw_udp,
550 | do: "urn:xmpp:tmp:jingle:transports:raw-udp"
551 |
552 | def ns_jingle_raw_udp_info,
553 | do: "urn:xmpp:tmp:jingle:transports:raw-udp:info"
554 |
555 | # Defined by XEP-0181: Jingle DTMF.
556 | def ns_jingle_dtmf_0,
557 | do: "urn:xmpp:jingle:dtmf:0"
558 |
559 | ## Deferred
560 | def ns_jingle_dtmf,
561 | do: "urn:xmpp:tmp:jingle:dtmf"
562 |
563 | # Defined by XEP-0184: Message Receipts.
564 | def ns_receipts,
565 | do: "urn:xmpp:receipts"
566 |
567 | # Defined by XEP-0186: Invisible Command.
568 | def ns_invisible_command_0,
569 | do: "urn:xmpp:invisible:0"
570 |
571 | ## Deferred
572 | def ns_invisible_command,
573 | do: "urn:xmpp:tmp:invisible"
574 |
575 | # Defined by XEP-0189: Public Key Publishing.
576 | def ns_pubkey_1,
577 | do: "urn:xmpp:pubkey:1"
578 |
579 | def ns_attest_1,
580 | do: "urn:xmpp:attest:1"
581 |
582 | def ns_revoke_1,
583 | do: "urn:xmpp:revoke:1"
584 |
585 | ## Deferred
586 | def ns_pubkey_tmp,
587 | do: "urn:xmpp:tmp:pubkey"
588 |
589 | # Defined by XEP-0191: Simple Communications Blocking.
590 | def ns_blocking,
591 | do: "urn:xmpp:blocking"
592 |
593 | def ns_blocking_errors,
594 | do: "urn:xmpp:blocking:errors"
595 |
596 | # Defined by XEP-0194: User Chatting.
597 | def ns_user_chatting_0,
598 | do: "urn:xmpp:chatting:0"
599 |
600 | ## Deferred
601 | def ns_user_chatting,
602 | do: "http://www.xmpp.org/extensions/xep-0194.html#ns"
603 |
604 | # Defined by XEP-0195: User Browsing.
605 | def ns_user_browsing_0,
606 | do: "urn:xmpp:browsing:0"
607 |
608 | ## Deferred
609 | def ns_user_browsing,
610 | do: "http://www.xmpp.org/extensions/xep-0195.html#ns"
611 |
612 | # Defined by XEP-0196: User Gaming.
613 | def ns_user_gaming_0,
614 | do: "urn:xmpp:gaming:0"
615 |
616 | ## Deferred
617 | def ns_user_gaming,
618 | do: "http://www.xmpp.org/extensions/xep-0196.html#ns"
619 |
620 | # Defined by XEP-0197: User Viewing.
621 | def ns_user_viewing_0,
622 | do: "urn:xmpp:viewing:0"
623 |
624 | ## Deferred
625 | def ns_user_viewing,
626 | do: "http://www.xmpp.org/extensions/xep-0197.html#ns"
627 |
628 | # Defined by XEP-0198: Stream Management.
629 | def ns_stream_mgnt_3,
630 | do: "urn:xmpp:sm:3"
631 |
632 | ## Deferred
633 | def ns_stream_mgnt_2,
634 | do: "urn:xmpp:sm:2"
635 |
636 | def ns_stream_mgnt_1,
637 | do: "urn:xmpp:sm:1"
638 |
639 | def ns_stream_mgnt_0,
640 | do: "urn:xmpp:sm:0"
641 |
642 | def ns_stanza_ack,
643 | do: "http://www.xmpp.org/extensions/xep-0198.html#ns"
644 |
645 | # Defined by XEP-0199: XMPP Ping.
646 | def ns_ping,
647 | do: "urn:xmpp:ping"
648 |
649 | # Defined by XEP-0202: Entity Time.
650 | def ns_time,
651 | do: "urn:xmpp:time"
652 |
653 | # Defined by XEP-0203: Delayed Delivery.
654 | def ns_delay,
655 | do: "urn:xmpp:delay"
656 |
657 | # Defined by XEP-0206: XMPP Over BOSH.
658 | def ns_xbosh,
659 | do: "urn:xmpp:xbosh"
660 |
661 | # Defined by XEP-0208: Bootstrapping Implementation of Jingle.
662 | def ns_jingle_bootstraping,
663 | do: "http://www.xmpp.org/extensions/xep-0208.html#ns"
664 |
665 | # Defined by XEP-0209: Metacontacts.
666 | def ns_metacontacts,
667 | do: "storage:metacontacts"
668 |
669 | # Defined by XEP-0215: External Service Discovery.
670 | def ns_external_disco_0,
671 | do: "urn:xmpp:extdisco:0"
672 |
673 | ## Deferred
674 | def ns_external_disco,
675 | do: "http://www.xmpp.org/extensions/xep-0215.html#ns"
676 |
677 | # Defined by XEP-0220: Server Dialback.
678 | def ns_dialback,
679 | do: "jabber:server:dialback"
680 |
681 | def ns_dialback_feat,
682 | do: "urn:xmpp:features:dialback"
683 |
684 | # Defined by XEP-0221: Data Forms Media Element.
685 | ## How about NS_DATA ?
686 | def ns_data_forms_media,
687 | do: "urn:xmpp:media-element"
688 |
689 | ## Deferred
690 | def ns_data_forms_media_tmp,
691 | do: "urn:xmpp:tmp:media-element"
692 |
693 | # Defined by XEP-0224: Attention.
694 | def ns_attention_0,
695 | do: "urn:xmpp:attention:0"
696 |
697 | ## Deferred
698 | def ns_attention,
699 | do: "http://www.xmpp.org/extensions/xep-0224.html#ns"
700 |
701 | # Defined by XEP-0225: Component Connections.
702 | def ns_component_connection_0,
703 | do: "urn:xmpp:component:0"
704 |
705 | ## Deferred
706 | def ns_component_connection,
707 | do: "urn:xmpp:tmp:component"
708 |
709 | # Defined by XEP-0227: Portable Import/Export Format for XMPP-IM Servers.
710 | def ns_server_import_export,
711 | do: "http://www.xmpp.org/extensions/xep-0227.html#ns"
712 |
713 | # Defined by XEP-0231: Data Element.
714 | def ns_bob,
715 | do: "urn:xmpp:bob"
716 |
717 | ## Deferred
718 | def ns_data,
719 | do: "urn:xmpp:tmp:data-element"
720 |
721 | # Defined by XEP-0233: Use of Domain-Based Service Names in XMPP SASL
722 | # Negotiation.
723 | def ns_domain_based_name,
724 | do: "urn:xmpp:tmp:domain-based-name"
725 |
726 | def ns_domain_based_name_b,
727 | do: "urn:xmpp:tmp:domain-based-name"
728 |
729 | # Defined by XEP-0234: Jingle File Transfer.
730 | def ns_jingle_ft_1,
731 | do: "urn:xmpp:jingle:apps:file-transfer:1"
732 |
733 | ## Deferred
734 | def ns_jingle_file_transfert,
735 | do: "urn:xmpp:tmp:jingle:apps:file-transfer"
736 |
737 | # Defined by XEP-0235: Authorization Tokens.
738 | def ns_oauth_0,
739 | do: "urn:xmpp:oauth:0"
740 |
741 | def ns_oauth_errors_0,
742 | do: "urn:xmpp:oauth:0:errors"
743 |
744 | ## Deferred : XEP-0235: Authorization Tokens.
745 | def ns_auth_token,
746 | do: "urn:xmpp:tmp:auth-token"
747 |
748 | # Defined by XEP-0237: Roster Versioning.
749 | def ns_roster_ver,
750 | do: "urn:xmpp:features:rosterver"
751 |
752 | ## Deferred : XEP-0237: Roster Sequencing.
753 | def ns_roster_seq,
754 | do: "urn:xmpp:tmp:roster-sequencing"
755 |
756 | # Defined by XEP-0244: IO Data.
757 | def ns_io_data_tmp,
758 | do: "urn:xmpp:tmp:io-data"
759 |
760 | # Defined by XEP-0247: Jingle XML Streams.
761 | def ns_jingle_xml_stream_0,
762 | do: "urn:xmpp:jingle:apps:xmlstream:0"
763 |
764 | # Deferred
765 | def ns_jingle_xml_stream,
766 | do: "urn:xmpp:tmp:jingle:apps:xmlstream"
767 |
768 | # Defined by XEP-0249: Direct MUC Invitations.
769 | def ns_jabber_x_conf,
770 | do: "jabber:x:conference"
771 |
772 | # Defined by XEP-0251: Jingle Session Transfer.
773 | def ns_jingle_transfer_0,
774 | do: "urn:xmpp:jingle:transfer:0"
775 |
776 | # Defined by XEP-0253: PubSub Chaining.
777 | def ns_pubsub_chaining,
778 | do: "http://jabber.org/protocol/pubsub#chaining"
779 |
780 | # Defined by XEP-0254: PubSub Queueing.
781 | def ns_pubsub_queueing_0,
782 | do: "urn:xmpp:pubsub:queueing:0"
783 |
784 | # Defined by XEP-0255: Location Query.
785 | def ns_location_query_0,
786 | do: "urn:xmpp:locationquery:0"
787 |
788 | # Defined by XEP-0257: Client Certificate Management for SASL EXTERNAL.
789 | def ns_sasl_cert_0,
790 | do: "urn:xmpp:saslcert:0"
791 |
792 | # Defined by XEP-0258: Security Labels in XMPP.
793 | def ns_sec_label_0,
794 | do: "urn:xmpp:sec-label:0"
795 |
796 | def ns_sec_label_catalog_1,
797 | do: "urn:xmpp:sec-label:catalog:1"
798 |
799 | def ns_sec_label_ess_0,
800 | do: "urn:xmpp:sec-label:ess:0"
801 |
802 | # Defined by XEP-0259: Message Mine-ing.
803 | def ns_mine_tmp_0,
804 | do: "urn:xmpp:tmp:mine:0"
805 |
806 | # Defined by XEP-0260: Jingle SOCKS5 Bytestreams Transport Method.
807 | def ns_jingle_transports_s5b_1,
808 | do: "urn:xmpp:jingle:transports:s5b:1"
809 |
810 | # Defined by XEP-0261: Jingle In-Band Bytestreams Transport Method.
811 | def ns_jingle_transports_s5b_0,
812 | do: "urn:xmpp:jingle:transports:s5b:0"
813 |
814 | # Defined by XEP-0262: Use of ZRTP in Jingle RTP Sessions.
815 | def ns_jingle_apps_rtp_zrtp_0,
816 | do: "urn:xmpp:jingle:apps:rtp:zrtp:0"
817 |
818 | # Defined by XEP-0264: File Transfer Thumbnails.
819 | def ns_ft_thumbs_0,
820 | do: "urn:xmpp:thumbs:0"
821 |
822 | # Defined by XEP-0265: Out-of-Band Stream Data.
823 | def ns_jingle_apps_oob_0,
824 | do: "urn:xmpp:jingle:apps:out-of-band:0"
825 |
826 | # Defined by XEP-0268: Incident Reporting.
827 | def ns_incident_report_0,
828 | do: "urn:xmpp:incident:0"
829 |
830 | # Defined by XEP-0272: Multiparty Jingle Muji).
831 | def ns_telepathy_muji,
832 | do: "http://telepathy.freedesktop.org/muji"
833 |
834 | # Defined by XEP-0273: Stanza Interception and Filtering Technology SIFT).
835 | def ns_sift_1,
836 | do: "urn:xmpp:sift:1"
837 |
838 | # Defined by XEP-0275: Entity Reputation.
839 | def ns_reputation_0,
840 | do: "urn:xmpp:reputation:0"
841 |
842 | # Defined by XEP-0276: Temporary Presence Sharing.
843 | def ns_temppres_0,
844 | do: "urn:xmpp:temppres:0"
845 |
846 | # Defined by XEP-0277: Microblogging over XMPP.
847 | def ns_mublog_0,
848 | do: "urn:xmpp:microblog:0"
849 |
850 | # Defined by XEP-0278: Jingle Relay Nodes.
851 | def ns_jingle_relay_nodes,
852 | do: "http://jabber.org/protocol/jinglenodes"
853 |
854 | # Defined by XEP-0279: Server IP Check.
855 | def ns_sic_0,
856 | do: "urn:xmpp:sic:0"
857 |
858 | # Defined by XHTML 1.0.
859 | def ns_xhtml,
860 | do: "http://www.w3.org/1999/xhtml"
861 | end
862 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Mixfile do
2 | use Mix.Project
3 |
4 | @version "0.7.0"
5 |
6 | def project do
7 | [app: :romeo,
8 | name: "Romeo",
9 | version: @version,
10 | elixir: "~> 1.1",
11 | build_embedded: Mix.env == :prod,
12 | start_permanent: Mix.env == :prod,
13 | description: description(),
14 | deps: deps(),
15 | docs: docs(),
16 | package: package(),
17 | test_coverage: [tool: ExCoveralls]]
18 | end
19 |
20 | def application do
21 | [applications: [:logger, :connection, :fast_xml],
22 | mod: {Romeo, []}]
23 | end
24 |
25 | defp description do
26 | "An XMPP Client for Elixir"
27 | end
28 |
29 | defp deps do
30 | [{:connection, "~> 1.0"},
31 | {:fast_xml, "~> 1.1"},
32 |
33 | # Docs deps
34 | {:ex_doc, "~> 0.18", only: :dev},
35 |
36 | # Test deps
37 | {:ejabberd, github: "scrogson/ejabberd", branch: "fix_mix_compile", only: :test},
38 | {:excoveralls, "~> 0.8", only: :test}]
39 | end
40 |
41 | defp docs do
42 | [extras: docs_extras(),
43 | main: "readme"]
44 | end
45 |
46 | defp docs_extras do
47 | ["README.md"]
48 | end
49 |
50 | defp package do
51 | [files: ["lib", "mix.exs", "README.md", "LICENSE"],
52 | maintainers: ["Sonny Scroggin"],
53 | licenses: ["MIT"],
54 | links: %{"GitHub" => "https://github.com/scrogson/romeo"}]
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "cache_tab": {:hex, :cache_tab, "1.0.12", "a06a4ffbd4da8469791ba941512a6a45ed8c11865b4606a368e21b332da3638a", [], [{:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
3 | "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [], [], "hexpm"},
4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [], [], "hexpm"},
5 | "distillery": {:hex, :distillery, "1.5.2", "eec18b2d37b55b0bcb670cf2bcf64228ed38ce8b046bb30a9b636a6f5a4c0080", [], [], "hexpm"},
6 | "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [], [], "hexpm"},
7 | "ejabberd": {:hex, :ejabberd, "18.1.0", "5bc81d975a0094c8ba9c809fdf6b8c03a90d4c62022c9c52a2adab235d5adb5d", [], [{:cache_tab, "~> 1.0", [hex: :cache_tab, repo: "hexpm", optional: false]}, {:distillery, "~> 1.0", [hex: :distillery, repo: "hexpm", optional: false]}, {:esip, "~> 1.0", [hex: :esip, repo: "hexpm", optional: false]}, {:ezlib, "~> 1.0", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "~> 1.0", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "~> 1.1", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:fast_yaml, "~> 1.0", [hex: :fast_yaml, repo: "hexpm", optional: false]}, {:fs, "~> 3.4", [hex: :fs, repo: "hexpm", optional: false]}, {:iconv, "~> 1.0", [hex: :iconv, repo: "hexpm", optional: false]}, {:jiffy, "~> 0.14.7", [hex: :jiffy, repo: "hexpm", optional: false]}, {:lager, "~> 3.4.0", [hex: :lager, repo: "hexpm", optional: false]}, {:p1_mysql, "~> 1.0", [hex: :p1_mysql, repo: "hexpm", optional: false]}, {:p1_oauth2, "~> 0.6.1", [hex: :p1_oauth2, repo: "hexpm", optional: false]}, {:p1_pgsql, "~> 1.1", [hex: :p1_pgsql, repo: "hexpm", optional: false]}, {:p1_utils, "~> 1.0", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "~> 1.0", [hex: :stringprep, repo: "hexpm", optional: false]}, {:stun, "~> 1.0", [hex: :stun, repo: "hexpm", optional: false]}, {:xmpp, "~> 1.1", [hex: :xmpp, repo: "hexpm", optional: false]}], "hexpm"},
8 | "esip": {:hex, :esip, "1.0.21", "711c704337d434db6d7c70bd0da868aaacd91b252c0bb63b4580e6c896164f1f", [], [{:fast_tls, "1.0.20", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.20", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm"},
9 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
10 | "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
11 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
12 | "ezlib": {:hex, :ezlib, "1.0.3", "c402c839ff5eab5b8a69efd9f60885c3ab3ca2489a6758bf8a67c914297597c5", [], [], "hexpm"},
13 | "fast_tls": {:hex, :fast_tls, "1.0.20", "edd241961ab20b71ec1e9f75a2a2c043128ff117adf3efd42e6cec94f1937539", [], [{:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
14 | "fast_xml": {:hex, :fast_xml, "1.1.28", "31ce5cf44d20e900e1a499009f886ff74b589324d532ed0ed7a432e4f498beb1", [:rebar3], [{:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
15 | "fast_yaml": {:hex, :fast_yaml, "1.0.12", "ee8527d388255cf7a24fc1e6cb2d09dca4e506966dd9d86e61d3d90f236a3e2e", [], [{:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
16 | "fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [], [], "hexpm"},
17 | "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [], [], "hexpm"},
18 | "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
19 | "iconv": {:hex, :iconv, "1.0.6", "3b424a80039059767f1037dc6a49ff07c2f88df14068c16dc938c4f377a77b4c", [], [{:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
20 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
21 | "jiffy": {:hex, :jiffy, "0.14.13", "225a9a35e26417832c611526567194b4d3adc4f0dfa5f2f7008f4684076f2a01", [], [], "hexpm"},
22 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [], [], "hexpm"},
23 | "lager": {:hex, :lager, "3.4.2", "150b9a17b23ae6d3265cc10dc360747621cf217b7a22b8cddf03b2909dbf7aa5", [], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"},
24 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], [], "hexpm"},
25 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], [], "hexpm"},
26 | "p1_mysql": {:hex, :p1_mysql, "1.0.4", "7b9d7957a9d031813a0e6bcea5a7f5e91b54db805a92709a445cf75cf934bc1d", [], [], "hexpm"},
27 | "p1_oauth2": {:hex, :p1_oauth2, "0.6.2", "cc381038920e3d34ef32aa10ba7eb637bdff38a946748c4fd99329ff484a3889", [], [], "hexpm"},
28 | "p1_pgsql": {:hex, :p1_pgsql, "1.1.4", "eadbbddee8d52145694bf86bdfe8c1ae8353a55e152410146b8c2711756d6041", [], [], "hexpm"},
29 | "p1_utils": {:hex, :p1_utils, "1.0.10", "a6d6927114bac79cf6468a10824125492034af7071adc6ed5ebc4ddb443845d4", [], [], "hexpm"},
30 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"},
31 | "stringprep": {:hex, :stringprep, "1.0.10", "552d784eb60652220fce9131f8bb0ebc62fdffd6482c4f08f2e7d61300227c28", [], [{:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
32 | "stun": {:hex, :stun, "1.0.20", "6b156fa11606bebb6086d02cb2f6532c84effb59c95ba93d0e2d8e2510970253", [], [{:fast_tls, "1.0.20", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"},
33 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [], [], "hexpm"},
34 | "xmpp": {:hex, :xmpp, "1.1.19", "ca0a89c567e972d119204b1296ffe58ad5d3237738950ae2c61043fbaf5e150e", [:rebar3], [{:fast_xml, "1.1.28", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.10", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.10", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm"},
35 | }
36 |
--------------------------------------------------------------------------------
/priv/ssl/ejabberd.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICsDCCAhmgAwIBAgIJALJmwzXUFrWmMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTI0MDYwODQwWhcNMTUxMjI0MDYwODQwWjBF
5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
7 | gQDByRnfvkks1Pq7B8ndtZT8CGcqy4Zh/Uhde4DNtT4iGDzmAje5xCggMWGl8qZG
8 | CFv4hLYuRGJwCQxwsDggabuNB7juoFpb2+OJZ6d3/DBHJfXSx0yR1zwVxA3rt/CV
9 | JZijSRyJJn2ZC7h3Qe/TMo8eibhc9+p55cuDoq2tNoYDvwIDAQABo4GnMIGkMB0G
10 | A1UdDgQWBBRUDEpv3D6naSZlsA4rc7tlXhaldjB1BgNVHSMEbjBsgBRUDEpv3D6n
11 | aSZlsA4rc7tlXhaldqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
12 | U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJALJmwzXU
13 | FrWmMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAwABkkw6XZ1kNXA1a
14 | stCIimJmeE+2lXa5POCh4YRg14Gvm4qCbfA8FPWO3Ld7cpJThYbiBxEwUovya8vl
15 | u70yn0ux71xjN2M6HMI8ytuB1br3VfiZlqM5QgAC/UspEqQEVbj7Sss48M/vAYa5
16 | fEtglhk2xO7xgiKJR7+7DzzwwDY=
17 | -----END CERTIFICATE-----
18 | -----BEGIN RSA PRIVATE KEY-----
19 | MIICXQIBAAKBgQDByRnfvkks1Pq7B8ndtZT8CGcqy4Zh/Uhde4DNtT4iGDzmAje5
20 | xCggMWGl8qZGCFv4hLYuRGJwCQxwsDggabuNB7juoFpb2+OJZ6d3/DBHJfXSx0yR
21 | 1zwVxA3rt/CVJZijSRyJJn2ZC7h3Qe/TMo8eibhc9+p55cuDoq2tNoYDvwIDAQAB
22 | AoGAflzTKWocr0ZGJRWEFbWla99S3r4OZ/FQcdzp9bmcxYDGnTmO+uylObDZuuuK
23 | bxpeVqS7Y1omUmYkHYtbXg90QvKPyqaKXiaEIVkAwSBKQvd7suJmYNYsJVvC3g9E
24 | x/9n73GxnEX+Wk2w2bZH3VGO0pwCy14K1gnUomErGh1GLuECQQD9D4k00HQ3+1zQ
25 | FSicmAQEFUsOiZIGKMWKDOh1swTcqOfskx1ij7Om9/YReDkkS2Ot49dRze5/4x4V
26 | LG+0PymvAkEAxAlQMzd/Jnpmm72ABvhrL4sOjZ4DWo4sZgUlrcZ0bCLwUyrf5Ekj
27 | 4RT5ExSvQly+rnyCPcZ/cv+xLsy0qWwa8QJBAPMMWNtA2l5qLVos+DRuTG0fhlcQ
28 | Cg+gWRmeDCX/KkxEbXvqT+651fIndU6SCU+ymKoKimMnRknN+LadVyvm/kECQFe/
29 | b3Wtdq2rhjhaB2+XTKsYTGhZfVjQYNE9ppL1TPGGZhpkC5msn3HFqIPA833586Q4
30 | uTebnTrFdvLi0E8xw5ECQQDIfmTr2TkqJOVgxexIIr09Ge8r7hHndxC3lnyscCjh
31 | OTKGzCjzSOSaN3XVCJko9Z018eQYPUSVV7WC3sSZ4wyC
32 | -----END RSA PRIVATE KEY-----
33 |
--------------------------------------------------------------------------------
/priv/templates/ejabberd.yml.eex:
--------------------------------------------------------------------------------
1 | loglevel: 0
2 |
3 | hosts:
4 | - "localhost"
5 |
6 | listen:
7 | -
8 | port: 52222
9 | module: ejabberd_c2s
10 | max_stanza_size: 65536
11 | shaper: c2s_shaper
12 | access: c2s
13 | -
14 | port: 52225
15 | module: ejabberd_c2s
16 | starttls_required: true
17 | certfile: "<%= cwd %>/priv/ssl/ejabberd.pem"
18 | max_stanza_size: 65536
19 | shaper: c2s_shaper
20 | access: c2s
21 | -
22 | port: 52227
23 | module: ejabberd_service
24 | hosts:
25 | "test.localhost":
26 | password: "secret"
27 |
28 | auth_method: internal
29 |
30 | shaper:
31 | normal: 1000
32 | fast: 50000
33 |
34 | max_fsm_queue: 1000
35 |
36 | acl:
37 | local:
38 | user_regexp: ""
39 | loopback:
40 | ip:
41 | - "127.0.0.0/8"
42 |
43 | access:
44 | max_user_sessions:
45 | all: 10
46 | max_user_offline_messages:
47 | admin: 5000
48 | all: 100
49 | local:
50 | local: allow
51 | c2s:
52 | blocked: deny
53 | all: allow
54 | c2s_shaper:
55 | admin: none
56 | all: normal
57 | s2s_shaper:
58 | all: fast
59 | announce:
60 | admin: allow
61 | configure:
62 | admin: allow
63 | muc_admin:
64 | admin: allow
65 | muc_create:
66 | local: allow
67 | muc:
68 | all: allow
69 | pubsub_createnode:
70 | local: allow
71 | register:
72 | all: allow
73 | trusted_network:
74 | loopback: allow
75 |
76 | language: "en"
77 |
78 | modules:
79 | mod_adhoc: {}
80 | mod_admin_extra: {}
81 | mod_announce:
82 | access: announce
83 | mod_blocking: {}
84 | mod_caps: {}
85 | mod_carboncopy: {}
86 | mod_client_state:
87 | drop_chat_states: true
88 | queue_presence: false
89 | mod_configure: {}
90 | mod_disco: {}
91 | mod_irc: {}
92 | mod_http_bind: {}
93 | mod_last: {}
94 | mod_muc:
95 | access: muc
96 | access_create: muc_create
97 | access_persistent: muc_create
98 | access_admin: muc_admin
99 | max_user_conferences: 20
100 | mod_offline:
101 | access_max_user_messages: max_user_offline_messages
102 | mod_ping: {}
103 | mod_privacy: {}
104 | mod_private: {}
105 | mod_pubsub:
106 | access_createnode: pubsub_createnode
107 | ignore_pep_from_offline: true
108 | last_item_cache: false
109 | plugins:
110 | - "flat"
111 | - "hometree"
112 | - "pep"
113 | mod_register:
114 | ip_access: trusted_network
115 | access: register
116 | mod_roster: {}
117 | mod_shared_roster: {}
118 | mod_stats: {}
119 | mod_time: {}
120 | mod_vcard: {}
121 | mod_version: {}
122 |
123 | allow_contrib_modules: true
124 |
--------------------------------------------------------------------------------
/test/romeo/connection/features_test.exs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scrogson/romeo/9a94c933598696ecb0c2e9dca7bf2b04dc6651d9/test/romeo/connection/features_test.exs
--------------------------------------------------------------------------------
/test/romeo/connection_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.ConnectionTest do
2 | use ExUnit.Case
3 |
4 | use UserHelper
5 | use Romeo.XML
6 |
7 | setup do
8 | romeo = build_user("romeo", tls: true)
9 | juliet = build_user("juliet", resource: "juliet", tls: true)
10 |
11 | setup_presence_subscriptions(romeo[:nickname], juliet[:nickname])
12 |
13 | {:ok, romeo: romeo, juliet: juliet}
14 | end
15 |
16 | test "connection no TLS" do
17 | romeo = build_user("romeo")
18 |
19 | {:ok, _pid} = Romeo.Connection.start_link(romeo)
20 |
21 | assert_receive {:resource_bound, _}
22 | assert_receive :connection_ready
23 | end
24 |
25 | test "connection TLS", %{romeo: romeo} do
26 | {:ok, _pid} = Romeo.Connection.start_link(romeo)
27 |
28 | assert_receive {:resource_bound, _}
29 | assert_receive :connection_ready
30 | end
31 |
32 | test "sending presence", %{romeo: romeo} do
33 | {:ok, pid} = Romeo.Connection.start_link(romeo)
34 |
35 | assert_receive :connection_ready
36 |
37 | assert :ok = Romeo.Connection.send(pid, Romeo.Stanza.presence)
38 | assert_receive {:stanza, %Presence{from: from, to: to} = presence}
39 | assert to_string(from) == "romeo@localhost/romeo"
40 | assert to_string(to) == "romeo@localhost/romeo"
41 |
42 | assert :ok = Romeo.Connection.send(pid, Romeo.Stanza.join("lobby@conference.localhost", "romeo"))
43 | assert_receive {:stanza, %Presence{from: from} = presence}
44 | assert to_string(from) == "lobby@conference.localhost/romeo"
45 | end
46 |
47 | test "resource conflict", %{romeo: romeo} do
48 | {:ok, pid1} = Romeo.Connection.start_link(romeo)
49 | assert_receive :connection_ready
50 | assert :ok = Romeo.Connection.send(pid1, Romeo.Stanza.presence)
51 |
52 | {:ok, pid2} = Romeo.Connection.start_link(romeo)
53 | assert_receive :connection_ready
54 | assert :ok = Romeo.Connection.send(pid2, Romeo.Stanza.presence)
55 |
56 | assert_receive {:stanza, %{name: "stream:error"}}
57 | assert_receive {:stanza, xmlstreamend()}
58 | end
59 |
60 | test "exchanging messages with others", %{romeo: romeo, juliet: juliet} do
61 | {:ok, romeo} = Romeo.Connection.start_link(romeo)
62 | assert_receive :connection_ready
63 | assert :ok = Romeo.Connection.send(romeo, Romeo.Stanza.presence)
64 | # Romeo receives presense from himself
65 | assert_receive {:stanza, %Presence{}}
66 |
67 | {:ok, juliet} = Romeo.Connection.start_link(juliet)
68 | assert_receive :connection_ready
69 | assert :ok = Romeo.Connection.send(juliet, Romeo.Stanza.presence)
70 |
71 | # Juliet receives presence from herself and each receive each others'
72 | assert_receive {:stanza, %Presence{}}
73 | assert_receive {:stanza, %Presence{}}
74 | assert_receive {:stanza, %Presence{}}
75 |
76 | # Juliet sends Romeo a message
77 | assert :ok = Romeo.Connection.send(juliet, Romeo.Stanza.chat("romeo@localhost/romeo", "Where art thou?"))
78 | assert_receive {:stanza, %Message{from: from, to: to, body: body}}
79 | assert to_string(from) == "juliet@localhost/juliet"
80 | assert to_string(to) == "romeo@localhost/romeo"
81 | assert body == "Where art thou?"
82 |
83 | # Romeo responds
84 | assert :ok = Romeo.Connection.send(romeo, Romeo.Stanza.chat("juliet@localhost/juliet", "Hey babe"))
85 | assert_receive {:stanza, %Message{from: from, to: to, body: body}}
86 | assert to_string(from) == "romeo@localhost/romeo"
87 | assert to_string(to) == "juliet@localhost/juliet"
88 | assert body == "Hey babe"
89 | end
90 |
91 | test "close connection", %{romeo: romeo} do
92 | {:ok, pid} = Romeo.Connection.start_link(romeo)
93 |
94 | assert_receive {:resource_bound, _}
95 | assert_receive :connection_ready
96 |
97 | assert :ok = Romeo.Connection.close(pid)
98 | refute_receive :connection_ready, 1000
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/test/romeo/jid_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.JidTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Romeo.JID
5 |
6 | doctest Romeo.JID
7 |
8 | test "String.Chars protocol converts structs to binaries" do
9 | jid = %JID{server: "example.com"}
10 | assert to_string(jid) == "example.com"
11 |
12 | jid = %JID{user: "jdoe", server: "example.com"}
13 | assert to_string(jid) == "jdoe@example.com"
14 |
15 | jid = %JID{user: "jdoe", server: "example.com", resource: "library"}
16 | assert to_string(jid) == "jdoe@example.com/library"
17 | end
18 |
19 | test "bare returns a JID without a resource" do
20 | jid = %JID{user: "jdoe", server: "example.com", resource: "library"}
21 | assert JID.bare(jid) == "jdoe@example.com"
22 | assert JID.bare("jdoe@example.com/library") == "jdoe@example.com"
23 | assert JID.bare("jdoe@example.com") == "jdoe@example.com"
24 | end
25 |
26 | test "it converts binaries into structs" do
27 | string = "jdoe@example.com"
28 | assert JID.parse(string) == %JID{user: "jdoe", server: "example.com", full: string}
29 |
30 | string = "jdoe@example.com/library"
31 | assert JID.parse(string) == %JID{user: "jdoe", server: "example.com", resource: "library", full: string}
32 |
33 | string = "jdoe@example.com/jdoe@example.com/resource"
34 | assert JID.parse(string) == %JID{user: "jdoe", server: "example.com", resource: "jdoe@example.com/resource", full: string}
35 |
36 | string = "example.com"
37 | assert JID.parse(string) == %JID{user: "", server: "example.com", resource: "", full: "example.com"}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/test/romeo/roster_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.RosterTest do
2 | use ExUnit.Case
3 |
4 | use UserHelper
5 | use Romeo.XML
6 |
7 | import Romeo.Roster
8 |
9 | alias Romeo.Roster.Item
10 |
11 | setup do
12 | romeo = build_user("romeo", tls: true)
13 | juliet = build_user("juliet", resource: "juliet", tls: true)
14 | mercutio = build_user("mercutio", resource: "mercutio", tls: true)
15 | benvolio = build_user("benvolio", resource: "benvolio", tls: true)
16 |
17 | setup_presence_subscriptions(romeo[:nickname], juliet[:nickname])
18 | setup_presence_subscriptions(romeo[:nickname], mercutio[:nickname])
19 |
20 | {:ok, pid} = Romeo.Connection.start_link(romeo)
21 | {:ok, romeo: romeo, juliet: juliet, mercutio: mercutio, benvolio: benvolio, pid: pid}
22 | end
23 |
24 | test "getting, adding, removing roster items", %{benvolio: benvolio, mercutio: mercutio, pid: pid} do
25 | assert [%Item{name: "juliet"}, %Item{name: "mercutio"}] = items(pid)
26 |
27 | b_jid = benvolio[:jid]
28 | assert :ok = add(pid, b_jid)
29 | assert [%Item{name: "juliet"}, %Item{name: "mercutio"}, %Item{name: "benvolio"}] = items(pid)
30 |
31 | m_jid = mercutio[:jid]
32 | assert :ok = remove(pid, m_jid)
33 | assert [%Item{name: "juliet"}, %Item{name: "benvolio"}] = items(pid)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/romeo/stanza/parser_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.Stanza.ParserTest do
2 | use ExUnit.Case, async: true
3 |
4 | use Romeo.XML
5 |
6 | alias Romeo.Stanza.Parser
7 |
8 | @iq {:xmlel, "iq", [{"from", "im.test.dev"}, {"to", "scrogson@im.test.dev/issues"}, {"id", "b0e3"}, {"type", "result"}], [
9 | {:xmlel, "query", [{"xmlns", "http://jabber.org/protocol/disco#items"}], [
10 | {:xmlel, "item", [{"jid", "conference.im.test.dev"}], []},
11 | {:xmlel, "item", [{"jid", "pubsub.im.test.dev"}], []}]}]}
12 |
13 | test "it parses stanzas" do
14 | parsed = Parser.parse(@iq)
15 | assert parsed.type == "result"
16 | assert parsed.id == "b0e3"
17 | assert %Romeo.JID{user: "scrogson", server: "im.test.dev", resource: "issues"} = parsed.to
18 | assert %Romeo.JID{user: "", server: "im.test.dev", resource: ""} = parsed.from
19 | xmlel(name: "iq") = parsed.xml
20 |
21 | query = Romeo.XML.subelement(parsed.xml, "query")
22 | assert Enum.count(xmlel(query, :children)) == 2
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/romeo/stanza_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.StanzaTest do
2 | use ExUnit.Case, async: true
3 | use Romeo.XML
4 |
5 | alias Romeo.Stanza
6 |
7 | doctest Romeo.Stanza
8 |
9 | test "to_xml for IQ struct" do
10 | assert %IQ{to: "test@localhost", type: "get", id: "123"} |> Stanza.to_xml ==
11 | ""
12 | end
13 |
14 | test "to_xml for Presence struct" do
15 | assert %Presence{to: "test@localhost", type: "subscribe"} |> Stanza.to_xml ==
16 | ""
17 | end
18 |
19 | test "start_stream with default xmlns" do
20 | assert Stanza.start_stream("im.wonderland.lit") |> Stanza.to_xml ==
21 | ""
22 | end
23 |
24 | test "start_stream with 'jabber:server' xmlns" do
25 | assert Stanza.start_stream("im.wonderland.lit", ns_jabber_server) |> Stanza.to_xml ==
26 | ""
27 | end
28 |
29 | test "end_stream" do
30 | assert Stanza.end_stream |> Stanza.to_xml == ""
31 | end
32 |
33 | test "start_tls" do
34 | assert Stanza.start_tls |> Stanza.to_xml ==
35 | ""
36 | end
37 |
38 | test "get_inband_register" do
39 | assert Stanza.get_inband_register |> Stanza.to_xml =~
40 | ~r""
41 | end
42 |
43 | test "set_inband_register" do
44 | assert Stanza.set_inband_register("username", "password") |> Stanza.to_xml =~
45 | ~r"usernamepassword"
46 | end
47 |
48 | test "subscribe" do
49 | assert Stanza.subscribe("pubsub.wonderland.lit", "posts", "alice@wonderland.lit") |> Stanza.to_xml =~
50 | ~r""
51 | end
52 |
53 | test "compress" do
54 | assert Stanza.compress("zlib") |> Stanza.to_xml ==
55 | "zlib"
56 | end
57 |
58 | test "auth" do
59 | data = <<0>> <> "username" <> <<0>> <> "password"
60 | assert Stanza.auth("PLAIN", Stanza.base64_cdata(data)) |> Stanza.to_xml ==
61 | "AHVzZXJuYW1lAHBhc3N3b3Jk"
62 | end
63 |
64 | test "auth anonymous" do
65 | assert Stanza.auth("ANONYMOUS") |> Stanza.to_xml == ""
66 | end
67 |
68 | test "bind" do
69 | assert Stanza.bind("hedwig") |> Stanza.to_xml =~
70 | ~r"hedwig"
71 | end
72 |
73 | test "session" do
74 | assert Stanza.session |> Stanza.to_xml =~
75 | ~r""
76 | end
77 |
78 | test "presence" do
79 | assert Stanza.presence |> Stanza.to_xml == ""
80 | end
81 |
82 | test "presence/1" do
83 | assert Stanza.presence("subscribe") |> Stanza.to_xml == ""
84 | end
85 |
86 | test "presence/2" do
87 | assert Stanza.presence("room@muc.localhost/nick", "unavailable") |> Stanza.to_xml ==
88 | ""
89 | end
90 |
91 | test "message" do
92 | assert Stanza.message("test@localhost", "chat", "Hello") |> Stanza.to_xml =~
93 | ~r"Hello"
94 | end
95 |
96 | test "message map" do
97 | msg = %{"to" => "test@localhost", "type" => "chat", "body" => "Hello"}
98 | assert Stanza.message(msg) |> Stanza.to_xml =~
99 | ~r"Hello"
100 | end
101 |
102 | test "normal chat" do
103 | assert Stanza.normal("test@localhost", "Hello") |> Stanza.to_xml =~
104 | ~r"Hello"
105 | end
106 |
107 | test "group chat" do
108 | assert Stanza.groupchat("test@localhost", "Hello") |> Stanza.to_xml =~
109 | ~r"Hello"
110 | end
111 |
112 | test "get_roster" do
113 | assert Stanza.get_roster |> Stanza.to_xml =~
114 | ~r""
115 | end
116 |
117 | test "set_roster_item" do
118 | assert Stanza.set_roster_item("test@localhost", "none", "test2", "buddies") |> Stanza.to_xml =~
119 | ~r"- buddies
"
120 | assert Stanza.set_roster_item("test@localhost") |> Stanza.to_xml =~
121 | ~r" "
122 | end
123 |
124 | test "get_vcard" do
125 | assert Stanza.get_vcard("test@localhost") |> Stanza.to_xml =~
126 | ~r""
127 | end
128 |
129 | test "disco_info" do
130 | assert Stanza.disco_info("test@localhost") |> Stanza.to_xml =~
131 | ~r""
132 | end
133 |
134 | test "disco_items" do
135 | assert Stanza.disco_items("test@localhost") |> Stanza.to_xml =~
136 | ~r""
137 | end
138 |
139 | test "xhtml im" do
140 | xhtml_msg = "Hello
"
141 | assert Stanza.xhtml_im(xhtml_msg) |> Stanza.to_xml ==
142 | "#{xhtml_msg}"
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/test/romeo/xml_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.XMLTest do
2 | use ExUnit.Case, async: true
3 |
4 | use Romeo.XML
5 | import Romeo.XML
6 |
7 | test "encode!" do
8 | xml = xmlel(name: "message", children: [
9 | xmlel(name: "body", children: [
10 | xmlcdata(content: "testing")
11 | ])
12 | ])
13 | assert encode!(xml) ==
14 | ~s(testing)
15 | end
16 |
17 | test "attr" do
18 | xml = xmlel(name: "message", attrs: [{"type", "chat"}])
19 | assert attr(xml, "type") == "chat"
20 | assert attr(xml, "non-existent") == nil
21 | assert attr(xml, "non-existent", "default") == "default"
22 | end
23 |
24 | test "subelement" do
25 | xml = xmlel(name: "message", children: [
26 | xmlel(name: "body", children: [
27 | xmlcdata(content: "testing")
28 | ])
29 | ])
30 | assert subelement(xml, "body") ==
31 | {:xmlel, "body", [], [xmlcdata(content: "testing")]}
32 |
33 | assert subelement(xml, "non-existent") == nil
34 | assert subelement(xml, "non-existent", []) == []
35 | end
36 |
37 | test "cdata" do
38 | body = xmlel(name: "body", children: [
39 | xmlcdata(content: "testing")
40 | ])
41 | assert cdata(body) == "testing"
42 | end
43 |
44 | test "empty cdata" do
45 | body = xmlel(name: "body", children: [
46 | xmlcdata(content: "testing")
47 | ])
48 | assert cdata(body) == "testing"
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/romeo/xmlns_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Romeo.XMLNSTest do
2 | use ExUnit.Case, async: true
3 |
4 | import Romeo.XMLNS
5 |
6 | test "it provides XML namespaces" do
7 | assert ns_xml == "http://www.w3.org/XML/1998/namespace"
8 | assert ns_xmpp == "http://etherx.jabber.org/streams"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/romeo_test.exs:
--------------------------------------------------------------------------------
1 | defmodule RomeoTest do
2 | use ExUnit.Case
3 | doctest Romeo
4 |
5 | test "the truth" do
6 | assert 1 + 1 == 2
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("user_helper.exs", __DIR__)
2 |
3 | Application.ensure_all_started(:ejabberd)
4 | ExUnit.start()
5 |
6 | System.at_exit(fn _ -> File.rm_rf("mnesia") end)
7 |
--------------------------------------------------------------------------------
/test/user_helper.exs:
--------------------------------------------------------------------------------
1 | defmodule UserHelper do
2 |
3 | defmacro __using__(_) do
4 | quote do
5 | import UserHelper
6 | import ExUnit.CaptureLog
7 | end
8 | end
9 |
10 | def build_user(username, opts \\ []) do
11 | {password, opts} = Keyword.pop(opts, :password, "password")
12 | {resource, opts} = Keyword.pop(opts, :resource, "romeo")
13 | {tls, _opts} = Keyword.pop(opts, :tls, false)
14 |
15 | register_user(username, password)
16 |
17 | [jid: username <> "@localhost",
18 | password: password,
19 | resource: resource,
20 | nickname: username,
21 | port: (if tls, do: 52225, else: 52222)]
22 | end
23 |
24 | def register_user(username, password \\ "password") do
25 | :ejabberd_admin.register(username, "localhost", password)
26 | end
27 |
28 | def unregister_user(username) do
29 | :ejabberd_admin.unregister(username, "localhost")
30 | end
31 |
32 | def setup_presence_subscriptions(user1, user2) do
33 | :mod_admin_extra.add_rosteritem(user1, "localhost", user2, "localhost", user2, "buddies", "both")
34 | :mod_admin_extra.add_rosteritem(user2, "localhost", user1, "localhost", user1, "buddies", "both")
35 | end
36 | end
37 |
--------------------------------------------------------------------------------