前兩個答案並不完全回答「他們如何做到這一點」的原始問題,以便從Messages應用程序中啓動機器人。
我重新創建了模擬Jeeves虛擬助理與機器人交互(以及獲取天氣)所需的確切工作流和腳本。
的完整詳細信息,請參見鏈接的PDF文檔:
https://s3.amazonaws.com/icefield/IntegratingXcodeBotsWithMessages.pdf
編輯:原來的答案被刪除,由於,我相信,以我通過鏈接到完整的答案引用的事實。此編輯添加完整的實現細節作爲此答案的一部分。我希望這個答案不會太長。
Xcode的集成與搜索引擎的消息
在WWDC 2014屆415,持續集成和Xcode 6,蘋果展示了集成的Xcode機器人與消息應用程序通過定製集成觸發。更具體地說,從該會話視頻的23分鐘標記開始(https://developer.apple.com/videos/play/wwdc2014-415/),Apple展示了使用集成觸發器與消息一起使用以接收構建服務器上的集成狀態。此外,通過使用虛擬聊天室成員Jeeves,他們展示了直接從Messages應用程序開始集成的能力。以下文章提供了重現該功能的分步說明。
客戶端和服務器的配置
要開始,這裏的客戶端和服務器的配置,我曾經模仿佔道功能:
客戶 OS X版本10.11(埃爾卡皮坦),Xcode 7.0.1
服務器 OS X版本10.11(El Capitan),OS X Server 5.0.4,Xcode 7.0.1, Ruby 2.0.0p645
網絡 對於我的開發和持續集成,我使用內部網絡。我的OS X服務器位於domain.local,而我的開發機器是同一內部網絡上的另一個節點。無論您使用的是內部服務器還是外部服務器,下面的說明都應該有效。
的Jabber - 消息
Jabber的的基礎是一個開源協議例如消息的原始名稱。 Jabber更名爲可擴展消息和呈現協議(XMPP)。 OS X Messages應用程序是使用Jabber核心構建的。
我們將在這項工作中廣泛使用Jabber(消息),因此我們確保它已開啓。從OS X Server應用程序中,選擇服務>消息視圖,然後切換右上角的消息。對於佔道經營,是我使用的消息服務設置如下:
從終端窗口中你的服務器上,如果你想檢查的Jabber特定的設置,使用
$ sudo serveradmin settings jabber
注特別是jabberClientPortTLS(5222)和jabberClientPortSSL(5223)值。這些是您的服務器上用於與Jabber服務進行通信的端口。
我們將使用Ruby編寫Jeeves的大部分腳本,我們需要一個XMPP/Jabber庫來完成這個任務。從你的服務器上的一個終端窗口,使用
$ gem install xmpp4r
對Jabber的服務建立用戶
因爲我的服務器是本地服務器,而無需任何開發者帳戶上安裝XMPP4R(一個XMPP/Jabber的庫紅寶石)它,我需要爲各種開發人員創建帳戶以登錄到Jabber。您可能需要或不需要此步驟,具體取決於您的服務器是否已定義用戶帳戶。
從您的服務器上的OS X Server應用程序,轉到帳戶>用戶列表,併爲每個將使用虛擬Jeeves助理的客戶端添加新用戶。一定要爲Jeeves創建一個新用戶。對於用戶'Tom',這裏是使用的設置。確保爲每個用戶創建一個電子郵件地址,但郵件服務不需要運行。這些電子郵件地址將用於從客戶端的消息應用程序登錄到Jabber服務。
登錄的Jabber客戶端從開發機
與您的服務器上定義的用戶帳戶,它現在是時候登錄到您的客戶端機器的Jabber帳號。在客戶端的消息應用程序中,轉到消息>首選項>帳戶。選擇左下方的+號,選擇「其他信息帳戶...」,然後按繼續。在「添加郵件帳戶」對話框中,爲帳戶類型選擇Jabber,然後填寫用戶的憑證信息。這裏是我使用的設置:
(注意:使用SSL啓用後,該端口(5223)您的服務器上檢查的Jabber服務的設置,當你前面列出的jabberClientPortSSL值相匹配。)
成功登錄到Jabber服務後,您可以選擇在Jabber帳戶的「聊天設置」頁面下更改您的帳戶暱稱。所有其他默認設置都可以保持原樣。
創建聊天室
我們希望所有的機器人集成狀態和溝通,我們的虛擬助理,佔道經營,要通過信息的聊天室。聊天室允許小組溝通,但您不需要邀請加入。要創建聊天室,請執行以下操作。
從郵件中,選擇「文件」>「轉到聊天室」。您應該看到您登錄到列出的Jabber服務的帳戶。鍵入房間名稱的集成@房間..本地,然後選擇執行。 (請注意,我發現「rooms..local」 .COM需要一個聊天室'>。如果使用其他字比‘房間’不會創建聊天室。)
配置服務器網站服務
當積分從Xcode中的客戶機上運行開始時,前,後集成腳本通過進行HTTP調用對OS X服務器網站服務文件與Jabber服務進行通信。您必須配置OS X Server網站服務來處理這些調用。
您需要修改非SSL http(端口80)站點的設置。這是我使用的設置。
選擇端口80網站,並選擇鉛筆圖標下方,使您的設置進行匹配。
選擇「編輯高級設置...」,並設置符合這些。 (啓用「允許CGI的執行......」可使Ruby腳本執行。)
最後,你需要能夠實現特定的文件(message_room - 我們將在後面討論)被配置爲運行作爲Ruby腳本。爲此,請放置以下內容。htaccess文件在您的Web服務器的默認主文件夾(通常是/ Library/Server/Web/Data/Sites/Default)。
Options +ExecCGI
<FilesMatch message_room$>
SetHandler cgi-script
</FilesMatch>
注:以下所有Ruby腳本的,你需要修改的變量只是下每個腳本的「資格證書」的評論,以配合您的域名和登錄證書。
前和後的整合腳本 當我們我們的客戶機上開始從Xcode的集成,我們希望將消息發送到Jabber集成聊天室,以便聊天室的所有成員都可以通知整合已經開始(並完成)。在Xcode的機器人觸發器頁面上,將以下預集成前後腳本添加到項目的bot中。
這是預集成觸發腳本:
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'uri'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<yourDomain>.local"
# -------------------------------------------------------------------------------------
# our messaging endpoint
uri = URI.parse("http://#{domain}:80/message_room")
# -------------------------------------------------------------------------------------
# what we want to say
message = "#{ENV['XCS_BOT_NAME']} integration #{ENV['XCS_INTEGRATION_NUMBER']} is now starting."
# -------------------------------------------------------------------------------------
# build up the request body
reqBody = {:message => message}
body = JSON.generate(reqBody)
# -------------------------------------------------------------------------------------
# the connect type
http = Net::HTTP.new(uri.host, uri.port)
# -------------------------------------------------------------------------------------
# build up the request
request = Net::HTTP::Post.new(uri.request_uri)
request.add_field('Content-type', 'application/json')
request.body = body
# -------------------------------------------------------------------------------------
# send the request and get the response
response = http.request(request)
這是整合後觸發腳本:
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'uri'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<yourDomain>.local"
# -------------------------------------------------------------------------------------
# our messaging endpoint
uri = URI.parse("http://#{domain}:80/message_room")
# -------------------------------------------------------------------------------------
# what we want to say
integrationResult = case ENV['XCS_INTEGRATION_RESULT']
when "succeeded"
"has completed successfully."
when "test-failures"
tc = ENV['XCS_TEST_FAILURE_COUNT'].to_i
"completed with #{tc} failing #{(tc ==1) ? 'test' : 'tests'}."
when "build-errors"
ec = ENV['XCS_ERROR_COUNT'].to_i
"failed with #{ec} build #{(ec == 1) ? 'error' : 'errors'}."
when "warnings"
wc = ENV['XCS_WARNING_COUNT'].to_i
"completed with #{wc} #{(wc == 1) ? 'warning' : 'warnings'}."
when "analyzer-warnings"
ic = ENV['XCS_ANALYZER_WARNING_COUNT'].to_i
"completed with #{ic} static analysis #{(ic == 1) ? 'issue' : 'issues'}."
when "trigger-error"
"failed running trigger script."
when "checkout-error"
"failed to checkout from source control."
else
"failed with unexpected errors."
end
message = "#{ENV['XCS_BOT_NAME']} integration #{ENV['XCS_INTEGRATION_NUMBER']} #{integrationResult}"
# -------------------------------------------------------------------------------------
# build up the request body
reqBody = {:message => message}
body = JSON.generate(reqBody)
# -------------------------------------------------------------------------------------
# the connect type
http = Net::HTTP.new(uri.host, uri.port)
# -------------------------------------------------------------------------------------
# build up the request
request = Net::HTTP::Post.new(uri.request_uri)
request.add_field('Content-type', 'application/json')
request.body = body
# -------------------------------------------------------------------------------------
# send the request and get the response
response = http.request(request)
前兩個的Ruby腳本撥打電話給居住在message_room文件您的OS X服務器網站主文件夾(通常是/ Library/Server/Web/Data/Sites/Default)。將以下message_room文件放入該文件夾中。
#!/usr/bin/env ruby
require 'cgi'
require 'json'
require 'xmpp4r'
require 'xmpp4r/muc'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<domain>.local"
userId = "[email protected]#{domain}"
userPw = "<jeevesAccountPassword>"
roomName = "[email protected]#{domain}"
# -------------------------------------------------------------------------------------
# header sent back
cgi = CGI.new
puts cgi.header("type" => "text/html", "status" => "OK")
# -------------------------------------------------------------------------------------
# get the message out of the json formatted text
keyValue = JSON.parse(cgi.params.keys.first)
key = "message"
value = keyValue[key] puts value
# -------------------------------------------------------------------------------------
# create the message to the iChat (jabber) room
fromJID = Jabber::JID.new(userId)
jabberClient = Jabber::Client.new(fromJID)
jabberClient.connect
jabberClient.auth(userPw)
jabberClient.send(Jabber::Presence.new.set_type(:available))
# -------------------------------------------------------------------------------------
# send the message to a chat room
roomID = roomName + "/" + jabberClient.jid.node
roomJID = Jabber::JID::new(roomID)
room = Jabber::MUC::MUCClient.new(jabberClient) room.join(roomJID)
roomMessage = Jabber::Message.new(roomJID, value) room.send(roomMessage)
從消息應用程序
開始集成我們希望能夠從消息應用程序中發出我們的虛擬助理佔道指令。我們要支持三條指令:
佔道,天氣#得到當前的天氣(W/O 拉鍊默認爲庫比蒂諾)
佔道經營,一體化(BOT名稱)#開始整合對於給定的 博特
佔道經營,你的OS X Server上退出#關機佔道
以下文件將放置在OS X Server網站的默認文件夾(通常爲/ Library/Server/Web/Data/Sites/Default)中。
處理虛擬助手Jeeves的主要文件是jeevesManager.rb。通過輸入
$ ruby ./jeevesManager.rb
從服務器上的網站默認文件夾中啓動該文件來喚醒Jeeves。
#!/usr/bin/env ruby
require 'xmpp4r'
require 'xmpp4r/muc'
require 'xmpp4r/delay'
require './jeevesWeather.rb'
require './jeevesIntegration.rb'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<domain>.local"
userId = "[email protected]#{domain}"
userPw = "<jeevesAccountPassword>"
roomName = "[email protected]#{domain}"
defaultWeatherZipCode = "95015"
# -------------------------------------------------------------------------------------
# create the client we'll use
fromJID = Jabber::JID.new(userId)
jabberClient = Jabber::Client.new(fromJID)
jabberClient.connect
jabberClient.auth(userPw)
jabberClient.send(Jabber::Presence.new.set_type(:available))
# -------------------------------------------------------------------------------------
# connect to the chatroom
roomID = roomName + "/" + jabberClient.jid.node
roomJID = Jabber::JID::new(roomID)
room = Jabber::MUC::MUCClient.new(jabberClient) room.join(roomJID)
# -------------------------------------------------------------------------------------
# weather
def getWeather(m)
begin
words = m.body.downcase.split("weather")
where = defaultWeatherZipCode
if (words.length == 2)
where = words[1].strip
end
weather = get_weather_for_city(where,'f')
rescue
weather = "Couldn't get weather for that location - try zip code"
end
return weather
end
# -------------------------------------------------------------------------------------
# integration
def startIntegration(m)
begin
words = m.body.split("integrate")
botName = "Invalid BOT Name"
if (words.length == 2)
botName = words[1].strip
end
integrationMessage = jeevesIntegration(botName)
rescue
integrationMessage = "Failed integrating #{botName}"
end
return integrationMessage
end
# -------------------------------------------------------------------------------------
# listen for messages in chatroom (this callback will run in a separate thread)
room.add_message_callback do |m|
if (m.x.nil?) # the msg is current
if m.type != :error
body = m.body;
if (body.downcase.include? "jeeves")
# assume Jeeves does not understand command
understood = 0
# exit Jeeves
if (body.downcase.include? "exit")
understood = 1
message = "Good-bye"
mainthread.wakeup
end
# Weather
if (body.downcase.include? "weather")
understood = 1
message = getWeather(m)
end
# Integrate BOT
if (body.downcase.include? "integrate")
understood = 1
message = startIntegration(m)
end
# Jeeves doesn't understand command
if (understood == 0)
message = "I don't understand that command!"
end
# let user know what has happened
roomMessage = Jabber::Message.new(roomJID, message)
room.send(roomMessage)
end
end
end
end
# -------------------------------------------------------------------------------------
# add the callback to respond to server ping (to keep the connect alive)
jabberClient.add_iq_callback do |iq_received|
if iq_received.type == :get
if iq_received.queryns.to_s != 'http://jabber.org/protocol/disco#info'
iq = Jabber::Iq.new(:result, jabberClient.jid.node)
iq.id = iq_received.id
iq.from = iq_received.to
iq.to = iq_received.from
jabberClient.send(iq)
end
end
end
# -------------------------------------------------------------------------------------
# stop the main thread (the call back will still be alive this way)
print "Connected to chat room...\n"
Thread.stop
print "Disconnected from chat room...\n"
# leave chat room and log out of Jabber
room.exit
jabberClient.close
上面的Jeeves管理器文件使用了兩個其他補充文件。下面的第一個處理獲取天氣預報和格式化,第二個處理開始集成。
######### Weather #########
require 'rexml/document'
require 'open-uri'
require 'net/smtp'
# -------------------------------------------------------------------------------------
# yahoo weather url info
# http://developer.yahoo.net/weather/#examples
# -------------------------------------------------------------------------------------
#Returns a hash containing the location and temperature information
#Accepts US zip codes or Yahoo location id's
def yahoo_weather_query(loc_id, units)
h = {}
open("http://xml.weather.yahoo.com/forecastrss?p=#{loc_id}&u=#{units}") do |http|
response = http.read
doc = REXML::Document.new(response)
root = doc.root
channel = root.elements['channel']
location = channel.elements['yweather:location']
h[:city] = location.attributes["city"]
h[:region] = location.attributes["region"]
h[:country] = location.attributes["country"]
h[:temp] = channel.elements["item"].elements["yweather:condition"].attributes["temp"]
h[:text] = channel.elements["item"].elements["yweather:condition"].attributes["text"]
h[:wind_speed] = channel.elements['yweather:wind'].attributes['speed']
h[:humidity] = channel.elements['yweather:atmosphere'].attributes['humidity']
h[:sunrise] = channel.elements['yweather:astronomy'].attributes['sunrise']
h[:sunset] = channel.elements['yweather:astronomy'].attributes['sunset']
h[:forecast_low] = channel.elements["item"].elements['yweather:forecast'].attributes['low']
h[:forecast_high] = channel.elements["item"].elements['yweather:forecast'].attributes['high'] end
return h
end
# -------------------------------------------------------------------------------------
def get_weather_for_city(city_code,units)
weather_info = yahoo_weather_query(city_code, units)
city = weather_info[:city]
region = weather_info[:region]
country = weather_info[:country]
temp = weather_info[:temp]
wind_speed = weather_info[:wind_speed]
humidity = weather_info[:humidity]
text = weather_info[:text]
sunrise = weather_info[:sunrise]
sunset = weather_info[:sunset]
forecast_low = weather_info[:forecast_low]
forecast_high = weather_info[:forecast_high]
return "#{city}, #{region}:\n" + " Currently #{temp} degrees, #{humidity}% humidity, #{wind_speed} mph winds, #{text}.\n" + " Forecast: #{forecast_low} low, #{forecast_high} high.\n" + " Sunrise: #{sunrise}, sunset: #{sunset}.\n"
end
最後,這是揭開序幕從消息應用程序
require 'json'
require 'open-uri'
require 'openssl'
# -------------------------------------------------------------------------------------
def jeevesIntegration(botToIntegrate)
# credentials
domain = "<domain>.local"
endpoint = "https://#{domain}:20343"
user = "your-integration-username (not Jeeves)"
password = "password"
# return message
message = "Bot '#{botToIntegrate}' does not exist on server #{domain}"
# request JSON construct with all the BOTS
botsRequestURI = URI.parse("#{endpoint}/api/bots")
output = open(botsRequestURI, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE})
bots = JSON.parse(output.readlines.join(""))
# loop through full list of BOTS for the one we're interested in
bots['results'].each do |bot|
botName = bot['name']
if (botName.downcase == botToIntegrate.downcase)
botID = bot['_id']
# curl -k -X POST -u "#{user}:#{password}" "#{endpoint}/api/bots/#{botid}/integrations" -i
# -------------------------------------------------------------------
# kickoff integration
uri = URI.parse(endpoint)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new("/api/bots/#{botID}/integrations")
request.basic_auth(user, password)
response = http.request(request)
message = "Integrating #{botName} on server #{domain}"
end
end
return message
end
注意,「凱明」上提交不啓動集成腳本,它會檢查每5分鐘,看是否有提交被推送,然後執行機器人。爲了在提交時獲得實際的集成,你需要一個類似於GitHub的鉤子,並讓該鉤子通過腳本手動運行。 – 2015-05-25 19:12:57