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 "\nPrivate conversations:\n----" 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 "\nPublic channels we are connected to:\n----" for {_chid, %SApp.Chat.Manifest{channel: chan}, _} <- Shard.Manager.list_shards do IO.puts "##{chan}" end IO.puts "" 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 IO.puts("\nList of shards:\n----") Shard.Manager.list_shards |> Enum.map(&(ShardURI.from_manifest(elem(&1, 1)))) |> Enum.sort() |> Enum.map(&IO.puts/1) IO.puts("") state end defp handle_command(state, ["delete", uri]) do manifest = ShardURI.to_manifest uri id = SData.term_hash manifest case Shard.Manager.delete(id) do :ok -> IO.puts "OK" {:error, :not_found} -> IO.puts "Shard not found" {:error, :pinned} -> IO.puts "Shard is pinned, could not be deleted" end state end defp handle_command(state, ["info", uri]) do manifest = ShardURI.to_manifest uri case manifest do %SApp.File.Manifest{} -> pid = Shard.Manager.find_or_start manifest info = GenServer.call(pid, :get_info) IO.puts(inspect(info, pretty: true, width: 40)) _ -> IO.puts("Info not supported for this shard") end state end defp handle_command(state, ["help"]) do IO.puts("See README.md for help") state end defp handle_command(state, _cmd) do IO.puts "Invalid command" state end end