はじめに
- Java楽しんでいますか
- この記事はJava開発者のためのAzure入門というキャンペーンへの投稿記事です
- この記事では、Azure Functionsを使って、朝06:30にJavaのプログラムを動かして以下のことを行います
- Bing News Search APIにて新鮮とれたてニュースを取得します
- LINEのMessaging APIを使って、Bing News Search APIで取得した新鮮とれたてニュースと「おはようございます」というあいさつを母が属するグループへ送りつけます
制作背景
- 最近、seventyになんなんとする母がスマートフォンを買いました
- 買い替えた理由は、「FOMAが終わると聞いたから早めに慣れておきたい」というものです
- 表向きもっともらしいことをいっていますが、本当はまわりの人がシュッとやっているのをみて母自身がやってみたくなっただけなのだとおもいます
- スマートフォンはファッションなのです
- パカパカの携帯電話はいやだ、あたいもスマホがいい
- いくつになっても女心は枯れてはいないとでも申しましょうか
- ショップに行ったら、本人は買いたいと言っているのに店員からは止められる始末
- 値段の高い端末しかショップにはおいてなくてお値打ち価格のものは入荷まで1ヶ月かかると言われて、「もう一度検討する」と言って帰りました
- 母のスマホを買いたい気持ちは止められるはずもなく、docomo Online Shopを利用したら2日後に届いて初期設定をやってあげました
-
はじめてスマホプランです
- 1GB/月で全然いいんです
- 余ります
- 4/16現在、残りは0.98GBありました
- 実家にインターネットは引いてありまして、Wi-Fiルーターはありますし、ほとんど家からでることはありませんし、外にでかけるときもスマホは忘れておいていくので1GBでギガは十分足ります
- そもそも母には意味がわかりませんし、いいんです
- 外で動画みたりするわけないし、そもそも動画アプリの起動ができるかあやしいし
-
$\huge{5G}$
- 対応機種です
- 実家はど田舎なので、5Gとか来ていませんが4G圏内でよかったよかった
- そんな母が、「LINEは難しい」と言います
- LINEと接する機会を増やしたほうがいいだろうということではじめは私が手打ちでメッセージを送っていました
- ただ、だんだん
- 面倒くさくなってきました
- $\huge{面倒くさくなってきました}$
- そこでボット に代行してもらうことにしました
- ちなみに母にボットと言っても通じないので「ロボット」が送っていると説明しています
- こういう用途に、Azure Functionsはうってつけだとおもいます
- Nervesならできるもん! ということで、Elixirというプログラミング言語をつかって、Raspberry Piで動かすのがいま一番私が得意とすることですが、それだとイベントに参加できないし、たまには違うことやってみるのが「そこがいいんじゃない!1」ということでAzure FunctionsでJavaのプログラムをイゴかしたいとおもいます
- まだ読んだことはありませんが、みうらじゅんさんの親孝行プレイに通じるものがあるのではないかと勝手におもっています
準備
- Azure Functions(Java)を利用するための準備運動
- にまとめました
- もうひとつ準備が必要です
-
ローカルでの Azure Storage の開発に Azurite エミュレーターを使用するを参考に、Azurite オープンソース エミュレーターをインストールしておいてください
-
Azurite Visual Studio Code の拡張機能をインストールして実行するでインストールしました
- VSCodeの右下のほうに [Azurite Blob Service] と表示されているものがあるはずでそれを迷わず押しておくとよいです
-
Azurite Visual Studio Code の拡張機能をインストールして実行するでインストールしました
-
ローカルでの Azure Storage の開発に Azurite エミュレーターを使用するを参考に、Azurite オープンソース エミュレーターをインストールしておいてください
- 以下、 の記事に書いていることはスミ2の前提で書いていきます
つくる
- 前置きが四の五の多かったですが、ここから先はあっさり楽勝です
- クイックスタート: コマンド ラインから Azure に Java 関数を作成する という公式の手順を適宜読み替えてすすめていきます
① プロジェクトをつくる
mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure \
-DarchetypeArtifactId=azure-functions-archetype \
-DjavaVersion=11 \
-DgroupId=tokyo.torifuku \
-DartifactId=torifuku-functions \
-Dtrigger=TimerTrigger
-
TimerTrigger
の指定は、こちらの記事にて教えてもらいました- Azure Functions(Java)のTimerTriggerをローカルで動かしてみる
- ありがとうございます!
② プログラム書く、書く、書く
pom.xml
<dependency>
<groupId>com.linecorp.bot</groupId>
<artifactId>line-bot-api-client</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>com.linecorp.bot</groupId>
<artifactId>line-bot-model</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
</dependencies>
pom.xml
<!-- function app name -->
<appName>${functionAppName}</appName>
<!-- function app resource group -->
- <resourceGroup>java-functions-group</resourceGroup>
+ <resourceGroup>java-torifuku-functions-20210411122137476</resourceGroup>
<!-- function app service plan name -->
<appServicePlanName>java-functions-app-service-plan</appServicePlanName>
<!-- function app region-->
<!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-De
tails#supported-regions for all valid values -->
- <region>westus</region>
+ <region>japaneast</region>
<!-- function pricingTier, default to be consumption if not specified -->
<!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-pricing-tiers for all valid values -->
<!-- <pricingTier></pricingTier> -->
@@ -76,7 +94,7 @@
<!-- <disableAppInsights></disableAppInsights> -->
<runtime>
<!-- runtime os, could be windows, linux or docker-->
- <os>windows</os>
+ <os>linux</os>
<javaVersion>11</javaVersion>
<!-- for docker function, please set the following parameters -->
<!-- <image>[hub-user/]repo-name[:tag]</image> -->
-
windows
だと、Bing News Search APIで取得したデータが文字化けしていたのでlinux
にしました - 他のもっといい解決方法があるかもしれません
- とりあえず母親に送りつけることができればいいのでOSは問いません
クイック スタート:Java と Bing News Search REST API を使用してニュース検索を実行する
- リンク先を参考にしてつくりました
- ほぼ同じです
- こちらもMicrosoft様のサービスです
- ありがとうございます!
src/main/java/tokyo/torifuku/BingNewsSearch.java
package tokyo.torifuku;
import java.io.InputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import javax.net.ssl.HttpsURLConnection;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class BingNewsSearch {
// Add your Bing Search V7 subscription key to your environment variables.
static String subscriptionKey = System.getenv("BING_SEARCH_V7_SUBSCRIPTION_KEY");
// Add your Bing Search V7 endpoint to your environment variables.
static String endpoint = "https://api.bing.microsoft.com/v7.0/news/search";
public static SearchResults searchNews(String searchQuery) throws Exception {
// Construct URL of search request (endpoint + query string)
URL url = new URL(endpoint + "?q=" + URLEncoder.encode(searchQuery, "UTF-8") + "&setLang=ja-JP" + "&mkt=ja-JP");
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setRequestProperty("Ocp-Apim-Subscription-Key", subscriptionKey);
// Receive JSON body
InputStream stream = connection.getInputStream();
Scanner scanner = new Scanner(stream);
String response = scanner.useDelimiter("\\A").next();
JsonObject jsonResponse = new JsonParser().parse(response).getAsJsonObject();
// Construct result object for return
SearchResults results = new SearchResults(new HashMap<String, String>(), jsonResponse);
// Extract Bing-related HTTP headers
Map<String, List<String>> headers = connection.getHeaderFields();
for (String header : headers.keySet()) {
if (header == null) continue; // may have null key
if (header.startsWith("BingAPIs-") || header.startsWith("X-MSEdge-")) {
results.relevantHeaders.put(header, headers.get(header).get(0));
}
}
scanner.close();
stream.close();
return results;
}
// Pretty-printer for JSON; uses GSON parser to parse and re-serialize
public static String prettify(JsonObject json) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
return gson.toJson(json);
}
}
src/main/java/tokyo/torifuku/SearchResults.java
package tokyo.torifuku;
import java.util.HashMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
public class SearchResults {
HashMap<String, String> relevantHeaders;
JsonObject jsonResponse;
SearchResults(HashMap<String, String> headers, JsonObject json) {
relevantHeaders = headers;
jsonResponse = json;
}
public String topNews() {
JsonArray array = jsonResponse.getAsJsonArray("value");
JsonObject first = array.get(0).getAsJsonObject();
String name = first.get("name").getAsString();
String url = first.get("url").getAsString();
return name + "\n\n" + url;
}
}
LINEのメッセージをおくる
- プッシュメッセージを送るのJavaのコードを参考に書きます
src/main/java/tokyo/torifuku/Postman.java
package tokyo.torifuku;
import com.linecorp.bot.model.PushMessage;
import com.linecorp.bot.model.message.TextMessage;
import com.linecorp.bot.client.LineMessagingClient;
import java.util.concurrent.ExecutionException;
public class Postman {
public void post(String message) {
final LineMessagingClient client = LineMessagingClient
.builder(System.getenv("LINE_CHANNEL_ACCESS_TOKEN"))
.build();
final TextMessage textMessage = new TextMessage(message);
final PushMessage pushMessage = new PushMessage(
System.getenv("LINE_TO"),
textMessage);
try {
client.pushMessage(pushMessage).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
return;
}
}
}
定期的に実行するrun
メソッド
src/main/java/tokyo/torifuku/Function.java
package tokyo.torifuku;
import java.time.*;
import com.microsoft.azure.functions.annotation.*;
import com.microsoft.azure.functions.*;
/**
* Azure Functions with Timer trigger.
*/
public class Function {
/**
* This function will be invoked periodically according to the specified schedule.
*/
@FunctionName("Function")
public void run(
@TimerTrigger(name = "timerInfo", schedule = "0 30 21 * * *") String timerInfo,
final ExecutionContext context
) {
context.getLogger().info("Java Timer trigger function executed at: " + LocalDateTime.now());
Postman kevin = new Postman();
kevin.post("おはようございます");
SearchResults result;
try {
result = BingNewsSearch.searchNews("");
context.getLogger().info(BingNewsSearch.prettify(result.jsonResponse));
String topNews = result.topNews();
context.getLogger().info(topNews);
kevin.post(topNews);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
-
schedule
はrun
メソッドをRunさせる時間をUTCで指定しています - 上の例ですと日本時間の06:30に送りつけることになります
- 朝は05:00くらいから母は起きだしてごそごそしているので問題ないです
-
Postman
のインスタンス名はもちろんkevin
にしました3- 城戸利成(元オートレース選手)と迷ったのですが、わかる人が少ないかなあとおもいまして世界的スターのほうを採用しました4
設定値
BING_SEARCH_V7_SUBSCRIPTION_KEY
LINE_CHANNEL_ACCESS_TOKEN
LINE_TO
local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "java",
"BING_SEARCH_V7_SUBSCRIPTION_KEY": "secret",
"LINE_CHANNEL_ACCESS_TOKEN": "secret",
"LINE_TO": "secret"
}
}
③ ローカルでイゴかす
- 関数をローカルで実行する
- 待ちきれない場合は、
schedule
の値を調整してください
mvn clean package
mvn azure-functions:run
④ デプロイする
mvn clean package azure-functions:deploy
- 設定値を設定しておいてください
BING_SEARCH_V7_SUBSCRIPTION_KEY
LINE_CHANNEL_ACCESS_TOKEN
LINE_TO
2021/05/01 追記
- 次は文字入力の練習だとおもい、ボットが返事するようにしてみました
- あんまり自信はないのですがNode.jsをなんとなく見様見真似で書いてみました
-
Azure VMでイゴかしています
- 素朴に
node index.js
- 素朴に
- Talk APIを利用させていただいています
index.js
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const { response } = require('express');
const config = {
channelAccessToken: 'ひみつ',
channelSecret: 'ひみつ'
};
const app = express();
app.post('/webhook', line.middleware(config), (req, res) => {
Promise
.all(req.body.events.map(handleEvent))
.then((result) => res.json(result));
});
const client = new line.Client(config);
function handleEvent(event) {
console.log(event);
if (event.type !== 'message' || event.message.type !== 'text') {
return Promise.resolve(null);
}
if (['カード', '家計簿', 'かけいぼ'].filter((element) => { return event.message.text.match(element); }).length > 0) {
return client.replyMessage(event.replyToken, {
type: 'text',
text: 'カード明細のまとめです。ご確認ください。https://docs.google.com/spreadsheets/d/ひみつ/preview'
});
}
runBot(event);
}
async function runBot(event) {
const params = new URLSearchParams();
params.append('apikey', 'ひみつ');
params.append('query', event.message.text);
const response = await axios.post('https://api.a3rt.recruit-tech.co.jp/talk/v1/smalltalk', params)
if (response.data.status === 0) {
const replyText = response.data.results[0].reply;
return client.replyMessage(event.replyToken, {
type: 'text',
text: replyText
});
} else {
return Promise.resolve(null);
}
}
app.listen(3000);
ボットとの会話を楽しんでいる様子
- 楽しんでくれているようです
Wrapping up
- とても簡単に親孝行ができるようになりました
- Azureの利用料はほとんどかかっていません
- リソース グループをわけているのですが0円な気がします
- まだ使いはじめて1年以内のアカウントなので無料枠の適用があるのかもしれません
- みなさんもAzure Functionsを使って、お手軽になにかの定期実行をしてみてはいかがでしょうか
- Happy coding!!!
最後に
プロジェクトをつくる
$ mix new good_son --sup
$ cd good_son
mix.exs
defp deps do
[
{:httpoison, "~> 1.8"},
{:jason, "~> 1.2"},
{:quantum, "~> 3.0"}
]
end
$ cd good_son
$ mix deps.get
プログラムを書く
- 詳しい解説はしますまい
- 感じてください
lib/good_son/scheduler.ex
defmodule GoodSon.Scheduler do
use Quantum, otp_app: :good_son
end
lib/good_son/application.ex
defmodule GoodSon.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: GoodSon.Worker.start_link(arg)
# {GoodSon.Worker, arg}
GoodSon.Scheduler # add
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: GoodSon.Supervisor]
Supervisor.start_link(children, opts)
end
end
Bing News Search
lib/good_son/bing_news_search.ex
defmodule GoodSon.BingNewsSearch do
@subscription_key "secret"
def top_news do
search()
|> Map.get("value")
|> Enum.at(0)
end
def search do
"https://api.bing.microsoft.com/v7.0/news/search?q=&setLang=ja-JP&mkt=ja-JP"
|> HTTPoison.get!("Ocp-Apim-Subscription-Key": @subscription_key)
|> Map.get(:body)
|> Jason.decode!()
end
end
LINE
lib/good_son/line.ex
defmodule GoodSon.Line do
@to "secret"
@channel_access_token "secret"
def push(msg \\ "Hello") do
body =
%{
to: @to,
messages: [
%{
type: "text",
text: msg
}
]
}
|> Jason.encode!()
HTTPoison.post!(
"https://api.line.me/v2/bot/message/push",
body,
"Content-Type": "application/json",
Authorization: "Bearer #{@channel_access_token}"
)
end
end
06:30に実行する関数
lib/good_son.ex
defmodule GoodSon do
def run do
GoodSon.Line.push("おはようございます")
%{"name" => name, "url" => url} = GoodSon.BingNewsSearch.top_news()
"#{name}\n\n#{url}"
|> GoodSon.Line.push()
end
end
config/config.exs
import Config
config :good_son, GoodSon.Scheduler,
jobs: [
{"30 21 * * *", {GoodSon, :run, []}}
]
実行
$ iex -S mix
- とりあえずローカル(macOS)でイゴくところまででこの記事は終わります
- ぜひ次は、@erinさんのAzure FunctionsをElixirで みたいなことをしたいです
- Nervesは得意としておりますし楽しいのですが、いつか自分の手元のハードウェア(Raspberry Pi 2)は壊れることがあるでしょうし、そういうことはクラウドサービスにまかせチャオ5 というのはすごく便利です
- ありがとうございます!
もう一度最後の最後に
Elixirって何よ という方へ
- 最後はがっつりElixirでしめました
- 2020/12/26時点くらいのスクリーンショット
- Elixirについてもっと知りたい方は下記の本をオススメします
-
elixir.jp Slackの
#autoracex
というところに私は入り浸っておりますのでお気軽にお声がけください -
勉強会が頻繁に行われています
- 私がよく参加している勉強会です
- autoracex 【毎週月曜】 主催
- Sapporo.beam 【毎週水曜】
- OkazaKirin.beam 【毎週木曜】
- fukuoka.ex/kokura.ex 【毎月2~3回】
- NervesJP 【毎月1回】
(@piacerex さん作 )
-
2021年本屋大賞 『発掘部門』 「超発掘本!」の『「ない仕事」の作り方』 より ↩
-
昔、銀牙 -流れ星 銀-という犬の漫画がありました。駄菓子屋でカードを売っていて、その中に当り🎯のカードがあるわけです。当りを引くと何をもらえたのかは忘れましたが、きなこ餅だかもう一枚だかをもらえました。その店の婆さんは景品と交換済みであることをわかるように油性マジックでスミと書いてくださっていたことをおもいだしました。ああいうカードで子供のときはたくさん集めていたわけですがどこに行ってしまったのでしょうね。 ↩
-
城戸利成選手のことです。競争車名に「ポストマン」を使われていたことがありました。第20回日本選手権オートレースにおいて優出を果たしているすごい選手です。 ↩