LINE Developer Day 2019 で PowerPoint スライドを LINE ボット経由で配布した話

5 回目にして 2 Days になった LINE Developer Day で登壇してきました。今回は登壇した話ではなく、その前後で行った開発の話です。

# Day 1 - 12:00 スライド見えない問題

Hall A+B で行われたキーノートに参加した後、意気揚々と Hall E の [gRPC service development in private Kubernetes cluster](https://linedevday.linecorp.com/jp/2019/sessions/E1-1) に参加。部屋は超満員。何とか後ろから 2 列目に座りましたが、そこで問題発覚。

**スライドがほぼ見えない**

前日リハーサルできなかったことから衝撃を受けつつも、スライドは変えようと決意。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">イベント前日になぜか PowerPoint とLINEボットの連携を仕込んでる。こんなに簡単にできるなんてさすが LINE と Microsoft Bot Framework と Visual Studio 2019 <a href="https://twitter.com/hashtag/linedevday?src=hash&amp;ref_src=twsrc%5Etfw">#linedevday</a></p>&mdash; kenichiro nakamura@AKS (@kenakamu108) <a href="https://twitter.com/kenakamu108/status/1197129232435957761?ref_src=twsrc%5Etfw">November 20, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

でも、その後ルンルンと他のセッションを見に行きました。折角参加したんだもんね！

# 17:00 変更を開始

疲れてきたためスピーカールームで休憩。ふと自分のツイートみてスライド変更する必要があることを思い出す。やるかぁ。。

# 17:02 すぐ断念

スライドを開いて変更を始めようとしたが、すでに提出済であったことと、画面の大半が見えなかったことから早々にスライドの変更を断念。。。

# 17:04 ボット開発を決意

前に出す画面が変えられないなら、手元に画面送るしかない！ということでボット開発を決意。一人じゃ心もとないので、小林さん (@pierusan2010) を呼び出し、構成を考えはじめる。制限を先に考えてみた。

- ボット使えないから OpenChat はだめ
- 実アカウント晒したくないからグループもだめ
- お金かかるからプッシュは使わない

これらの制限を踏まえて以下の構成を考える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/de1b3bf1-ef1b-9434-1552-6cffd676562a.png)

1. プレゼンの画像をひたすら blob にあげてスライド番号とマップ (小林さん)
2. スライドショー実行時に、スライド番号を Web API 経由で更新
3. LINE からボットの「現在のスライド」を要求
4. 現在のスライド番号から対応する画像の URL を返信として返す

案はできたのでいざ実装!!

# 17:10 実装開始

## 画像を blob にあげる

C# 組である小林さんにスライドを託し、すべてのスライドの画像をとって blob にあげてもらうよう依頼。多分これが作業の大半を占める大役。ありがとう。

## LINE ボット部分

同時並行でボットの開発。ボットは Microsoft Bot Service の Echo Bot (オウム返しボット) テンプレートを利用。まず blob のアドレスとぺージ番号を Dictionary に格納。

```csharp
public class CacheService 
{
    public Dictionary<int, string> Pages = new Dictionary<int, string>();
    public int CurrentPage = 1;

    public CacheService()
    {
        for (int i = 1; i < 53; i++)
        {
            Pages.Add(i, $"https://linedd19f3.blob.core.windows.net/slides/linedd19-{i.ToString().PadLeft(2,'0')}.png");
        }
    }
}
```

Startup.cs で IoC。

```csharp:Startup.cs
services.AddSingleton<CacheService, CacheService>();
```

既定では送ったメッセージをそのまま返すようになっているが、画像を返すように変更。現在のページから画像の URL を取るだけの非常に簡単なもの。

```csharp
public class EchoBot : ActivityHandler
{
    CacheService cache;
    public EchoBot(ConversationState conversationState, UserState userState, CacheService cache)
    {
        this.cache = cache;
    }

    protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
    {
        var page = cache.Pages[cache.CurrentPage];
        await turnContext.SendActivityAsync(
            MessageFactory.Attachment(new Attachment("image/png", page)),
            cancellationToken);
    } 
}
```

## ページ番号更新部分

ボットと同じ Web API で現在の番号を更新するだけの API を定義。GET でページ番号を送るだけの簡単仕様で乗り切る。

```csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.BotBuilderSamples.Services;
using System.Threading.Tasks;

namespace Microsoft.BotBuilderSamples.Controllers
{
    [Route("api/pages")]
    [ApiController]
    public class PagesController : ControllerBase
    {
        private CacheService cacheService;
   
        public PagesController(CacheService cacheService)
        {
            this.cacheService = cacheService;
        }

        [HttpGet]
        public async Task SetPageAsync(int id)
        {
            cacheService.CurrentPage = id;
        }
    }
}
```
## パワーポイントアドイン

Office アドインと VSTO を迷ったが、リッチクライアントであったことから VSTO を選択。スライドショーのページ変更イベントを実装しただけ。なにも考えていない最小限コードで乗り切る。

```csharp
using System.Net.Http;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;

namespace LDD19
{
    public partial class ThisAddIn
    {
        private HttpClient client;
        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
            client = new HttpClient();
            this.Application.SlideShowNextSlide += Application_SlideShowNextSlide;
        }

        private void Application_SlideShowNextSlide(PowerPoint.SlideShowWindow Wn)
        {
            var r = client.GetAsync($"http://<サーバー名>/api/pages?id={Wn.View.Slide.SlideNumber}").Result;
        }
        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
        }

        #region VSTO で生成されたコード

        /// <summary>
        /// デザイナーのサポートに必要なメソッドです。
        /// このメソッドの内容をコード エディターで変更しないでください。
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        
        #endregion
    }
}
```
## リッチメニュー登録

