Skip to content

Commit fbbb5c7

Browse files
committed
Adds support for fetching members using pagination
1 parent c1c2185 commit fbbb5c7

File tree

3 files changed

+164
-3
lines changed

3 files changed

+164
-3
lines changed

lib/conversations_client.ex

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ defmodule ExMicrosoftBot.Client.Conversations do
99
alias ExMicrosoftBot.Models
1010
alias ExMicrosoftBot.Client
1111

12+
@type pagination_opts :: [
13+
page_size: non_neg_integer(),
14+
continuation_token: String.t()
15+
]
16+
1217
@doc """
1318
Create a new Conversation.
1419
@@ -87,14 +92,43 @@ defmodule ExMicrosoftBot.Client.Conversations do
8792
|> deserialize_response(&Models.ResourceResponse.parse/1)
8893
end
8994

95+
@doc """
96+
Fetchs all members in a conversation, with pagination.
97+
98+
@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)
99+
100+
## Options
101+
* `page_size`: the suggested page size. As the name indicates, this might not
102+
be honored by the BotFramework API (i.e. all members are
103+
returned instead).
104+
* `continuation_token`: a token received from a previous request to this API
105+
that indicates the next page.
106+
107+
*Note:* The REST API reference doesn't mention `page_size` being a suggested
108+
value, but empirical testing and the
109+
[JavaScript SDK docs](https://docs.microsoft.com/en-us/javascript/api/botframework-connector/conversationsgetconversationpagedmembersoptionalparams?view=botbuilder-ts-latest)
110+
corroborate this.
111+
"""
112+
@spec get_paged_members(
113+
service_url :: String.t(),
114+
conversation_id :: String.t(),
115+
opts :: pagination_opts()
116+
) :: {:ok, Models.PagedMembersResult.t()} | Client.error_type()
117+
def get_paged_members(service_url, conversation_id, opts \\ []) do
118+
service_url
119+
|> conversations_url("/#{conversation_id}/pagedmembers")
120+
|> add_pagination_opts(opts)
121+
|> get()
122+
|> deserialize_response(&Models.PagedMembersResult.parse/1)
123+
end
124+
90125
@doc """
91126
This function takes a ConversationId and returns an array of ChannelAccount[]
92127
objects which are the members of the conversation.
93128
@see [API Reference](https://docs.botframework.com/en-us/restapi/connector/#!/Conversations/Conversations_GetConversationMembers).
94129
95130
When ActivityId is passed in then it returns the members of the particular
96131
activity in the conversation.
97-
98132
@see [API Reference](https://docs.botframework.com/en-us/restapi/connector/#!/Conversations/Conversations_GetActivityMembers)
99133
"""
100134
@spec get_members(
@@ -192,4 +226,27 @@ defmodule ExMicrosoftBot.Client.Conversations do
192226

193227
defp conversations_url(service_url, path),
194228
do: conversations_url(service_url) <> path
229+
230+
defp add_pagination_opts(url, []), do: url
231+
232+
defp add_pagination_opts(url, opts) do
233+
query =
234+
Enum.reduce(opts, %{}, fn
235+
{:page_size, page_size}, query -> Map.put(query, :pageSize, page_size)
236+
{:continuation_token, token}, query -> Map.put(query, :continuationToken, token)
237+
{unknown, _value}, _query -> raise "unknown pagination param: #{inspect(unknown)}"
238+
end)
239+
240+
url
241+
|> URI.parse()
242+
|> Map.put(:query, encode_query(query))
243+
|> URI.to_string()
244+
end
245+
246+
if Kernel.function_exported?(URI, :encode_query, 2) do
247+
# Elixir > v1.12
248+
defp encode_query(query), do: URI.encode_query(query, :rfc3986)
249+
else
250+
defp encode_query(query), do: URI.encode_query(query)
251+
end
195252
end

lib/models/paged_members_result.ex

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule ExMicrosoftBot.Models.PagedMembersResult do
2+
@moduledoc """
3+
BotFramework structure for returning paginated conversation member data.
4+
@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)
5+
"""
6+
7+
@derive [Poison.Encoder]
8+
defstruct [:members, :continuationToken]
9+
10+
@type t :: %__MODULE__{
11+
members: [ExMicrosoftBot.Models.ChannelAccount.t()],
12+
continuationToken: String.t() | nil
13+
}
14+
15+
@doc """
16+
Decodes a map into `ExMicrosoftBot.Models.PagedMembersResult`
17+
"""
18+
@spec parse(map :: map()) :: {:ok, __MODULE__.t()}
19+
def parse(map) when is_map(map) do
20+
{:ok, Poison.Decode.transform(map, %{as: decoding_map()})}
21+
end
22+
23+
@doc """
24+
Decodes a JSON string into `ExMicrosoftBot.Models.PagedMembersResult`
25+
"""
26+
@spec parse(json :: String.t()) :: __MODULE__.t()
27+
def parse(json) when is_binary(json) do
28+
elem(parse(Poison.decode!(json)), 1)
29+
end
30+
31+
@doc false
32+
def decoding_map() do
33+
%__MODULE__{
34+
members: [ExMicrosoftBot.Models.ChannelAccount.decoding_map()]
35+
}
36+
end
37+
end

test/client/conversations_test.exs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
defmodule ExMicrosoftBot.Client.ConversationsTest do
22
use ExUnit.Case
33

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

6-
alias ExMicrosoftBot.Models.{Activity, ChannelAccount, ResourceResponse}
6+
alias ExMicrosoftBot.Models.{Activity, ChannelAccount, PagedMembersResult, ResourceResponse}
77
alias ExMicrosoftBot.Client.Conversations
88

99
@bypass_port Application.fetch_env!(:ex_microsoftbot, Bypass) |> Keyword.fetch!(:port)
@@ -135,4 +135,71 @@ defmodule ExMicrosoftBot.Client.ConversationsTest do
135135
) == {:ok, ""}
136136
end
137137
end
138+
139+
describe "get_paged_members/3" do
140+
setup do
141+
bypass = Bypass.open(port: @bypass_port)
142+
{:ok, bypass: bypass}
143+
end
144+
145+
test "fetches paginated member results", %{bypass: bypass} do
146+
Bypass.expect_once(bypass, "GET", "/v3/conversations/xyz/pagedmembers", fn conn ->
147+
assert conn |> get_req_header("content-type") |> List.first() == "application/json"
148+
assert conn |> get_req_header("accept") |> List.first() == "application/json"
149+
assert conn |> get_req_header("authorization") |> List.first() == "Bearer"
150+
151+
assert {:ok, "", conn} = read_body(conn)
152+
153+
response =
154+
Poison.encode!(%{
155+
members: [
156+
%{
157+
id: "42",
158+
name: "James",
159+
surname: "Lindy",
160+
161+
}
162+
],
163+
continuationToken: "zzz"
164+
})
165+
166+
resp(conn, 200, response)
167+
end)
168+
169+
assert {:ok, %PagedMembersResult{members: members, continuationToken: token}} =
170+
Conversations.get_paged_members("http://localhost:#{@bypass_port}", "xyz")
171+
172+
assert members == [
173+
%ChannelAccount{
174+
id: "42",
175+
name: "James",
176+
surname: "Lindy",
177+
178+
objectId: nil,
179+
userPrincipalName: nil,
180+
tenantId: nil
181+
}
182+
]
183+
184+
assert token == "zzz"
185+
end
186+
187+
test "accepts pagination args", %{bypass: bypass} do
188+
Bypass.expect_once(bypass, "GET", "/v3/conversations/xyz/pagedmembers", fn conn ->
189+
assert {:ok, "", conn} = read_body(conn)
190+
191+
%{query_params: query} = fetch_query_params(conn)
192+
193+
assert query["pageSize"] == "42"
194+
assert query["continuationToken"] == "xxx"
195+
196+
resp(conn, 200, Poison.encode!(%{members: []}))
197+
end)
198+
199+
paging = [page_size: 42, continuation_token: "xxx"]
200+
201+
assert {:ok, %PagedMembersResult{members: [], continuationToken: nil}} =
202+
Conversations.get_paged_members("http://localhost:#{@bypass_port}", "xyz", paging)
203+
end
204+
end
138205
end

0 commit comments

Comments
 (0)