From 7b48a7e045919b40ee3e5f8fa75d9d544a483a21 Mon Sep 17 00:00:00 2001 From: dtibbe Date: Tue, 7 Jul 2020 20:30:14 +0200 Subject: [PATCH 1/5] Added EFT Provisioning API enhancements * Support for WxC extension and location in People * Support for location endpoint --- APIPartials/SparkLocations.cs | 50 +++++++++++++++++++++ APIPartials/SparkPeople.cs | 83 ++++++++++++++++++++++++----------- Models/Location.cs | 33 ++++++++++++++ Models/LocationAddress.cs | 37 ++++++++++++++++ Models/Person.cs | 10 +++++ README.md | 3 +- 6 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 APIPartials/SparkLocations.cs create mode 100644 Models/Location.cs create mode 100644 Models/LocationAddress.cs diff --git a/APIPartials/SparkLocations.cs b/APIPartials/SparkLocations.cs new file mode 100644 index 0000000..0f752bf --- /dev/null +++ b/APIPartials/SparkLocations.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SparkDotNet +{ + + public partial class Spark + { + private string locationsBase = "/v1/locations"; + + /// + /// List locations for an organization. + /// Use query parameters to filter the response. + /// Long result sets will be split into pages. + /// + /// List locations whose name contains this string (case-insensitive). + /// List locations by ID. + /// List locations in this organization. Only admin users of another organization (such as partners) may use this parameter. + /// Limit the maximum number of location in the response. Default: 100 + /// A list of matching Location objects + public async Task> GetLocationsAsync(string name = null, string id = null, string orgId = null, int max = 0) + { + var queryParams = new Dictionary(); + + if (name != null) queryParams.Add("name", name); + if (id != null) queryParams.Add("id", id); + if (orgId != null) queryParams.Add("orgId", orgId); + if (max > 0) queryParams.Add("max", max.ToString()); + + var path = getURL(locationsBase, queryParams); + return await GetItemsAsync(path); + } + + /// + /// Shows details for a location, by ID. + /// Specify the location ID in the locationId parameter in the URI. + /// Partner administrators should use the List Locations endpoint to retrieve information about locations. + /// + /// A unique identifier for the location. + /// A location object + public async Task GetLocationAsync(string locationId) + { + var queryParams = new Dictionary(); + var path = getURL($"{locationsBase}/{locationId}", queryParams); + return await GetItemAsync(path); + } + } + +} \ No newline at end of file diff --git a/APIPartials/SparkPeople.cs b/APIPartials/SparkPeople.cs index fe8bfd3..b2ecbf0 100644 --- a/APIPartials/SparkPeople.cs +++ b/APIPartials/SparkPeople.cs @@ -16,8 +16,10 @@ public partial class Spark /// List people by ID. Accepts up to 85 person IDs separated by commas. If this parameter is provided then presence information (such as the lastActivity or status properties) will not be included in the response. /// List people in this organization. Only admin users of another organization (such as partners) may use this parameter. /// Limit the maximum number of people in the response. Default: 100 + /// Include BroadCloud user details in the response. Default: false + /// List people present in this location. /// List of People objects. - public async Task> GetPeopleAsync(string email = null, string displayName = null, string id = null, string orgId = null, int max = 0) + public async Task> GetPeopleAsync(string email = null, string displayName = null, string id = null, string orgId = null, int max = 0, bool? callingData = null, string locationId = null) { var queryParams = new Dictionary(); if (email != null) queryParams.Add("email",email); @@ -25,6 +27,8 @@ public async Task> GetPeopleAsync(string email = null, string displ if (id != null) queryParams.Add("id", id); if (orgId != null) queryParams.Add("orgId", orgId); if (max > 0) queryParams.Add("max",max.ToString()); + if (callingData != null) queryParams.Add("callingData", callingData.ToString()); + if (locationId != null) queryParams.Add("locationId", locationId); var path = getURL(peopleBase, queryParams); return await GetItemsAsync(path); } @@ -32,9 +36,11 @@ public async Task> GetPeopleAsync(string email = null, string displ /// /// Show the profile for the authenticated user. /// + /// Include BroadCloud user details in the response. Default: false /// A person object representing the querying user. - public async Task GetMeAsync() { + public async Task GetMeAsync(bool? callingData = false) { var queryParams = new Dictionary(); + if (callingData != null) queryParams.Add("callingData", callingData.ToString()); var path = getURL($"{peopleBase}/me", queryParams); return await GetItemAsync(path); } @@ -44,9 +50,11 @@ public async Task GetMeAsync() { /// Specify the person ID in the personId parameter in the URI. /// /// A unique identifier for the person. + /// Include BroadCloud user details in the response. Default: false /// Person object. - public async Task GetPersonAsync(string personId) { + public async Task GetPersonAsync(string personId, bool? callingData = null) { var queryParams = new Dictionary(); + if (callingData != null) queryParams.Add("callingData", callingData.ToString()); var path = getURL($"{peopleBase}/{personId}", queryParams); return await GetItemAsync(path); } @@ -62,30 +70,42 @@ public async Task GetPersonAsync(string personId) { /// The ID of the organization to which this person belongs. /// An array of role strings representing the roles to which this person belongs. /// An array of license strings allocated to this person. + /// The extension of the person retrieved from BroadCloud. + /// The ID of the location for this person retrieved from BroadCloud. /// Person object. - public async Task CreatePersonAsync(string[] emails, string displayName = null, string firstName = null, string lastName = null, string avatar = null, string orgId = null, string[] roles = null, string[] licenses = null ) + public async Task CreatePersonAsync(string[] emails, string displayName = null, string firstName = null, string lastName = null, + string avatar = null, string orgId = null, string[] roles = null, string[] licenses = null, + bool? callingData = null, string extension = null, string locationId = null) { + var queryParams = new Dictionary(); + if (callingData != null) queryParams.Add("callingData", callingData.ToString()); + var path = getURL(peopleBase, queryParams); + var postBody = new Dictionary(); postBody.Add("emails", emails); - if (displayName != null) { postBody.Add("displayName", displayName); } - if (firstName != null) { postBody.Add("firstName", firstName); } - if (lastName != null) { postBody.Add("lastName", lastName); } - if (avatar != null) { postBody.Add("avatar", avatar); } - if (orgId != null) { postBody.Add("orgId", orgId); } - if (roles != null) { postBody.Add("roles", roles); } - if (licenses != null) { postBody.Add("licenses", licenses); } - - return await PostItemAsync(peopleBase, postBody); + if (displayName != null) postBody.Add("displayName", displayName); + if (firstName != null) postBody.Add("firstName", firstName); + if (lastName != null) postBody.Add("lastName", lastName); + if (avatar != null) postBody.Add("avatar", avatar); + if (orgId != null) postBody.Add("orgId", orgId); + if (roles != null) postBody.Add("roles", roles); + if (licenses != null) postBody.Add("licenses", licenses); + if (extension != null) postBody.Add("extension", extension); + if (locationId != null) postBody.Add("locationId", locationId); + + return await PostItemAsync(path, postBody); } /// /// Create a new user account for a given organization. Only an admin can create a new user account. /// /// A person object representing the person to be created + /// Include BroadCloud user details in the response. Default: false /// The newly created person - public async Task CreatePersonAsync(Person person) + public async Task CreatePersonAsync(Person person, bool? callingData = false) { - return await CreatePersonAsync(person.emails, person.displayName, person.firstName, person.lastName, person.avatar, person.orgId, person.roles, person.licenses); + return await CreatePersonAsync(person.emails, person.displayName, person.firstName, person.lastName, person.avatar, person.orgId, + person.roles, person.licenses, callingData, person.Extension, person.LocationId); } /// @@ -125,20 +145,29 @@ public async Task DeletePersonAsync(Person person) /// The last name of the person. /// The URL to the person's avatar in PNG format. /// An array of license strings allocated to this person. + /// Include BroadCloud user details in the response. Default: false + /// The extension of the person retrieved from BroadCloud. /// Person object. - public async Task UpdatePersonAsync(string personId, string[] emails = null, string orgId = null, string[] roles = null, string displayName = null, string firstName = null, string lastName = null, string avatar = null, string[] licenses = null ) + public async Task UpdatePersonAsync(string personId, string[] emails = null, string orgId = null, string[] roles = null, + string displayName = null, string firstName = null, string lastName = null, string avatar = null, + string[] licenses = null, bool? callingData = null, string extension = null) { + var queryParams = new Dictionary(); + if (callingData != null) queryParams.Add("callingData", callingData.ToString()); + var path = getURL($"{peopleBase}/{personId}", queryParams); + var putBody = new Dictionary(); putBody.Add("personId",personId); if (emails != null) putBody.Add("emails", emails); - if (displayName != null) { putBody.Add("displayName", displayName); } - if (firstName != null) { putBody.Add("firstName", firstName); } - if (lastName != null) { putBody.Add("lastName", lastName); } - if (avatar != null) { putBody.Add("avatar", avatar); } - if (orgId != null) { putBody.Add("orgId", orgId); } - if (roles != null) { putBody.Add("roles", roles); } - if (licenses != null) { putBody.Add("licenses", licenses); } - var path = $"{peopleBase}/{personId}"; + if (displayName != null) putBody.Add("displayName", displayName); + if (firstName != null) putBody.Add("firstName", firstName); + if (lastName != null) putBody.Add("lastName", lastName); + if (avatar != null) putBody.Add("avatar", avatar); + if (orgId != null) putBody.Add("orgId", orgId); + if (roles != null) putBody.Add("roles", roles); + if (licenses != null) putBody.Add("licenses", licenses); + if (extension != null) putBody.Add("extension", extension); + return await UpdateItemAsync(path, putBody); } @@ -147,10 +176,12 @@ public async Task UpdatePersonAsync(string personId, string[] emails = n /// Specify the person ID in the personId parameter in the URI. Only an admin can update a person details. /// /// The person object to update + /// Include BroadCloud user details in the response. Default: false /// Person object. - public async Task UpdatePersonAsync(Person person) + public async Task UpdatePersonAsync(Person person, bool? callingData = null) { - return await UpdatePersonAsync(person.id, person.emails, person.orgId, person.roles, person.displayName, person.firstName, person.lastName, person.avatar, person.licenses); + return await UpdatePersonAsync(person.id, person.emails, person.orgId, person.roles, person.displayName, person.firstName, person.lastName, + person.avatar, person.licenses, callingData, person.Extension); } } diff --git a/Models/Location.cs b/Models/Location.cs new file mode 100644 index 0000000..1297968 --- /dev/null +++ b/Models/Location.cs @@ -0,0 +1,33 @@ +using System; + +namespace SparkDotNet { + + /// + /// Locations are used to organize Webex Calling (BroadCloud) features within physical locations. + /// Webex Control Hub may be used to define new locations. + /// Searching and viewing locations in your organization requires an administrator auth token + /// with the spark-admin:people_read or spark-admin:device_read scope. + /// + public class Location : WebexObject + { + /// + /// A unique identifier for the location. + /// + public string Id { get; set; } + + /// + /// The name of the location. + /// + public string Name { get; set; } + + /// + /// The ID of the organization to which this location belongs. + /// + public string OrgId { get; set; } + + /// + /// The address of the location. + /// + public LocationAddress Address { get; set; } + } +} \ No newline at end of file diff --git a/Models/LocationAddress.cs b/Models/LocationAddress.cs new file mode 100644 index 0000000..150e3c8 --- /dev/null +++ b/Models/LocationAddress.cs @@ -0,0 +1,37 @@ +using System; + +namespace SparkDotNet { + + public class LocationAddress : WebexObject + { + /// + /// Address line 1 + /// + public string Address1 { get; set; } + + /// + /// Address line 2 + /// + public string Address2 { get; set; } + + /// + /// City + /// + public string City { get; set; } + + /// + /// State + /// + public string State { get; set; } + + /// + /// ZIP/Postal Code + /// + public string PostalCode { get; set; } + + /// + /// Country + /// + public string Country { get; set; } + } +} \ No newline at end of file diff --git a/Models/Person.cs b/Models/Person.cs index f8e3bc5..c23cca7 100644 --- a/Models/Person.cs +++ b/Models/Person.cs @@ -33,6 +33,16 @@ public class Person : WebexObject /// public SipAddress[] SipAddresses { get; set; } + /// + /// The extension of the person retrieved from BroadCloud. + /// + public string Extension { get; set; } + + /// + /// The ID of the location for this person retrieved from BroadCloud. + /// + public string LocationId { get; set; } + /// /// The full name of the person. /// diff --git a/README.md b/README.md index 4b5225e..0ad6b24 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ namespace ConsoleApplication # Reference -There are 24 endpoints covered by this library. Please refer to the Cisco documentation for details of their use: +There are 25 endpoints covered by this library. Please refer to the Cisco documentation for details of their use: * [Admin Audit Events] (https://developer.webex.com/docs/api/v1/admin-audit-events) * [Attachment Actions] (https://developer.webex.com/docs/api/v1/attachment-actions) @@ -158,6 +158,7 @@ There are 24 endpoints covered by this library. Please refer to the Cisco docume * [Hybrid Clusters] (https://developer.webex.com/docs/api/v1/hybrid-clusters) * [Hybrid Connectors] (https://developer.webex.com/docs/api/v1/hybrid-connectors) * [Licenses] (https://developer.webex.com/docs/api/v1/licenses) +* [Locations] (https://developer.webex.com/docs/api/v1/locations) * [Meeting Invitees] (https://developer.webex.com/docs/api/v1/meeting-invitees) * [Meetings] (https://developer.webex.com/docs/api/v1/meetings) * [Memberships] (https://developer.webex.com/docs/api/v1/memberships) From a23ca09d2dfef3a147507ec7ee050df7d4072815 Mon Sep 17 00:00:00 2001 From: dtibbe Date: Wed, 8 Jul 2020 08:37:25 +0200 Subject: [PATCH 2/5] * Added missing parameter in Person endpoint * Fixed Object-to-JSon conversion for some properties --- APIPartials/SparkPeople.cs | 15 +++++++++++---- Models/AttachmentActionInput.cs | 6 ++++++ Models/MeetingInvetees.cs | 6 ++++++ Models/Person.cs | 1 + Models/PhoneNumber.cs | 4 ++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/APIPartials/SparkPeople.cs b/APIPartials/SparkPeople.cs index b2ecbf0..7271e9d 100644 --- a/APIPartials/SparkPeople.cs +++ b/APIPartials/SparkPeople.cs @@ -70,12 +70,15 @@ public async Task GetPersonAsync(string personId, bool? callingData = nu /// The ID of the organization to which this person belongs. /// An array of role strings representing the roles to which this person belongs. /// An array of license strings allocated to this person. + /// Include BroadCloud user details in the response. Default: false + /// Phone numbers for the person. /// The extension of the person retrieved from BroadCloud. /// The ID of the location for this person retrieved from BroadCloud. /// Person object. public async Task CreatePersonAsync(string[] emails, string displayName = null, string firstName = null, string lastName = null, string avatar = null, string orgId = null, string[] roles = null, string[] licenses = null, - bool? callingData = null, string extension = null, string locationId = null) + bool? callingData = null, PhoneNumber[] phoneNumbers = null, string extension = null, + string locationId = null) { var queryParams = new Dictionary(); if (callingData != null) queryParams.Add("callingData", callingData.ToString()); @@ -90,9 +93,11 @@ public async Task CreatePersonAsync(string[] emails, string displayName if (orgId != null) postBody.Add("orgId", orgId); if (roles != null) postBody.Add("roles", roles); if (licenses != null) postBody.Add("licenses", licenses); + if (phoneNumbers != null) postBody.Add("phoneNumbers", phoneNumbers); if (extension != null) postBody.Add("extension", extension); if (locationId != null) postBody.Add("locationId", locationId); + return await PostItemAsync(path, postBody); } @@ -105,7 +110,7 @@ public async Task CreatePersonAsync(string[] emails, string displayName public async Task CreatePersonAsync(Person person, bool? callingData = false) { return await CreatePersonAsync(person.emails, person.displayName, person.firstName, person.lastName, person.avatar, person.orgId, - person.roles, person.licenses, callingData, person.Extension, person.LocationId); + person.roles, person.licenses, callingData, person.PhoneNumbers, person.Extension, person.LocationId); } /// @@ -150,7 +155,8 @@ public async Task DeletePersonAsync(Person person) /// Person object. public async Task UpdatePersonAsync(string personId, string[] emails = null, string orgId = null, string[] roles = null, string displayName = null, string firstName = null, string lastName = null, string avatar = null, - string[] licenses = null, bool? callingData = null, string extension = null) + string[] licenses = null, bool? callingData = null, PhoneNumber[] phoneNumbers = null, + string extension = null) { var queryParams = new Dictionary(); if (callingData != null) queryParams.Add("callingData", callingData.ToString()); @@ -166,6 +172,7 @@ public async Task UpdatePersonAsync(string personId, string[] emails = n if (orgId != null) putBody.Add("orgId", orgId); if (roles != null) putBody.Add("roles", roles); if (licenses != null) putBody.Add("licenses", licenses); + if (phoneNumbers != null) putBody.Add("phoneNumbers", phoneNumbers); if (extension != null) putBody.Add("extension", extension); return await UpdateItemAsync(path, putBody); @@ -181,7 +188,7 @@ public async Task UpdatePersonAsync(string personId, string[] emails = n public async Task UpdatePersonAsync(Person person, bool? callingData = null) { return await UpdatePersonAsync(person.id, person.emails, person.orgId, person.roles, person.displayName, person.firstName, person.lastName, - person.avatar, person.licenses, callingData, person.Extension); + person.avatar, person.licenses, callingData, person.PhoneNumbers, person.Extension); } } diff --git a/Models/AttachmentActionInput.cs b/Models/AttachmentActionInput.cs index 2110f0c..547db66 100644 --- a/Models/AttachmentActionInput.cs +++ b/Models/AttachmentActionInput.cs @@ -1,10 +1,16 @@ +using Newtonsoft.Json; namespace SparkDotNet { public class AttachmentActionInput : WebexObject { + + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("url")] public string Url { get; set; } + [JsonProperty("email")] public string Email { get; set; } + [JsonProperty("tel")] public string Tel { get; set; } } } \ No newline at end of file diff --git a/Models/MeetingInvetees.cs b/Models/MeetingInvetees.cs index da5327d..f8bc390 100644 --- a/Models/MeetingInvetees.cs +++ b/Models/MeetingInvetees.cs @@ -1,10 +1,16 @@ +using Newtonsoft.Json; + namespace SparkDotNet { public class MeetingInvitee : WebexObject { + [JsonProperty("email")] public string Email { get; set; } + [JsonProperty("displayName")] public string DisplayName { get; set; } + [JsonProperty("coHost")] public bool CoHost { get; set; } + [JsonProperty("meetingId")] public string MeetingId { get; set; } } diff --git a/Models/Person.cs b/Models/Person.cs index c23cca7..86c3eb0 100644 --- a/Models/Person.cs +++ b/Models/Person.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace SparkDotNet { diff --git a/Models/PhoneNumber.cs b/Models/PhoneNumber.cs index bab805b..b32fb4a 100644 --- a/Models/PhoneNumber.cs +++ b/Models/PhoneNumber.cs @@ -1,7 +1,11 @@ +using Newtonsoft.Json; + namespace SparkDotNet { public class PhoneNumber : WebexObject { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("value")] public string Value { get; set; } } } \ No newline at end of file From cac2dc7cd14edebda7046b7e4ff0fbb9c3a98b80 Mon Sep 17 00:00:00 2001 From: dtibbe Date: Fri, 17 Jul 2020 22:34:49 +0200 Subject: [PATCH 3/5] * fixed typo in file name --- Models/{MeetingInvetees.cs => MeetingInvitees.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Models/{MeetingInvetees.cs => MeetingInvitees.cs} (100%) diff --git a/Models/MeetingInvetees.cs b/Models/MeetingInvitees.cs similarity index 100% rename from Models/MeetingInvetees.cs rename to Models/MeetingInvitees.cs From be7b763df5b7beb58bf9d3dc5016f036a0413aa6 Mon Sep 17 00:00:00 2001 From: dtibbe Date: Thu, 20 Aug 2020 22:04:38 +0200 Subject: [PATCH 4/5] API Changes Augist 4th, 2020 * List Rooms and List Events support a maximum of 1000 items --- APIPartials/SparkEvents.cs | 2 +- APIPartials/SparkRooms.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/APIPartials/SparkEvents.cs b/APIPartials/SparkEvents.cs index b85efe0..9c49d55 100644 --- a/APIPartials/SparkEvents.cs +++ b/APIPartials/SparkEvents.cs @@ -29,7 +29,7 @@ public async Task> GetEventsAsync(string resource = null, string typ if (resource != null) queryParams.Add("resource", resource); if (type != null) queryParams.Add("type", type); if (actorId != null) queryParams.Add("actorId", actorId); - if (max > 0) queryParams.Add("max", max.ToString()); + if (max > 0) queryParams.Add("max", System.Math.Max(max, 1000).ToString()); if (from != null) queryParams.Add("from", ((DateTime)from).ToString("o")); if (to != null) queryParams.Add("to", ((DateTime)to).ToString("o")); diff --git a/APIPartials/SparkRooms.cs b/APIPartials/SparkRooms.cs index 8a5b5c7..c992dcf 100644 --- a/APIPartials/SparkRooms.cs +++ b/APIPartials/SparkRooms.cs @@ -23,7 +23,7 @@ public async Task> GetRoomsAsync(string teamId = null, int max = 0, s var queryParams = new Dictionary(); if (teamId != null) queryParams.Add("teamId",teamId); if (type != null) queryParams.Add("type",type); - if (max > 0) queryParams.Add("max",max.ToString()); + if (max > 0) queryParams.Add("max", System.Math.Max(max, 1000).ToString()); if (sortBy != null) queryParams.Add("sortBy", sortBy); var path = getURL(roomsBase, queryParams); From e4ad1142c34fe36fabe33ba1d9100bf2692c5cdf Mon Sep 17 00:00:00 2001 From: dtibbe Date: Thu, 20 Aug 2020 22:13:02 +0200 Subject: [PATCH 5/5] Updates from August 10th, 2020 * isRoomHidden can be updated in memberships --- APIPartials/SparkMemberships.cs | 14 +++++++++++++- Models/Membership.cs | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/APIPartials/SparkMemberships.cs b/APIPartials/SparkMemberships.cs index 4c482a0..692e08f 100644 --- a/APIPartials/SparkMemberships.cs +++ b/APIPartials/SparkMemberships.cs @@ -90,13 +90,25 @@ public async Task DeleteMembershipAsync(Membership membership) /// /// The unique identifier for the membership. /// Whether or not the participant is a room moderator. + /// Whether or not the room is hidden in the Webex Teams clients. /// Membership object. - public async Task UpdateMembershipAsync(string membershipId, bool isModerator) + public async Task UpdateMembershipAsync(string membershipId, bool isModerator, bool isRoomHidden) { var putBody = new Dictionary(); putBody.Add("isModerator",isModerator); + putBody.Add("isRoomHidden", isRoomHidden); var path = $"{membershipsBase}/{membershipId}"; return await UpdateItemAsync(path, putBody); } + + /// + /// Updates properties for a membership by object. + /// + /// The membership object to be updatad. + /// Membership object. + public async Task UpdateMembershipAsync(Membership membership) + { + return await UpdateMembershipAsync(membership.id, membership.isModerator, membership.isRoomHidden); + } } } \ No newline at end of file diff --git a/Models/Membership.cs b/Models/Membership.cs index 6d27de2..6ab0c76 100644 --- a/Models/Membership.cs +++ b/Models/Membership.cs @@ -20,6 +20,13 @@ public class Membership : WebexObject /// public string roomId { get; set; } + /// + /// The type of room the membership is associated with. + /// direct: 1:1 room + /// group: group room + /// + public string roomType { get; set; } + /// /// The person ID. /// @@ -45,6 +52,11 @@ public class Membership : WebexObject /// public bool isModerator { get; set; } + /// + /// Whether or not the room is hidden in the Webex Teams clients. + /// + public bool isRoomHidden { get; set; } + /// /// The date and time when the membership was created. ///