aboutsummaryrefslogtreecommitdiff
path: root/lib/app/chat.ex
blob: bc9f5debb0de73ce3e63c4950a9ccfa2e86bec67 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
defmodule SApp.Chat do
  @moduledoc """
  Shard application for a replicated chat room with full history.

  Chat rooms are globally identified by their channel name.
  A chat room manifest is of the form:

      {:chat, channel_name}

  Future improvements:
  - message signing
  - storage of the chatroom messages to disk
  - storage of the known peers that have this channel to disk
  - use a DHT to find peers that are interested in this channel
  - epidemic broadcast (carefull not to be too costly,
    maybe by limiting the number of peers we talk to)
  - partial synchronization only == data distributed over peers
  """

  use GenServer

  alias SData.MerkleList, as: ML

  @doc """
  Start a process that connects to a given channel
  """
  def start_link(channel) do
    GenServer.start_link(__MODULE__, channel)
  end

  @doc """
  Initialize channel process.
  """
  def init(channel) do
    store = ML.new(&msg_cmp/2)
    manifest = {:chat, channel}
    id = SData.term_hash manifest

    GenServer.cast(Shard.Manager, {:register, id, manifest, self()})
    GenServer.cast(self(), :init_pull)

    {:ok,
      %{channel: channel,
        id: id,
        manifest: manifest,
        store: store, 
        peers: MapSet.new,
        subs: MapSet.new,
      }
    }
  end

  @doc """
  Implementation of the :manifest call that returns the chat room's manifest
  """
  def handle_call(:manifest, _from, state) do
    {:reply, state.manifest, state}
  end

  def handle_call({:read_history, start, num}, _from, state) do
    ret = ML.read(state.store, start, num)
    {:reply, ret, state}
  end

  @doc """
  Implementation of the :redundant handler: if another process is already
  synchronizing this channel then we exit.
  """
  def handle_cast({:redundant, _}, _state) do
    exit :normal
  end

  @doc """
  Implementation of the :init_pull handler, which is called when the
  process starts. It contacts all currently connected peers and asks them to
  send data for this channel if they have some.
  """
  def handle_cast(:init_pull, state) do
    for {_, pid, _, _} <- :ets.tab2list(:peer_db) do
      GenServer.cast(pid, {:send_msg, {:interested, [state.id]}})
    end
    {:noreply, state}
  end

  @doc """
  Implementation of the :chat_send handler. This is the main handler that is used
  to send a message to the chat room. Puts the message in the store and syncs
  with all connected peers.
  """
  def handle_cast({:chat_send, msg}, state) do
    msgitem = {(System.os_time :seconds),
               Shard.Identity.get_nickname(),
               msg}
    new_state = %{state | store: ML.insert(state.store, msgitem)}

    for pid <- state.subs do
      if Process.alive?(pid) do
        send(pid, {:chat_send, state.channel, msgitem})
      end
    end

    for peer <- state.peers do
      push_messages(new_state, peer, nil, 5)
    end

    {:noreply, new_state}
  end

  @doc """
  Implementation of the :interested handler, this is called when a peer we are
  connected to asks to recieve data for this channel.
  """
  def handle_cast({:interested, peer_id}, state) do
    push_messages(state, peer_id, nil, 10)
    new_peers = MapSet.put(state.peers, peer_id)
    {:noreply, %{ state | peers: new_peers }}
  end

  def handle_cast({:subscribe, pid}, state) do
    Process.monitor(pid)
    new_subs = MapSet.put(state.subs, pid)
    {:noreply, %{ state | subs: new_subs }}
  end

  @doc """
  Implementation of the :msg handler, which is the main handler for messages
  comming from other peers concerning this chat room.

  Messages are:
  - `{:get, start}`: get some messages starting at a given Merkle hash
  - `{:info, start, list, rest}`: put some messages and informs of the
    Merkle hash of the store of older messages.
  """
  def handle_cast({:msg, peer_id, msg}, state) do
    case msg do
      {:get_manifest} ->
        Shard.Manager.send(peer_id, {state.id, {:manifest, state.manifest}})
      {:get, start} -> push_messages(state, peer_id, start, 20)
      {:info, _start, list, rest} ->
        if rest != nil and not ML.has(state.store, rest) do
          Shard.Manager.send(peer_id, {state.id, {:get, rest}})
        end
        who = self()
        spawn_link(fn ->
          Process.sleep 1000
          GenServer.cast(who, {:deferred_insert, list})
        end)
      _ -> nil
    end

    if MapSet.member?(state.peers, peer_id) do
      {:noreply, state}
    else
      handle_cast({:interested, peer_id}, state)
    end
  end

  def handle_cast({:deferred_insert, list}, state) do
    new_store = ML.insert_many(state.store, list, (fn msg -> msg_callback(state, msg) end))
    {:noreply, %{state | store: new_store}}
  end

  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    new_subs = MapSet.delete(state.subs, pid)
    {:noreply, %{ state | subs: new_subs }}
  end

  defp push_messages(state, to, start, num) do
    case ML.read(state.store, start, num) do
      {:ok, list, rest} ->
        Shard.Manager.send(to, {state.id, {:info, start, list, rest}})
      _ -> nil
    end
  end

  defp msg_callback(state, {ts, nick, msg}) do
    for pid <- state.subs do
      if Process.alive?(pid) do
        send(pid, {:chat_recv, state.channel, {ts, nick, msg}})
      end
    end
  end

  defp msg_cmp({ts1, nick1, msg1}, {ts2, nick2, msg2}) do
    SData.MerkleList.cmp_ts_str({ts1, nick1<>"|"<>msg1}, 
                                   {ts2, nick2<>"|"<>msg2})
  end 
end