BOTの作り方
Eternity の BOT は、あなたが書いたプログラムで動きます。Python の公式ライブラリ eternity-py を使うと、コマンド・カード型の発言・ボタン・選択メニュー・入力フォームまで、短いコードで作れます。
1. はじめる
BOT を作って、トークンをもらう
BOT は 開発者ポータル(Eternity にログインしたまま開けます)で作ります。アプリの「BOTを作る」からも開けます。
- 開発者ポータルで「新しいアプリ」を押して、名前を決める
- 「BOT」でトークンを表示して、コピーする
- 「コマンド」で、「/」で出すコマンドと入力欄を画面で作れます(プログラムから登録してもかまいません)
- 「招待と導入」で、動かしたいコミュニティに入れる
トークンはパスワードと同じです。人に見せたり、GitHub に上げたりしないでください。漏れたときは「作り直す」で新しくできます。
ライブラリを入れる
Python 3.9 以降が必要です。
pip install https://eternity-help.web.app/library/eternity-py-2.11.0.tar.gz
いちばん小さい BOT
import eternity
bot = eternity.Bot("ここにトークン")
@bot.event("ready")
def ready():
print("動きはじめました")
@bot.event("message")
def on_message(msg):
if msg.content == "ping":
msg.reply("pong")
bot.run()
保存して python bot.py で動きます。止めるときは Ctrl + C。切れても自動でつなぎ直します。
2. スラッシュコマンド
コマンドを登録すると、チャットの「/」ボタンから選べるようになります。引数には型を付けられ、型に合った入力欄(メンバーの一覧、ロールの一覧など)が出ます。値はサーバーで確かめられてから届くので、そのまま使えます。
@bot.command("あいさつ", "あいさつします", options=[
eternity.Option("相手", "あいさつする人", type="user", required=True),
eternity.Option("回数", "くり返す回数", type="integer"),
eternity.Option("色", "カードの色", choices=[
eternity.Choice("赤", "red"), eternity.Choice("青", "blue"),
]),
])
def greet(ctx):
count = ctx.options.get("回数", 1)
ctx.respond("こんにちは!" * count)
| type | 入力欄 | 届く値 |
|---|---|---|
string | 文字 | 文字列 |
integer / number | 数 | 整数 / 小数 |
boolean | はい/いいえ | True / False |
user | メンバーの一覧 | ユーザーID |
role | ロールの一覧 | ロールID |
room | 部屋の一覧 | 部屋ID |
ctx は Interaction です。ctx.user_id・ctx.user_name・ctx.room_id・ctx.space_id・ctx.options が使えます。
コマンドへの返事は15分以内にしてください。BOT のプログラムが動いていないと、「オフラインです」と使った人に表示されます。
右クリックで使うコマンド
発言やメンバーを右クリック(スマホは長押し)したときの「アプリ」に、BOT のコマンドを出せます(Discord のアプリのコマンドと同じ)。選ばれた発言・メンバーは ctx.target_message・ctx.target_user に入って届きます。
@bot.message_command("ブックマークに送る")
def save(ctx):
m = ctx.target_message # {"id", "content", "author", "user_id", "created_at"}
ctx.respond(f"{m['author']} さんの発言を保存しました", ephemeral=True)
@bot.user_command("あいさつする")
def hello(ctx):
ctx.respond(f"{ctx.target_user['name']} さん、こんにちは!")
打っている途中で候補を出す(autocomplete)
選択肢が多くて決めておけないときは、autocomplete=True にすると、使う人が打つたびに BOT が候補を出せます(25個まで)。
CITIES = ["東京", "大阪", "名古屋", "札幌", "福岡", "那覇"]
@bot.command("天気", "町の天気", options=[eternity.Option("町", autocomplete=True, required=True)])
def weather(ctx):
ctx.respond(f"{ctx.options['町']} は晴れです")
@bot.autocomplete("天気", "町")
def cities(ctx):
# ctx.focused_value:いま打っている途中の文字。ctx.options:ほかの欄に入っている値
return [c for c in CITIES if ctx.focused_value in c] # ("見える名前", 値) の並びでもよい
候補は1秒ほどで返してください(HTTP で受ける BOT は3秒まで)。候補にない値も送れるので、コマンドの中でも確かめてください。
時間がかかるとき(考えています…)
返事を作るのに時間がかかる(外のサイトに聞きに行くなど)ときは、先に ctx.defer() を呼ぶと、使った人に「考えています…」が出ます。あとで ctx.respond() すると、それが消えて返事が出ます(15分まで)。
@bot.command("要約", "長い文をまとめる")
def summary(ctx):
ctx.defer()
text = slow_work() # 時間のかかる処理
ctx.respond(text)
HTTP で受ける BOT は、HTTP の返事を3秒以内に返す必要があります。時間のかかる処理は defer() のあと別のスレッドで動かし、HttpBot(secret, token=...) にトークンを渡して返事を送ってください。
使える人を決める
permission= に権限を書くと、その権限がある人だけが使えて、ほかの人の「/」の一覧には出ません(管理用のコマンドに)。
@bot.command("片づけ", "発言をまとめて消す", permission="manage_messages")
def clean(ctx): ...
続けて使えないようにする(クールダウン)
cooldown= に秒数を書くと、同じ人はその秒数がたつまで使えません。止めるのはサーバーなので、連打されても BOT には届きません(2.8 から)。
@bot.command("ガチャ", "1回引く", cooldown=30)
def gacha(ctx): ...
BOT のひと言
bot.set_status("/help で使い方", "🤖") # メンバー一覧に出る
3. 発言とカード(Embed)
card = eternity.Embed(title="今日の天気", description="**晴れ** のち くもり", color=0xE07A3C)
card.add_field("気温", "24℃", inline=True)
card.add_field("降水確率", "10%", inline=True)
card.footer = "天気BOT"
bot.send(room_id, "おはようございます", embed=card)
| 項目 | 内容 |
|---|---|
title / url | 見出し(url を付けるとリンクになる) |
description | 本文。**太字** や `コード` が使える(4000文字まで) |
color | 左の線の色(0xRRGGBB) |
fields | 項目(25個まで)。inline=True で横に並ぶ |
image / thumbnail | 大きい画像 / 右の小さい画像(https の URL) |
author / footer | 上の小さな文字 / 下の小さな文字 |
返事・書きなおし・削除・リアクション
m = msg.reply("考え中…")
m.edit("できました!") # 自分(BOT)の発言だけ書きなおせる
m.react("👍")
m.delete()
4. ボタンと選択メニュー
bot.send(room_id, "どうする?", components=[
eternity.Button("参加する", id="join", style="primary"),
eternity.Button("見送る", id="skip"),
eternity.Button("くわしく", style="link", url="https://example.com"),
eternity.Select("game", [
eternity.SelectOption("マイクラ", "mc"),
eternity.SelectOption("APEX", "apex"),
], placeholder="やりたいゲーム"),
])
@bot.on_component("join")
def join(it):
it.respond(f"{it.user_name} さんが参加します")
@bot.on_component("game")
def game(it):
it.update(content=f"{it.values[0]} に決まりました", components=[]) # 元の発言を書きかえる
ボタンの style は primary(目立つ色)・secondary・success・danger・link(URL を開く)。ボタンは1行に5つ、行は5行まで。id の最後を * にすると、前が同じ id をまとめて受け取れます(例:@bot.on_component("vote:*"))。
本人にだけ見える返事
it.respond("あなたの番号は 7 です", ephemeral=True)
ephemeral の返事は、押した本人の画面にだけ出て、ほかの人には見えません。サーバーにも残りません。
5. 入力フォーム(Modal)
ボタンやコマンドへの返事として、入力フォームを出せます。送信されると on_modal に届きます。
@bot.on_component("apply")
def apply(it):
it.show_modal(eternity.Modal("apply-form", "参加の申しこみ", [
eternity.TextInput("name", "呼んでほしい名前"),
eternity.TextInput("why", "ひとこと", style="paragraph", required=False),
]))
@bot.on_modal("apply-form")
def submitted(it):
it.respond(f"{it.fields['name']} さん、受け付けました", ephemeral=True)
6. イベント
| 名前 | 届くもの | いつ |
|---|---|---|
ready | — | つながったとき |
message | Message | BOT が入っているコミュニティで発言があったとき(BOT 自身のものは除く) |
message_update | Message | 発言が書きなおされたとき |
message_delete | {"message_id", "room_id"} | 発言が消されたとき |
reaction | {"message_id", "room_id", "reactions"} | リアクションが変わったとき |
member_join / member_leave | {"space_id", "user"} | 人が入った・出たとき |
interaction | Interaction | ボタン・選択・コマンド・フォーム(全部) |
@bot.event("member_join")
def welcome(ev):
rooms = bot.rooms(ev["space_id"])
bot.send(rooms[0]["id"], f"{ev['user']['name']} さん、ようこそ!")
受け取る情報(インテント)
開発者ポータルの「BOT」で、BOT に届ける情報を選べます(Discord の Gateway Intents と同じ考え方)。
| 名前 | オフにすると |
|---|---|
| 発言の中身 | 発言のイベントは届くが、content が空になり、画像・カードも届かない。BOT あて(@BOTの名前・BOT への返信・「/」で始まるもの)は読める |
| メンバーの出入り | member_join・member_leave が届かない |
| オンライン状態 | だれがオンラインかが届かない |
コマンド・ボタン・フォームだけで動く BOT は、「発言の中身」をオフにしておくと、入れる人も安心できます。
7. REST API
ライブラリを使わずに、どの言語からでも使えます。すべて Authorization: Bearer トークン を付けて送ります。つなぎ先は https://app.connectia-zz.online。リアルタイムのイベントは WebSocket(wss://…/ws?token=トークン)で届きます。
| メソッド | パス | 内容 |
|---|---|---|
| GET | /api/v1/me | BOT 自身 |
| GET | /api/v1/spaces | 入っているコミュニティ |
| GET | /api/v1/spaces/{id}/rooms | 部屋の一覧 |
| GET | /api/v1/spaces/{id}/roles | ロールの一覧 |
| GET | /api/v1/spaces/{id}/members?limit=&after= | メンバー(100人ずつ) |
| PUT / DELETE | /api/v1/spaces/{id}/members/{user}/roles/{role} | ロールを付ける / 外す(「ロールを管理する」が要る)。?days=7 を付けると、7日たつと自動で外れる |
| GET | /api/v1/spaces/{id}/members/{user} | メンバーひとり(roles・permissions・timeout_until) |
| DELETE | /api/v1/spaces/{id}/members/{user} | コミュニティから外す(「メンバーを外す」が要る) |
| PUT / DELETE | /api/v1/spaces/{id}/bans/{user} | 出入り禁止 / 解除 {reason, delete_hours}(「出入り禁止」が要る) |
| POST | /api/v1/users/{user}/notify | ひとりにお知らせ(DM のかわり){content, embeds}。同じコミュニティの人だけ・相手が止めていないときだけ・1人に1分5件まで |
| PUT / DELETE | /api/v1/messages/{id}/pin | ピン留め / 外す(「発言を片づける」が要る) |
| GET | /api/v1/spaces/{id}/audit-logs?limit= | 操作ログ(「ログを見る」が要る) |
| PUT / DELETE | /api/v1/spaces/{id}/timeouts/{user} | タイムアウト / 解除 {minutes, reason}(「タイムアウト」が要る) |
| GET | /api/v1/rooms/{id}/messages?limit=&before= | 発言の履歴(100件まで) |
| POST | /api/v1/rooms/{id}/messages | 発言する {content, embeds, components, reply_to} |
| PATCH | /api/v1/messages/{id} | 自分の発言を書きなおす |
| DELETE | /api/v1/messages/{id} | 消す(人の発言は「発言を片づける」が要る) |
| PUT / DELETE | /api/v1/messages/{id}/reactions/{絵文字} | リアクション |
| POST | /api/v1/rooms/{id}/polls | 投票を出す {question, options, multi, anon, hours} |
| POST | /api/v1/rooms/{id}/threads | スレッドを作る {name, first, message_id} |
| POST | /api/v1/spaces/{id}/events | イベントを作る {title, starts_at, room_id, descr, post_room}(管理の権限が要る) |
| PUT | /api/v1/commands | コマンドの登録 {commands: [{name, description, options}]} |
| POST | /api/v1/interactions/{id}/respond | インタラクションへの返事 {type: message|update|modal, …} |
ライブラリでの書き方(2.1 から)
bot.poll(room_id, "次の集まりは?", ["土曜", "日曜"], hours=24)
bot.thread(room_id, "週末の予定", "ここで決めよう")
bot.create_event(space_id, "ゲーム大会", time.time() + 86400, room_id=voice_room_id)
bot.create_event(space_id, "週1の集まり", next_friday_21, repeat="weekly") # くり返し(2.8 から)
2.1.0 では bot.event(...) という名前でしたが、イベントを受け取る @bot.event とぶつかって動かなくなっていたため、2.2.0 で create_event に変えました。2.1.0 を使っている人は、2.2.0 に入れなおしてください。
管理する BOT(2.6 から)
@bot.command("黙らせる", "10分のタイムアウト", options=[eternity.Option("相手", type="user", required=True)],
permission="timeout")
def mute(ctx):
bot.timeout(ctx.space_id, ctx.options["相手"], minutes=10, reason=f"{ctx.user_name} さんが実行")
ctx.respond("タイムアウトにしました", ephemeral=True)
# 使った人の権限は ctx.permissions・ctx.has_permission("kick") で確かめられます
bot.kick(space_id, user_id)
bot.ban(space_id, user_id, reason="荒らし", delete_hours=24)
bot.member(space_id, user_id) # {"roles": [...], "permissions": [...], "timeout_until": ...}
人と同じ決まりを通ります:BOT にその権限が要り、BOT のいちばん上のロールより下の人にしか使えません。作成者・運営には使えません。1分に30回まで。
メンバー・ロール・部屋から選ぶメニュー(2.9 から)
bot.send(room_id, "配るロールを選んでください", components=[
eternity.Select("pick", source="role", max=3, placeholder="ロールを選ぶ"), # "user" / "role" / "room"
])
@bot.on_component("pick")
def picked(ctx):
ctx.respond(f"選ばれたロールの id:{ctx.values}", ephemeral=True)
選択肢は、見ている人の画面がそのコミュニティから出します。ほかのコミュニティのものは、サーバーが通しません。
components の形
[
{"type": "buttons", "items": [{"id": "ok", "label": "OK", "style": "primary"},
{"label": "サイト", "style": "link", "url": "https://example.com"}]},
{"type": "select", "id": "pick", "placeholder": "選ぶ", "min": 1, "max": 1,
"options": [{"label": "A", "value": "a", "description": "説明"}]}
]
8. HTTP で受ける(プログラムを動かし続けなくてよい)
コマンド・ボタン・フォームが使われたときだけ、決めたアドレスに HTTP で届けることもできます。無料のホスティングやサーバーレスでも動かせます(Discord の Interactions Endpoint と同じ考え方です)。WebSocket でつながっているときは、そちらが先です。
- 開発者ポータル →「HTTP で受ける」で、あて先(https://〜)を入れて「保存して確かめる」
- 保存するときに
{"type": "ping"}が届きます。{"type": "pong"}を返すと保存されます - 届く中身には署名がつきます。「署名の鍵」で必ず確かめてください
署名の確かめ方
X-Eternity-Timestamp: 送った時刻(秒)
X-Eternity-Signature: HMAC-SHA256(署名の鍵, 時刻 + "." + 中身) の16進
import hmac, hashlib, time
def verified(secret, headers, body: bytes) -> bool:
ts = headers.get("X-Eternity-Timestamp", "")
sig = headers.get("X-Eternity-Signature", "")
good = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(good, sig) and abs(time.time() - int(ts or 0)) <= 300
返す中身
3秒以内に JSON で返してください。
{"type": "message", "content": "返事", "embeds": [...], "components": [...], "ephemeral": false}
{"type": "update", "content": "ボタンの付いた発言を書きかえる"}
{"type": "modal", "title": "フォーム", "custom_id": "form1", "fields": [{"id": "q", "label": "質問"}]}
{"type": "ack"} ← あとで POST /api/v1/interactions/{id}/respond で返す(15分まで)
ライブラリで書く(HttpBot)
eternity-py 2.2.0 からは、署名の確かめや返事の形をライブラリがやってくれます。
import eternity
bot = eternity.HttpBot("署名の鍵")
@bot.command("さいころ", "さいころを振ります")
def dice(ctx):
import random
ctx.respond(f"{random.randint(1, 6)} が出ました")
bot.serve(port=8080) # Flask・gunicorn などに入れるときは bot.wsgi を渡す
ポータルの「HTTP で受ける」には、ライブラリを使わずに Python だけで動くサンプルもあります。
9. 招待リンク(ほかのコミュニティに入れてもらう)
- 開発者ポータル → アプリを選ぶ →「招待と導入」
- 「BOT」で「だれでも招待できるようにする」をオンにする(オフなら自分だけが使える)
- BOT に渡す権限を選んで、招待リンクをコピーする
リンクを開いた人は、自分が「BOTを管理する」権限を持っているコミュニティを選んで BOT を入れられます。BOT には、選んだ権限を持つ「BOT の名前」のロールが付きます(入れる人が持っている権限だけ渡せます)。
ウェブフック(プログラムなしで、部屋に書く)
部屋の歯車 →「ウェブフック」で URL を作ると、その URL に JSON を送るだけで部屋に書けます。GitHub・IFTTT・監視ツールなど、Discord のウェブフックに送れるものは、たいていそのまま使えます。
curl -X POST -H "Content-Type: application/json" -d '{"content": "ビルドが終わりました", "username": "CI",
"embeds": [{"title": "main に push", "description": "3 commits", "color": 5814783}]}' ここにウェブフックの URL
受けられるもの:content(2,000字まで)・username(その発言だけ名前を変える)・embeds(5枚まで。footer.text・image.url など Discord の形も OK)。1分に30回まで送れます。URL は人に見せないでください。
GitHub の通知を受ける
ウェブフックのアドレスの最後に /github を付けて、GitHub のリポジトリの Settings → Webhooks の「Payload URL」に入れます(Content type はどちらでも)。push・プルリクエスト・Issue・コメント・リリース・スター・フォーク・Actions の結果を、カードにして部屋に出します。
https://app.connectia-zz.online/api/webhooks/番号/トークン/github
10. BOTを探す(みんなに使ってもらう)
開発者ポータルの「BOTを探すに出す」をオンにすると、Eternity を使っているみんなが「BOTを探す」から見つけて、自分のコミュニティに入れられるようになります。だれでも招待できる設定と、短い説明が必要です。アイコン・くわしい説明・タグ・カテゴリーを入れておくと見つけてもらいやすくなります。
11. 決まりと制限
- 発言・書きなおしは 10秒に30回まで、リアクションは 10秒に20回まで。こえると 429 が返ります(ライブラリは少し待ってやり直します)
- インタラクションへの返事は15分以内
- カードは1つの発言に5枚まで、ボタンは1行5つ・5行まで、選択メニューの選択肢は25個まで
- BOT も、部屋の権限(発言する・画像を貼るなど)とタイムアウトを守ります
- 作れる BOT は1人10個まで
- BOTの決まりを守ってください