defmodule SCLI do @moduledoc """ Small command line interface for the chat application """ defmodule State do defstruct [:room_pid, :id_pid, :pk] end def run() do for {_chid, %SApp.Chat.Manifest{}, chpid} <- Shard.Manager.list_shards do GenServer.cast(chpid, {:subscribe, self()}) end pk = Shard.Keys.get_any_identity run(%State{room_pid: nil, 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) _ -> info = GenServer.call(id_pid, :get_info) info.nick end prompt = case state.room_pid do nil -> "(no channel) #{nick}: " _ -> case GenServer.call(state.room_pid, :manifest) 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 GenServer.cast(state.room_pid, {:chat_send, 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 "List of known channels:" for {_chid, %SApp.Chat.Manifest{channel: chan}, _chpid} <- 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 GenServer.call(state.room_pid, {:read_history, 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 = SApp.Chat.find_proc qchan case pid do nil -> {:ok, pid} = Shard.Manifest.start %SApp.Chat.Manifest{channel: qchan} GenServer.cast(pid, {:subscribe, self()}) IO.puts "Joining ##{qchan} (new shard)" %{state | room_pid: pid} pid -> GenServer.cast(pid, {:subscribe, self()}) IO.puts "Switching to ##{qchan}" %{state | room_pid: pid} end end defp handle_command(state, ["pm" | people_list]) do known_people = for {_, %SApp.Identity.Manifest{pk: pk}, pid} <- Shard.Manager.list_shards() do info = GenServer.call(pid, :get_info) {pk, info.nick} 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 pk_list = [state.pk | pk_list] manifest = SApp.Chat.PrivChat.Manifest.new(pk_list) id = SData.term_hash manifest case Shard.Manager.find_proc id do nil -> {:ok, pid} = Shard.Manifest.start manifest GenServer.cast(pid, {:subscribe, self()}) IO.puts "Joining private conversation (new shard)." %{state | room_pid: pid} pid -> GenServer.cast(pid, {:subscribe, self()}) IO.puts "Switching to private conversation." %{state | room_pid: pid} end 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 = GenServer.call(pid, :get_info) GenServer.call(pid, {:set_info, %{info | nick: nick}}) end state end defp handle_command(state, _cmd) do IO.puts "Invalid command" state end end