BOTの作り方

Eternity の BOT は、あなたが書いたプログラムで動きます。Python の公式ライブラリ eternity-py を使うと、コマンド・カード型の発言・ボタン・選択メニュー・入力フォームまで、短いコードで作れます。

1. はじめる

BOT を作って、トークンをもらう

BOT は 開発者ポータル(Eternity にログインしたまま開けます)で作ります。アプリの「BOTを作る」からも開けます。

  1. 開発者ポータルで「新しいアプリ」を押して、名前を決める
  2. 「BOT」でトークンを表示して、コピーする
  3. 「コマンド」で、「/」で出すコマンドと入力欄を画面で作れます(プログラムから登録してもかまいません)
  4. 「招待と導入」で、動かしたいコミュニティに入れる

トークンはパスワードと同じです。人に見せたり、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 の返事は、押した本人の画面にだけ出て、ほかの人には見えません。サーバーにも残りません。

ボタンやコマンドへの返事として、入力フォームを出せます。送信されると 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—つながったとき
messageMessageBOT が入っているコミュニティで発言があったとき(BOT 自身のものは除く)
message_updateMessage発言が書きなおされたとき
message_delete{"message_id", "room_id"}発言が消されたとき
reaction{"message_id", "room_id", "reactions"}リアクションが変わったとき
member_join / member_leave{"space_id", "user"}人が入った・出たとき
interactionInteractionボタン・選択・コマンド・フォーム(全部)
@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/meBOT 自身
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 でつながっているときは、そちらが先です。

  1. 開発者ポータル →「HTTP で受ける」で、あて先(https://〜)を入れて「保存して確かめる」
  2. 保存するときに {"type": "ping"} が届きます。{"type": "pong"} を返すと保存されます
  3. 届く中身には署名がつきます。「署名の鍵」で必ず確かめてください

署名の確かめ方

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. 招待リンク(ほかのコミュニティに入れてもらう)

  1. 開発者ポータル → アプリを選ぶ →「招待と導入」
  2. 「BOT」で「だれでも招待できるようにする」をオンにする(オフなら自分だけが使える)
  3. 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. 決まりと制限