Current section
Files
Jump to
Current section
Files
lib/types/session.ex
defmodule UrbitEx.Session do
@moduledoc """
GenServer to keep state of an Urbit Session.
Creates a struct keeping track of global data, and client functions to add and remove channels.
"""
alias UrbitEx.{API, Channel, S3Config, Unread, Resource, Notification}
@derive Jason.Encoder
use GenServer
defstruct ship: "",
url: "",
cookie: "",
keys: [],
groups: [],
channels: [],
unread: [],
profile: %{},
contacts: %{},
invites: [],
metadata: [],
notifications: %{},
group_joins: [],
pending_dms: [],
s3: %S3Config{},
settings: %{},
status: :init
@doc """
Starts the GenServer as a linked process.
Takes a keyword list with initialization options, and an atom to name the process.
"""
def start_link(options \\ [], name \\ :urbit) when is_list(options) do
GenServer.start_link(__MODULE__, struct(__MODULE__, options), name: name)
end
@doc """
Starts the GenServer as an unlinked process.
Takes a keyword list with initialization options, and an atom to name the process.
"""
def start(options \\ [], name \\ :urbit) when is_list(options) do
GenServer.start(__MODULE__, struct(__MODULE__, options), name: name)
end
@doc """
Resets state with a new url and cookie.
Takes a pid or atom name of the server to reset, an url string and a cookie string.
"""
def reconnect(pid, url, cookie), do: GenServer.call(pid, {:login, url, cookie})
@doc """
Creates an eyre channel and attackes it to the session
Takes a keyword list with three optional keys: `:name` to give the new channel, `:parent`, name or pid of the parent session, and `:keep_state`, a boolean to determine whether the channel should send events for automatic state-keeping by the parent session.
Takes a pid or atom name of the server to reset, an url string and a cookie string.
"""
def add_channel(opts \\ []) do
name = opts[:name] || :main
parent = opts[:parent] || :urbit
keep_state = opts[:keep_state] || true
{:ok, cpid} = Channel.start([parent: parent, keep_state: keep_state, name: name], name)
# todo error handling
Channel.connect(cpid)
end
@doc """
Closes an open eyre channel.
Takes a pid or atom name of the parent session, and atom name or pid of the channel to close.
Returns :ok
"""
def close_channel(pid \\ :urbit, name) do
s = read(pid)
c = Channel.read(name)
API.close_channel(s, c)
cpid = Process.whereis(name)
GenServer.cast(pid, {:channel_closed, cpid})
Process.exit(cpid, :kill)
:ok
end
@doc """
Returns the state of the Session.
Takes a pid or atom name of the Session process.
Aliased as `UrbitEx.get`.
"""
def read(pid \\ :urbit) do
GenServer.call(pid, :get)
end
def save_contacts(pid, contacts), do: GenServer.cast(pid, {:save_contacts, contacts})
def set_value(pid, key, value), do: GenServer.cast(pid, {:save_entry, key, value})
def add(pid, key, item), do: GenServer.cast(pid, {:add, key, item})
# server
@impl true
def init(session) do
{:ok, session}
end
@impl true
def handle_call({:reconnect, url, cookie} , _from, session) do
[ship] = Regex.run(~r|~.+=|, cookie)
ship = String.replace(ship, ~r|[~=]|,"")
ns = %{session | url: url, cookie: cookie, ship: ship}
{:reply, ns, ns}
end
@impl true
def handle_call(:get, _from, session) do
{:reply, session, session}
end
@impl true
def handle_call({:save, {:contacts, contacts}}, from, session) do
profile = contacts[UrbitEx.Utils.add_tilde(session.ship)]
new_session = %{session | contacts: contacts, profile: profile}
{:reply, :contacts, new_session}
end
@impl true
def handle_call({:save, {key, value}}, from, session) do
ns = Map.put(session, key, value)
{:reply, key, ns}
end
@impl true
def handle_cast({:channel_closed, pid}, session) do
new_session = %{session | channels: List.delete(session.channels, pid)}
IO.inspect("closing channel")
{:noreply, new_session}
end
@impl true
def handle_info({:channel_added, name, pid}, session) do
IO.inspect("channel #{name} added")
new_session = %{session | channels: [pid | session.channels]}
{:noreply, new_session}
end
# TODO move data processing to reducer, keep genserver as dumb as possible
@impl true
defp lookup_notes(type) when is_atom(type) do
case type do
:mention -> :mentions
:message -> :messages
:link -> :links
:post -> :posts
:note -> :notes
:comment -> :comments
:"add-members" -> :joined
:"remove-members" -> :left
end
end
def handle_info({:add_or_update, {:notifications, notif}}, session) do
type = lookup_notes(notif.type)
notes = session.notifications[type]
filtered = Enum.filter(notes, & &1.resource != notif.resource)
new = [notif | filtered]
notifications = Map.put(session.notifications, type, new)
{:noreply, %{session | notifications: notifications}}
end
@impl true
def handle_info({:add, {key, item}}, session) do
{:noreply, Map.put(session, key, [item | Map.get(session, key)])}
end
@impl true
def handle_info({:remove, {:notifications, type, resource, _index, _time}}, session) do
notifications = session.notifications[lookup_notes(type)]
filtered = Enum.filter(notifications, & &1.resource != resource)
new = Map.put(session.notifications, lookup_notes(type), filtered)
{:noreply, %{session | notifications: new}}
end
@impl true
def handle_info({:remove, {key, item}}, session) do
{:noreply, Map.put(session, key, List.delete(Map.get(session, key), item))}
end
@impl true
def handle_info({:add_or_update, {:contacts, ship, key, value}}, session) do
to_update = session.contacts[ship]
new_contacts =
if to_update do
updated = Map.put(to_update, key, value)
Map.put(session.contacts, ship, updated)
else
contact = %{key => value}
Map.put(session.contacts, ship, contact)
end
{:noreply, %{session | contacts: new_contacts}}
end
# this applies to metadata and groups
@impl true
def handle_info({:add_or_update, {key, item}}, session) do
list = Map.get(session, key)
old = list |> Enum.find(& &1.resource == item.resource)
if old do
newlist = [item | List.delete(list, old)]
ns = Map.put(session, key, newlist)
{:noreply, ns}
else
{:noreply, Map.put(session, key, [item | list])}
end
end
@impl true
def handle_info({:update, {:unread, :add_count, resource, index, timestamp}}, session) do
old = Enum.find(session.unread, & &1.resource == resource && &1.index == index && &1.count)
new = if old do
%{old | last: timestamp, count: old.count + 1}
else
Unread.newcount(resource, index, timestamp)
end
new_unreads = [new | List.delete(session.unread, old)]
{:noreply, %{session | unread: new_unreads}}
end
@impl true
def handle_info({:update, {:unread, :clear_count, resource, index}}, session) do
old = Enum.find(session.unread, & &1.resource == resource && &1.index == index && &1.count)
new_unreads = List.delete(session.unread, old)
{:noreply, %{session | unread: new_unreads}}
end
@impl true
def handle_info({:update, {:unread, :add_each, resource, index,timestamp}}, session) do
old = Enum.find(session.unread, & &1.resource == resource && &1.each)
new = if old do
%{old | last: timestamp, each: [ index | old.each]}
else
Unread.neweach(resource, index, timestamp)
end
new_unreads = [new | List.delete(session.unread, old)]
{:noreply, %{session | unread: new_unreads}}
end
@impl true
def handle_info({:update, {:unread, :clear_each, resource, index }}, session) do
# todo fix this
old = Enum.find(session.unread, & &1.resource == resource && &1.each)
if old do
new = %{old | each: List.delete(old.each, index)}
new_unreads = [new | List.delete(session.unread, old)]
{:noreply, %{session | unread: new_unreads}}
else
{:noreply, session}
end
end
@impl true
def handle_info({:update, {:groups, :add_members, resource, members}}, session) do
group = session.groups |> Enum.find(& &1.resource == resource)
if group do
updated_group = %{group | members: group.members ++ members}
new_groups = [updated_group | List.delete(session.groups, updated_group)]
{:noreply, %{session | groups: new_groups}}
else
{:noreply, session}
end
end
@impl true
def handle_info({:update, {:groups, :remove_members, resource, members}}, session) do
group = session.groups |> Enum.find(& &1.resource == resource)
updated_group = %{group | members: group.members -- members}
new_groups = [updated_group | List.delete(session.groups, updated_group)]
{:noreply, %{session | groups: new_groups}}
end
@impl true
def handle_info({:update, {:groups, :policy, resource, diff}}, session) do
group = session.groups |> Enum.find(& &1.resource == resource)
updated_group = %{group | policy: diff["replace"]}
new_groups = [updated_group | List.delete(session.groups, updated_group)]
{:noreply, %{session | groups: new_groups}}
end
@impl true
def handle_info({:update, {:groups, :tags, resource, tag, ships}}, session) do
group = session.groups |> Enum.find(& &1.resource == resource)
updated_group = %{group | tags: %{tag => ships}}
new_groups = [updated_group | List.delete(session.groups, updated_group)]
{:noreply, %{session | groups: new_groups}}
end
@impl true
def handle_info({:update, {:s3, struct}}, session) do
data = Map.merge(session.s3, struct, fn (_k, v1, v2) ->
s1 = Jason.encode!(v1)
s2 = Jason.encode!(v2)
if String.length(s1) > String.length(s2), do: v1, else: v2
end)
{:noreply, %{session | s3: data}}
end
@impl true
def handle_info({:update, {:s3, :add_bucket, bucket}}, session) do
newbucket = [bucket | session.s3.buckets]
news3 = Map.put(session.s3, :buckets, newbucket)
{:noreply, %{session | s3: news3}}
end
@impl true
def handle_info({:update, {:s3, :remove_bucket, bucket}}, session) do
newbucket = List.delete(session.s3.buckets, bucket)
news3 = Map.put(session.s3, :buckets, newbucket)
{:noreply, %{session | s3: news3}}
end
@impl true
def handle_info({:update, {:s3, key, value}}, session) do
ns3 = Map.put(session.s3, key, value)
{:noreply, %{session | s3: ns3}}
end
@impl true
def handle_info({:update, {_key, _item}}, session) do
{:noreply, session}
end
end
# login: 370310366L
# contrasena: router2727