defmodule SApp.Identity do @moduledoc""" Shard application for keeping state associated with a user's identity. Current functionality: - nickname - peer info: ip, port to connect to if we want a secure connection with this person (used for private chat) Future functionnality: - friend list - notifications & invites """ use GenServer require Logger defmodule Manifest do @moduledoc""" Manifest for a user identity shard, defined by the public key of the user. """ defstruct [:pk] defimpl Shard.Manifest do def module(_m), do: SApp.Identity def is_valid?(m) do byte_size(m.pk) == 32 end end end defmodule State do @moduledoc""" Internal state struct for identity shard. """ defstruct [:pk, :id, :state, :netgroup] end @doc """ Start a process that connects to a given channel. Don't call directly, use for instance: Shard.Manager.find_or_start %SApp.Identity.Manifest{pk: some_public_key} """ def start_link(manifest) do GenServer.start_link(__MODULE__, manifest) end def init(manifest) do %Manifest{pk: pk} = manifest id = SData.term_hash manifest Shard.Manager.dispatch_to(id, nil, self()) state = case Shard.Manager.load_state(id) do nil -> info = %{nick: default_nick(pk)} SData.SignRev.new info st -> st end netgroup = %SNet.PubShardGroup{id: id} SNet.Group.init_lookup(netgroup, self()) if Shard.Keys.have_sk? pk do GenServer.cast(self(), :update_peer_info) end {:ok, %State{pk: pk, id: id, state: state, netgroup: netgroup}} end def handle_call(:manifest, _from, state) do {:reply, state.manifest, state} end def handle_call(:delete_shard, _from, state) do {:stop, :normal, :ok, state} end def handle_call(:get_info, _from, state) do {:reply, SData.SignRev.get(state.state), state} end def handle_call({:set_info, new_info}, _from, state) do if Shard.Keys.have_sk?(state.pk) do {:ok, st2} = SData.SignRev.set(state.state, new_info, state.pk) Shard.Manager.save_state(state.id, st2) state = put_in(state.state, st2) bcast_state(state) {:reply, :ok, state} else {:reply, :impossible, state} end end def handle_cast(:send_deps, state) do deps = if Shard.Keys.have_sk?(state.pk) do [ %SApp.Directory.Manifest{owner: state.pk, public: true, name: "collection"}, %SApp.Directory.Manifest{owner: state.pk, public: false, name: "collection"} ] else [] end GenServer.cast(Shard.Manager, {:dep_list, state.id, deps}) {:noreply, state} end def handle_cast({:peer_connected, peer_pid}, state) do GenServer.cast(peer_pid, {:send_msg, {:interested, [state.id]}}) {:noreply, state} end def handle_cast({:interested, peer_pid, _auth}, state) do SNet.Manager.send_pid(peer_pid, {state.id, nil, {:update, SData.SignRev.signed(state.state), true}}) {:noreply, state} end def handle_cast({:msg, conn_pid, _auth, _shard_id, nil, msg}, state) do state = case msg do {:update, signed, ask_reply} when signed != nil -> state = case SData.SignRev.merge(state.state, signed, state.pk) do {true, st2} -> Shard.Manager.save_state(state.id, st2) state = put_in(state.state, st2) bcast_state(state, [conn_pid]) state {false, _} -> state end if ask_reply do SNet.Manager.send_pid(conn_pid, {state.id, nil, {:update, SData.SignRev.signed(state.state), false}}) end state _ -> state end {:noreply, state} end def handle_cast(:update_peer_info, state) do peer_info = SNet.Addr.get_all_inet4() |> Enum.map(&({:inet, &1, Application.get_env(:shard, :port)})) prev_info = SData.SignRev.get(state.state) # TODO multi peer info new_info = Map.put(prev_info, :peer_info, peer_info) {:ok, st2} = SData.SignRev.set(state.state, new_info, state.pk) Shard.Manager.save_state(state.id, st2) state = put_in(state.state, st2) bcast_state(state) {:noreply, state} end defp bcast_state(state, exclude \\ []) do msg = {state.id, nil, {:update, SData.SignRev.signed(state.state), false}} SNet.Group.broadcast(state.netgroup, msg, exclude_pid: exclude) end # ================ # PUBLIC INTERFACE # ================ @doc""" Return the default nickname associated to a pk, in the form "Anonxxxxxxxx" with some bytes of the pk in hex. """ def default_nick(pk) do nick_suffix = Shard.Keys.pk_display pk "Anon" <> nick_suffix end @doc""" Find the shard process for an identity. Launches such a process if necessary. """ def find_proc(pk) do Shard.Manager.find_or_start %Manifest{pk: pk} end @doc""" Get the info dict of an identity shard. The pid of the shard must be given as an argument. """ def get_info(pid) do GenServer.call(pid, :get_info) end @doc""" Set the info dict of an identity shard. """ def set_info(pid, new_info) do GenServer.call(pid, {:set_info, new_info}) end @doc""" Get a user's nickname from his pk """ def get_nick(pk) do get_info(find_proc pk).nick end end