slackAppで絵文字ジェネレータっぽいのを作る(Python&Gyazo&slack-bolt)

# slackAppで絵文字ジェネレータっぽいのを作る(Python&Gyazo&slack-bolt)

## 目次

<!-- TOC -->

- [slackAppで絵文字ジェネレータっぽいのを作るPython&Gyazo&slack-bolt](#slackapp%E3%81%A7%E7%B5%B5%E6%96%87%E5%AD%97%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%A3%E3%81%BD%E3%81%84%E3%81%AE%E3%82%92%E4%BD%9C%E3%82%8Bpythongyazoslack-bolt)
    - [目次](#%E7%9B%AE%E6%AC%A1)
    - [概要](#%E6%A6%82%E8%A6%81)
        - [目標](#%E7%9B%AE%E6%A8%99)
        - [使うもの](#%E4%BD%BF%E3%81%86%E3%82%82%E3%81%AE)
        - [構成](#%E6%A7%8B%E6%88%90)
    - [絵文字画像を生成する](#%E7%B5%B5%E6%96%87%E5%AD%97%E7%94%BB%E5%83%8F%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)
        - [まず該当コード](#%E3%81%BE%E3%81%9A%E8%A9%B2%E5%BD%93%E3%82%B3%E3%83%BC%E3%83%89)
        - [詳細](#%E8%A9%B3%E7%B4%B0)
            - [補足](#%E8%A3%9C%E8%B6%B3)
    - [生成した絵文字画像をgyazoに送り、そのリンクを取得する](#%E7%94%9F%E6%88%90%E3%81%97%E3%81%9F%E7%B5%B5%E6%96%87%E5%AD%97%E7%94%BB%E5%83%8F%E3%82%92gyazo%E3%81%AB%E9%80%81%E3%82%8A%E3%81%9D%E3%81%AE%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)
        - [まずは該当コード](#%E3%81%BE%E3%81%9A%E3%81%AF%E8%A9%B2%E5%BD%93%E3%82%B3%E3%83%BC%E3%83%89)
        - [詳細](#%E8%A9%B3%E7%B4%B0)
    - [アプリを立ち上げslack上でコマンドの受信、gyazoリンクの送信を行う](#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E7%AB%8B%E3%81%A1%E4%B8%8A%E3%81%92slack%E4%B8%8A%E3%81%A7%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AE%E5%8F%97%E4%BF%A1gyazo%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%AE%E9%80%81%E4%BF%A1%E3%82%92%E8%A1%8C%E3%81%86)
        - [まずは該当コード](#%E3%81%BE%E3%81%9A%E3%81%AF%E8%A9%B2%E5%BD%93%E3%82%B3%E3%83%BC%E3%83%89)
        - [詳細](#%E8%A9%B3%E7%B4%B0)
            - [補足](#%E8%A3%9C%E8%B6%B3)
    - [参考](#%E5%8F%82%E8%80%83)

<!-- /TOC -->

## 概要

### 目標
salckでテキストを送ったらその絵文字画像を生成してほしい

- 完成例

![image (33).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652143/a18bd6b9-ecc2-b504-6e6c-8c1052bb39d3.png)


### 使うもの

- [pillow(pythonのライブラリ)](https://pillow.readthedocs.io/en/stable/index.html)
    - 絵文字画像生成を担当
- [gyazo-API](https://gyazo.com/api/docs)
    - 生成した絵文字画像をslackに共有する
- [slack_bolt](https://slack.dev/bolt-python/concepts#socket-mode)
    - slack上のメッセ―ジを受信、送信する
    
### 構成

- 絵文字画像を生成するemojicreate.py
- 生成した絵文字画像をgyazoに送り、そのリンクを取得するusegyazo.py
- アプリを立ち上げslack上でコマンドの受信、gyazoリンクの送信を行うapp.py
からなる

## 絵文字画像を生成する

### まず該当コード


``` python:emojicreate.py

gitfrom PIL import Image, ImageDraw, ImageFont
import math
import traceback

# テキストの文字数が自身の平方数以下となる平方根nを見つける（10文字ならn=4）


def create(text):
    row_text = text
    print(row_text)
    n = math.ceil(math.sqrt(len(row_text)))

    print(n)

    # nだけテキストを分割し、文字列末尾に\nを入れる
    textlist = []
    i = 0
    while i < n:
        textlist_element = row_text[i * n:(i * n) + n]
        if textlist_element == "":
            break
        textlist.append(textlist_element)
        i += 1

    print(textlist)

    newlined_text = ""
    for j in textlist:
        newlined_text += j + "\n"
        print(newlined_text)
    newlined_text = newlined_text[:-1]

    # テキストの矩形最大幅がimage.size[0]*img_fraction を上回るまでフォントサイズを挙げていく
    out = Image.new("RGB", (256, 256), (256, 256, 256))
    d = ImageDraw.Draw(out)

    fontsize = 1
    img_fraction = 0.9
    cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)
    cur_size = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
                                    anchor="mm", font=cur_fnt, spacing=5) 
                                    # 矩形サイズ取得

    while out.size[0] * img_fraction > (cur_size[2] - cur_size[0]) and out.size[0] * img_fraction > (cur_size[3] - cur_size[1]):
        fontsize += 5
        cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)

        cur_size = textbbox = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
                                                   anchor="mm", font=cur_fnt, spacing=5)

    if fontsize <= 5:
        fontsize = 6
    fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize-5)
    print(cur_size)
    print(out.size)
    # # アンカーポイントは画像の中心、アンカーはmmにして描画

    textbbox = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
                                    anchor="mm", font=fnt, spacing=5)
    print(textbbox)

    d.multiline_text(xy=(128, 128), text=newlined_text,
                     anchor="mm", font=fnt, spacing=5, fill="black")

    out.save("sample.png")

```

### 詳細

絵文字画像が正方形なので、文字も正方形に収まる必要がある。
具体的には、mの長さの文字列が与えられた時、(n-1)^2 < m <= n^2　を満たす
n行n列の空間に文字を収めたい（この空間をテキストボックスと呼ぶことにする）。
コードでは `n = math.ceil(math.sqrt(len(row_text)))` でnを求め
改行コードを適切に入れることでn行n列のテキストボックスに納めている


- イメージ

ありがとう　
↓

|    | 1 | 2| 3|
| - | - | - | -|
| 1   |あ | り  |が|
| 2    | と| う  ||
| 3    | |   ||


``` python

gitfrom PIL import Image, ImageDraw, ImageFont
import math
import traceback

# テキストの文字数が自身の平方数以下となる平方根nを見つける（10文字ならn=4）


def create(text):
    row_text = text
    print(row_text)
    n = math.ceil(math.sqrt(len(row_text)))

    print(n)

    # nだけテキストを分割し、文字列末尾に\nを入れる
    textlist = []
    i = 0
    while i < n:
        textlist_element = row_text[i * n:(i * n) + n]
        if textlist_element == "":
            break
        textlist.append(textlist_element)
        i += 1

    print(textlist)

    newlined_text = ""
    for j in textlist:
     newlined_text += j + "\n"
        print(newlined_text)
    newlined_text = newlined_text[:-1]

```

Image.new()でベースとなる背景画像のImageオブジェクトを作成
更にImageオブジェクトを引数に指定して書き込みのためのDrawオブジェクトを生成。

```python
    out = Image.new("RGB", (256, 256), (256, 256, 256))
    d = ImageDraw.Draw(out)
```

テキストボックスに文字列を収められたので、次はフォントサイズをベース画像に合うよう調整したい。
ImageDrawモジュールの`multiline_textbbox`関数は
テキスト、フォント、フォントサイズ、テキストの（ベース画像上での）位置を指定するとテキストボックスの（ベース画像上での）左上の頂点と右下の頂点の座標を返してくれる
` 
    cur_size = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
                                    anchor="mm", font=cur_fnt, spacing=5) `
#### 補足
テキストの位置というのはアンカーとアンカーポイントで決まる。
アンカーというのが自分には馴染みがなかったけど
簡単に言うとテキストの位置を決める基準のようなもの。
アンカーポイントというのを座標(x1,y1)に指定し、アンカーをmmという二文字で指定したとする。
するとx=x1にテキストの水平中央、y=y1にテキストの垂直中央がくるようにテキストの位置が決定される。
アンカーは 2文字の文字列で指定される。最初の文字は水平配置で、2番目の文字は垂直方向の配置を意味している。mmだとmiddle&middleということになる。
アンカーには他にも色々あるけど[公式](https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html)の図が分かりやすい
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652143/c6982d19-3f18-aa0f-766c-a45fd6148c76.png)

因みにさっきの`multiline_textbbox`関数の引数`xy`と`anchor`はそれぞれアンカーポイントとアンカーに対応している

テキストボックスの幅と高さが分かったら、幅と高さがベース画像からはみ出ない程度にフォントサイズを調整していく。
フォントサイズは1からはじめ、whileを回す

```python
    # テキストの矩形最大幅がimage.size[0]*img_fraction を上回るまでフォントサイズを挙げていく
    
    fontsize = 1
    img_fraction = 0.9
    cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)
    cur_size = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
                                    anchor="mm", font=cur_fnt, spacing=5) 
                                    # 矩形サイズ取得

    while out.size[0] * img_fraction > (cur_size[2] - cur_size[0]) and out.size[0] * img_fraction > (cur_size[3] - cur_size[1]):
        # テキストボックスの幅、高さがベース画像の辺の長さを越えるまでfontsizeを5ずつプラス
        fontsize += 5
        cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)

        cur_size = textbbox = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
                                                   anchor="mm", font=cur_fnt, spacing=5)

    if fontsize <= 5:
        fontsize = 6
    fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize-5)
    # # アンカーポイントは画像の中心、アンカーはmmにして描画
    d.multiline_text(xy=(128, 128), text=newlined_text,
                     anchor="mm", font=fnt, spacing=5, fill="black")

```

最後に保存して終了
```python
    out.save("sample.png")
```
## 生成した絵文字画像をgyazoに送り、そのリンクを取得する
### まずは該当コード

```python:usegyazo.py
import requests as req
import traceback


class usegyazo:

    def upload(self):
        try:
            with open('sample.png', 'br') as f:
                f_bi = f.read()
                XLSX_MIMETYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
                files = {'imagedata': ("sample.png", f_bi, XLSX_MIMETYPE)}
                headers = {
                    "access_token": アクセストークン}
                res = req.post(
                    url="https://upload.gyazo.com/api/upload", files=files, data=headers)

            if 'json' in res.headers.get('content-type'):
                result = res.json()
                print(result)
                return(result["permalink_url"])
            else:
                result = res.text

                
        except Exception as err:
            print(err)
            print(traceback.format_exc())

```

### 詳細
gyazoAPIでは`https://upload.gyazo.com/api/upload`に
POSTすることで画像をアップロードすることができる。
必須のパラメータはaccess_token(string)とimagedata(binary)。レスポンスとして画像のidやアップロード日時、共有用のurl(permalink_url)が返ってくる。
今回はslackに画像を共有したいのでpermalink_urlを得る。
注意点はmultipart/form-data を使うこと。imagedataに画像のバイナリを渡しただけでは不正とみなされる

## アプリを立ち上げslack上でコマンドの受信、gyazoリンクの送信を行う

### まずは該当コード

```python:app.py
import requests as req
import emojicreate as ec
import logging
import traceback
import usegyazo as ga
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

slackBotToken = ボットトークン
slackAppToken = アプリトークン
app = App(token=slackBotToken)

ga = ga.usegyazo()

# "emoji "というメッセージをコマンドとしてハンドリングしている
@app.message("emoji ")
def handle_message_events(message, say):
    try:
        messagelist = message["text"].split(" ")
        if messagelist[0] == "emoji":
            try:
                content = message["text"][6:]
                if content == "" or content == " " or content == "　":
                    say("単語を指定してください")
            except IndexError as err:
                say("単語を指定してください")
            text = content
            # 絵文字画像を作る
            say(text)
            ec.create(text=text)
            # gyazoにアップロード＆リンク取得
            emoji_url = ga.upload()
            # slackに投稿
            say(emoji_url)

    except Exception as err:
        print(err)
        print(traceback.format_exc())


@app.error
def custom_error_handler(error, body, logger):
    logger.exception(f"Error: {error}")
    logger.info(f"Request body: {body}")


if __name__ == "__main__":
    handler = SocketModeHandler(app, slackAppToken)
    handler.start()
```

### 詳細

`app.message(特定の文字列)`を使うと特定の文字列が含まれるメッセージをハンドリングしてくれる。
ここでは　`emoji[半角スペース]絵文字用テキスト`　をコマンドにしたいので`@app.message("emoji ")`としている。
メッセージの詳細(tsとかtextとかの情報が辞書で構造化されている)を取得出来たら必要な情報を抜きだして`say(メッセージ内容)`で応答する

#### 補足
slackAPIには数種類あるけど今回使ったのはwebsocketを用いたSocketModeに分類される。
websocketを用いる前世代のAPIであるRTMよりかは権限がちゃんと管理できるようになっていて、
HTTPのエンドポイントを用意する方法よりかはセキュリティ的に安全とのこと
[公式](https://api.slack.com/apis/connections/socket
)

## 結果
最後にapp.pyを走らせると無事絵文字ジェネレータっぽいbotが稼働するようになる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652143/e85295a6-a2f8-406a-b30d-2732dbc20e86.png)
長文もいける

## 参考

[Intro to Socket Mode](https://api.slack.com/apis/connections/socket)
[Slack ソケットモードの最も簡単な始め方](https://qiita.com/seratch/items/1a460c08c3e245b56441)
[Using Socket Mode](https://slack.dev/bolt-python/concepts#socket-mode)

[Text anchors(Pillow公式)](https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html)
[ImageDrawモジュール(Pillow公式)](https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html)

[Gyazo API](https://gyazo.com/api/docs/image)

[コトバンク-「観光」](https://kotobank.jp/word/%E8%A6%B3%E5%85%89-469729)