Azure Functions ile Slack Bot Yapımı

IT dünyasının son dönemde merkezinde yer alan bir kavramın — Serverless Architecture — tasarım ve uygulamasında kullanılan Azure Functions ile (ki bir dönem WebJobs olarak adlandırılan bir yapı idi) küçük bir uygulama yaparak sistemin mantığını çözebilmek derdimiz.

Ne yazık ki Türkçe karşılığını tam olarak ifade edemediğim bir kavram Serverless Architecture. Sunucusuz mimari olarak düz bir çeviri yapsak içime sinmiyor çünkü en nihayetinde bulut ortamda olsa dahi bir sunucu ortamı mevcut. Tüm bu sunucu bakım ve zaman maliyetlerinin minimize edildiği desek farklı bulut çözümleri ile karışıyor. Tam bu noktada Functions as a Service (FaaS) kavramı kurtarıcı olarak hayatımıza giriyor. Birim işlemlerin, event-based uygulamalarla sunucu yönetimine gerek olmaksızın fonksiyon seviyesinde karşılanması ve işlenmesi yöntemi ile bu mimariyi canlandırabiliyoruz.

Yazının ana konusuna girmeden hemen önce yukarıda bahsettiğimiz kavramın teknik detaylarına dair çok değerli bulduğum üç referansı buraya bırakmak istiyorum:

Hikayemiz nedir?

Ekip olarak yemek yemeyi çok — fazlaca çok — seviyoruz. Öğlen yemekleri için yaptığımız gurme kaçamakları organizasyonlarımız da meşhurdur :)

Hızlıca organize olabilmek için bir mail başlatıyoruz ve katılanlar, katılmayanlar +-1 ile dönüş yapıyorlar. Organizatör gurmemiz ise son listeyi toplayıp ona göre planlama yapıyor.

Düşündüm ki “Slack” üzerinde bir kanalımız olsa; ismi “Gurmecikler”. Slack çalışma grubumuzun da bir bot’u olsa (Gurmetör) ve Gurmecikler kanalına üye tüm arkadaşlara özelden “Bu öğlen yemeğe gidiyoruz, geliyor musun?” diye mesaj atsa. Katılanları da bir yere yazsa. Sonrasında organizatör arkadaş hiç sayım zahmetine girmeden bu listeden planlamayı gerçekleştirse. Hayat daha kolay — ve lezzetli — olmaz mıydı?

Organizatör genelde ben olmuyorum ama olsaydım bu olay çok hoşuma giderdi diye düşündüm ve kolları sıvadım (yemek için değil, uygulama için :))

O halde uygulayalım

Yazının devamındaki uygulamaları çalıştırabilmek için bir takım ön gereksinimlerimiz mevcut. Şöyle ki;

  • Bir Azure hesabına ihtiyacımız var. Azure Functions ve Redis kuracağız.
  • Bir Slack çalışma grubuna ihtiyacımız var. Bot ve Slack uygulaması için kullanacağız.

Tüm bunlara sahip olduğumuzu düşünerek yola devam ediyoruz.

Slack Konfigürasyonu

Slack üzerinde bir çalışma grubu oluşturduysak bu adrese giderek “Create App” butonu yardımıyla bir uygulama oluşturmamız gerekiyor. Ben uygulamama “Gurmetör” adını verdim.

Sol taraftaki menüden “Bot Users” seçeneğini seçiyor ve uygulamamıza bağlı bir bot oluşturuyoruz. Display Name ve Default Username özelliğini dilersek değiştirebiliriz. Bu isimler Slack ekranında görünecektir. Default Username özelliği Türkçe karakter barındıramayacağı için bunu gurmetor olarak değiştiriyoruz.

Sırada uygulamaya tanımlı yetkiler var. Bunun için yine sol menüden “OAuth & Permissions” seçeneğini seçiyoruz ve Scopes alt başlığından yetki tanımlarını gerçekleştiriyoruz. Bu botu bir kanala üye olmuş kullanıcıların listesini çekerken kullanacağımız için channels:read yetkisi ile ve bu kullanıcılara mesaj atabileceği için chat:write:bot yetkisi ile sarmalıyoruz.

Yetkilerle ilgili en önemli adım, programatik olarak tüm bu işlemleri yapacağımız için bir Token’a ihtiyaç duyuyoruz ve bunu üreteceğiz. Bunun için yine aynı ekrandaki en üst bölümde yer alan Tokens for Your Workspace bölümüne çıkıyoruz ve “Install App” butonu ile uygulamamızı çalışma grubumuza ekliyoruz. Sonrasında bu alanda iki token görülecek. Biz bot olarak devam edeceğimiz için Bot User OAuth Access Token altındaki değeri bir yere not alıyoruz.

