Alfred WorkflowでSlack投稿いろいろ

# TL;DR

* Slackのあるチャンネルに任意の文字列を投稿する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/0a8a7992-e27b-80fa-4867-a78b777e8fbd.png)


 * 投稿するチャンネルを選ぶ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/3bf0c068-87f1-6124-fa6e-7f31c13d4688.png)

* 投稿する時間を選ぶ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/56504f9a-ddb7-1f81-2906-0916cb315945.png)


* あるチャンネルの特定のスレッドに返信する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/4a8634d7-c807-e545-9068-5bb637c249ba.png)

* 絵文字を自動でつける
![画面収録-2022-05-15-13.45.03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/dd62c9c7-cc2f-47ab-57b4-5e58250a0309.gif)

* 匿名で投稿する
![2022-07-09-14-46-14.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/0308a1b8-212c-6f58-ce22-f8aefa0d1cb9.gif)


# How to install

https://github.com/kentoak/slack_post_alfred/blob/main/slack_post.alfredworkflow
のView raw　をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/744af4e6-0e0b-2bce-57f3-70cf5ba5c5cc.png)

# Slack APIを準備
https://api.slack.com/apps/ でBotをつくる。

OAuth & Permissionsの`Bot Token Scopes`は以下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/4106ee9f-4287-cc56-9f71-ca65a3334840.png)
BotではなくWorkspace（自分）が投稿したいなら`User Token Scopes`の方で。

Reinstall to Workspace して、Tokenをメモ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/88f6e782-b7a2-05b2-14ee-95ec5ee31ac6.png)

メモしたトークンは、Sourceの{YOUR TOKEN}に直書きするか、[x]のところで設定する（後述）。

※追記 匿名投稿するには、`chat:write.customize` （https://api.slack.com/scopes/chat:write.customize） が必要

# Source

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/c7b56360-407e-f1c0-40b2-0c47124f9cbf.png)

## Slack チャンネル情報を取得する Script Filter
```
import alfy from 'alfy';
const api = {YOUR TOKEN}
const token = process.env.TOKEN || api
const fetch_all_channels_url = "https://slack.com/api/conversations.list";
var obj=[];
const message = alfy.input;

(async() => {
    try {
        const response = await alfy.fetch(
            fetch_all_channels_url,
            {
                method: "GET",
                headers: {
                    Authorization: `Bearer ${token}`,
                },
            }
        )
        for(let i=0; i<response.channels.length; i++) {
            if (response.channels[i].is_archived || response.channels[i].num_members == 0){
                continue
            }
            obj.push({
                title: response.channels[i].name,
                subtitle: response.channels[i].id,
                icon: {path: './img/main04.jpg'},
                arg: message+"%"+response.channels[i].id+"$"+response.channels[i].name
            });
        }
        alfy.output(obj);
    }
    catch {
        console.log("チャンネル情報取得に失敗しました!!!!")
    }
})();
```

## 投稿時間を指定する Script Filter

```
import alfy from 'alfy';

var obj = []
obj.push({
    title: "10秒後",
    subtitle: 10,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"10"
});
obj.push({
    title: "30秒後",
    subtitle: 30,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"30"
});
obj.push({
    title: "1分後",
    subtitle: 60,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"60"
});
obj.push({
    title: "3分後",
    subtitle: 180,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"180"
});
obj.push({
    title: "5分後",
    subtitle: 300,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"300"
});
obj.push({
    title: "10分後",
    subtitle: 600,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"600"
});
obj.push({
    title: "30分後",
    subtitle: 1800,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"1800"
});
obj.push({
    title: "60分後",
    subtitle: 3600,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"3600"
});
obj.push({
    title: "3時間後",
    subtitle: 10800,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"10800"
});
obj.push({
    title: "5時間後",
    subtitle: 3600*5,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"18000"
});
obj.push({
    title: "24時間後",
    subtitle: 3600*24,
    icon: {path: './img/main03.jpg'},
    arg: alfy.input+"&"+"86400"
});
alfy.output(obj);
```

## 指定したチャンネルに指定した時間に投稿する
使用法：alfredの検索窓で、`ttt （投稿内容）` enter `(チャンネルを選ぶ)` enter `（時刻を選ぶ）`

