1. ヒーローズリーグについて
What's HL
Proto Pedia作品チラ見!
LINE APIを使った作品が盛りだくさん紹介!!
2. LINE APIを見てみよう!
Messaging API
LINE Bot を開発するならまずはこれです!
グループに追加されていれば、グループ内でメッセージを送信することも
Messaging APIの概要
https://developers.line.biz/ja/docs/messaging-api/overview/
ハンズオン!
以下を使用します。
LINE Developer console: https://developers.line.biz/ja/docs/line-developers-console/
Replit: https://replit.com/
Make: https://www.make.com/en
LINE Notify: https://notify-bot.line.me/ja/
Open AI: https://platform.openai.com/
とにもかくにも、まずはおうむ返しボットを作成してみます。
以下を参考に進めます。
色々なメッセージタイプ
https://developers.line.biz/ja/docs/messaging-api/message-types/
Messaging API リファレンス
https://developers.line.biz/ja/reference/messaging-api/
公式SDK
https://developers.line.biz/ja/docs/messaging-api/line-bot-sdk/
実装サンプル(Node.js)
https://github.com/line/line-bot-sdk-nodejs
Flex Message Simulator
https://developers.line.biz/flex-simulator/
スタンプ定義
https://developers.line.biz/ja/docs/messaging-api/sticker-list/
Replit
ボットをホストするサーバー を準備とありますが、ここでは Replit
を使います。
ハッカソンなどでも役立ちそうです!
アカウントを作成後、node.js プロジェクトを新規に作成し、以下のサンプルコードをまるっとコピペします。
無料プランの場合はプロジェクトがpublicになるので、アクセスキーなどの取り扱いにはご注意ください。
今回使うサンプルコード
クリック
参考: https://developers.line.biz/ja/docs/messaging-api/line-bot-sdk/
'use strict';
const line = require('@line/bot-sdk');
const express = require('express');
const OpenAI = require("openai");
const axios = require('axios');
// create LINE SDK config from env variables
const config = {
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.LINE_CHANNEL_SECRET,
};
const IMG_URL = 'https://mashandroom.org/wp-content/uploads/2017/04/logo-320x320.png';
// ユーザーとGPTのメッセージを合わせて最大6つ(ユーザー3、GPT3)
const MAX_HISTORY = 6;
const userMessages = {};
// create LINE SDK client
const client = new line.Client(config);
// create Express app
// about Express itself: https://expressjs.com/
const app = express();
// register a webhook handler with middleware
// about the middleware, please refer to doc
app.post('/callback', line.middleware(config), (req, res) => {
Promise
.all(req.body.events.map(handleEvent))
// .all(req.body.events.map(handleEventWithGPT))
// .all(req.body.events.map(handleEventWithGPTHistory))
.then((result) => res.status(200).json(result))
.catch((err) => {
console.error(err);
res.status(500).end();
});
});
// event handler
function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
// ignore non-text-message event
return Promise.resolve(null);
}
if (event.message.text === "キノコ") {
return notify();
}
const messageHandlers = {
// おうむ返し+スタンプ
'スタンプ': createEchoWithStamp,
// おうむ返し+画像
'画像': createEchoWithImage,
// おうむ返し+Flex Message
'フレックス1': createFlex1,
'フレックス2': createFlex2,
"フレックス3": createFlex3
};
const handler = messageHandlers[event.message.text];
const reply = handler ? handler(event.message.text) : createEcho(event.message.text);
// use reply API
return client.replyMessage(event.replyToken, reply);
}
function createEcho(userMessage) {
return { type: 'text', text: userMessage };
}
function createEchoWithStamp(userMessage) {
return [
{ type: 'text', text: userMessage },
{ type: 'sticker', packageId: '446', stickerId: '1989' }
];
}
function createEchoWithImage(userMessage) {
return [
{ type: 'text', text: userMessage },
{ type: 'image', originalContentUrl: IMG_URL, previewImageUrl: IMG_URL }
];
}
function createFlex1(userMessage) {
return [
{ type: 'text', text: userMessage },
{
"type": "flex",
"altText": "this is a flex message",
"contents": {
"type": "bubble",
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "hello"
},
{
"type": "text",
"text": "world"
}
]
}
}
}];
}
function createFlex2(userMessage) {
return [
{ type: 'text', text: userMessage },
{
"type": "flex",
"altText": "this is a flex message",
"contents": {
"type": "bubble",
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "image",
"url": IMG_URL
},
{
"type": "separator"
},
{
"type": "text",
"text": "Text in the box"
},
{
"type": "box",
"layout": "vertical",
"contents": [],
"width": "30px",
"height": "30px",
"background": {
"type": "linearGradient",
"angle": "90deg",
"startColor": "#FFFF00",
"endColor": "#0080ff"
}
}
],
"height": "400px",
"justifyContent": "space-evenly",
"alignItems": "center"
}
}
}];
}
function createFlex3(userMessage) {
return [
{ type: 'text', text: userMessage },
{
"type": "flex",
"altText": "this is a flex message",
"contents": {
"type": "carousel",
"contents": [
{
"type": "bubble",
"size": "micro",
"hero": {
"type": "image",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip10.jpg",
"size": "full",
"aspectMode": "cover",
"aspectRatio": "320:213"
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "Brown Cafe",
"weight": "bold",
"size": "sm",
"wrap": true
},
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
},
{
"type": "text",
"text": "4.0",
"size": "xs",
"color": "#8c8c8c",
"margin": "md",
"flex": 0
}
]
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "box",
"layout": "baseline",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": "東京旅行",
"wrap": true,
"color": "#8c8c8c",
"size": "xs",
"flex": 5
}
]
}
]
}
],
"spacing": "sm",
"paddingAll": "13px"
}
},
{
"type": "bubble",
"size": "micro",
"hero": {
"type": "image",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip11.jpg",
"size": "full",
"aspectMode": "cover",
"aspectRatio": "320:213"
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "Brow&Cony's Restaurant",
"weight": "bold",
"size": "sm",
"wrap": true
},
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
},
{
"type": "text",
"text": "4.0",
"size": "sm",
"color": "#8c8c8c",
"margin": "md",
"flex": 0
}
]
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "box",
"layout": "baseline",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": "東京旅行",
"wrap": true,
"color": "#8c8c8c",
"size": "xs",
"flex": 5
}
]
}
]
}
],
"spacing": "sm",
"paddingAll": "13px"
}
},
{
"type": "bubble",
"size": "micro",
"hero": {
"type": "image",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip12.jpg",
"size": "full",
"aspectMode": "cover",
"aspectRatio": "320:213"
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "Tata",
"weight": "bold",
"size": "sm"
},
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "xs",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
},
{
"type": "text",
"text": "4.0",
"size": "sm",
"color": "#8c8c8c",
"margin": "md",
"flex": 0
}
]
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "box",
"layout": "baseline",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": "東京旅行",
"wrap": true,
"color": "#8c8c8c",
"size": "xs",
"flex": 5
}
]
}
]
}
],
"spacing": "sm",
"paddingAll": "13px"
}
}
]
}
}
];
}
// GhatGPT event handler
// !!!!SecretsにOPENAI_API_KEYを設定してください。
async function handleEventWithGPT(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
// ignore non-text-message event
return Promise.resolve(null);
}
const messages =
[
// systemメッセージ: gptの役割
// ChatGPTに役割を与えることで、精度を高められる
{ "role": "system", "content": "優秀な詩人です。" },
// userメッセージ: ユーザーの入力テキスト
{ "role": "user", "content": event.message.text }
// assistantメッセージ: gptの応答
// 応答を保存することで履歴に沿った文脈で会話ができる
]
const gptResponse = await getGptResponse(messages);
const assistantMessage = gptResponse.choices[0].message.content;
const reply = [
{
type: 'text',
text: assistantMessage
},
{
type: 'text',
text: createUsageMessage(gptResponse.usage)
}];
// use reply API
return client.replyMessage(event.replyToken, reply);
}
// GhatGPT event handler(会話履歴を保持)
// !!!!SecretsにOPENAI_API_KEYを設定してください。
async function handleEventWithGPTHistory(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
// ignore non-text-message event
return Promise.resolve(null);
}
const userId = event.source.userId;
// 初めてのユーザーの場合、データ構造を初期化
if (!userMessages[userId]) {
userMessages[userId] = [
{ "role": "system", "content": "優秀な詩人です。" }
];
}
// ユーザーのメッセージをデータ構造に追加
userMessages[userId].push({ "role": "user", "content": event.message.text });
const gptResponse = await getGptResponse(userMessages[userId]);
const assistantMessage = gptResponse.choices[0].message.content;
// GPTの応答をデータ構造に追加
userMessages[userId].push({
"role": "assistant",
"content": assistantMessage
});
// 履歴が最大数を超えた場合、古いメッセージを削除
if (userMessages[userId].length - 1 > MAX_HISTORY) {
// role以降先頭のuser, assistantのペアを削除
userMessages[userId].splice(1, 2);
}
const reply = [
{
type: 'text',
text: assistantMessage
},
{
type: 'text',
text: createUsageMessage(gptResponse.usage)
}];
// use reply API
return client.replyMessage(event.replyToken, reply);
}
async function getGptResponse(messages) {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const response = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: messages,
temperature: 0,
max_tokens: 150,
});
console.log(response);
console.log(response.choices[0].message);
return response;
}
function createUsageMessage(usage) {
return `prompt_tokens: ${usage.prompt_tokens}\ncompletion_tokens:${usage.prompt_tokens}\ntotal_tokens: ${usage.total_tokens}`;
}
async function notify() {
const message = '\n何か呼んだ?';
const options = {
method: 'post',
url: 'https://notify-api.line.me/api/notify',
headers: {
'Authorization': `Bearer ${process.env.LINE_NOTIFY_TOKEN}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: 'message=' + encodeURIComponent(message)
};
await axios(options);
}
// listen on port
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`listening on ${port}`);
});
Secretsに以下を設定

Replitで発行されたURLをWebhookに以下の通り設定

3. ChatGPTと連携
Chat Completion API
ライブラリ
モデル
レスポンス
Secretsに OPENAI_API_KEY
を追加
ソース修正
app.post('/callback', line.middleware(config), (req, res) => {
Promise
// .all(req.body.events.map(handleEvent)) // ← 削除 or コメントアウト
.all(req.body.events.map(handleEventWithGPT)) // ← 有効に
// .all(req.body.events.map(handleEventWithGPTHistory)) // ←3つの会話履歴を保持
.then((result) => res.status(200).json(result))
.catch((err) => {
console.error(err);
res.status(500).end();
});
});
4. LINE Notify
LINEと外部のサービスやアプリを連携して、LINE Notifyという公式アカウント(Bot)から通知を受け取れるサービス
Make x LINE Notify 3分クッキング
MAKE もハッカソンでは活躍しそうなのでLINE Notifyと連携させてみましょう!

ChatGPTも

プログラムからもNotify



LINE_NOTIFY_TOKEN を追加

if (event.message.text === "キノコ") {
return notify();
}
async function notify() {
const message = '\n何か呼んだ?';
const options = {
method: 'post',
url: 'https://notify-api.line.me/api/notify',
headers: {
'Authorization': `Bearer ${process.env.LINE_NOTIFY_TOKEN}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: 'message=' + encodeURIComponent(message)
};
await axios(options);
}
LIFF , LINE ログイン(参考)