├── .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 | [![Build Status](https://travis-ci.org/scrogson/romeo.svg?branch=master)](https://travis-ci.org/scrogson/romeo) 6 | [![Coverage Status](https://coveralls.io/repos/scrogson/romeo/badge.svg?branch=master&service=github)](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 | ![Romeo and Juliet](https://upload.wikimedia.org/wikipedia/commons/c/cc/Picou%2C_Henri_Pierre_-_Romeo_and_Juliet.jpg 100 | "Henri-Pierre Picou [Public domain], via Wikimedia Commons") 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 | "" 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 | --------------------------------------------------------------------------------