なぜか https://slack.com/api/chat.scheduleMessage を使うときに`curl`のヘッダー `'Content-Type: application/json; charset=utf-8'` と、`--data-binary` が必要らしく、`--data-binary`がalfyになさそう？なので必要なものを書き出して最後にRun Scriptでbashコマンドを実行することで対応した。

```
import alfy from 'alfy';
const api = {YOUR TOKEN}
const token = process.env.TOKEN || api

const mytext = alfy.input.substring(0,alfy.input.indexOf('%'));
const channel = alfy.input.substring(alfy.input.indexOf('%')+1,alfy.input.indexOf('$'));
const channelName = alfy.input.substring(alfy.input.indexOf('$')+1,alfy.input.indexOf('&'));
const timeStamp = Number(alfy.input.substring(alfy.input.indexOf('&')+1,));

(async() => {
    try {
        const now0 = new Date()
        const targetDate0 = new Date(now0.getFullYear(), now0.getMonth(), now0.getDate(), now0.getHours(), now0.getMinutes(), now0.getSeconds());
        const targetUnixTimestanp0 = Math.floor(targetDate0.getTime() / 1000)+timeStamp
        var obj = {"channel":channel,"post_at":targetUnixTimestanp0,"text":mytext}
        console.log(obj)
    }
    catch {
        console.log("投稿失敗しました.......")
    }
})();
```

Run Scriptにて。

```
curl -POST 'https://slack.com/api/chat.scheduleMessage' -H 'Authorization: Bearer ${TOKEN}' -H 'Content-Type: application/json; charset=utf-8' --data-binary "$1"
```

ここの`TOKEN` は Workflowの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/8974ef59-eec7-e591-f264-cc771ed6df08.png)
[x]のところで設定するか、直に書き換える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/ab0e08ee-c445-cb35-ecae-40c1477a5f00.png)

## 特定の文字列が含まれる投稿に返信する
使用法：alfredの検索窓で、`sl （返信内容）` を打つ。

reminderという投稿が今日されていたとき、その最新の投稿に、日報のようなものを投稿する。（参考 https://zenn.dev/tomsan96/articles/0f96954f831f97 ）

```
import alfy from 'alfy';

const api = {YOUR TOKEN}
const token = process.env.TOKEN || api
const channel = process.env.CHANNEL || {YOUR CHANNEL};
const word = process.env.WORD || "reminder";
const mytext = process.argv[2];
const name = process.env.URL || "from Taoka ";

const now = new Date()
// 当日0時をターゲット日時に設定(ミリ秒)
const targetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);//100件以降はキャッチしない
//const targetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 23, 0);//new Date(year, month[, day, hour, minutes, seconds, milliseconds]);

// Unix時間(秒単位)に変換
const targetUnixTimestanp = Math.floor(targetDate.getTime() / 1000)

// oldestパラメータ:メッセージ履歴の取得開始時期を指定
const getHistoryUrl = `https://slack.com/api/conversations.history?channel=${channel}&oldest=${targetUnixTimestanp}`;
const postUrl = "https://slack.com/api/chat.postMessage";
var tsList=[];