Son yapmamız gereken işlem ise uygulamamızda kullanacağımız özelliği devreye almak. Bunun için Slack bir çok özellik sunuyor (interaktif menü, interaktif mesaj, slaş komutları, vs…). Bizim uygulamamız ise kullanıcıya bir soru sorup butonlar yardımıyla cevap alacağı için sol menüden “Interactive Components”i seçiyor ve aktif hale getiriyoruz:

Bizden iki özellik istiyor — biri zorunlu olmak üzere — :

Request URL: Uygulamamız, kullanıcıdan gelen cevabı bir webhook yardımıyla burada verdiğimiz adrese POST edecek.
Options Load URL (for Message Menus): Mesaj menüleri, buton içerikleri gibi alanları dinamik yönetmek istersek bu alana bir adres tanımlaması yapabilir ve uygulamanın, içerikleri bu alandan çekmesini sağlayabiliriz.

Tahmin ettiğiniz üzere bizim doldurmamız gereken alan Request URL. Buradan bir event fırlayacak ve biz bu event’i yakalayıp bir fonksiyon aracılığıyla yorumlayacağız, işlemler yapacağız ve kullanıcıya cevap döneceğiz. Bu adrese sahip olmak ve fonksiyonumuzu geliştirmek üzere Azure’a gidiyoruz.

Azure Konfigürasyonu

Azure üzerinde iki uygulama kullanacağız.

Bunlardan ilki Azure Functions. Görevi; Slack’ten gelen isteği karşılamak, kullanıcı cevabını yorumlamak ve kullanıcıya uygun cevabı dönmek.

Diğeri ise Redis Cache. Görevi; Azure Functions üzerinden aldığımız kullanıcı cevabını saklamak.

Redis Cache kurulumu oldukça basit. Yapılması gereken Azure portal üzerinden New butonuna basarak açılan pencereye Redis Cache yazmak ve sunulan tek ürünün kurulumunu en maliyetsiz haliyle gerçekleştirmek.

Kurulum bittikten sonra Redis’e ulaşmak için gerekli olan bağlantı cümlesini temin etmek üzere ana ekran üzerindeki Show Access Keys linkine tıklıyoruz.

Açılan yeni penceredeki Primary connection string (StackExchange.Redis) başlığı altında yer alan bağlantı cümlesini bir yere saklıyoruz.

Sırada uygulamanın kalbi, Azure Functions kurulumu var. Bunu Azure portali üzerinden kurabileceğimiz gibi Visual Studio 2017 üzerinden de Publish Profile ile kolaylıkla gerçekleştirebiliriz. Ben de bu şekilde devam ediyorum. Öncelikle yeni bir proje oluşturuyorum:

Sonrasında ise açılan projeme sağ tıklayarak Add > New Azure Function diyor ve ismini MessagingFunction adını verdiğim fonksiyonu ekliyorum.

Uygulamanın tamamına aşağıdaki repodan ulaşmanız mümkün. Ben tıpkı diğer yazılarda olduğu gibi ana fonksiyona yoğunlaşacağım.

Kodu incelemeden hemen önce belirtmem gereken nokta şu ki; Slack payload body parametresi ile application/x-www-form-urlencoded tipinde aşağıdaki gibi bir istek yolluyor:

