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
defstruct [:pk, :id, :state, :netgroup]
end
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