(async() => {
    try {
        // 該当チャンネル内のその日に投稿されるメッセージ履歴を取得する(api: conversations.history)
        const response = await alfy.fetch(
            getHistoryUrl,
            {
                headers: {
                    Authorization: `Bearer ${token}`
                },
            }
        )
	    // 取得したメッセージのうち、文言の一致を条件に日報報告用のメッセージと判定する
        for(let i=0; i<response.messages.length; i++) {
            if (response.messages[i].text==word){
                tsList.push(response.messages[i].ts)
            }
        }
        await alfy.fetch(
            postUrl,
            {
                method: "POST",
                headers: {
                    Authorization: `Bearer ${token}`
                },
                json: {
                    channel: channel,
                    thread_ts: tsList[0],
                    text: `日報です${name}`+"\n"+mytext
                }
            }
        )
        console.log("%sを#apiに投稿成功!!!",`日報です${name}`+mytext)
    }
    catch {
        console.log("投稿失敗しました.....")
    }

})();
```

## emojiリアクションする
tsが必要。今回は、"remainder"と投稿された時刻。

```
var reaction_emojis = [
    "thinking_face",
    "thumbsup",
    "gorilla",
    "grin",
    "kissing_smiling_eyes",
    "sweat_smile",
    "blue_heart",
    "point_right",
    "princess",
    "microbe", 
    "parrot", 
    "pleading_face", 
    "turkey", 
    "cook", 
    "woman_in_lotus_position", 
    "merperson", 
    "peanuts", 
    "broccoli"
];
for(let i=0; i<reaction_emojis.length; i++) {
    var myurl = `https://slack.com/api/reactions.add?channel=${channel}&timestamp=${tsList[0]}&name=${reaction_emojis[i]}`;
    await alfy.fetch(
        myurl,{
            method: "POST",
            headers:  {
                Authorization : `Bearer ${token}`
            }
        }
    )
}
```

参考（GASでやったやつ）

https://github.com/kentoak/slack_emoji

c.f. 絵文字一覧はこれ
https://qiita.com/yamadashy/items/ae673f2bae8f1525b6af

## 匿名で投稿する
使用法：alfredの検索窓で、`tokumei （名前） （投稿内容）` を打つ。

アイコンはランダムで選ばれる。
```
import alfy from 'alfy';
var emojis = [
    "thinking_face",
    "thumbsup",
    "gorilla",
    "grin",
    "kissing_smiling_eyes",
    "sweat_smile",
    "blue_heart",
    "point_right",
    "princess",
    "microbe", 
    "parrot", 
    "pleading_face", 
    "turkey", 
    "cook", 
    "woman_in_lotus_position", 
    "merperson", 
    "peanuts", 
    "broccoli",
    "sunglasses"
];

var config = {
  slack: {
    token: {YOUR TOKEN},
    iconEmoji: emojis[Math.floor(Math.random()*emojis.length+1)],
  },
  channel: {YOUR CHANNEL},
};

var input = process.argv[2];
var name = input.split(" ")[0];
var text = input.split(" ")[1];
if (input.split(" ")[2]==1){
    text = fujiwarify(text);
}

// console.log("name is ",name)
// console.log("text is ",text)
// console.log(config.slack.token)
// console.log(config.channel)
// console.log(config.slack.iconEmoji)

var url = 'https://slack.com/api/chat.postMessage';
var payload = {
    channel: config.channel,
    text: text,
    username: makeHandle(name),
    // attachments: [{
    //     text: '```' + text + '```',
    //     color: "good",
    //     mrkdwn_in: 'text'
    // }],
    icon_emoji: config.slack.iconEmoji
};

(async() => {
    try {
        await alfy.fetch(
            url,
            {
                method: "POST",
                headers: {
                    Authorization: `Bearer ${config.slack.token}`
                },
                json: payload
            }
        )
        console.log("投稿成功！")
    } catch {
        console.log("投稿失敗しました!!!!")
    }
})();


function stringToArray (str) {
  return str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g) || [];
}

function fujiwarify(s) {
  s = s.replace(/(ください|下さい|ます|です|たい|う|た|よ)([。！!]|$)/g, function (s, a, b) {
    if (a === 'よ') {
      return a + 'ぉぉぉぉ' + b;
    } else {
      return a + 'よぉぉぉぉ' + b;
    }
  });
  var chars = stringToArray(s);
  var result = '';
  chars.forEach(function (c) {
    result += c + '゛';
  });
  return result;
}

function makeHandle (nickname) {
  return nickname.replace(/◆/g, '◇')
    .replace(/(#[\x20-\x7e]{1,8})$/, function (s, a) {
      var salt = 'long long text here.'
      var sha1 = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_1, salt + a);
      var sha1text = Utilities.base64Encode(sha1);
      //console.log('◆' + sha1text.substring(0,10))
      return '◆' + sha1text.substring(0,10);
    });
}
```
スニペットで、`tokumei （名前） （投稿内容） （1）` とすれば、投稿内容が藤原竜也風になる。

参考： https://techblog.securesky-tech.com/entry/2020/01/31/


# おわり
今回はAlfred Workflowを使ってみた。JavaScriptでcurlコマンドを使うために、`alfy`なる（Alfred workflow を作るための）JavaScript フレームワークを使用した。 Pythonで書いたほうが扱いやすいかも。