payload={
"type": "interactive_message",
"actions": [
{
"name": "backgammon",
"type": "button",
"value": "tavla"
}
],
"callback_id": "menu_options_2319",
"team": {
"id": "T8UQAFR3N",
"domain": "azurefans"
},
"channel": {
"id": "C8UMRQYCB",
"name": "general"
},
"user": {
"id": "U8VLEHP6J",
"name": "selcukusta"
},
"action_ts": "1516342843.789131",
"message_ts": "1516342841.000102",
"attachment_id": "1",
"token": "S1GwWyH8SygTOIctpxk7EDgK",
"is_app_unfurl": false,
"original_message": {
"text": "Would you like to play a game?",
"username": "Hook Sample",
"bot_id": "B8UMZLBDH",
"attachments": [
{
"callback_id": "menu_options_2319",
"text": "Choose a game to play",
"id": 1,
"actions": [
{
"id": "1",
"name": "chess",
"text": "Chess",
"type": "button",
"value": "satranc",
"style": ""
},
{
"id": "2",
"name": "backgammon",
"text": "Backgammon",
"type": "button",
"value": "tavla",
"style": ""
}
],
"fallback": "Choose a game to play"
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1516342841.000102"
},
"response_url": "https:\/\/hooks.slack.com\/actions\/T8UQAFR3N\/302432451511\/BPXRvlrVHZ6K6Yl3YxDT7zYG",
"trigger_id": "301273509075.300826535124.7ece498ce2b00cea11c798fdb7efa461"
}

Ancak Azure Functions mevcut sürümünde application/x-www-form-urlencoded tipine özel bir kabul gerçekleştiremiyor. Dolayısıyla kodda yer alan 6. satırdaki ReadAsStringAsync ifadesini ReadAsFormDataAsync olarak değiştiremiyoruz. Bu sebeple gelen içeriği olduğu gibi metinsel bir şekilde okuyup payload= ifadesinden sonraki json ifadeyi alıyor ve Newtonsoft.Json kütüphanesi ile işliyoruz.

Kodun geri kalan kısmında ise gelen cevaptaki kullanıcı seçimini (actions[0].value) ve kullanıcı adını (user.name) alıyor, Redis’e yazma işlemini gerçekleştiriyor ve kullanıcıya; seçimine göre bir cevap dönüyoruz.

Azure Functions üzerindeki hassas verileri (appsettings ve connectionstrings olarak özelleştirebiliriz) kod üzerinde tutmak yerine oluşturduğumuz Function’a özel Application Settings alanında tutmamız mantıklı:

Tabii henüz yukarıdaki pencereyi görebilecek duruma gelemedik. Bunun için Azure üzerinde Azure Functions servisimizin kurulu olması gerekli. Yukarıda da bahsettiğim üzere bunun için Publish Profile kullanacağız. Yapmamız gereken projemize sağ tıklayarak “Publish” butonuna basmak.

Açılan penceredeki sağ üst köşede Azure hesabımızla ilişkilendirilmiş Microsoft hesabımızı görmemiz gerekli. Eğer göremiyorsak giriş yapmamız yeterli olacaktır. Sonrasında ise tüm alanlar otomatik olarak doluyor ve bize düşen tek şey “Create” butonuna basmak oluyor.

Tüm işlemler başarıyla gerçekleştikten sonra ise “Publish” butonu yardımıyla uygulamamızı Azure’a yüklüyoruz. Yükleme işlemi başarıyla gerçekleştikten sonra ise iki üst görselde yer alan ekrana ulaşabiliyor olmalıyız. Görselde de göreceğimiz üzere iki anahtarı ekliyoruz (REDIS_CONNECTION ve REDIS_DEFAULT_DB). Bu iki değer, uygulamadaki Redis’e bağlantı gerçekleştiren RedisCacheProvider sınıfı içerisinde kullanılacak.

Son adımda yapmamız gereken ise fonksiyona ulaşacağımız public adresi edinmek. Functions penceresinden ilgili function’ı buluyor ve sağ üst köşedeki “</> Get function URL” linki ile açılan pop-up’dan URL’i kopyalıyoruz.

Kopyaladığımız function URL’ini, tekrar Slack paneline dönerek Request URL olarak gördüğümüz alana yapıştırıyor ve aktif hale getiriyoruz.

Sistemler hazır

Son durumda ihtiyacımız olan 3 sistem de (Slack, Redis, Azure Functions) hazır durumda. Yapmamız gereken son şey, bot kullanıcısı ile istediğimiz kanaldaki üyelerin listesini almak ve her birine mesaj atmak.

Bitirelim şu işi

Mesaj atma işi için küçük bir Python scripti geliştirelim.

3. satır en önemli kısım. Yazının en başında bahsettiğim ve bir yere not aldığımız Bot User OAuth Access Token değerini buraya yapıştırıyoruz.

İlk fonksiyon (get_users_in_channel) parametre olarak kanal id’si alıyor ve bu kanaldeki kullanıcıların id’lerini döndürüyor.

İkinci fonksiyon ise (send_message) yine parametre olarak kanal id’si alıyor ve o kanala özelleştirilmiş bir mesaj gönderiyor. Bu kanal id’si public bir kanal olabilir ya da private bir kanal olabilir. Ayrıca her kullanıcı id’si, o kullanıcı ile yapılan özel görüşmelerdeki kanalın da id’si olarak ifade ediliyor.

Ana fonksiyonda ise kanaldaki kullanıcılarda dönerek her kullanıcıya mesaj gönderme işlemi gerçekleştiriliyor.

Sonuç

Gif’i kaydederken arka tarafta Python uygulamasını tetikledim, kısa bir süre sonra mesaj geldi ve cevabı verdim. Sonrasında ise Azure Functions tarafından nihai cevap bana ulaştı. Bir de Redis tarafını kontrol edelim dilerseniz:

Burada da gelmeyecek olarak kayıt altına alındığımı görebiliyoruz. Uygulama istenen kıvama gelmiş durumda.

Bu yazının sonuna gelirken karnımın acıktığını söylemem gerekli. Eğer aynı etkiyi yarattıysam özür diliyorum :) Bir sonraki yazıya dek, bot’lardan aldığınız cevaplarınız hep anlamlı olsun.