From 23eba81b20d06469a045111533bb28afa46d33bc Mon Sep 17 00:00:00 2001 From: Michael Shaffer Date: Wed, 6 Dec 2023 20:55:05 -0500 Subject: [PATCH 1/4] Added the ability to pull account level call logs. GET phone/call_logs (#328) * Added the ability to pull account level call logs. GET phone/call_logs * Reordered parameters for account call log retrieval and added time type to the request parameters. --------- Co-authored-by: Michael Shaffer --- .../Resources/AccountCallLogsTests.cs | 316 ++++++++++++++++++ .../Json/ZoomNetJsonSerializerContext.cs | 6 + Source/ZoomNet/Models/AccountCallLog.cs | 39 +++ Source/ZoomNet/Models/CallLogOwnerInfo.cs | 26 ++ Source/ZoomNet/Models/CallLogOwnerType.cs | 40 +++ Source/ZoomNet/Models/CallLogPathType.cs | 142 ++++++++ Source/ZoomNet/Models/CallLogTimeType.cs | 22 ++ Source/ZoomNet/Resources/CallLogs.cs | 38 +++ Source/ZoomNet/Resources/ICallLogs.cs | 18 + 9 files changed, 647 insertions(+) create mode 100644 Source/ZoomNet.UnitTests/Resources/AccountCallLogsTests.cs create mode 100644 Source/ZoomNet/Models/AccountCallLog.cs create mode 100644 Source/ZoomNet/Models/CallLogOwnerInfo.cs create mode 100644 Source/ZoomNet/Models/CallLogOwnerType.cs create mode 100644 Source/ZoomNet/Models/CallLogPathType.cs create mode 100644 Source/ZoomNet/Models/CallLogTimeType.cs diff --git a/Source/ZoomNet.UnitTests/Resources/AccountCallLogsTests.cs b/Source/ZoomNet.UnitTests/Resources/AccountCallLogsTests.cs new file mode 100644 index 00000000..254b38f5 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Resources/AccountCallLogsTests.cs @@ -0,0 +1,316 @@ +using RichardSzalay.MockHttp; +using Shouldly; +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using ZoomNet.Json; +using ZoomNet.Models; +using ZoomNet.Resources; + +namespace ZoomNet.UnitTests.Resources +{ + public class AccountCallLogsTests { + #region FIELDS + + private const string SINGLE_ACCOUNT_CALL_LOGS_JSON = @" + { + ""id"": ""c82112cd-0916-412c-8d2d-4620c93edfa6"", + ""user_id"": ""1LzxYZ4WUsVp1zn7hsT6B"", + ""call_type"": ""pstn"", + ""caller_number"": ""1100"", + ""caller_number_type"": 1, + ""caller_name"": ""Main Auto Receptionist"", + ""callee_number"": ""+17506977821"", + ""callee_number_type"": 2, + ""callee_number_source"": ""external"", + ""callee_location"": ""Texas"", + ""direction"": ""outbound"", + ""duration"": 3, + ""result"": ""Auto Recorded"", + ""date_time"": ""2023-12-06T03:10:06Z"", + ""path"": ""pstn"", + ""recording_id"": ""0b24374d5d687899a021b28334b5e6a7"", + ""recording_type"": ""Automatic"", + ""has_voicemail"": false, + ""call_id"": ""7102333465063798289"", + ""owner"": { + ""type"": ""autoReceptionist"", + ""id"": ""o-EoDdR-eey3q9_wdGMheR"", + ""name"": ""Main Auto Receptionist"", + ""extension_number"": 1100 + }, + ""caller_did_number"": ""+17506476849"", + ""caller_country_code"": ""1"", + ""caller_country_iso_code"": ""US"", + ""callee_country_code"": ""1"", + ""callee_country_iso_code"": ""US"", + ""call_end_time"": ""2023-12-06T03:10:24Z"", + ""department"": """", + ""cost_center"": """" + } +"; + + private const string MULTIPLE_ACCOUNT_CALL_LOGS_JSON = @" +{ + ""next_page_token"": ""drhhjtYGfgrg56456456"", + ""page_size"": 5, + ""total_records"": 95, + ""from"": ""2023-12-05"", + ""to"": ""2023-12-06"", + ""call_logs"": [ + { + ""id"": ""c82112cd-0916-412c-8d2d-4620c93edfa6"", + ""user_id"": ""1LzxYZ4WUsVp1zn7hsT6B"", + ""call_type"": ""pstn"", + ""caller_number"": ""1100"", + ""caller_number_type"": 1, + ""caller_name"": ""Main Auto Receptionist"", + ""callee_number"": ""+17506977821"", + ""callee_number_type"": 2, + ""callee_number_source"": ""external"", + ""callee_location"": ""Texas"", + ""direction"": ""outbound"", + ""duration"": 3, + ""result"": ""Auto Recorded"", + ""date_time"": ""2023-12-06T03:10:06Z"", + ""path"": ""pstn"", + ""recording_id"": ""0b24374d5d687899a021b28334b5e6a7"", + ""recording_type"": ""Automatic"", + ""has_voicemail"": false, + ""call_id"": ""7102333465063798289"", + ""owner"": { + ""type"": ""autoReceptionist"", + ""id"": ""o-EoDdR-eey3q9_wdGMheR"", + ""name"": ""Main Auto Receptionist"", + ""extension_number"": 1100 + }, + ""caller_did_number"": ""+17506476849"", + ""caller_country_code"": ""1"", + ""caller_country_iso_code"": ""US"", + ""callee_country_code"": ""1"", + ""callee_country_iso_code"": ""US"", + ""call_end_time"": ""2023-12-06T03:10:24Z"", + ""department"": """", + ""cost_center"": """" + }, + { + ""id"": ""b3d4f865-24d8-4f36-afd3-3492c09c69ed"", + ""user_id"": ""4Lm9W8KHooPnQrt6Y4RuF"", + ""call_type"": ""pstn"", + ""caller_number"": ""1100"", + ""caller_number_type"": 1, + ""caller_name"": ""Main Auto Receptionist"", + ""callee_number"": ""+15162788092"", + ""callee_number_type"": 2, + ""callee_number_source"": ""external"", + ""callee_location"": ""North Carolina"", + ""direction"": ""outbound"", + ""duration"": 15, + ""result"": ""Auto Recorded"", + ""date_time"": ""2023-12-06T00:47:43Z"", + ""path"": ""pstn"", + ""recording_id"": ""963526f778894a6bbcfd2e6f5304f556"", + ""recording_type"": ""Automatic"", + ""has_voicemail"": false, + ""call_id"": ""7309475677182123450"", + ""owner"": { + ""type"": ""autoReceptionist"", + ""id"": ""o-RtfghR-Rdg569_PDefGLhA"", + ""name"": ""Main Auto Receptionist"", + ""extension_number"": 1100 + }, + ""caller_did_number"": ""+175004345645"", + ""caller_country_code"": ""1"", + ""caller_country_iso_code"": ""US"", + ""callee_country_code"": ""1"", + ""callee_country_iso_code"": ""US"", + ""call_end_time"": ""2023-12-06T00:48:05Z"", + ""department"": """", + ""cost_center"": """" + }, + { + ""id"": ""a7c1480c-7da1-4f7b-9913-10448ab4e993"", + ""call_type"": ""voip"", + ""caller_number"": ""+17566678189"", + ""caller_number_type"": 2, + ""caller_number_source"": ""external"", + ""caller_name"": ""WAL MART"", + ""callee_number"": ""1100"", + ""callee_number_type"": 1, + ""callee_name"": ""Main Auto Receptionist"", + ""direction"": ""inbound"", + ""duration"": 103, + ""result"": ""Call connected"", + ""date_time"": ""2023-12-06T00:44:44Z"", + ""path"": ""autoReceptionist"", + ""has_recording"": false, + ""has_voicemail"": false, + ""call_id"": ""7405266708095066735"", + ""owner"": { + ""type"": ""autoReceptionist"", + ""id"": ""o-WodfT-rTHghH_TfgRteA"", + ""name"": ""Main Auto Receptionist"", + ""extension_number"": 1100 + }, + ""caller_country_code"": ""1"", + ""caller_country_iso_code"": ""US"", + ""callee_did_number"": ""+175056644545"", + ""callee_country_code"": ""1"", + ""callee_country_iso_code"": ""US"", + ""answer_start_time"": ""2023-12-06T00:44:44Z"", + ""department"": """", + ""cost_center"": """" + }, + { + ""id"": ""53a60306-ab5b-4c5e-b2c3-a1da5a47608e"", + ""user_id"": ""vD9AUZ7rtx9Ueqa6F3Kuq"", + ""call_type"": ""pstn"", + ""caller_number"": ""1100"", + ""caller_number_type"": 1, + ""caller_name"": ""Main Auto Receptionist"", + ""callee_number"": ""+184567434534"", + ""callee_number_type"": 2, + ""callee_number_source"": ""external"", + ""callee_location"": ""United States"", + ""direction"": ""outbound"", + ""duration"": 24, + ""result"": ""Auto Recorded"", + ""date_time"": ""2023-12-06T00:44:13Z"", + ""path"": ""pstn"", + ""recording_id"": ""1d224334a596477788d87849f9f999c"", + ""recording_type"": ""Automatic"", + ""has_voicemail"": false, + ""call_id"": ""7392374044525562788"", + ""owner"": { + ""type"": ""autoReceptionist"", + ""id"": ""o-RregY-DfgGfg_ERdfg"", + ""name"": ""Main Auto Receptionist"", + ""extension_number"": 1100 + }, + ""caller_did_number"": ""+1704556777"", + ""caller_country_code"": ""1"", + ""caller_country_iso_code"": ""US"", + ""callee_country_code"": ""1"", + ""callee_country_iso_code"": ""US"", + ""call_end_time"": ""2023-12-06T00:44:38Z"", + ""department"": """", + ""cost_center"": """" + }, + { + ""id"": ""f64e97b9-b234-487e-8c12-299e7f664922"", + ""user_id"": ""1C6XSI2pjAblbUsJkSAmy"", + ""call_type"": ""pstn"", + ""caller_number"": ""1100"", + ""caller_number_type"": 1, + ""caller_name"": ""Main Auto Receptionist"", + ""callee_number"": ""+145665788674"", + ""callee_number_type"": 2, + ""callee_number_source"": ""external"", + ""callee_location"": ""United States"", + ""direction"": ""outbound"", + ""duration"": 36, + ""result"": ""Auto Recorded"", + ""date_time"": ""2023-12-06T00:43:26Z"", + ""path"": ""pstn"", + ""recording_id"": ""d3456767517fh556576554456565656909"", + ""recording_type"": ""Automatic"", + ""has_voicemail"": false, + ""call_id"": ""73455748542158465463"", + ""owner"": { + ""type"": ""autoReceptionist"", + ""id"": ""o-Edcfr-RfFdfG_SDfdfA"", + ""name"": ""Main Auto Receptionist"", + ""extension_number"": 1100 + }, + ""caller_did_number"": ""+17045644435"", + ""caller_country_code"": ""1"", + ""caller_country_iso_code"": ""US"", + ""callee_country_code"": ""1"", + ""callee_country_iso_code"": ""US"", + ""call_end_time"": ""2023-12-06T00:44:04Z"", + ""department"": """", + ""cost_center"": """" + } + ] +} +"; + + #endregion + + [Fact] + public void Parse_json() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize(SINGLE_ACCOUNT_CALL_LOGS_JSON, JsonFormatter.SerializerOptions); + + // Assert + result.ShouldNotBeNull(); + result.Id.ShouldBe("c82112cd-0916-412c-8d2d-4620c93edfa6"); + result.UserId.ShouldBe("1LzxYZ4WUsVp1zn7hsT6B"); + result.CallType.ShouldBe(CallLogCallType.Pstn); + result.CallerNumber.ShouldBe("1100"); + result.CallerNumberType.ShouldBe(CallLogCallerNumberType.Extension); + result.CallerName.ShouldBe("Main Auto Receptionist"); + result.CalleeNumber.ShouldBe("+17506977821"); + result.CalleeNumberType.ShouldBe(CallLogCalleeNumberType.Phone); + result.CalleeNumberSource.ShouldBe(CallLogNumberSource.External); + result.Direction.ShouldBe(CallLogDirection.Outbound); + result.Duration.ShouldBe(3); + result.Result.ShouldBe(CallLogResult.AutoRecorded); + result.StartedTime.ShouldBe(new DateTime(2023, 12, 6, 3, 10, 6, DateTimeKind.Utc));// DateTime.Parse("2023-12-06T03:10:06Z")); + result.Path.ShouldBe("pstn"); + result.RecordingId.ShouldBe("0b24374d5d687899a021b28334b5e6a7"); + result.RecordingType.ShouldBe("Automatic"); + result.CallId.ShouldBe("7102333465063798289"); + result.Owner.ShouldNotBeNull(); + result.Owner.Type.ShouldBe(CallLogOwnerType.AutoReceptionist); + result.Owner.Id.ShouldBe("o-EoDdR-eey3q9_wdGMheR"); + result.Owner.Name.ShouldBe("Main Auto Receptionist"); + result.Owner.ExtensionNumber.ShouldBe(1100); + result.CallerDidNumber.ShouldBe("+17506476849"); + result.CallerCountryCode.ShouldBe("1"); + result.CallerCountryIsoCode.ShouldBe(Country.United_States_of_America); + result.CalleeCountryCode.ShouldBe("1"); + result.CalleeCountryIsoCode.ShouldBe(Country.United_States_of_America); + result.CallEndTime.ShouldBe(new DateTime(2023, 12, 6, 3, 10, 24, DateTimeKind.Utc));// DateTime.Parse("2023-12-06T03:10:24Z")); + result.UserDepartment.ShouldBe(""); + result.UserCostCenter.ShouldBe(""); + } + + [Fact] + public async Task GetCallLogsForAccountAsync() + { + // Arrange + var from = new DateTime(2023, 12, 5, 0, 0, 0, DateTimeKind.Utc); + var to = new DateTime(2023, 12, 6, 0, 0, 0, DateTimeKind.Utc); + var recordsPerPage = 5; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp.Expect(HttpMethod.Get, Utils.GetZoomApiUri("phone", "call_logs")).Respond("application/json", MULTIPLE_ACCOUNT_CALL_LOGS_JSON); + + var client = Utils.GetFluentClient(mockHttp); + var calllogs = new CallLogs(client); + + // Act + var result = await calllogs.GetAsync(from, to, CallLogType.All, null, CallLogTimeType.StartTime, null, false, recordsPerPage, null, CancellationToken.None).ConfigureAwait(true); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.PageSize.ShouldBe(recordsPerPage); + result.NextPageToken.ShouldNotBeNullOrEmpty(); + result.MoreRecordsAvailable.ShouldBeTrue(); + result.TotalRecords.ShouldBe(95); + result.From.ShouldBe(from); + result.To.ShouldBe(to); + result.Records.ShouldNotBeNull(); + result.Records.Length.ShouldBe(5); + } + } +} diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index f94c4742..e6f5f015 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -6,6 +6,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(System.Text.Json.Nodes.JsonObject[]))] [JsonSerializable(typeof(ZoomNet.Models.Account))] + [JsonSerializable(typeof(ZoomNet.Models.AccountCallLog))] [JsonSerializable(typeof(ZoomNet.Models.ApprovalType))] [JsonSerializable(typeof(ZoomNet.Models.Assistant))] [JsonSerializable(typeof(ZoomNet.Models.AttendeeChatSaveType))] @@ -27,8 +28,12 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogCallType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogDirection))] [JsonSerializable(typeof(ZoomNet.Models.CallLogNumberSource))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogOwnerInfo))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogOwnerType))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogPathType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogResult))] [JsonSerializable(typeof(ZoomNet.Models.CallLogSite))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogTimeType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfo))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType))] @@ -282,6 +287,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ZoomRoomWithIssuesReport))] [JsonSerializable(typeof(ZoomNet.Models.Account[]))] + [JsonSerializable(typeof(ZoomNet.Models.AccountCallLog[]))] [JsonSerializable(typeof(ZoomNet.Models.ApprovalType[]))] [JsonSerializable(typeof(ZoomNet.Models.Assistant[]))] [JsonSerializable(typeof(ZoomNet.Models.AttendeeChatSaveType[]))] diff --git a/Source/ZoomNet/Models/AccountCallLog.cs b/Source/ZoomNet/Models/AccountCallLog.cs new file mode 100644 index 00000000..3819d635 --- /dev/null +++ b/Source/ZoomNet/Models/AccountCallLog.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// An account call log item. + /// + /// + public class AccountCallLog : CallLog + { + /// + /// Gets or sets the device's private IP address if the account has the show_device_ip_for_call_log parameter set to enabled. + /// + /// Indicates the device's private IP address. + [JsonPropertyName("device_private_ip")] + public string DevicePrivateIp { get; set; } + + /// + /// Gets or sets the device's public IP address if the account has the show_device_ip_for_call_log parameter set to enabled. + /// + /// Indicates the device's public IP address. + [JsonPropertyName("device_public_ip")] + public string DevicePublicIp { get; set; } + + /// + /// Gets or sets the "owner" information. + /// + /// Call "owner" information. + [JsonPropertyName("owner")] + public CallLogOwnerInfo Owner { get; set; } + + /// + /// Gets or sets the unique identifier of the call recording. + /// + /// Unique identifier of the call recording. + [JsonPropertyName("recording_id")] + public string RecordingId { get; set; } + } +} diff --git a/Source/ZoomNet/Models/CallLogOwnerInfo.cs b/Source/ZoomNet/Models/CallLogOwnerInfo.cs new file mode 100644 index 00000000..02f9df89 --- /dev/null +++ b/Source/ZoomNet/Models/CallLogOwnerInfo.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Call transfer/forward information. + /// + public class CallLogOwnerInfo + { + /// Gets or sets the extension number. + [JsonPropertyName("extension_number")] + public int ExtensionNumber { get; set; } + + /// Gets or sets the owner ID. + [JsonPropertyName("id")] + public string Id { get; set; } + + /// Gets or sets the name. + [JsonPropertyName("name")] + public string Name { get; set; } + + /// Gets or sets the owner type. + [JsonPropertyName("type")] + public CallLogOwnerType? Type { get; set; } + } +} diff --git a/Source/ZoomNet/Models/CallLogOwnerType.cs b/Source/ZoomNet/Models/CallLogOwnerType.cs new file mode 100644 index 00000000..395932aa --- /dev/null +++ b/Source/ZoomNet/Models/CallLogOwnerType.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the owner type. + /// + public enum CallLogOwnerType + { + /// + /// user. + /// + [EnumMember(Value = "user")] + User, + + /// + /// callQueue. + /// + [EnumMember(Value = "callQueue")] + CallQueue, + + /// + /// autoReceptionist. + /// + [EnumMember(Value = "autoReceptionist")] + AutoReceptionist, + + /// + /// commonAreaPhone. + /// + [EnumMember(Value = "commonAreaPhone")] + CommonAreaPhone, + + /// + /// sharedLineGroup. + /// + [EnumMember(Value = "sharedLineGroup")] + SharedLineGroup + } +} diff --git a/Source/ZoomNet/Models/CallLogPathType.cs b/Source/ZoomNet/Models/CallLogPathType.cs new file mode 100644 index 00000000..be9ffe04 --- /dev/null +++ b/Source/ZoomNet/Models/CallLogPathType.cs @@ -0,0 +1,142 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the extension type. + /// + public enum CallLogPathType + { + /// + /// voiceMail. + /// + [EnumMember(Value = "voiceMail")] + VoiceMail, + + /// + /// message. + /// + [EnumMember(Value = "message")] + Message, + + /// + /// forward. + /// + [EnumMember(Value = "forward")] + Forward, + + /// + /// extension. + /// + [EnumMember(Value = "extension")] + Extension, + + /// + /// callQueue. + /// + [EnumMember(Value = "callQueue")] + CallQueue, + + /// + /// ivrMenu. + /// + [EnumMember(Value = "ivrMenu")] + IvrMenu, + + /// + /// companyDirectory. + /// + [EnumMember(Value = "companyDirectory")] + CompanyDirectory, + + /// + /// autoReceptionist. + /// + [EnumMember(Value = "autoReceptionist")] + AutoReceptionist, + + /// + /// contactCenter. + /// + [EnumMember(Value = "contactCenter")] + ContactCenter, + + /// + /// disconnected. + /// + [EnumMember(Value = "disconnected")] + Disconnected, + + /// + /// commonAreaPhone. + /// + [EnumMember(Value = "commonAreaPhone")] + CommonAreaPhone, + + /// + /// pstn. + /// + [EnumMember(Value = "pstn")] + Pstn, + + /// + /// transfer. + /// + [EnumMember(Value = "transfer")] + Transfer, + + /// + /// sharedLines. + /// + [EnumMember(Value = "sharedLines")] + SharedLines, + + /// + /// sharedLineGroup. + /// + [EnumMember(Value = "sharedLineGroup")] + SharedLineGroup, + + /// + /// tollFreeBilling. + /// + [EnumMember(Value = "tollFreeBilling")] + TollFreeBilling, + + /// + /// meetingService. + /// + [EnumMember(Value = "meetingService")] + MeetingService, + + /// + /// parkPickup. + /// + [EnumMember(Value = "parkPickup")] + ParkPickup, + + /// + /// parkTimeout. + /// + [EnumMember(Value = "parkTimeout")] + ParkTimeout, + + /// + /// monitor. + /// + [EnumMember(Value = "monitor")] + Monitor, + + /// + /// takeover. + /// + [EnumMember(Value = "takeover")] + Takeover, + + /// + /// sipGroup. + /// + [EnumMember(Value = "sipGroup")] + SipGroup + } +} diff --git a/Source/ZoomNet/Models/CallLogTimeType.cs b/Source/ZoomNet/Models/CallLogTimeType.cs new file mode 100644 index 00000000..78af5240 --- /dev/null +++ b/Source/ZoomNet/Models/CallLogTimeType.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of the call. + /// + public enum CallLogTimeType + { + /// + /// Enables you to search call logs by start time. + /// + [EnumMember(Value = "startTime")] + StartTime, + + /// + /// Enables you to search call logs by end time. + /// + [EnumMember(Value = "endTime")] + EndTime + } +} diff --git a/Source/ZoomNet/Resources/CallLogs.cs b/Source/ZoomNet/Resources/CallLogs.cs index 6c781cd6..9f2f4441 100644 --- a/Source/ZoomNet/Resources/CallLogs.cs +++ b/Source/ZoomNet/Resources/CallLogs.cs @@ -58,5 +58,43 @@ public Task> GetAsync(string userId, Dat .WithCancellationToken(cancellationToken) .AsPaginatedResponseWithToken("call_logs"); } + + /// + /// Get call logs for an entire account. + /// + /// The start date. + /// The end date. + /// Type of call log. + /// Filter the API response by path of the call. + /// Enables you to search call logs by start or end time. + /// Unique identifier of the site. Use this query parameter if you have enabled multiple sites and would like to filter the response of this API call by call logs of a specific phone site. + /// Whether to filter API responses to include call logs that only have a non-zero charge. + /// The number of records to return. + /// The paging token. + /// The cancellation token. + /// + /// An array of . + /// + public Task> GetAsync(DateTime? from = null, DateTime? to = null, CallLogType type = CallLogType.All, CallLogPathType? pathType = null, CallLogTimeType? timeType = CallLogTimeType.StartTime, string siteId = null, bool chargedCallLogs = false, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) + { + if (recordsPerPage < 1 || recordsPerPage > 300) + { + throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 300"); + } + + return _client + .GetAsync($"phone/call_logs") + .WithArgument("from", from.ToZoomFormat(dateOnly: true)) + .WithArgument("to", to.ToZoomFormat(dateOnly: true)) + .WithArgument("type", type.ToEnumString()) + .WithArgument("path", pathType?.ToEnumString()) + .WithArgument("timeType", timeType?.ToEnumString()) + .WithArgument("site_id", siteId) + .WithArgument("charged_call_logs", chargedCallLogs) + .WithArgument("page_size", recordsPerPage) + .WithArgument("next_page_token", pagingToken) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponseWithTokenAndDateRange("call_logs"); + } } } diff --git a/Source/ZoomNet/Resources/ICallLogs.cs b/Source/ZoomNet/Resources/ICallLogs.cs index 977de171..3e378ad2 100644 --- a/Source/ZoomNet/Resources/ICallLogs.cs +++ b/Source/ZoomNet/Resources/ICallLogs.cs @@ -28,5 +28,23 @@ public interface ICallLogs /// An array of . /// Task> GetAsync(string userId, DateTime? from = null, DateTime? to = null, CallLogType type = CallLogType.All, string phoneNumber = null, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); + + /// + /// Get call logs for an entire account. + /// + /// The start date. + /// The end date. + /// Type of call log. + /// Filter the API response by path of the call. + /// Enables you to search call logs by start or end time. + /// Unique identifier of the site. Use this query parameter if you have enabled multiple sites and would like to filter the response of this API call by call logs of a specific phone site. + /// Whether to filter API responses to include call logs that only have a non-zero charge. + /// The number of records to return. + /// The paging token. + /// The cancellation token. + /// + /// An array of . + /// + Task> GetAsync(DateTime? from = null, DateTime? to = null, CallLogType type = CallLogType.All, CallLogPathType? pathType = null, CallLogTimeType? timeType = CallLogTimeType.StartTime, string siteId = null, bool chargedCallLogs = false, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); } } From bd69f1c70f4dac9dad6906ae1ecf2ac04550d5f2 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 6 Dec 2023 21:03:08 -0500 Subject: [PATCH 2/4] Run our automated serializer context generator to make sure all expected types are specified --- Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index e6f5f015..b3b958f1 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -309,8 +309,12 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogCallType[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogDirection[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogNumberSource[]))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogOwnerInfo[]))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogOwnerType[]))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogPathType[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogResult[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogSite[]))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogTimeType[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfo[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType[]))] @@ -575,7 +579,10 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogCallType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogDirection?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogNumberSource?))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogOwnerType?))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogPathType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogResult?))] + [JsonSerializable(typeof(ZoomNet.Models.CallLogTimeType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogType?))] From de538a8d8b23f3f9de1de07463cd5befaa5e0a2b Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 7 Dec 2023 15:26:57 -0500 Subject: [PATCH 3/4] (doc) Fix typos in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2368ecd9..169fbf2b 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ var zoomClient = new ZoomClient(connectionInfo); Using OAuth is much more complicated than using JWT but at the same time, it is more flexible because you can define which permissions your app requires. When a user installs your app, they are presented with the list of permissions your app requires and they are given the opportunity to accept. The Zoom documentation has a document about [how to create an OAuth app](https://marketplace.zoom.us/docs/guides/build/oauth-app) and another document about the [OAuth autorization flow](https://marketplace.zoom.us/docs/guides/auth/oauth) but I personnality was very confused by the later document so here is a brief step-by-step summary: -- you create an OAuth app, define which permissions your app requires and publish the app in the Zoom marketplace. -- user installs your app. During installation, user is presentd with a screen listing the permissons your app requires. User must click `accept`. +- you create an OAuth app, define which permissions your app requires and publish the app to the Zoom marketplace. +- user installs your app. During installation, user is presented with a screen listing the permissons your app requires. User must click `accept`. - Zoom generates an "authorization code". This code can be used only once to generate the first access token and refresh token. I CAN'T STRESS THIS ENOUGH: the authorization code can be used only one time. This was the confusing part to me: somehow I didn't understand that this code could be used only one time and I was attempting to use it repeatedly. Zoom would accept the code the first time and would reject it subsequently, which lead to many hours of frustration while trying to figure out why the code was sometimes rejected. - The access token is valid for 60 minutes and must therefore be "refreshed" periodically. From aa6a8bebbbd79b6082c44cefc20e84d07a3b315a Mon Sep 17 00:00:00 2001 From: jericho Date: Fri, 8 Dec 2023 09:06:33 -0500 Subject: [PATCH 4/4] (doc) Update comment in appveyor.psm1 --- appveyor.psm1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/appveyor.psm1 b/appveyor.psm1 index aa39548b..40eb9f04 100644 --- a/appveyor.psm1 +++ b/appveyor.psm1 @@ -42,6 +42,11 @@ function Invoke-AppVeyorInstall { # I spent a whole day trying to find a solution but ultimately the only reliable solution # I was able to come up with is to install in the default location (which is /usr/share/dotnet) # using 'sudo' because you need admin privileges to access the default install location. + # + # November 2022: I tried removing this workaround since GetVersion.Tool was updated more + # than 2 years ago but it led to another problem: https://ci.appveyor.com/project/Jericho/zoomnet/builds/48579496/job/pymt60j9b53ayxta#L78 + # + # Therefore this workaround seems like a permanent solution. sudo bash dotnet-install.sh --version $desiredDotNetCoreSDKVersion --install-dir /usr/share/dotnet }