aboutsummaryrefslogtreecommitdiff
path: root/shard/lib/cli/cli.ex
blob: 8495b93a754225a72e4728cb6ea34726758c58d3 (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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
defmodule SCLI do
  @moduledoc """
  Small command line interface for the chat application. Supports public chat rooms,
  private conversations, sending files (but not receiving them - could be done easily).

  The code of this module is intended as an example of how to use the Shard library.

  TODO: more commands.
  """

  defmodule State do
    @moduledoc"""
    Internal state struct of the CLI.
    """
    defstruct [:room_pid, :id_pid, :pk]
  end

  @doc"""
  Call this from the iex prompt to launch the CLI.
  """
  def run() do
    for {_chid, manifest, _} <- Shard.Manager.list_shards do
      case manifest do
        %SApp.Chat.Manifest{} ->
          SApp.Chat.subscribe(Shard.Manager.find_or_start manifest)
        _ -> nil
      end
    end

    pk = Shard.Keys.get_any_identity
    room_pid = Shard.Manager.find_or_start %SApp.Chat.Manifest{channel: "lobby"}
    run(%State{room_pid: room_pid, id_pid: nil, pk: pk})
  end

  defp run(state) do
    handle_messages(state)

    id_pid = case state.id_pid do
      nil -> SApp.Identity.find_proc(state.pk)
      x -> x
    end
    state = put_in(state.id_pid, id_pid)

    nick = case id_pid do
      nil -> SApp.Identity.default_nick(state.pk)
      _ ->
        SApp.Identity.get_info(id_pid).nick
    end

    prompt = case state.room_pid do
      nil -> "(no channel) #{nick}: "
      _ -> 
        case SApp.Chat.get_manifest(state.room_pid) do
          %SApp.Chat.Manifest{channel: chan} ->
            "##{chan} #{nick}: "
          %SApp.Chat.PrivChat.Manifest{pk_list: pk_list} ->
            nicks = pk_list
                    |> Enum.filter(&(&1 != state.pk))
                    |> Enum.map(&("#{SApp.Identity.get_nick &1} #{Shard.Keys.pk_display &1}"))
                    |> Enum.join(", ")
            "PM #{nicks}  #{nick}: "
        end
    end

    str = prompt |> IO.gets |> String.trim
    cond do
      str == "/quit" ->
        nil
      String.slice(str, 0..0) == "/" ->
        command = str |> String.slice(1..-1) |> String.split(" ")
        state = handle_command(state, command)
        run(state)
      true -> 
        if str != "" do
          SApp.Chat.chat_send(state.room_pid, state.pk, str)
        end
        run(state)
    end
  end

  defp handle_messages(state) do
    receive do
      {:chat_recv, manifest, {pk, msgbin, _sign}} ->
        {ts, msg} = SData.term_unbin msgbin
        nick = SApp.Identity.get_nick pk
        case manifest do
          %SApp.Chat.Manifest{channel: chan} ->
            IO.puts "#{ts |> DateTime.from_unix! |> DateTime.to_iso8601}  ##{chan}  <#{nick} #{Shard.Keys.pk_display pk}> #{msg}"
          %SApp.Chat.PrivChat.Manifest{pk_list: pk_list} ->
            IO.puts "#{ts |> DateTime.from_unix! |> DateTime.to_iso8601}  PM(#{Enum.count pk_list})  <#{nick} #{Shard.Keys.pk_display pk}> #{msg}"
        end
        handle_messages(state)
      {:chat_send, _, _} ->
        # do nothing
        handle_messages(state)
    after 10 -> nil
    end
  end

  defp handle_command(state, ["connect", ipstr, portstr]) do
    {:ok, ip} = :inet.parse_address (to_charlist ipstr)
    {port, _} = Integer.parse portstr
    SNet.Manager.add_peer({:inet, ip, port})
    state
  end

  defp handle_command(state, ["list"]) do
    IO.puts "Private conversations:"
    for {_chid, %SApp.Chat.PrivChat.Manifest{pk_list: pk_list}, _} <- Shard.Manager.list_shards do
      pk_list
      |> Enum.filter(&(&1 != state.pk))
      |> Enum.map(fn pk -> "#{SApp.Identity.get_nick pk} #{Shard.Keys.pk_display pk}" end)
      |> Enum.join(", ")
      |> IO.puts
    end

    IO.puts "Public channels we are connected to:"
    for {_chid, %SApp.Chat.Manifest{channel: chan}, _} <- Shard.Manager.list_shards do
      IO.puts "##{chan}"
    end
    state
  end

  defp handle_command(state, ["hist"]) do
    if state.room_pid == nil do
      IO.puts "Not currently on a channel!"
    else
      SApp.Chat.read_history(state.room_pid, nil, 25)
      |> Enum.each(fn {{pk, msgbin, _sign}, true} ->
          {ts, msg} = SData.term_unbin msgbin
          nick = SApp.Identity.get_nick pk
          IO.puts "#{ts |> DateTime.from_unix! |> DateTime.to_iso8601}  <#{nick} #{Shard.Keys.pk_display pk}> #{msg}"
        end)
    end
    state
  end

  defp handle_command(state, ["join", qchan]) do
    pid = Shard.Manager.find_or_start %SApp.Chat.Manifest{channel: qchan}
    SApp.Chat.subscribe(pid)
    IO.puts "Switching to ##{qchan}"
    %{state | room_pid: pid}
  end

  defp handle_command(state, ["pm" | people_list]) do
    known_people = for {_, %SApp.Identity.Manifest{pk: pk}, _} <- Shard.Manager.list_shards() do
      {pk, SApp.Identity.get_nick(pk)}
    end
    pk_list = for qname <- people_list do
      candidates = for {pk, nick} <- known_people,
        :binary.longest_common_prefix([qname, nick]) == byte_size(qname)
          or :binary.longest_common_prefix([qname, Shard.Keys.pk_display pk]) == byte_size(qname),
        do: {pk, nick}
      case candidates do
        [] ->
          IO.puts "Not found: #{qname}"
          :error
        [{pk, _}] -> pk
        _ ->
          IO.puts "Several people matching for #{qname}:"
          for {pk, nick} <- candidates do
            IO.puts "- #{nick} #{Shard.Keys.pk_display pk}"
          end
          :error
      end
    end
    if Enum.all?(pk_list, &(&1 != :error)) do
      manifest = SApp.Chat.PrivChat.Manifest.new([state.pk | pk_list])
      pid = Shard.Manager.find_or_start manifest
      SApp.Chat.subscribe(pid)
      IO.puts "Switching to private conversation."
      %{state | room_pid: pid}
    else
      state
    end
  end

  defp handle_command(state, ["nick", nick]) do
    pid = case state.id_pid do
      nil -> SApp.Identity.find_proc state.pk
      x -> x
    end
    if pid == nil do
      IO.puts "Sorry, we have a problem with the identity shard"
    else
      info = SApp.Identity.get_info(pid)
      SApp.Identity.set_info(pid,  %{info | nick: nick})
    end
    state
  end

  defp handle_command(state, ["send_file", path]) do
    {mime_type, 0} = System.cmd("file", ["-b", "--mime-type", path])
    mime_type = String.trim mime_type
    IO.puts("Guessed mime type: #{mime_type}")
    handle_command(state, ["send_file", path, mime_type])
  end

  defp handle_command(state, ["send_file", path, mime_type]) do
    if state.room_pid != nil do
      {:ok, m, _} = SApp.File.create(path, mime_type)
      uri = ShardURI.from_manifest(m)
      IO.puts("sending URI: #{uri}")
      SApp.Chat.chat_send(state.room_pid, state.pk, uri)
    else
      IO.puts("Not in a chat room!")
    end
    state
  end

  defp handle_command(state, ["shards"]) do
    Shard.Manager.list_shards
    |> Enum.map(&(ShardURI.from_manifest(elem(&1, 1))))
    |> Enum.sort()
    |> Enum.map(&IO.puts/1)
    state
  end

  defp handle_command(state, _cmd) do
    IO.puts "Invalid command"
    state
  end
end