Current section
Files
Jump to
Current section
Files
lib/hedwig_xmpp.ex
defmodule Hedwig.Adapters.XMPP do
@moduledoc false
use Hedwig.Adapter
use Romeo.XML
require Logger
defmodule State do
defstruct conn: nil,
jid_mapping: %{},
opts: nil,
robot: nil,
rooms: [],
roster: []
end
## Callbacks
def init({robot, opts}) do
connection_opts = Keyword.put_new(opts, :nickname, opts[:name])
{:ok, conn} = Romeo.Connection.start_link(connection_opts)
{:ok, %State{conn: conn, opts: opts, robot: robot}}
end
def handle_cast({:send, %{text: text} = msg}, %{conn: conn} = state) do
case text do
xmlel() ->
Romeo.Connection.send(conn, text)
_ ->
msg = romeo_message(msg)
Romeo.Connection.send(conn, %{msg | body: text})
end
{:noreply, state}
end
def handle_cast({:reply, %{user: user, text: text} = msg}, %{conn: conn} = state) do
msg = romeo_message(msg)
Romeo.Connection.send(conn, %{msg | body: "#{user.name}: #{text}"})
{:noreply, state}
end
def handle_cast({:emote, %{text: text} = msg}, %{conn: conn} = state) do
msg = romeo_message(msg)
Romeo.Connection.send(conn, %{msg | body: "/me #{text}"})
{:noreply, state}
end
def handle_cast({:rooms_results, %IQ{xml: xml}}, state) do
rooms =
xml
|> Romeo.XML.subelement("query")
|> Romeo.XML.subelements("item")
|> Enum.map(&Romeo.XML.attr(&1, "jid"))
{:noreply, %{state | rooms: rooms}}
end
def handle_cast({:roster_results, %IQ{xml: xml}}, state) do
roster =
xml
|> Romeo.XML.subelement("query")
|> Romeo.XML.subelements("item")
|> Enum.map(&Romeo.XML.attr(&1, "jid"))
|> Enum.reduce(%{}, &(Map.put(&2, &1, %{})))
{:noreply, %{state | roster: roster}}
end
def handle_info({:stanza, xmlstreamstart()}, state) do
{:noreply, state}
end
def handle_info({:stanza, xmlstreamend()}, state) do
exit(:stream_ended)
{:noreply, state}
end
def handle_info({:stanza, %{type: "error"} = stanza}, state) do
Logger.error fn -> "There was an error: #{inspect stanza}" end
{:noreply, state}
end
def handle_info({:stanza, %Message{body: "", type: "groupchat"}}, state) do
# Most likely a room topic
{:noreply, state}
end
def handle_info({:stanza, %Message{} = msg}, %{robot: robot, opts: opts} = state) do
unless from_self?(msg, opts[:name]) do
Hedwig.Robot.handle_message(robot, hedwig_message(msg, state.jid_mapping))
end
{:noreply, state}
end
def handle_info({:stanza, %Presence{from: from, xml: xml}}, state) do
state =
if from_room?(from, state.rooms) do
real_jid = real_jid_from_room_presence(xml)
update_in(state.jid_mapping, &(Map.put(&1, from.full, real_jid)))
else
state
end
{:noreply, state}
end
def handle_info({:resource_bound, resource}, %{robot: robot, opts: opts} = state) do
Hedwig.Robot.handle_in(robot, {:resource_bound, resource})
{:noreply, state}
end
def handle_info(:connection_ready, %{conn: conn, robot: robot, opts: opts} = state) do
conn
|> get_roster(opts)
|> request_all_rooms(opts)
|> send_presence(opts)
|> join_rooms(opts)
Hedwig.Robot.after_connect(robot)
{:noreply, state}
end
def handle_info(msg, %{robot: robot} = state) do
Hedwig.Robot.handle_in(robot, msg)
{:noreply, state}
end
## Helpers
defp real_jid_from_room_presence(xml) do
case Romeo.XML.subelements(xml, "x") do
[] ->
nil
elems ->
try do
elems
|> Enum.filter(&contains_muc_user_namespace?/1)
|> Enum.at(0)
|> Romeo.XML.subelement("item")
|> Romeo.XML.attr("jid")
catch
_ -> nil
end
end
end
defp contains_muc_user_namespace?(xmlel(attrs: attrs)) do
Enum.any?(attrs, fn {k, v} -> k == "xmlns" && v == ns_muc_user end)
end
defp get_roster(conn, _opts) do
stanza = Romeo.Stanza.get_roster
id = Romeo.XML.attr(stanza, "id")
Romeo.Connection.send(conn, stanza)
receive do
{:stanza, %IQ{id: ^id, type: "result"} = iq} ->
GenServer.cast(self, {:roster_results, iq})
end
conn
end
defp request_all_rooms(conn, opts) do
rooms = Keyword.get(opts, :rooms)
if rooms do
stanza =
rooms
|> hd
|> elem(0)
|> Romeo.JID.server
|> Romeo.Stanza.disco_items
id = Romeo.XML.attr(stanza, "id")
Romeo.Connection.send(conn, stanza)
receive do
{:stanza, %IQ{id: ^id, type: "result"} = iq} ->
GenServer.cast(self, {:rooms_results, iq})
end
end
conn
end
defp send_presence(conn, opts) do
if Keyword.get(opts, :send_presence, true) do
Romeo.Connection.send(conn, Romeo.Stanza.presence)
end
conn
end
defp join_rooms(conn, opts) do
rooms = Keyword.get(opts, :rooms, [])
if Keyword.get(opts, :join_rooms, true) do
for {room, room_opts} <- rooms do
Romeo.Connection.send(conn, Romeo.Stanza.join(room, opts[:name], room_opts))
end
end
conn
end
defp from_room?(from, rooms) do
Romeo.JID.bare(from) in rooms
end
defp from_self?(%Message{from: %{resource: name}, type: "groupchat"}, name) do
true
end
defp from_self?(%Message{from: %{user: name}}, name) do
true
end
defp from_self?(_, _), do: false
defp hedwig_message(%Message{body: body, type: type} = msg, mapping) do
{room, user} = extract_room_and_user(msg, mapping)
%Hedwig.Message{
ref: make_ref(),
room: room,
text: body,
type: type,
user: user
}
end
defp romeo_message(%Hedwig.Message{room: room, type: type, text: text, user: user}) do
%Romeo.Stanza.Message{
to: send_to(type, room, user),
type: type,
body: text
}
end
defp extract_room_and_user(%Message{from: from, type: "groupchat"}, mapping) do
room = Romeo.JID.bare(from)
user = %Hedwig.User{
id: mapping[from.full] || from.full,
name: Romeo.JID.resource(from)
}
{room, user}
end
defp extract_room_and_user(%Romeo.Stanza.Message{from: from}, _mapping) do
user = %Hedwig.User{
id: from.full,
name: Romeo.JID.user(from)
}
{nil, user}
end
defp send_to("groupchat", room, _user) do
Romeo.JID.bare(room)
end
defp send_to(_type, _room, user) do
Romeo.JID.parse(user.id)
end
end