LoginSignup
6
1

More than 3 years have passed since last update.

Azure FunctionsにてJavaプログラムをイゴかして、Bing News Search APIで取得したとれたて新鮮ニュースを毎朝母にLINEメッセージでおくりつける

Last updated at Posted at 2021-04-16

はじめに

  • Java楽しんでいますか:bangbang::bangbang::bangbang:
  • この記事は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{面倒くさくなってきました}$
  • そこでボット :robot: に代行してもらうことにしました
    • ちなみに母にボットと言っても通じないので「ロボット」が送っていると説明しています
  • こういう用途に、Azure Functionsはうってつけだとおもいます
  • Nervesならできるもん! ということで、Elixirというプログラミング言語をつかって、Raspberry Piで動かすのがいま一番私が得意とすることですが、それだとイベントに参加できないし、たまには違うことやってみるのが「そこがいいんじゃない!1」ということでAzure FunctionsでJavaのプログラムをイゴかしたいとおもいます
  • まだ読んだことはありませんが、みうらじゅんさんの親孝行プレイに通じるものがあるのではないかと勝手におもっています

準備

スクリーンショット 2021-04-16 1.30.34.png

  • 以下、:point_up::point_up_tone1::point_up_tone2::point_up_tone3::point_up_tone4::point_up_tone5: の記事に書いていることはスミ2の前提で書いていきます

つくる

① プロジェクトをつくる

mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure \
-DarchetypeArtifactId=azure-functions-archetype \
-DjavaVersion=11 \
-DgroupId=tokyo.torifuku \
-DartifactId=torifuku-functions \
-Dtrigger=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のメッセージをおくる

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();
        }
    }
}
  • schedulerunメソッドを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"
  }
}

③ ローカルでイゴかす

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-04-16 2.05.21.png

  • 以上で、毎朝06:30にBing News Search APIで取得したとれたて新鮮ニュースがLINEメッセージとして配信されるはずです :robot::rocket::rocket::rocket: Screenshot_20210416_021014_jp.naver.line.android.jpg

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);

ボットとの会話を楽しんでいる様子

  • 楽しんでくれているようです

Screenshot_20210501_153952_jp.naver.line.android.jpg

Screenshot_20210501_150955_jp.naver.line.android (1).jpg

Wrapping up :lgtm::lgtm::lgtm::lgtm:

  • とても簡単に親孝行ができるようになりました
  • Azureの利用料はほとんどかかっていません
    • リソース グループをわけているのですが0円な気がします
    • まだ使いはじめて1年以内のアカウントなので無料枠の適用があるのかもしれません
  • みなさんもAzure Functionsを使って、お手軽になにかの定期実行をしてみてはいかがでしょうか
  • Happy coding!!!

最後に

  • 私はElixirというプログラミング言語が好きです
  • ここからは同じことをElixirでやってみます

プロジェクトをつくる

$ 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って何よ:interrobang: という方へ

  • 最後はがっつりElixirでしめました

image.png

EsvA7uQU0AEoTuX.jpeg

(@piacerex さん作 :pray::pray_tone1::pray_tone2::pray_tone3::pray_tone4::pray_tone5:)


  1. 2021年本屋大賞 『発掘部門』 「超発掘本!」の『「ない仕事」の作り方』 より 

  2. 昔、銀牙 -流れ星 銀-という犬の漫画がありました。駄菓子屋でカードを売っていて、その中に当り🎯のカードがあるわけです。当りを引くと何をもらえたのかは忘れましたが、きなこ餅だかもう一枚だかをもらえました。その店の婆さんは景品と交換済みであることをわかるように油性マジックでスミと書いてくださっていたことをおもいだしました。ああいうカードで子供のときはたくさん集めていたわけですがどこに行ってしまったのでしょうね。 

  3. ポストマンの主演ケビン・コスナーさん 

  4. 城戸利成選手のことです。競争車名に「ポストマン」を使われていたことがありました。第20回日本選手権オートレースにおいて優出を果たしているすごい選手です。 

  5. https://www.honda.co.jp/ciao/ 

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1