Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion lib/conversations_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ defmodule ExMicrosoftBot.Client.Conversations do
alias ExMicrosoftBot.Models
alias ExMicrosoftBot.Client

@type pagination_opts :: [
page_size: non_neg_integer(),
continuation_token: String.t()
]

@doc """
Create a new Conversation.

Expand Down Expand Up @@ -87,14 +92,43 @@ defmodule ExMicrosoftBot.Client.Conversations do
|> deserialize_response(&Models.ResourceResponse.parse/1)
end

@doc """
Fetches all members in a conversation, with pagination.

@see [API Reference](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#get-conversation-paged-members)

## Options
* `page_size`: the suggested page size. As the description indicates, this
might not be honored by the BotFramework API (i.e. all members
are returned instead).
* `continuation_token`: a token received from a previous request to this API
that indicates the next page.

*Note:* The REST API reference doesn't mention `page_size` being a suggested
value, but empirical testing and the JavaScript SDK docs corroborate it.

@see [JavaScript SDK docs](https://docs.microsoft.com/en-us/javascript/api/botframework-connector/conversationsgetconversationpagedmembersoptionalparams?view=botbuilder-ts-latest)
"""
@spec get_paged_members(
service_url :: String.t(),
conversation_id :: String.t(),
opts :: pagination_opts()
) :: {:ok, Models.PagedMembersResult.t()} | Client.error_type()
def get_paged_members(service_url, conversation_id, opts \\ []) do
service_url
|> conversations_url("/#{conversation_id}/pagedmembers")
|> add_pagination_opts(opts)
|> get()
|> deserialize_response(&Models.PagedMembersResult.parse/1)
end

@doc """
This function takes a ConversationId and returns an array of ChannelAccount[]
objects which are the members of the conversation.
@see [API Reference](https://docs.botframework.com/en-us/restapi/connector/#!/Conversations/Conversations_GetConversationMembers).

When ActivityId is passed in then it returns the members of the particular
activity in the conversation.

@see [API Reference](https://docs.botframework.com/en-us/restapi/connector/#!/Conversations/Conversations_GetActivityMembers)
"""
@spec get_members(
Expand Down Expand Up @@ -192,4 +226,27 @@ defmodule ExMicrosoftBot.Client.Conversations do

defp conversations_url(service_url, path),
do: conversations_url(service_url) <> path

defp add_pagination_opts(url, []), do: url

defp add_pagination_opts(url, opts) do
query =
Enum.reduce(opts, %{}, fn
{:page_size, page_size}, query -> Map.put(query, :pageSize, page_size)
{:continuation_token, token}, query -> Map.put(query, :continuationToken, token)
{unknown, _value}, _query -> raise "unknown pagination param: #{inspect(unknown)}"
end)

url
|> URI.parse()
|> Map.put(:query, encode_query(query))
|> URI.to_string()
end

if Kernel.function_exported?(URI, :encode_query, 2) do
# Elixir > v1.12
defp encode_query(query), do: URI.encode_query(query, :rfc3986)
else
defp encode_query(query), do: URI.encode_query(query)
end
end
37 changes: 37 additions & 0 deletions lib/models/paged_members_result.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule ExMicrosoftBot.Models.PagedMembersResult do
@moduledoc """
BotFramework structure for returning paginated conversation member data.
@see [API Reference](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#pagedmembersresult-object)
"""

@derive [Poison.Encoder]
defstruct [:members, :continuationToken]

@type t :: %__MODULE__{
members: [ExMicrosoftBot.Models.ChannelAccount.t()],
continuationToken: String.t() | nil
}

@doc """
Decodes a map into `ExMicrosoftBot.Models.PagedMembersResult`
"""
@spec parse(map :: map()) :: {:ok, __MODULE__.t()}
def parse(map) when is_map(map) do
{:ok, Poison.Decode.transform(map, %{as: decoding_map()})}
end

@doc """
Decodes a JSON string into `ExMicrosoftBot.Models.PagedMembersResult`
"""
@spec parse(json :: String.t()) :: __MODULE__.t()
def parse(json) when is_binary(json) do
elem(parse(Poison.decode!(json)), 1)
end

@doc false
def decoding_map() do
%__MODULE__{
members: [ExMicrosoftBot.Models.ChannelAccount.decoding_map()]
}
end
end
71 changes: 69 additions & 2 deletions test/client/conversations_test.exs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule ExMicrosoftBot.Client.ConversationsTest do
use ExUnit.Case

import Plug.Conn, only: [read_body: 1, resp: 3, get_req_header: 2]
import Plug.Conn, only: [fetch_query_params: 1, read_body: 1, resp: 3, get_req_header: 2]

alias ExMicrosoftBot.Models.{Activity, ChannelAccount, ResourceResponse}
alias ExMicrosoftBot.Models.{Activity, ChannelAccount, PagedMembersResult, ResourceResponse}
alias ExMicrosoftBot.Client.Conversations

@bypass_port Application.fetch_env!(:ex_microsoftbot, Bypass) |> Keyword.fetch!(:port)
Expand Down Expand Up @@ -135,4 +135,71 @@ defmodule ExMicrosoftBot.Client.ConversationsTest do
) == {:ok, ""}
end
end

describe "get_paged_members/3" do
setup do
bypass = Bypass.open(port: @bypass_port)
{:ok, bypass: bypass}
end

test "fetches paginated member results", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/v3/conversations/xyz/pagedmembers", fn conn ->
assert conn |> get_req_header("content-type") |> List.first() == "application/json"
assert conn |> get_req_header("accept") |> List.first() == "application/json"
assert conn |> get_req_header("authorization") |> List.first() == "Bearer"

assert {:ok, "", conn} = read_body(conn)

response =
Poison.encode!(%{
members: [
%{
id: "42",
name: "James",
surname: "Lindy",
email: "[email protected]"
}
],
continuationToken: "zzz"
})

resp(conn, 200, response)
end)

assert {:ok, %PagedMembersResult{members: members, continuationToken: token}} =
Conversations.get_paged_members("http://localhost:#{@bypass_port}", "xyz")

assert members == [
%ChannelAccount{
id: "42",
name: "James",
surname: "Lindy",
email: "[email protected]",
objectId: nil,
userPrincipalName: nil,
tenantId: nil
}
]

assert token == "zzz"
end

test "accepts pagination args", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/v3/conversations/xyz/pagedmembers", fn conn ->
assert {:ok, "", conn} = read_body(conn)

%{query_params: query} = fetch_query_params(conn)

assert query["pageSize"] == "42"
assert query["continuationToken"] == "xxx"

resp(conn, 200, Poison.encode!(%{members: []}))
end)

paging = [page_size: 42, continuation_token: "xxx"]

assert {:ok, %PagedMembersResult{members: [], continuationToken: nil}} =
Conversations.get_paged_members("http://localhost:#{@bypass_port}", "xyz", paging)
end
end
end