diff --git a/apps/wocky/lib/wocky/bot/bot.ex b/apps/wocky/lib/wocky/bot/bot.ex index a1e49b9a4..44e923733 100644 --- a/apps/wocky/lib/wocky/bot/bot.ex +++ b/apps/wocky/lib/wocky/bot/bot.ex @@ -349,6 +349,21 @@ defmodule Wocky.Bot do user |> by_relationship_query(:subscribed) |> where([..., s], s.visitor) + |> order_by([..., s], desc: s.visited_at) + end + + @spec active_bots_query(User.t()) :: Ecto.Queryable.t() + def active_bots_query(user) do + user + |> by_relationship_query(:guest) + |> is_visible_query(user) + |> join( + :inner, + [b], + a in "bot_activity", + b.id == a.bot_id + ) + |> order_by([..., a], desc: a.visited_at) end @spec subscribers_query(t, boolean()) :: [User.t()] diff --git a/apps/wocky/lib/wocky/bot/subscription.ex b/apps/wocky/lib/wocky/bot/subscription.ex index 94972831d..e3d6edc76 100644 --- a/apps/wocky/lib/wocky/bot/subscription.ex +++ b/apps/wocky/lib/wocky/bot/subscription.ex @@ -17,6 +17,8 @@ defmodule Wocky.Bot.Subscription do field :bot_id, :binary_id, primary_key: true field :guest, :boolean, default: false field :visitor, :boolean, default: false + field :visited_at, :utc_datetime + field :departed_at, :utc_datetime timestamps() @@ -58,9 +60,16 @@ defmodule Wocky.Bot.Subscription do def depart(user, bot), do: visit(user, bot, false) defp visit(user, bot, enter) do + timestamps = + if enter do + [visited_at: DateTime.utc_now(), departed_at: nil] + else + [departed_at: DateTime.utc_now()] + end + Subscription |> where(user_id: ^user.id, bot_id: ^bot.id) - |> Repo.update_all(set: [visitor: enter]) + |> Repo.update_all(set: [visitor: enter] ++ timestamps) :ok end @@ -79,7 +88,13 @@ defmodule Wocky.Bot.Subscription do end defp maybe_set_visitor(changes, true), do: changes - defp maybe_set_visitor(changes, false), do: Map.put(changes, :visitor, false) + + defp maybe_set_visitor(changes, false) do + changes + |> Map.put(:visitor, false) + |> Map.put(:visited_at, nil) + |> Map.put(:departed_at, nil) + end defp make_changeset(changes), do: changeset(%Subscription{}, changes) diff --git a/apps/wocky/priv/repo/migrations/20180427175151_add_visit_timestamps.exs b/apps/wocky/priv/repo/migrations/20180427175151_add_visit_timestamps.exs new file mode 100644 index 000000000..8bcf069d0 --- /dev/null +++ b/apps/wocky/priv/repo/migrations/20180427175151_add_visit_timestamps.exs @@ -0,0 +1,25 @@ +defmodule Wocky.Repo.Migrations.AddVisitTimestamps do + use Wocky.Repo.Migration + + def change do + alter table(:bot_subscriptions) do + add :visited_at, :timestamptz + add :departed_at, :timestamptz + end + + flush() + + execute """ + CREATE VIEW bot_activity AS + SELECT bot_id, MAX(visited_at) AS visited_at + FROM bot_subscriptions AS subs + WHERE EXISTS ( + SELECT 1 + FROM bot_subscriptions + WHERE bot_id = subs.bot_id + AND visitor + ) + GROUP BY bot_id; + """ + end +end diff --git a/apps/wocky_api/lib/wocky_api/channels/user_socket.ex b/apps/wocky_api/lib/wocky_api/channels/user_socket.ex index 9d0d15a66..3930e688c 100644 --- a/apps/wocky_api/lib/wocky_api/channels/user_socket.ex +++ b/apps/wocky_api/lib/wocky_api/channels/user_socket.ex @@ -39,7 +39,7 @@ defmodule WockyAPI.UserSocket do {:ok, socket} end - defp host() do + defp host do {:ok, host} = :inet.gethostname() to_string(host) end diff --git a/apps/wocky_api/lib/wocky_api/plugs/absinthe_conn_data.ex b/apps/wocky_api/lib/wocky_api/plugs/absinthe_conn_data.ex index 8ad7a06d9..e684dda01 100644 --- a/apps/wocky_api/lib/wocky_api/plugs/absinthe_conn_data.ex +++ b/apps/wocky_api/lib/wocky_api/plugs/absinthe_conn_data.ex @@ -29,7 +29,7 @@ defmodule WockyAPI.Plugs.AbsintheConnData do to_string(:inet.ntoa(addr)) <> ":" <> to_string(port) end - defp host() do + defp host do {:ok, host} = :inet.gethostname() to_string(host) end diff --git a/apps/wocky_api/lib/wocky_api/resolvers/bot.ex b/apps/wocky_api/lib/wocky_api/resolvers/bot.ex index d25c9ae1a..b5da6b477 100644 --- a/apps/wocky_api/lib/wocky_api/resolvers/bot.ex +++ b/apps/wocky_api/lib/wocky_api/resolvers/bot.ex @@ -73,6 +73,12 @@ defmodule WockyAPI.Resolvers.Bot do {:ok, Bot.lon(bot)} end + def get_active_bots(_root, args, %{context: %{current_user: user}}) do + user + |> Bot.active_bots_query() + |> Utils.connection_from_query(user, args) + end + def create_bot(_root, args, %{context: %{current_user: user}}) do args[:input][:values] |> parse_lat_lon() diff --git a/apps/wocky_api/lib/wocky_api/resolvers/utils.ex b/apps/wocky_api/lib/wocky_api/resolvers/utils.ex index c351b2243..5d0558fc8 100644 --- a/apps/wocky_api/lib/wocky_api/resolvers/utils.ex +++ b/apps/wocky_api/lib/wocky_api/resolvers/utils.ex @@ -63,6 +63,7 @@ defmodule WockyAPI.Resolvers.Utils do defp get_count(query) do query |> exclude(:preload) + |> exclude(:order_by) |> select([x], count(1)) |> Repo.one() |> Kernel.||(0) diff --git a/apps/wocky_api/lib/wocky_api/schema/bot_types.ex b/apps/wocky_api/lib/wocky_api/schema/bot_types.ex index d0e3570c5..d777424e1 100644 --- a/apps/wocky_api/lib/wocky_api/schema/bot_types.ex +++ b/apps/wocky_api/lib/wocky_api/schema/bot_types.ex @@ -30,11 +30,13 @@ defmodule WockyAPI.Schema.BotTypes do enum :subscription_type do @desc "A user who is subscribed to the bot" value :subscriber + @desc """ A user who is subscribed to the bot and is a guest (entry/exit will be reported) """ value :guest + @desc """ A user who is subscribed to the bot and is a guest who is currently visiting the bot @@ -188,6 +190,7 @@ defmodule WockyAPI.Schema.BotTypes do input do @desc "ID of bot to which to subscribe" field :id, non_null(:uuid) + @desc """ Whether to enable guest functionality for the user (default: false) """ diff --git a/apps/wocky_api/lib/wocky_api/schema/user_types.ex b/apps/wocky_api/lib/wocky_api/schema/user_types.ex index 4351ab0d8..39e84b57e 100644 --- a/apps/wocky_api/lib/wocky_api/schema/user_types.ex +++ b/apps/wocky_api/lib/wocky_api/schema/user_types.ex @@ -29,11 +29,6 @@ defmodule WockyAPI.Schema.UserTypes do field :tagline, :string @desc "A list of roles assigned to the user" field :roles, non_null(list_of(non_null(:string))) - @desc "The user's ID for the external auth system (eg Firebase or Digits)" - field :external_id, :string - @desc "The user's phone number in E.123 international notation" - field :phone_number, :string - field :email, :string @desc "Bots related to the user specified by either relationship or ID" connection field :bots, node_type: :bots do @@ -42,12 +37,6 @@ defmodule WockyAPI.Schema.UserTypes do resolve &Bot.get_bots/3 end - @desc "The user's location history for a given device" - connection field :locations, node_type: :locations do - arg :device, non_null(:string) - resolve &User.get_locations/3 - end - @desc """ The user's contacts (ie the XMPP roster) optionally filtered by relationship """ @@ -56,6 +45,36 @@ defmodule WockyAPI.Schema.UserTypes do resolve &User.get_contacts/3 end + @desc "The user's owned bot collections" + connection field :collections, node_type: :collections do + resolve &Collection.get_collections/3 + end + + @desc "Bot collections to which the user is subscribed" + connection field :subscribed_collections, node_type: :collections do + resolve &Collection.get_subscribed_collections/3 + end + end + + object :current_user do + import_fields :user + + @desc "The user's ID for the external auth system (eg Firebase or Digits)" + field :external_id, :string + @desc "The user's phone number in E.123 international notation" + field :phone_number, :string + field :email, :string + + connection field :active_bots, node_type: :bots do + resolve &Bot.get_active_bots/3 + end + + @desc "The user's location history for a given device" + connection field :locations, node_type: :locations do + arg :device, non_null(:string) + resolve &User.get_locations/3 + end + @desc "The user's home stream items" connection field :home_stream, node_type: :home_stream do resolve &User.get_home_stream/3 @@ -67,16 +86,6 @@ defmodule WockyAPI.Schema.UserTypes do connection field :conversations, node_type: :conversations do resolve &User.get_conversations/3 end - - @desc "The user's owned bot collections" - connection field :collections, node_type: :collections do - resolve &Collection.get_collections/3 - end - - @desc "Bot collections to which the user is subscribed" - connection field :subscribed_collections, node_type: :collections do - resolve &Collection.get_subscribed_collections/3 - end end enum :user_bot_relationship do @@ -170,6 +179,7 @@ defmodule WockyAPI.Schema.UserTypes do field :other_jid, non_null(:string) @desc "The contents of the message" field :message, non_null(:string) + @desc """ True if the message was sent from the user, false if it was received by them. @@ -224,7 +234,7 @@ defmodule WockyAPI.Schema.UserTypes do object :user_queries do @desc "Retrive the currently authenticated user" - field :current_user, :user do + field :current_user, :current_user do resolve &User.get_current_user/3 end diff --git a/apps/wocky_api/test/wocky_api/graphql/bot_test.exs b/apps/wocky_api/test/wocky_api/graphql/bot_test.exs index 01c9fca53..44ebb4170 100644 --- a/apps/wocky_api/test/wocky_api/graphql/bot_test.exs +++ b/apps/wocky_api/test/wocky_api/graphql/bot_test.exs @@ -278,6 +278,79 @@ defmodule WockyAPI.GraphQL.BotTest do end end + describe "active bots" do + setup %{user: user, bot: bot, user2: user2, bot2: bot2} do + Bot.subscribe(bot, user, true) + Bot.subscribe(bot2, user, true) + Bot.visit(bot, user, false) + + Bot.subscribe(bot2, user2, true) + Bot.visit(bot2, user2, false) + + for b <- Factory.insert_list(3, :bot, public: true) do + Bot.subscribe(b, user, true) + end + + :ok + end + + @query """ + { + currentUser { + activeBots(first: 5) { + edges { + node { + id + subscribers(first: 5, type: VISITOR) { + edges { + node { + id + } + } + } + } + } + } + } + } + """ + + test "get active bots", %{user: user, bot: bot, user2: user2, bot2: bot2} do + result = run_query(@query, user) + + refute has_errors(result) + + assert result.data == %{ + "currentUser" => %{ + "activeBots" => %{ + "edges" => [ + %{ + "node" => %{ + "id" => bot2.id, + "subscribers" => %{ + "edges" => [ + %{"node" => %{"id" => user2.id}} + ] + } + } + }, + %{ + "node" => %{ + "id" => bot.id, + "subscribers" => %{ + "edges" => [ + %{"node" => %{"id" => user.id}} + ] + } + } + } + ] + } + } + } + end + end + describe "bot mutations" do test "create bot", %{user: user} do fields = [:title, :server, :lat, :lon, :radius, :description, :shortname] diff --git a/apps/wocky_api/test/wocky_api/graphql/user_test.exs b/apps/wocky_api/test/wocky_api/graphql/user_test.exs index e4b254269..d7f2d3669 100644 --- a/apps/wocky_api/test/wocky_api/graphql/user_test.exs +++ b/apps/wocky_api/test/wocky_api/graphql/user_test.exs @@ -168,33 +168,6 @@ defmodule WockyAPI.GraphQL.UserTest do assert error_msg(result) =~ "User not found" assert result.data == %{"user" => nil} end - - @query """ - query ($id: String!) { - user (id: $id) { - id - email - phone_number - external_id - } - } - """ - - test "get protected field on other user", %{user: user, user2: user2} do - result = run_query(@query, user, %{"id" => user2.id}) - - assert error_count(result) == 3 - assert error_msg(result) =~ "authenticated user" - - assert result.data == %{ - "user" => %{ - "id" => user2.id, - "email" => nil, - "phone_number" => nil, - "external_id" => nil - } - } - end end describe "location" do @@ -239,37 +212,6 @@ defmodule WockyAPI.GraphQL.UserTest do } end - @query """ - query ($device: String!, $id: UUID!) { - user (id: $id) { - locations (device: $device, first: 1) { - totalCount - edges { - node { - lat - lon - accuracy - } - } - } - } - } - """ - - test "get locations for other user", %{user: user, user2: user2} do - loc = Factory.insert(:location, user_id: user2.id) - - result = - run_query(@query, user, %{ - "id" => user2.id, - "device" => loc.resource - }) - - assert error_count(result) == 1 - assert error_msg(result) =~ "the authenticated user" - assert result.data == %{"user" => %{"locations" => nil}} - end - @query """ mutation ($input: UserLocationUpdateInput!) { userLocationUpdate (input: $input) { @@ -491,8 +433,13 @@ defmodule WockyAPI.GraphQL.UserTest do test "get conversations", %{user: user, user2: user2} do other_jid = JID.to_binary(User.to_jid(user2, Lorem.word())) message = Lorem.sentence() - Factory.insert(:conversation, other_jid: other_jid, - user: user, message: message) + + Factory.insert( + :conversation, + other_jid: other_jid, + user: user, + message: message + ) result = run_query(@query, user)