From d59b50edf5dd6c1578bf414f204c9296156088f2 Mon Sep 17 00:00:00 2001 From: Giorgi Kavrelishvili Date: Thu, 27 Oct 2022 08:59:40 +0400 Subject: [PATCH 1/5] Update from XML to JSON using the same Authorization technique --- drivers/cisco/ise/guests.cr | 70 +++++++------------ drivers/cisco/ise/guests_spec.cr | 36 ++++++---- drivers/cisco/ise/models/guest_access_info.cr | 23 ++++++ drivers/cisco/ise/models/guest_info.cr | 39 +++++++++++ drivers/cisco/ise/models/guest_user.cr | 49 +++++++++++++ 5 files changed, 158 insertions(+), 59 deletions(-) create mode 100644 drivers/cisco/ise/models/guest_access_info.cr create mode 100644 drivers/cisco/ise/models/guest_info.cr create mode 100644 drivers/cisco/ise/models/guest_user.cr diff --git a/drivers/cisco/ise/guests.cr b/drivers/cisco/ise/guests.cr index a18b11848f7..57e55242fc0 100644 --- a/drivers/cisco/ise/guests.cr +++ b/drivers/cisco/ise/guests.cr @@ -1,5 +1,6 @@ require "placeos-driver" require "xml" +require "./models/guest_user" # Tested with Cisco ISE API v2.2 # https://developer.cisco.com/docs/identity-services-engine/3.0/#!guest-user/resource-definition @@ -30,7 +31,7 @@ class Cisco::Ise::Guests < PlaceOS::Driver @location : String? = nil @custom_data = {} of String => JSON::Any::Type - TYPE_HEADER = "application/vnd.com.cisco.ise.identity.guestuser.2.0+xml" + TYPE_HEADER = "application/json" TIME_FORMAT = "%m/%d/%Y %H:%M" def on_load @@ -80,45 +81,24 @@ class Cisco::Ise::Guests < PlaceOS::Driver # Hackily grab a company name from the attendee's email (we may be able to grab this from the signal if possible) company_name ||= attendee_email.split('@')[1].split('.')[0].capitalize - # Now generate our XML body - xml_string = %( - ) - - # customFields is required for ISE API v2.2 - # since location is also required for 2.2, we can check if location is present - xml_string += %( - ) if @location - - xml_string += %( - - #{from_date}) - - xml_string += %( - #{@location}) if @location - - xml_string += %( - #{to_date} - 1 - - - #{company_name} - #{attendee_email} - #{first_name} - #{last_name} - English - #{phone_number}) - - xml_string += %( - #{sms_service_provider}) if sms_service_provider - - xml_string += %( - #{username} - - #{guest_type} - #{portal_id} - ) - - response = post("/guestuser/", body: xml_string, headers: { + guest_user = Models::GuestUser.from_json(%({})) + + guest_user.guest_access_info.from_date = from_date + guest_user.guest_access_info.location = @location + guest_user.guest_access_info.to_date = to_date + guest_user.guest_access_info.valid_days = 1 + guest_user.guest_info.company = company_name + guest_user.guest_info.email_address = attendee_email + guest_user.guest_info.first_name = first_name + guest_user.guest_info.last_name = last_name + guest_user.guest_info.notification_language = "English" + guest_user.guest_info.phone_number = phone_number + guest_user.guest_info.sms_service_provider = sms_service_provider + guest_user.guest_info.user_name = username + guest_user.guest_type = guest_type + guest_user.portal_id = portal_id + + response = post("/guestuser/", body: {"GuestUser" => guest_user}.to_json, headers: { "Accept" => TYPE_HEADER, "Content-Type" => TYPE_HEADER, "Authorization" => @basic_auth, @@ -142,12 +122,12 @@ class Cisco::Ise::Guests < PlaceOS::Driver "Content-Type" => TYPE_HEADER, "Authorization" => @basic_auth, }) - parsed_body = XML.parse(response.body) - guest_user = parsed_body.first_element_child.not_nil! - guest_info = guest_user.children.find { |c| c.name == "guestInfo" }.not_nil! + parsed_body = JSON.parse(response.body) + guest_user = Models::GuestUser.from_json(parsed_body["GuestUser"].to_json) + { - "username" => guest_info.children.find { |c| c.name == "userName" }.not_nil!.content, - "password" => guest_info.children.find { |c| c.name == "password" }.not_nil!.content, + "username" => guest_user.guest_info.user_name.to_s, + "password" => guest_user.guest_info.password.to_s, } end end diff --git a/drivers/cisco/ise/guests_spec.cr b/drivers/cisco/ise/guests_spec.cr index 3cec5a58151..9e19549dbaa 100644 --- a/drivers/cisco/ise/guests_spec.cr +++ b/drivers/cisco/ise/guests_spec.cr @@ -1,6 +1,7 @@ require "xml" require "placeos-driver" require "./guests" +require "./models/guest_user" require "placeos-driver/spec" DriverSpecs.mock_driver "Cisco::Ise::Guests" do @@ -26,33 +27,40 @@ DriverSpecs.mock_driver "Cisco::Ise::Guests" do # POST to /guestuser/ expect_http_request do |request, response| - parsed_body = XML.parse(request.body.not_nil!) - guest_user = parsed_body.first_element_child.not_nil! + guest_user = Cisco::Ise::Models::GuestUser.from_json(request.body.not_nil!) - guest_access_info = guest_user.children.find { |c| c.name == "guestAccessInfo" }.not_nil! - from_date = guest_access_info.children.find { |c| c.name == "fromDate" }.not_nil!.content + guest_access_info = guest_user.guest_access_info + + from_date = guest_access_info.from_date from_date.should eq start_date - to_date = guest_access_info.children.find { |c| c.name == "toDate" }.not_nil!.content + + to_date = guest_access_info.to_date to_date.should eq end_date - guest_info = guest_user.children.find { |c| c.name == "guestInfo" }.not_nil! - company = guest_info.children.find { |c| c.name == "company" }.not_nil!.content + guest_info = guest_user.guest_info + + company = guest_info.company company.should eq company_name - email_address = guest_info.children.find { |c| c.name == "emailAddress" }.not_nil!.content + + email_address = guest_info.email_address email_address.should eq attendee_email - first_name = guest_info.children.find { |c| c.name == "firstName" }.not_nil!.content + + first_name = guest_info.first_name first_name.should eq "First" - last_name = guest_info.children.find { |c| c.name == "lastName" }.not_nil!.content + + last_name = guest_info.last_name last_name.should eq "Last" - phone_number = guest_info.children.find { |c| c.name == "phoneNumber" }.not_nil!.content + + phone_number = guest_info.phone_number phone_number.should eq phone - sms_service_provider = guest_info.children.find { |c| c.name == "smsServiceProvider" }.not_nil!.content + + sms_service_provider = guest_info.sms_service_provider sms_service_provider.should eq sms - portal_id = guest_user.children.find { |c| c.name == "portalId" }.not_nil!.content + portal_id = guest_user.portal_id portal_id.should eq portal - guest_type = guest_user.children.find { |c| c.name == "guestType" }.not_nil!.content + guest_type = guest_user.guest_type guest_type.should eq "Daily" response.status_code = 201 diff --git a/drivers/cisco/ise/models/guest_access_info.cr b/drivers/cisco/ise/models/guest_access_info.cr new file mode 100644 index 00000000000..c64368fee11 --- /dev/null +++ b/drivers/cisco/ise/models/guest_access_info.cr @@ -0,0 +1,23 @@ +require "json" + +class Cisco::Ise::Models::GuestAccessInfo + include JSON::Serializable + + @[JSON::Field(key: "validDays")] + property valid_days : Int32? + + @[JSON::Field(key: "fromDate")] + property from_date : String? + + @[JSON::Field(key: "toDate")] + property to_date : String? + + @[JSON::Field(key: "location")] + property location : String? + + @[JSON::Field(key: "ssid")] + property ssid : String? + + @[JSON::Field(key: "groupTag")] + property group_tag : String? +end diff --git a/drivers/cisco/ise/models/guest_info.cr b/drivers/cisco/ise/models/guest_info.cr new file mode 100644 index 00000000000..35907f5ca7b --- /dev/null +++ b/drivers/cisco/ise/models/guest_info.cr @@ -0,0 +1,39 @@ +require "json" +require "uuid" + +class Cisco::Ise::Models::GuestInfo + include JSON::Serializable + + @[JSON::Field(key: "emailAddress")] + property email_address : String? + + @[JSON::Field(key: "enabled")] + property enabled : Bool = true + + @[JSON::Field(key: "password")] + property password : String = UUID.random.to_s.gsub("-", "") + + @[JSON::Field(key: "phoneNumber")] + property phone_number : String? + + @[JSON::Field(key: "smsServiceProvider")] + property sms_service_provider : String? + + @[JSON::Field(key: "userName")] + property user_name : String? + + @[JSON::Field(key: "firstName")] + property first_name : String? + + @[JSON::Field(key: "lastName")] + property last_name : String? + + @[JSON::Field(key: "company")] + property company : String? + + @[JSON::Field(key: "creationTime")] + property creation_time : String? + + @[JSON::Field(key: "notificationLanguage")] + property notification_language : String? +end diff --git a/drivers/cisco/ise/models/guest_user.cr b/drivers/cisco/ise/models/guest_user.cr new file mode 100644 index 00000000000..5fb886b50b4 --- /dev/null +++ b/drivers/cisco/ise/models/guest_user.cr @@ -0,0 +1,49 @@ +require "json" +require "./guest_info" +require "./guest_access_info" + +class Cisco::Ise::Models::GuestUser + include JSON::Serializable + + @[JSON::Field(key: "name")] + property name : String = "gusetUser" + + @[JSON::Field(key: "id")] + property id : String? + + @[JSON::Field(key: "description")] + property description : String? + + @[JSON::Field(key: "customFields")] + property custom_fields : Hash(String, String) = {} of String => String + + @[JSON::Field(key: "guestType")] + property guest_type : String? + + @[JSON::Field(key: "status")] + property status : String? + + @[JSON::Field(key: "reasonForVisit")] + property reason_for_visit : String? + + @[JSON::Field(key: "personBeingVisited")] + property person_being_visited : String? + + @[JSON::Field(key: "sponsorUserName")] + property sponsor_user_name : String? + + @[JSON::Field(key: "sponsorUserId")] + property sponsor_user_id : String? + + @[JSON::Field(key: "statusReason")] + property status_reason : String? + + @[JSON::Field(key: "portalId")] + property portal_id : String? + + @[JSON::Field(key: "guestAccessInfo")] + property guest_access_info : GuestAccessInfo = GuestAccessInfo.from_json(%({})) + + @[JSON::Field(key: "guestInfo")] + property guest_info : GuestInfo = GuestInfo.from_json(%({})) +end From 74de6afbf80037f7289646486294bdbdf0fc2eb6 Mon Sep 17 00:00:00 2001 From: Giorgi Kavrelishvili Date: Fri, 18 Nov 2022 17:34:19 +0400 Subject: [PATCH 2/5] Remove the webex inbuilt library --- drivers/cisco/webex/api/messages.cr | 41 -- drivers/cisco/webex/api/people.cr | 15 - drivers/cisco/webex/api/rooms.cr | 41 -- drivers/cisco/webex/client.cr | 153 ----- drivers/cisco/webex/command.cr | 9 - drivers/cisco/webex/commands/echo.cr | 19 - drivers/cisco/webex/commands/greeting.cr | 19 - drivers/cisco/webex/constants.cr | 45 -- drivers/cisco/webex/exceptions/argument.cr | 8 - drivers/cisco/webex/exceptions/method.cr | 8 - drivers/cisco/webex/exceptions/rate_limit.cr | 8 - drivers/cisco/webex/exceptions/status_code.cr | 8 - drivers/cisco/webex/extensions/chainable.cr | 536 ------------------ drivers/cisco/webex/models/device.cr | 15 - drivers/cisco/webex/models/event.cr | 27 - drivers/cisco/webex/models/events/activity.cr | 35 -- drivers/cisco/webex/models/events/actor.cr | 32 -- drivers/cisco/webex/models/events/data.cr | 17 - drivers/cisco/webex/models/events/target.cr | 23 - drivers/cisco/webex/models/events/type.cr | 14 - drivers/cisco/webex/models/message.cr | 77 --- drivers/cisco/webex/models/peek.cr | 15 - drivers/cisco/webex/models/person.cr | 12 - drivers/cisco/webex/models/room.cr | 45 -- drivers/cisco/webex/session.cr | 82 --- drivers/cisco/webex/status_code.cr | 23 - drivers/cisco/webex/utils.cr | 23 - 27 files changed, 1350 deletions(-) delete mode 100644 drivers/cisco/webex/api/messages.cr delete mode 100644 drivers/cisco/webex/api/people.cr delete mode 100644 drivers/cisco/webex/api/rooms.cr delete mode 100644 drivers/cisco/webex/client.cr delete mode 100644 drivers/cisco/webex/command.cr delete mode 100644 drivers/cisco/webex/commands/echo.cr delete mode 100644 drivers/cisco/webex/commands/greeting.cr delete mode 100644 drivers/cisco/webex/constants.cr delete mode 100644 drivers/cisco/webex/exceptions/argument.cr delete mode 100644 drivers/cisco/webex/exceptions/method.cr delete mode 100644 drivers/cisco/webex/exceptions/rate_limit.cr delete mode 100644 drivers/cisco/webex/exceptions/status_code.cr delete mode 100644 drivers/cisco/webex/extensions/chainable.cr delete mode 100644 drivers/cisco/webex/models/device.cr delete mode 100644 drivers/cisco/webex/models/event.cr delete mode 100644 drivers/cisco/webex/models/events/activity.cr delete mode 100644 drivers/cisco/webex/models/events/actor.cr delete mode 100644 drivers/cisco/webex/models/events/data.cr delete mode 100644 drivers/cisco/webex/models/events/target.cr delete mode 100644 drivers/cisco/webex/models/events/type.cr delete mode 100644 drivers/cisco/webex/models/message.cr delete mode 100644 drivers/cisco/webex/models/peek.cr delete mode 100644 drivers/cisco/webex/models/person.cr delete mode 100644 drivers/cisco/webex/models/room.cr delete mode 100644 drivers/cisco/webex/session.cr delete mode 100644 drivers/cisco/webex/status_code.cr delete mode 100644 drivers/cisco/webex/utils.cr diff --git a/drivers/cisco/webex/api/messages.cr b/drivers/cisco/webex/api/messages.cr deleted file mode 100644 index 24bfa2edc2c..00000000000 --- a/drivers/cisco/webex/api/messages.cr +++ /dev/null @@ -1,41 +0,0 @@ -module Cisco - module Webex - module Api - class Messages - def initialize(@session : Session) - end - - def list(room_id : String, parent_id : String = "", mentioned_people : String = "", before : String = "", before_message : String = "", max : Int32 = 50) : Array(Models::Message) - params = Utils.hash_from_items_with_values(roomId: room_id, parentId: parent_id, mentionedPeople: mentioned_people, before: before, beforeMessage: before_message, max: max) - response = @session.get([Constants::MESSAGES_ENDPOINT, "/"].join(""), params: params) - data = JSON.parse(response.body) - - data.["items"].as_a.map do |item| - Models::Message.from_json(item.to_json) - end - end - - def list_direct(person_id : String = "", person_email : String = "", parent_id : String = "") : Array(Models::Message) - params = Utils.hash_from_items_with_values(personId: person_id, personEmail: person_email, parentId: parent_id) - response = @session.get([Constants::MESSAGES_ENDPOINT, "/"].join(""), params: params) - data = JSON.parse(response.body) - - data.["items"].as_a.map do |item| - Models::Message.from_json(item.to_json) - end - end - - def create(room_id : String = "", parent_id : String = "", to_person_id : String = "", to_person_email : String = "", text : String = "", markdown : String = "") : Models::Message - json = Utils.hash_from_items_with_values(roomId: room_id, parentId: parent_id, toPersonId: to_person_id, toPersonEmail: to_person_email, text: text, markdown: markdown) - response = @session.post([Constants::MESSAGES_ENDPOINT, "/"].join(""), json: json) - Models::Message.from_json(response.body) - end - - def get(message_id : String) : Models::Message - response = @session.get([Constants::MESSAGES_ENDPOINT, "/", message_id].join("")) - Models::Message.from_json(response.body) - end - end - end - end -end diff --git a/drivers/cisco/webex/api/people.cr b/drivers/cisco/webex/api/people.cr deleted file mode 100644 index 500cd455c05..00000000000 --- a/drivers/cisco/webex/api/people.cr +++ /dev/null @@ -1,15 +0,0 @@ -module Cisco - module Webex - module Api - class People - def initialize(@session : Session) - end - - def me : Models::Person - response = @session.get([Constants::PEOPLE_ENDPOINT, "/", "me"].join("")) - Models::Person.from_json(response.body) - end - end - end - end -end diff --git a/drivers/cisco/webex/api/rooms.cr b/drivers/cisco/webex/api/rooms.cr deleted file mode 100644 index bf80133d252..00000000000 --- a/drivers/cisco/webex/api/rooms.cr +++ /dev/null @@ -1,41 +0,0 @@ -module Cisco - module Webex - module Api - class Rooms - def initialize(@session : Session) - end - - def list(room_id : String, parent_id : String = "", mentioned_people : String = "", before : String = "", before_message : String = "", max : Int32 = 50) : Array(Models::Message) - params = Utils.hash_from_items_with_values(roomId: room_id, parentId: parent_id, mentionedPeople: mentioned_people, before: before, beforeMessage: before_message, max: max) - response = @session.get([Constants::MESSAGES_ENDPOINT, "/"].join(""), params: params) - data = JSON.parse(response.body) - - data.["items"].as_a.map do |item| - Models::Message.from_json(item.to_json) - end - end - - def list_direct(person_id : String = "", person_email : String = "", parent_id : String = "") : Array(Models::Message) - params = Utils.hash_from_items_with_values(personId: person_id, personEmail: person_email, parentId: parent_id) - response = @session.get([Constants::MESSAGES_ENDPOINT, "/"].join(""), params: params) - data = JSON.parse(response.body) - - data.["items"].as_a.map do |item| - Models::Message.from_json(item.to_json) - end - end - - def create(room_id : String = "", parent_id : String = "", to_person_id : String = "", to_person_email : String = "", text : String = "", markdown : String = "") : Models::Message - json = Utils.hash_from_items_with_values(roomId: room_id, parentId: parent_id, toPersonId: to_person_id, toPersonEmail: to_person_email, text: text, markdown: markdown) - response = @session.post([Constants::MESSAGES_ENDPOINT, "/"].join(""), json: json) - Models::Message.from_json(response.body) - end - - def get(message_id : String) : Models::Message - response = @session.get([Constants::MESSAGES_ENDPOINT, "/", message_id].join("")) - Models::Message.from_json(response.body) - end - end - end - end -end diff --git a/drivers/cisco/webex/client.cr b/drivers/cisco/webex/client.cr deleted file mode 100644 index 8d5dc1aed32..00000000000 --- a/drivers/cisco/webex/client.cr +++ /dev/null @@ -1,153 +0,0 @@ -module Cisco - module Webex - class Client - Log = ::Log.for(self) - - property id : String - property keywords : Hash(String, Command) - property socket : HTTP::WebSocket? - - def initialize(@name : String, @access_token : String, @emails : String, @session : Session, @commands : Array(Command)) - @rooms = Api::Rooms.new(@session) - @people = Api::People.new(@session) - @messages = Api::Messages.new(@session) - - @keywords = - @commands - .flat_map { |command| command.keywords.map { |keyword| {"#{keyword}" => command} } } - .reduce { |acc, i| acc.try(&.merge(i.not_nil!)) } - - @id = @people.me.id - end - - def rooms - @rooms - end - - def people - @people - end - - def messages - @messages - end - - private def device(check_existing : Bool = true) : Models::Device - if check_existing - response = @session.get([Constants::DEFAULT_DEVICE_URL, "/", "devices"].join("")) - data = JSON.parse(response.body) - - devices = data.["devices"].as_a.map do |item| - Models::Device.from_json(item.to_json) - end - - devices.each do |device| - if device.name == nil - next - end - - if device.name == Constants::DEVICE["name"] - return device - end - end - end - - response = @session.post([Constants::DEFAULT_DEVICE_URL, "/", "devices"].join(""), json: Constants::DEVICE) - Models::Device.from_json(response.body) - end - - private def message_id(activity) : String - # In order to geo-locate the correct DC to fetch the message from, you need to use the base64 Id of the message. - id = activity.id - target_url = activity.target.url - target_id = activity.target.id - - verb = activity.verb == "post" ? "messages" : "attachment/actions" - - message_url = target_url.gsub(["conversations", "/", target_id].join(""), [verb, "/", id].join("")) - response = Halite.get(message_url, headers: {"Authorization" => ["Bearer", @access_token].join(" ")}) - - message = JSON.parse(response.body) - message["id"].to_s - end - - private def process_incoming_websocket_message(socket, message) - peek = Models::Peek.from_json(message) - return if peek.data.event_type == "status.start_typing" - - begin - event = Models::Event.from_json(message) - - if event.data.event_type == "conversation.activity" - activity = event.data.activity - Log.debug { "Activity verb is: #{activity.verb}" } - - if activity.verb == "post" - id = message_id(activity) - message = self.messages.get(id) - - if message.person_id != @id - # Ack that this message has been processed. This will prevent the message coming again. - socket.send({"type" => "ack", "messageId" => id}.to_json) - - if message.text.starts_with?(@name) - message.text = message.text.sub(@name, "").strip - end - - return if @emails.none?(activity.actor.email) - - keyword = message.text.split.first.downcase - - if @keywords[keyword]? - message.text = message.text.sub(keyword, "").strip - message = @keywords[keyword].execute(event, keyword, message) - - room_id = message["id"]? || "" - parent_id = message["parent_id"]? || "" - to_person_id = message["to_person_id"]? || "" - to_person_email = message["to_person_email"]? || "" - text = message["text"]? || "" - markdown = message["markdown"]? || "" - - self.messages.create(room_id, parent_id, to_person_id, to_person_email, text, markdown) - else - end - end - else - end - end - rescue e : Exception - Log.debug(exception: e) { } - end - end - - def run : Void - device = device() - @socket = socket = HTTP::WebSocket.new(URI.parse(device.websocket_url)) - - socket.on_message do |message| - process_incoming_websocket_message(socket, message) - end - - socket.on_binary do |binary| - process_incoming_websocket_message(socket, String.new(binary)) - end - - message = { - "id" => UUID.random.to_s, - "type" => "authorization", - "trackingId" => ["webex", "-", UUID.random.to_s].join(""), - "data" => { - "token" => ["Bearer", @access_token].join(" "), - }, - } - socket.send(message.to_json) - socket.run - end - - def stop : Void - @socket.close - end - end - end -end diff --git a/drivers/cisco/webex/command.cr b/drivers/cisco/webex/command.cr deleted file mode 100644 index 4fdac03b1d3..00000000000 --- a/drivers/cisco/webex/command.cr +++ /dev/null @@ -1,9 +0,0 @@ -module Cisco - module Webex - abstract class Command - abstract def keywords : Array(String) - abstract def description : String - abstract def execute(event, keyword, message) - end - end -end diff --git a/drivers/cisco/webex/commands/echo.cr b/drivers/cisco/webex/commands/echo.cr deleted file mode 100644 index 5014b58595a..00000000000 --- a/drivers/cisco/webex/commands/echo.cr +++ /dev/null @@ -1,19 +0,0 @@ -module Cisco - module Webex - module Commands - class Echo < Command - def keywords : Array(String) - ["echo"] - end - - def description : String - "This command simply replies your message!" - end - - def execute(_event, _keyword, message) - {"id" => message.room_id, "text" => message.text} - end - end - end - end -end diff --git a/drivers/cisco/webex/commands/greeting.cr b/drivers/cisco/webex/commands/greeting.cr deleted file mode 100644 index 13843de0810..00000000000 --- a/drivers/cisco/webex/commands/greeting.cr +++ /dev/null @@ -1,19 +0,0 @@ -module Cisco - module Webex - module Commands - class Greeting < Command - def keywords : Array(String) - ["hello", "hi"] - end - - def description : String - "This command simply responds to hello, hi, how are you, etc." - end - - def execute(_event, _keyword, message) - {"id" => message.room_id, "text" => "👋"} - end - end - end - end -end diff --git a/drivers/cisco/webex/constants.cr b/drivers/cisco/webex/constants.cr deleted file mode 100644 index ef5605e0096..00000000000 --- a/drivers/cisco/webex/constants.cr +++ /dev/null @@ -1,45 +0,0 @@ -module Cisco - module Webex - module Constants - VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify.downcase }} - - STATUS_CODES = { - 200 => "Successful request with body content.", - 204 => "Successful request without body content.", - 400 => "The request was invalid or cannot be otherwise served.", - 401 => "Authentication credentials were missing or incorrect.", - 403 => "The request is understood, but it has been refused or access is not allowed.", - 404 => "The URI requested is invalid or the resource requested, such as a user, does not exist. Also returned when the requested format is not supported by the requested method.", - 405 => "The request was made to a resource using an HTTP request method that is not supported.", - 409 => "The request could not be processed because it conflicts with some established rule of the system. For example, a person may not be added to a room more than once.", - 410 => "The requested resource is no longer available.", - 415 => "The request was made to a resource without specifying a media type or used a media type that is not supported.", - 423 => "The requested resource is temporarily unavailable. A `Retry-After` header may be present that specifies how many seconds you need to wait before attempting the request again.", - 429 => "Too many requests have been sent in a given amount of time and the request has been rate limited. A `Retry-After` header should be present that specifies how many seconds you need to wait before a successful request can be made.", - 500 => "Something went wrong on the server. If the issue persists, feel free to contact the Webex Developer Support team (https://developer.webex.com/support).", - 502 => "The server received an invalid response from an upstream server while processing the request. Try again later.", - 503 => "Server is overloaded with requests. Try again later.", - } - - DEFAULT_BASE_URL = "https://webexapis.com/v1/" - DEFAULT_DEVICE_URL = "https://wdm-a.wbx2.com/wdm/api/v1/" - DEFAULT_SINGLE_REQUEST_TIMEOUT = 60 - DEFAULT_WAIT_ON_RATE_LIMIT = true - - DEVICE = { - "deviceType" => "DESKTOP", - "localizedModel" => "crystal", - "model" => "crystal", - "name" => UUID.random.to_s, - "systemName" => "webex-bot-client", - "systemVersion" => VERSION, - } - - ROOMS_ENDPOINT = "rooms" - PEOPLE_ENDPOINT = "people" - MESSAGES_ENDPOINT = "messages" - - WEBEX_TEAMS_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" - end - end -end diff --git a/drivers/cisco/webex/exceptions/argument.cr b/drivers/cisco/webex/exceptions/argument.cr deleted file mode 100644 index 4885bbce93f..00000000000 --- a/drivers/cisco/webex/exceptions/argument.cr +++ /dev/null @@ -1,8 +0,0 @@ -module Cisco - module Webex - module Exceptions - class Argument < Exception - end - end - end -end diff --git a/drivers/cisco/webex/exceptions/method.cr b/drivers/cisco/webex/exceptions/method.cr deleted file mode 100644 index 3daec0b5636..00000000000 --- a/drivers/cisco/webex/exceptions/method.cr +++ /dev/null @@ -1,8 +0,0 @@ -module Cisco - module Webex - module Exceptions - class Method < Exception - end - end - end -end diff --git a/drivers/cisco/webex/exceptions/rate_limit.cr b/drivers/cisco/webex/exceptions/rate_limit.cr deleted file mode 100644 index af22d61182f..00000000000 --- a/drivers/cisco/webex/exceptions/rate_limit.cr +++ /dev/null @@ -1,8 +0,0 @@ -module Cisco - module Webex - module Exceptions - class RateLimit < Exception - end - end - end -end diff --git a/drivers/cisco/webex/exceptions/status_code.cr b/drivers/cisco/webex/exceptions/status_code.cr deleted file mode 100644 index de113fad804..00000000000 --- a/drivers/cisco/webex/exceptions/status_code.cr +++ /dev/null @@ -1,8 +0,0 @@ -module Cisco - module Webex - module Exceptions - class StatusCode < Exception - end - end - end -end diff --git a/drivers/cisco/webex/extensions/chainable.cr b/drivers/cisco/webex/extensions/chainable.cr deleted file mode 100644 index 2707afeb345..00000000000 --- a/drivers/cisco/webex/extensions/chainable.cr +++ /dev/null @@ -1,536 +0,0 @@ -require "base64" - -module Halite - module Chainable - {% for verb in %w(get head) %} - # {{ verb.id.capitalize }} a resource - # - # ``` - # Halite.{{ verb.id }}("http://httpbin.org/anything", params: { - # first_name: "foo", - # last_name: "bar" - # }) - # ``` - def {{ verb.id }}(uri : String, *, - headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response - request({{ verb }}, uri, headers: headers, params: params, raw: raw, tls: tls) - end - - # {{ verb.id.capitalize }} a streaming resource - # - # ``` - # Halite.{{ verb.id }}("http://httpbin.org/anything") do |response| - # puts response.status_code - # while line = response.body_io.gets - # puts line - # end - # end - # ``` - def {{ verb.id }}(uri : String, *, - headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil, - &block : Halite::Response ->) - request({{ verb }}, uri, headers: headers, params: params, raw: raw, tls: tls, &block) - end - {% end %} - - {% for verb in %w(put post patch delete options) %} - # {{ verb.id.capitalize }} a resource - # - # ### Request with form data - # - # ``` - # Halite.{{ verb.id }}("http://httpbin.org/anything", form: { - # first_name: "foo", - # last_name: "bar" - # }) - # ``` - # - # ### Request with json data - # - # ``` - # Halite.{{ verb.id }}("http://httpbin.org/anything", json: { - # first_name: "foo", - # last_name: "bar" - # }) - # ``` - # - # ### Request with raw string - # - # ``` - # Halite.{{ verb.id }}("http://httpbin.org/anything", raw: "name=Peter+Lee&address=%23123+Happy+Ave&Language=C%2B%2B") - # ``` - def {{ verb.id }}(uri : String, *, - headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response - request({{ verb }}, uri, headers: headers, params: params, form: form, json: json, raw: raw, tls: tls) - end - - # {{ verb.id.capitalize }} a streaming resource - # - # ``` - # Halite.{{ verb.id }}("http://httpbin.org/anything") do |response| - # puts response.status_code - # while line = response.body_io.gets - # puts line - # end - # end - # ``` - def {{ verb.id }}(uri : String, *, - headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil, - &block : Halite::Response ->) - request({{ verb }}, uri, headers: headers, params: params, form: form, json: json, raw: raw, tls: tls, &block) - end - {% end %} - - # Adds a endpoint to the request. - # - # - # ``` - # Halite.endpoint("https://httpbin.org") - # .get("/get") - # ``` - def endpoint(endpoint : String | URI) : Halite::Client - branch(default_options.with_endpoint(endpoint)) - end - - # Make a request with the given Basic authorization header - # - # ``` - # Halite.basic_auth("icyleaf", "p@ssw0rd") - # .get("http://httpbin.org/get") - # ``` - # - # See Also: [http://tools.ietf.org/html/rfc2617](http://tools.ietf.org/html/rfc2617) - def basic_auth(user : String, pass : String) : Halite::Client - auth("Basic " + Base64.strict_encode(user + ":" + pass)) - end - - # Make a request with the given Authorization header - # - # ``` - # Halite.auth("private-token", "6abaef100b77808ceb7fe26a3bcff1d0") - # .get("http://httpbin.org/get") - # ``` - def auth(value : String) : Halite::Client - headers({"Authorization" => value}) - end - - # Accept the given MIME type - # - # ``` - # Halite.accept("application/json") - # .get("http://httpbin.org/get") - # ``` - def accept(value : String) : Halite::Client - headers({"Accept" => value}) - end - - # Set requests user agent - # - # ``` - # Halite.user_agent("Custom User Agent") - # .get("http://httpbin.org/get") - # ``` - def user_agent(value : String) : Halite::Client - headers({"User-Agent" => value}) - end - - # Make a request with the given headers - # - # ``` - # Halite.headers({"Content-Type", "application/json", "Connection": "keep-alive"}) - # .get("http://httpbin.org/get") - # # Or - # Halite.headers({content_type: "application/json", connection: "keep-alive"}) - # .get("http://httpbin.org/get") - # ``` - def headers(headers : Hash(String, _) | NamedTuple) : Halite::Client - branch(default_options.with_headers(headers)) - end - - # Make a request with the given headers - # - # ``` - # Halite.headers(content_type: "application/json", connection: "keep-alive") - # .get("http://httpbin.org/get") - # ``` - def headers(**kargs) : Halite::Client - branch(default_options.with_headers(kargs)) - end - - # Make a request with the given cookies - # - # ``` - # Halite.cookies({"private-token", "6abaef100b77808ceb7fe26a3bcff1d0"}) - # .get("http://httpbin.org/get") - # # Or - # Halite.cookies({private-token: "6abaef100b77808ceb7fe26a3bcff1d0"}) - # .get("http://httpbin.org/get") - # ``` - def cookies(cookies : Hash(String, _) | NamedTuple) : Halite::Client - branch(default_options.with_cookies(cookies)) - end - - # Make a request with the given cookies - # - # ``` - # Halite.cookies(name: "icyleaf", "gender": "male") - # .get("http://httpbin.org/get") - # ``` - def cookies(**kargs) : Halite::Client - branch(default_options.with_cookies(kargs)) - end - - # Make a request with the given cookies - # - # ``` - # cookies = HTTP::Cookies.from_client_headers(headers) - # Halite.cookies(cookies) - # .get("http://httpbin.org/get") - # ``` - def cookies(cookies : HTTP::Cookies) : Halite::Client - branch(default_options.with_cookies(cookies)) - end - - # Adds a timeout to the request. - # - # How long to wait for the server to send data before giving up, as a int, float or time span. - # The timeout value will be applied to both the connect and the read timeouts. - # - # Set `nil` to timeout to ignore timeout. - # - # ``` - # Halite.timeout(5.5).get("http://httpbin.org/get") - # # Or - # Halite.timeout(2.minutes) - # .post("http://httpbin.org/post", form: {file: "file.txt"}) - # ``` - def timeout(timeout : (Int32 | Float64 | Time::Span)?) - timeout ? timeout(timeout, timeout, timeout) : branch - end - - # Adds a timeout to the request. - # - # How long to wait for the server to send data before giving up, as a int, float or time span. - # The timeout value will be applied to both the connect and the read timeouts. - # - # ``` - # Halite.timeout(3, 3.minutes, 5) - # .post("http://httpbin.org/post", form: {file: "file.txt"}) - # # Or - # Halite.timeout(3.04, 64, 10.0) - # .get("http://httpbin.org/get") - # ``` - def timeout(connect : (Int32 | Float64 | Time::Span)? = nil, - read : (Int32 | Float64 | Time::Span)? = nil, - write : (Int32 | Float64 | Time::Span)? = nil) - branch(default_options.with_timeout(connect, read, write)) - end - - # Returns `Options` self with automatically following redirects. - # - # ``` - # # Automatically following redirects. - # Halite.follow - # .get("http://httpbin.org/relative-redirect/5") - # - # # Always redirect with any request methods - # Halite.follow(strict: false) - # .get("http://httpbin.org/get") - # ``` - def follow(strict = Halite::Options::Follow::STRICT) : Halite::Client - branch(default_options.with_follow(strict: strict)) - end - - # Returns `Options` self with given max hops of redirect times. - # - # ``` - # # Max hops 3 times - # Halite.follow(3) - # .get("http://httpbin.org/relative-redirect/3") - # - # # Always redirect with any request methods - # Halite.follow(4, strict: false) - # .get("http://httpbin.org/relative-redirect/4") - # ``` - def follow(hops : Int32, strict = Halite::Options::Follow::STRICT) : Halite::Client - branch(default_options.with_follow(hops, strict)) - end - - # Returns `Options` self with enable or disable logging. - # - # #### Enable logging - # - # Same as call `logging` method without any argument. - # - # ``` - # Halite.logging.get("http://httpbin.org/get") - # ``` - # - # #### Disable logging - # - # ``` - # Halite.logging(false).get("http://httpbin.org/get") - # ``` - def logging(enable : Bool = true) - options = default_options - options.logging = enable - branch(options) - end - - # Returns `Options` self with given the logging which it integration from `Halite::Logging`. - # - # #### Simple logging - # - # ``` - # Halite.logging - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # - # => 2018-08-28 14:33:19 +08:00 | request | POST | http://httpbin.org/post - # => 2018-08-28 14:33:21 +08:00 | response | 200 | http://httpbin.org/post | 1.61s | application/json - # { ... } - # ``` - # - # #### Logger configuration - # - # By default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level. - # You can configuring the following options: - # - # - `skip_request_body`: By default is `false`. - # - `skip_response_body`: By default is `false`. - # - `skip_benchmark`: Display elapsed time, by default is `false`. - # - `colorize`: Enable colorize in terminal, only apply in `common` format, by default is `true`. - # - # ``` - # Halite.logging(skip_request_body: true, skip_response_body: true) - # .post("http://httpbin.org/get", form: {image: File.open("halite-logo.png")}) - # - # # => 2018-08-28 14:33:19 +08:00 | request | POST | http://httpbin.org/post - # # => 2018-08-28 14:33:21 +08:00 | response | 200 | http://httpbin.org/post | 1.61s | application/json - # ``` - # - # #### Use custom logging - # - # Creating the custom logging by integration `Halite::Logging::Abstract` abstract class. - # Here has two methods must be implement: `#request` and `#response`. - # - # ``` - # class CustomLogger < Halite::Logging::Abstract - # def request(request) - # @logger.info "| >> | %s | %s %s" % [request.verb, request.uri, request.body] - # end - # - # def response(response) - # @logger.info "| << | %s | %s %s" % [response.status_code, response.uri, response.content_type] - # end - # end - # - # # Add to adapter list (optional) - # Halite::Logging.register_adapter "custom", CustomLogger.new - # - # Halite.logging(logging: CustomLogger.new) - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # - # # We can also call it use format name if you added it. - # Halite.logging(format: "custom") - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # - # # => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar - # # => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json - # ``` - def logging(logging : Halite::Logging::Abstract = Halite::Logging::Common.new) - branch(default_options.with_logging(logging)) - end - - # Returns `Options` self with given the file with the path. - # - # #### JSON-formatted logging - # - # ``` - # Halite.logging(format: "json") - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # ``` - # - # #### create a http request and log to file - # - # ``` - # Log.setup("halite.file", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "a"))) - # Halite.logging(for: "halite.file") - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # ``` - # - # #### Always create new log file and store data to JSON formatted - # - # ``` - # Log.setup("halite.file", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "w")) - # Halite.logging(for: "halite.file", format: "json") - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # ``` - # - # Check the log file content: **/tmp/halite.log** - def logging(format : String = "common", *, for : String = "halite", - skip_request_body = false, skip_response_body = false, - skip_benchmark = false, colorize = true) - opts = { - for: for, - skip_request_body: skip_request_body, - skip_response_body: skip_response_body, - skip_benchmark: skip_benchmark, - colorize: colorize, - } - branch(default_options.with_logging(format, **opts)) - end - - # Turn on given features and its options. - # - # Available features to review all subclasses of `Halite::Feature`. - # - # #### Use JSON logging - # - # ``` - # Halite.use("logging", format: "json") - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # - # # => { ... } - # ``` - # - # #### Use common format logging and skip response body - # ``` - # Halite.use("logging", format: "common", skip_response_body: true) - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # - # # => 2018-08-28 14:58:26 +08:00 | request | GET | http://httpbin.org/get - # # => 2018-08-28 14:58:27 +08:00 | response | 200 | http://httpbin.org/get | 615.8ms | application/json - # ``` - def use(feature : String, **opts) - branch(default_options.with_features(feature, **opts)) - end - - # Turn on given the name of features. - # - # Available features to review all subclasses of `Halite::Feature`. - # - # ``` - # Halite.use("logging", "your-custom-feature-name") - # .get("http://httpbin.org/get", params: {name: "foobar"}) - # ``` - def use(*features) - branch(default_options.with_features(*features)) - end - - # Make an HTTP request with the given verb - # - # ``` - # Halite.request("get", "http://httpbin.org/get", { - # "headers" = { "user_agent" => "halite" }, - # "params" => { "nickname" => "foo" }, - # "form" => { "username" => "bar" }, - # }) - # ``` - def request(verb : String, uri : String, *, - headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response - request(verb, uri, options_with(headers, params, form, json, raw, tls)) - end - - # Make an HTTP request with the given verb and options - # - # > This method will be executed with oneshot request. - # - # ``` - # Halite.request("get", "http://httpbin.org/stream/3", headers: {"user-agent" => "halite"}) do |response| - # puts response.status_code - # while line = response.body_io.gets - # puts line - # end - # end - # ``` - def request(verb : String, uri : String, *, - headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil, - &block : Halite::Response ->) - request(verb, uri, options_with(headers, params, form, json, raw, tls), &block) - end - - # Make an HTTP request with the given verb and options - # - # > This method will be executed with oneshot request. - # - # ``` - # Halite.request("get", "http://httpbin.org/get", Halite::Options.new( - # "headers" = { "user_agent" => "halite" }, - # "params" => { "nickname" => "foo" }, - # "form" => { "username" => "bar" }, - # ) - # ``` - def request(verb : String, uri : String, options : Halite::Options? = nil) : Halite::Response - branch(options).request(verb, uri) - end - - # Make an HTTP request with the given verb and options - # - # > This method will be executed with oneshot request. - # - # ``` - # Halite.request("get", "http://httpbin.org/stream/3") do |response| - # puts response.status_code - # while line = response.body_io.gets - # puts line - # end - # end - # ``` - def request(verb : String, uri : String, options : Halite::Options? = nil, &block : Halite::Response ->) - branch(options).request(verb, uri, &block) - end - - private def branch(options : Halite::Options? = nil) : Halite::Client - options ||= default_options - Halite::Client.new(options) - end - - private def default_options - {% if @type.superclass %} - @default_options - {% else %} - DEFAULT_OPTIONS.clear! - {% end %} - end - - private def options_with(headers : (Hash(String, _) | NamedTuple)? = nil, - params : (Hash(String, _) | NamedTuple)? = nil, - form : (Hash(String, _) | NamedTuple)? = nil, - json : (Hash(String, _) | NamedTuple)? = nil, - raw : String? = nil, - tls : OpenSSL::SSL::Context::Client? = nil) - options = Halite::Options.new(headers: headers, params: params, form: form, json: json, raw: raw, tls: tls) - default_options.merge!(options) - end - end -end diff --git a/drivers/cisco/webex/models/device.cr b/drivers/cisco/webex/models/device.cr deleted file mode 100644 index 3ccad2ccce3..00000000000 --- a/drivers/cisco/webex/models/device.cr +++ /dev/null @@ -1,15 +0,0 @@ -module Cisco - module Webex - module Models - class Device - include JSON::Serializable - - @[JSON::Field(key: "webSocketUrl")] - property websocket_url : String - - @[JSON::Field(key: "name")] - property name : String? - end - end - end -end diff --git a/drivers/cisco/webex/models/event.cr b/drivers/cisco/webex/models/event.cr deleted file mode 100644 index d0ff44cd00b..00000000000 --- a/drivers/cisco/webex/models/event.cr +++ /dev/null @@ -1,27 +0,0 @@ -module Cisco - module Webex - module Models - class Event - include JSON::Serializable - - @[JSON::Field(key: "id")] - property id : String - - @[JSON::Field(key: "data")] - property data : Events::Data - - @[JSON::Field(key: "timestamp")] - property timestamp : Int64 - - @[JSON::Field(key: "trackingId")] - property tracking_id : String - - @[JSON::Field(key: "sequenceNumber")] - property sequence_number : Int64 - - @[JSON::Field(key: "filterMessage")] - property filter_message : Bool - end - end - end -end diff --git a/drivers/cisco/webex/models/events/activity.cr b/drivers/cisco/webex/models/events/activity.cr deleted file mode 100644 index 504b2b87c60..00000000000 --- a/drivers/cisco/webex/models/events/activity.cr +++ /dev/null @@ -1,35 +0,0 @@ -module Cisco - module Webex - module Models - module Events - class Activity - include JSON::Serializable - - @[JSON::Field(key: "id")] - property id : String - - @[JSON::Field(key: "objectType")] - property object_type : String - - @[JSON::Field(key: "url")] - property url : String - - @[JSON::Field(key: "published")] - property published : String - - @[JSON::Field(key: "verb")] - property verb : String - - @[JSON::Field(key: "actor")] - property actor : Actor - - @[JSON::Field(key: "target")] - property target : Target - - @[JSON::Field(key: "clientTempId")] - property client_temp_id : String? - end - end - end - end -end diff --git a/drivers/cisco/webex/models/events/actor.cr b/drivers/cisco/webex/models/events/actor.cr deleted file mode 100644 index 01c3b9f8007..00000000000 --- a/drivers/cisco/webex/models/events/actor.cr +++ /dev/null @@ -1,32 +0,0 @@ -module Cisco - module Webex - module Models - module Events - class Actor - include JSON::Serializable - - @[JSON::Field(key: "id")] - property id : String - - @[JSON::Field(key: "objectType")] - property object_type : String - - @[JSON::Field(key: "displayName")] - property display_name : String - - @[JSON::Field(key: "orgId")] - property organisation_id : String - - @[JSON::Field(key: "emailAddress")] - property email : String - - @[JSON::Field(key: "entryUUID")] - property entry_uuid : String - - @[JSON::Field(key: "type")] - property type : String - end - end - end - end -end diff --git a/drivers/cisco/webex/models/events/data.cr b/drivers/cisco/webex/models/events/data.cr deleted file mode 100644 index 6cb51913264..00000000000 --- a/drivers/cisco/webex/models/events/data.cr +++ /dev/null @@ -1,17 +0,0 @@ -module Cisco - module Webex - module Models - module Events - class Data - include JSON::Serializable - - @[JSON::Field(key: "activity")] - property activity : Activity - - @[JSON::Field(key: "eventType")] - property event_type : String - end - end - end - end -end diff --git a/drivers/cisco/webex/models/events/target.cr b/drivers/cisco/webex/models/events/target.cr deleted file mode 100644 index 4c5b77d196c..00000000000 --- a/drivers/cisco/webex/models/events/target.cr +++ /dev/null @@ -1,23 +0,0 @@ -module Cisco - module Webex - module Models - module Events - class Target - include JSON::Serializable - - @[JSON::Field(key: "id")] - property id : String - - @[JSON::Field(key: "objectType")] - property object_type : String - - @[JSON::Field(key: "url")] - property url : String - - @[JSON::Field(key: "published")] - property published : String - end - end - end - end -end diff --git a/drivers/cisco/webex/models/events/type.cr b/drivers/cisco/webex/models/events/type.cr deleted file mode 100644 index f4a6f84f360..00000000000 --- a/drivers/cisco/webex/models/events/type.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Cisco - module Webex - module Models - module Events - class Type - include JSON::Serializable - - @[JSON::Field(key: "eventType")] - property event_type : String - end - end - end - end -end diff --git a/drivers/cisco/webex/models/message.cr b/drivers/cisco/webex/models/message.cr deleted file mode 100644 index a2136fcc694..00000000000 --- a/drivers/cisco/webex/models/message.cr +++ /dev/null @@ -1,77 +0,0 @@ -module Cisco - module Webex - module Models - class Message - include JSON::Serializable - - # The unique identifier for the message. - @[JSON::Field(key: "id")] - property id : String - - # The unique identifier for the parent message. - @[JSON::Field(key: "parentId")] - property parent_id : String? - - # The room ID of the message. - @[JSON::Field(key: "roomId")] - property room_id : String - - # The type of room. - @[JSON::Field(key: "roomType")] - property room_type : String - - # The person ID of the recipient when sending a 1:1 message. - @[JSON::Field(key: "toPersonId")] - property to_person_id : String? - - # The email address of the recipient when sending a 1:1 message. - @[JSON::Field(key: "toPersonEmail")] - property to_person_email : String? - - # The message, in plain text. - @[JSON::Field(key: "text")] - property text : String - - # The message, in Markdown format. - @[JSON::Field(key: "markdown")] - property markdown : String? - - # The text content of the message, in HTML format. This read-only property is used by the Webex Teams clients. - @[JSON::Field(key: "html")] - property html : String? - - # Public URLs for files attached to the message. - @[JSON::Field(key: "files")] - property files : Array(String)? - - # The person ID of the message author. - @[JSON::Field(key: "personId")] - property person_id : String - - # The email address of the message author. - @[JSON::Field(key: "personEmail")] - property person_email : String - - # People IDs for anyone mentioned in the message. - @[JSON::Field(key: "mentionedPeople")] - property mentioned_people : Array(String)? - - # Group names for the groups mentioned in the message. - @[JSON::Field(key: "mentionedGroups")] - property mentioned_groups : Array(String)? - - # Message content attachments attached to the message. - @[JSON::Field(key: "attachments")] - property attachments : Array(String)? - - # The date and time the message was created. - @[JSON::Field(key: "created")] - property created : String - - # The date and time the message was created. - @[JSON::Field(key: "updated")] - property updated : String? - end - end - end -end diff --git a/drivers/cisco/webex/models/peek.cr b/drivers/cisco/webex/models/peek.cr deleted file mode 100644 index 2198be1757f..00000000000 --- a/drivers/cisco/webex/models/peek.cr +++ /dev/null @@ -1,15 +0,0 @@ -module Cisco - module Webex - module Models - class Peek - include JSON::Serializable - - @[JSON::Field(key: "id")] - property id : String - - @[JSON::Field(key: "data")] - property data : Events::Type - end - end - end -end diff --git a/drivers/cisco/webex/models/person.cr b/drivers/cisco/webex/models/person.cr deleted file mode 100644 index 0d74284904b..00000000000 --- a/drivers/cisco/webex/models/person.cr +++ /dev/null @@ -1,12 +0,0 @@ -module Cisco - module Webex - module Models - class Person - include JSON::Serializable - - @[JSON::Field(key: "id")] - property id : String - end - end - end -end diff --git a/drivers/cisco/webex/models/room.cr b/drivers/cisco/webex/models/room.cr deleted file mode 100644 index d674cda6f9c..00000000000 --- a/drivers/cisco/webex/models/room.cr +++ /dev/null @@ -1,45 +0,0 @@ -module Cisco - module Webex - module Models - class Room - include JSON::Serializable - - # A unique identifier for the room. - @[JSON::Field(key: "id")] - property id : String - - # The name of the room. - @[JSON::Field(key: "title")] - property title : String - - # The room type. - @[JSON::Field(key: "type")] - property type : String - - # Whether the room is moderated (locked) or not. - @[JSON::Field(key: "isLocked")] - property is_locked : Bool - - # The ID for the team with which this room is associated.. - @[JSON::Field(key: "teamId")] - property team_id : String? - - # The date and time of the room"s last activity.. - @[JSON::Field(key: "lastActivity")] - property last_activity : String - - # The ID of the person who created this room. - @[JSON::Field(key: "creatorId")] - property creator_id : String - - # The date and time the room was created. - @[JSON::Field(key: "created")] - property created : String - - # The ID of the organization which owns this room. - @[JSON::Field(key: "ownerId")] - property owner_id : String - end - end - end -end diff --git a/drivers/cisco/webex/session.cr b/drivers/cisco/webex/session.cr deleted file mode 100644 index f67597a687c..00000000000 --- a/drivers/cisco/webex/session.cr +++ /dev/null @@ -1,82 +0,0 @@ -module Cisco - module Webex - class Session - Log = ::Log.for(self) - - property base_url : String = Constants::DEFAULT_BASE_URL - property single_request_timeout : Int32 = Constants::DEFAULT_SINGLE_REQUEST_TIMEOUT - property user_agent : String = ["Tepha", Constants::VERSION].join(" ") - property wait_on_rate_limit : Bool = Constants::DEFAULT_WAIT_ON_RATE_LIMIT - - private property client : Halite::Client = Halite::Client.new - - def initialize(@access_token : String) - end - - def request(method : String, url : String, **kwargs) : Halite::Response - # Abstract base method for making requests to the Webex Teams APIs. - # This base method: - # * Expands the API endpoint URL to an absolute URL - # * Makes the actual HTTP request to the API endpoint - # * Provides support for Webex Teams rate-limiting - # * Inspects response codes and raises exceptions as appropriate - - absolute_url = URI.parse(base_url).resolve(url).to_s - - @client.headers({"Authorization" => ["Bearer", @access_token].join(" ")}) - @client.headers({"Content-Type" => "application/json;charset=utf-8"}) - @client.timeout single_request_timeout - - loop do - case method - when "GET" - response = @client.get absolute_url, **kwargs - when "POST" - response = @client.post absolute_url, **kwargs - when "PUT" - response = @client.put absolute_url, **kwargs - when "DELETE" - response = @client.delete absolute_url, **kwargs - else - raise Exceptions::Method.new("The request-method type is invalid.") - end - - begin - status_code = StatusCode.new(response.status_code) - raise Exceptions::RateLimit.new(status_code.message) if response.status_code == 429 - raise Exceptions::StatusCode.new(status_code.message) if !status_code.valid? - - return response - rescue e : Exceptions::StatusCode - Log.error(exception: e) { } - rescue e : Exceptions::RateLimit - Log.error(exception: e) { } - - retry_after = (response.headers["Retry-After"]? || "15").to_i * 1000 - sleep(retry_after) - end - end - end - - def get(url : String, **kwargs) : Halite::Response - # Sends a GET request. - request("GET", url, **kwargs) - end - - def post(url : String, **kwargs) : Halite::Response - # Sends a POST request. - request("POST", url, **kwargs) - end - - def put(url : String, **kwargs) : Halite::Response - # Sends a PUT request. - request("PUT", url, **kwargs) - end - - def delete(url : String, **kwargs) : Halite::Response - # Sends a DELETE request. - request("DELETE", url, **kwargs) - end - end - end -end diff --git a/drivers/cisco/webex/status_code.cr b/drivers/cisco/webex/status_code.cr deleted file mode 100644 index 4e48f8d80ba..00000000000 --- a/drivers/cisco/webex/status_code.cr +++ /dev/null @@ -1,23 +0,0 @@ -module Cisco - module Webex - class StatusCode - private property code : Int32 - - def initialize(@code : Int32) - end - - def valid? : Bool - case @code - when 200, 204 - true - else - false - end - end - - def message : String - Constants::STATUS_CODES[@code] - end - end - end -end diff --git a/drivers/cisco/webex/utils.cr b/drivers/cisco/webex/utils.cr deleted file mode 100644 index 1dc7c015cc1..00000000000 --- a/drivers/cisco/webex/utils.cr +++ /dev/null @@ -1,23 +0,0 @@ -module Cisco - module Webex - module Utils - def self.hash_from_items_with_values(**kwargs) - kwargs = kwargs.map { |k, v| - if v != nil && v != "" - {"#{k}" => v} - end - } - - kwargs.reject!(nil) - kwargs = kwargs.reduce { |acc, i| acc.try(&.merge(i.not_nil!)) } - - kwargs - end - - def self.named_tuple_from_hash(hash) - named_tuple = NamedTuple.new(roomId: String, text: String) - named_tuple.from(hash) - end - end - end -end From 0d92cf15b0665102313ce78d7019faac9e62554c Mon Sep 17 00:00:00 2001 From: Giorgi Kavrelishvili Date: Tue, 29 Nov 2022 08:29:05 +0400 Subject: [PATCH 3/5] Add webex related logic to handle incomming messages from the webex microservice --- drivers/cisco/webex/booking.cr | 66 ++++++++++++++++ drivers/cisco/webex/communication.cr | 61 +++++++++++++++ drivers/cisco/webex/models/event.cr | 29 +++++++ drivers/cisco/webex/models/events/activity.cr | 35 +++++++++ drivers/cisco/webex/models/events/actor.cr | 32 ++++++++ drivers/cisco/webex/models/events/data.cr | 17 ++++ drivers/cisco/webex/models/events/target.cr | 23 ++++++ drivers/cisco/webex/models/events/type.cr | 14 ++++ drivers/cisco/webex/models/message.cr | 77 +++++++++++++++++++ shard.lock | 6 +- shard.yml | 2 +- 11 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 drivers/cisco/webex/booking.cr create mode 100644 drivers/cisco/webex/communication.cr create mode 100644 drivers/cisco/webex/models/event.cr create mode 100644 drivers/cisco/webex/models/events/activity.cr create mode 100644 drivers/cisco/webex/models/events/actor.cr create mode 100644 drivers/cisco/webex/models/events/data.cr create mode 100644 drivers/cisco/webex/models/events/target.cr create mode 100644 drivers/cisco/webex/models/events/type.cr create mode 100644 drivers/cisco/webex/models/message.cr diff --git a/drivers/cisco/webex/booking.cr b/drivers/cisco/webex/booking.cr new file mode 100644 index 00000000000..00d6bcd8a1f --- /dev/null +++ b/drivers/cisco/webex/booking.cr @@ -0,0 +1,66 @@ +require "placeos-driver" +require "placeos-driver/interface/chat_bot" +require "placeos-driver/interface/locatable" +require "place_calendar" + +module Cisco + module Webex + class Booking < PlaceOS::Driver + default_settings({keyword: "book", organization_id: ""}) + + def on_load + on_update + end + + def on_update + organization_id = setting(String, :organization_id) + monitor("chat/webex/#{organization_id}/message") { |_subscription, payload| on_message(payload) } + end + + def on_message(message : String) + message = Interface::ChatBot::Message.from_json(message) + + keyword = message.text.split.first.downcase + text = message + .text + .sub(keyword, "") + .sub("a room", "") + .strip + + # Ignore the message if the keyword doesn't match the booking keyword specified in the settings + if keyword != setting(String, :keyword) + send_message(message.id, "Specified keyword is not recognized as a valid acommand for the PlaceOS Bot, #{keyword}.") + send_message(message.id, "An example booking command would look something like this: #{setting(String, :keyword)} a room for 30 minutes") + + return + end + + # Notify the user to await for a free room + send_message(message.id, "Looking for an available room to book, please wait!") + + # Split the remaining text into chunks to process them + conjunction, period, measurement = text.split + + case measurement + when "hours" + period_in_seconds = (period.to_i * 3600).to_i64 + event = PlaceCalendar::Event.from_json(system.implementing(Interface::Locatable).book_now(period_in_seconds).get.first.to_json) + send_message(message.id, "Successfully booked an event #{event.title}, from #{event.event_start}, to #{event.event_end}, in #{event.timezone}, on #{event.host}.") + when "minutes" + period_in_seconds = (period.to_i * 60).to_i64 + event = PlaceCalendar::Event.from_json(system.implementing(Interface::Locatable).book_now(period_in_seconds).get.first.to_json) + send_message(message.id, "Successfully booked an event #{event.title}, from #{event.event_start}, to #{event.event_end}, in #{event.timezone}, on #{event.host}.") + when "seconds" + event = PlaceCalendar::Event.from_json(system.implementing(Interface::Locatable).book_now(period.to_i64).get.first.to_json) + send_message(message.id, "Successfully booked an event #{event.title}, from #{event.event_start}, to #{event.event_end}, in #{event.timezone}, on #{event.host}.") + else + send_message(message.id, "Specified measurement is not recognized as a valid measurement, please use: minutes, seconds or hours.") + end + end + + private def send_message(id : Interface::ChatBot::Id, response : String) + system.implementing(Interface::ChatBot).reply(Interface::ChatBot::Message.new(id, response).to_json) + end + end + end +end diff --git a/drivers/cisco/webex/communication.cr b/drivers/cisco/webex/communication.cr new file mode 100644 index 00000000000..8f47af61bdd --- /dev/null +++ b/drivers/cisco/webex/communication.cr @@ -0,0 +1,61 @@ +require "placeos-driver" +require "placeos-driver/interface/chat_bot" +require "http" + +require "./models/**" + +module Cisco + module Webex + class Communication < PlaceOS::Driver + include Interface::ChatBot + + descriptive_name "Cisco Webex Bot Communication" + generic_name :Communication + uri_base "wss://webex.placeos.com/ws/messages" + + default_settings({ + organization_id: "", + api_key: "", + }) + + protected getter! socket : HTTP::WebSocket + + def on_load + on_update + end + + def on_update + headers = HTTP::Headers.new + + organization_id = setting(String, :organization_id) + + headers.merge!({"Organization-ID" => organization_id}) + headers.merge!({"X-API-Key" => setting(String, :api_key)}) + + @socket = HTTP::WebSocket.new(URI.parse(config.uri.not_nil!.to_s), headers) + + spawn do + socket.try(&.on_message do |message| + event = Models::Event.from_json(JSON.parse(message).as_h.["event"].to_json) + event_message = Models::Message.from_json(JSON.parse(message).as_h.["message"].to_json) + + id = Interface::ChatBot::Id.new(event_message.id.to_s, event_message.room_id.to_s, event.data.activity.actor.id, event.data.activity.actor.organization_id) + bot_message = Interface::ChatBot::Message.new(id, event_message.text.to_s) + + publish("chat/webex/#{organization_id}/message", bot_message.to_json) + end) + + socket.try(&.run) + end + end + + def notify_typing(id : Interface::ChatBot::Id) + end + + def reply(id : Interface::ChatBot::Id, response : String, url : String? = nil, attachment : Interface::ChatBot::Attachment? = nil) + files = [url.to_s] if url + socket.try(&.send({"roomId" => id.room_id.to_s, "text" => response, "files" => files || [] of String}.to_json)) + end + end + end +end diff --git a/drivers/cisco/webex/models/event.cr b/drivers/cisco/webex/models/event.cr new file mode 100644 index 00000000000..0d2493989f4 --- /dev/null +++ b/drivers/cisco/webex/models/event.cr @@ -0,0 +1,29 @@ +require "./events/**" + +module Cisco + module Webex + module Models + class Event + include JSON::Serializable + + @[JSON::Field(key: "id")] + property id : String + + @[JSON::Field(key: "data")] + property data : Events::Data + + @[JSON::Field(key: "timestamp")] + property timestamp : Int64 + + @[JSON::Field(key: "trackingId")] + property tracking_id : String + + @[JSON::Field(key: "sequenceNumber")] + property sequence_number : Int64 + + @[JSON::Field(key: "filterMessage")] + property filter_message : Bool + end + end + end +end diff --git a/drivers/cisco/webex/models/events/activity.cr b/drivers/cisco/webex/models/events/activity.cr new file mode 100644 index 00000000000..504b2b87c60 --- /dev/null +++ b/drivers/cisco/webex/models/events/activity.cr @@ -0,0 +1,35 @@ +module Cisco + module Webex + module Models + module Events + class Activity + include JSON::Serializable + + @[JSON::Field(key: "id")] + property id : String + + @[JSON::Field(key: "objectType")] + property object_type : String + + @[JSON::Field(key: "url")] + property url : String + + @[JSON::Field(key: "published")] + property published : String + + @[JSON::Field(key: "verb")] + property verb : String + + @[JSON::Field(key: "actor")] + property actor : Actor + + @[JSON::Field(key: "target")] + property target : Target + + @[JSON::Field(key: "clientTempId")] + property client_temp_id : String? + end + end + end + end +end diff --git a/drivers/cisco/webex/models/events/actor.cr b/drivers/cisco/webex/models/events/actor.cr new file mode 100644 index 00000000000..8ea659c923d --- /dev/null +++ b/drivers/cisco/webex/models/events/actor.cr @@ -0,0 +1,32 @@ +module Cisco + module Webex + module Models + module Events + class Actor + include JSON::Serializable + + @[JSON::Field(key: "id")] + property id : String + + @[JSON::Field(key: "objectType")] + property object_type : String + + @[JSON::Field(key: "displayName")] + property display_name : String + + @[JSON::Field(key: "orgId")] + property organization_id : String + + @[JSON::Field(key: "emailAddress")] + property email : String + + @[JSON::Field(key: "entryUUID")] + property entry_uuid : String + + @[JSON::Field(key: "type")] + property type : String + end + end + end + end +end diff --git a/drivers/cisco/webex/models/events/data.cr b/drivers/cisco/webex/models/events/data.cr new file mode 100644 index 00000000000..6cb51913264 --- /dev/null +++ b/drivers/cisco/webex/models/events/data.cr @@ -0,0 +1,17 @@ +module Cisco + module Webex + module Models + module Events + class Data + include JSON::Serializable + + @[JSON::Field(key: "activity")] + property activity : Activity + + @[JSON::Field(key: "eventType")] + property event_type : String + end + end + end + end +end diff --git a/drivers/cisco/webex/models/events/target.cr b/drivers/cisco/webex/models/events/target.cr new file mode 100644 index 00000000000..4c5b77d196c --- /dev/null +++ b/drivers/cisco/webex/models/events/target.cr @@ -0,0 +1,23 @@ +module Cisco + module Webex + module Models + module Events + class Target + include JSON::Serializable + + @[JSON::Field(key: "id")] + property id : String + + @[JSON::Field(key: "objectType")] + property object_type : String + + @[JSON::Field(key: "url")] + property url : String + + @[JSON::Field(key: "published")] + property published : String + end + end + end + end +end diff --git a/drivers/cisco/webex/models/events/type.cr b/drivers/cisco/webex/models/events/type.cr new file mode 100644 index 00000000000..f4a6f84f360 --- /dev/null +++ b/drivers/cisco/webex/models/events/type.cr @@ -0,0 +1,14 @@ +module Cisco + module Webex + module Models + module Events + class Type + include JSON::Serializable + + @[JSON::Field(key: "eventType")] + property event_type : String + end + end + end + end +end diff --git a/drivers/cisco/webex/models/message.cr b/drivers/cisco/webex/models/message.cr new file mode 100644 index 00000000000..bb2f02f291c --- /dev/null +++ b/drivers/cisco/webex/models/message.cr @@ -0,0 +1,77 @@ +module Cisco + module Webex + module Models + class Message + include JSON::Serializable + + # The unique identifier for the message. + @[JSON::Field(key: "id")] + property id : String? + + # The unique identifier for the parent message. + @[JSON::Field(key: "parentId")] + property parent_id : String? + + # The room ID of the message. + @[JSON::Field(key: "roomId")] + property room_id : String? + + # The type of room. + @[JSON::Field(key: "roomType")] + property room_type : String? + + # The person ID of the recipient when sending a 1:1 message. + @[JSON::Field(key: "toPersonId")] + property to_person_id : String? + + # The email address of the recipient when sending a 1:1 message. + @[JSON::Field(key: "toPersonEmail")] + property to_person_email : String? + + # The message, in plain text. + @[JSON::Field(key: "text")] + property text : String? + + # The message, in Markdown format. + @[JSON::Field(key: "markdown")] + property markdown : String? + + # The text content of the message, in HTML format. This read-only property is used by the Webex Teams clients. + @[JSON::Field(key: "html")] + property html : String? + + # Public URLs for files attached to the message. + @[JSON::Field(key: "files")] + property files : Array(String)? + + # The person ID of the message author. + @[JSON::Field(key: "personId")] + property person_id : String? + + # The email address of the message author. + @[JSON::Field(key: "personEmail")] + property person_email : String? + + # People IDs for anyone mentioned in the message. + @[JSON::Field(key: "mentionedPeople")] + property mentioned_people : Array(String)? + + # Group names for the groups mentioned in the message. + @[JSON::Field(key: "mentionedGroups")] + property mentioned_groups : Array(String)? + + # Message content attachments attached to the message. + # @[JSON::Field(key: "attachments")] + # property attachments : Array(Attachment)? + + # The date and time the message was created. + @[JSON::Field(key: "created")] + property created : String? + + # The date and time the message was created. + @[JSON::Field(key: "updated")] + property updated : String? + end + end + end +end diff --git a/shard.lock b/shard.lock index fa0bd55c606..8457803079f 100644 --- a/shard.lock +++ b/shard.lock @@ -51,7 +51,7 @@ shards: crunits: git: https://github.com/spider-gazelle/crunits.git - version: 1.1.0 + version: 1.1.1 csuuid: git: https://github.com/wyhaines/csuuid.cr.git @@ -175,7 +175,7 @@ shards: neuroplastic: git: https://github.com/spider-gazelle/neuroplastic.git - version: 1.12.1 + version: 1.12.2 ntlm: git: https://github.com/spider-gazelle/ntlm.git @@ -243,7 +243,7 @@ shards: placeos-driver: git: https://github.com/placeos/driver.git - version: 6.6.0 + version: 6.6.2 placeos-log-backend: git: https://github.com/place-labs/log-backend.git diff --git a/shard.yml b/shard.yml index bd8e0817d60..dd682c8b247 100644 --- a/shard.yml +++ b/shard.yml @@ -118,4 +118,4 @@ dependencies: github: grkek/stripetease desigo: - github: place-technology/desigo \ No newline at end of file + github: place-technology/desigo From 302738f45bfae86c29590353528f625b634e908c Mon Sep 17 00:00:00 2001 From: Giorgi Kavrelishvili Date: Wed, 7 Dec 2022 11:07:45 +0400 Subject: [PATCH 4/5] Add an example comment --- drivers/cisco/webex/booking.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/cisco/webex/booking.cr b/drivers/cisco/webex/booking.cr index 00d6bcd8a1f..0754eea1c20 100644 --- a/drivers/cisco/webex/booking.cr +++ b/drivers/cisco/webex/booking.cr @@ -21,6 +21,9 @@ module Cisco message = Interface::ChatBot::Message.from_json(message) keyword = message.text.split.first.downcase + + # An example message text would look something like this: + # {% keyword %} a room for 30 minutes text = message .text .sub(keyword, "") From 47aa59d47d7a599c4b117fe3ece072319ae3b787 Mon Sep 17 00:00:00 2001 From: Giorgi Kavrelishvili Date: Mon, 19 Dec 2022 08:32:26 +0400 Subject: [PATCH 5/5] Run crystal format on the location service spec --- drivers/xy_sense/location_service_spec.cr | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/drivers/xy_sense/location_service_spec.cr b/drivers/xy_sense/location_service_spec.cr index c2f86fc32ae..3a1142ed47f 100644 --- a/drivers/xy_sense/location_service_spec.cr +++ b/drivers/xy_sense/location_service_spec.cr @@ -33,18 +33,18 @@ class XYSenseMock < DriverSpecs::MockDriver capacity: 1, category: "Workpoint", }, - { - id: "xysense-desk-456-id", - name: "desk-456", - capacity: 1, - category: "Workpoint", - }, - { - id: "xysense-area-567-id", - name: "area-567", - capacity: 20, - category: "Lobby", - }], + { + id: "xysense-desk-456-id", + name: "desk-456", + capacity: 1, + category: "Workpoint", + }, + { + id: "xysense-area-567-id", + name: "area-567", + capacity: 20, + category: "Lobby", + }], }, }