極力簡単にボットに要求を送れるよう、リッチメニューを小林さんが作成。ボット的には何を送ってもいいのだが、「現在のスライド」というメッセージを送れるようになる。

# 18:40 ローカルデバッグ完了

ngrok を使って構成した状態ですべて動作することを確認。※画像はさっき取りました。
リッチメニューの画像かわいい。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/567ba790-04fd-2999-2d9e-57019b714a59.png"/>

# 18:45 問題発生

自信満々にいざサーバーにすべてを上げたところ、いきなり動かない。デバッグした結果、PowerPoint アドインから WebAPI にメッセージを送れない。結局 19 時からの懇親会参加のため一旦断念。

# 21:20 問題回避 (解決ではない)

返りの電車でなんとなく CORS かなと思ったため Web API 側で CORS 対応するもいまいち。残念だがあきらめて https -> http にして逃げる。orz

# Day 2 - 8:30

早朝リハのため 8:30 から会場にチェックイン。ボットの動作も OK で一安心。

# 10:40 2 日目キーノート

2 日目のキーノートも非常に興味深い。学びが多くて感銘を受ける。LINE っていい会社だな。

# 12:00 セッション

キーノートの後は立花さんの [つながろういつでも、何とでも。LINE Thingsからはじまるモノと人との新しいコミュニケーション](https://linedevday.linecorp.com/jp/2019/sessions/F2-1) を満喫。濃い内容だった。LINE Things もっと使ってみよう。

# 12:40 英語対応開始

ランチタイムに、「そうだ、英語のスライドも送れるようにしよう」と思い立ち再び盟友小林さんをゲートボックスばりに召喚。資料の英語化を開始。

# 12:55 作業開始

小林さんにスライドを送りまた画像を blob にあげてもらう。また自分はボットの改修を開始。同時平行作業だ。

## ボットの改修

必要な機能を洗い出し。

- ユーザーごとに言語指定できるようにする
- 言語によって適切なスライドを返す

うん、ほぼ Bot Framework の機能でできるな。さすが。

# 13:30

ほぼ実装は終わり動作も Bot Emulator で確認。完璧。

# 13:45 また問題発生

サーバーにデプロイするも、LINE からテストするとうまく動作しない。なんでだ？となって頑張ってデバッグ。どうやら一部の機能が Bot Framework と LINE の間で動作しない模様。。。なんてこった。

# 13:55 ロールバック

セッションが 14:30 からであったため、14:00 にはリリース判定をすると決めていました。よってこのタイミングで断念を決定し、ロールバック。すべて Azure DevOps でソース管理、リリース管理をしていたため、正しいバージョンに一発でロールバック成功。

※そう、DevOps は大事なのです。セッションもこの話だった。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/linedevday?src=hash&amp;ref_src=twsrc%5Etfw">#linedevday</a> Day2 F3 セッション資料はこちらです。 資料だけみても謎な罠ですが。<a href="https://t.co/SBdY3dkRrr">https://t.co/SBdY3dkRrr</a></p>&mdash; kenichiro nakamura@AKS (@kenakamu108) <a href="https://twitter.com/kenakamu108/status/1197686636483465216?ref_src=twsrc%5Etfw">November 22, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

# 本番！だがしかし

そして無事本番にのぞみ、スライドもボットで順調に取得できました！！と言いたかったのだが、そうは問屋がおろしてくれず、問題が再度発生。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">あれ？動かなくなっちゃった<a href="https://twitter.com/hashtag/linedevday?src=hash&amp;ref_src=twsrc%5Etfw">#linedevday</a> <a href="https://t.co/oD8JIzGYpW">pic.twitter.com/oD8JIzGYpW</a></p>&mdash; k-murakami (@MRKMKYMS39) <a href="https://twitter.com/MRKMKYMS39/status/1197390075161530368?ref_src=twsrc%5Etfw">November 21, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

セッション終わってからその話をきいて愕然としつつログを確認。プッシュの制限到達。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/8c89419c-47fe-05d9-a5cd-e5ee3b051a00.png)

# フィードバック完了!!

プッシュを避けたいためにユーザーからスライドを取ってもらうように設計したにも関わらずプッシュになった理由は、残念ながら Bot Framework の不具合でした。。。こんなことなら友達登録してくれた人全員にマルチキャストした方が良かったじゃん。

**でも大丈夫**

本日開発にしっかりフィードバックして修正してもらいました。次回のリリースで治ります。スピード対応素晴らしい。

# まとめ

今回かなり自分の思い付きのせいでかなりバタバタしましたが、いろいろ学びもありました。

- こんな短時間で色々ためして公開までできる、LINE API + Bot Framework の組み合わせは最高だ
- 不具合は悲しいけど、このクラウド時代、すぐ修正でるから嬉しい
- Visual Studio と公式ドキュメントあれば、初めてやる Office アドイン開発なども瞬殺できる
- 無茶ぶりに付き合ってくれる友人がいることが幸せ (←これが最も重要)

最後に素晴らしいイベントを企画、運営してくださった LINE の皆様と会場および運営関係者の方々に深く感謝いたします。ありがとうございました。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/4bafbdad-1a75-5911-c27e-4a75f6d3fa7f.jpeg"/>