Skip to content

モジュールを最初から最後まで

Notes モジュールを作ります。メモを保存し、一覧を出し、1件削除する。 ストレージ、引数、エイリアス、フィルタ、翻訳 — 実用的なモジュールに必要なものは ほぼ揃います。

1. 生成する

.newmod Notes

2. 保存の形

メモはチャットごとなので、チャット ID をキーにしたマップ1つに、メモをリストで 持たせます。

go
package modules

import (
	"fmt"
	"strconv"
	"strings"

	"goroku/goroku"
	"goroku/goroku/utils"
)

type Notes struct {
	goroku.Base
}

func (m *Notes) Name() string { return "Notes" }

// notesFor は1つのチャットに保存されたメモを返します。
func (m *Notes) notesFor(chatID int64) []string {
	all := m.DB.GetStringMapStringSlice(m.Name(), "notes", nil)
	if all == nil {
		return nil
	}
	return all[strconv.FormatInt(chatID, 10)]
}

// saveNotes は1つのチャットのメモを置き換えます。
func (m *Notes) saveNotes(chatID int64, notes []string) error {
	all := m.DB.GetStringMapStringSlice(m.Name(), "notes", nil)
	if all == nil {
		all = map[string][]string{}
	}
	all[strconv.FormatInt(chatID, 10)] = notes
	if err := m.DB.SetStringMapStringSlice(m.Name(), "notes", all); err != nil {
		return fmt.Errorf("メモの保存: %w", err)
	}
	return nil
}

ゲッターはコピーを返すので、読んで、変えて、書き戻します。

3. 文言

go
func (m *Notes) Strings() map[string]string {
	return map[string]string{
		"name":            "Notes",
		"_cls_doc":        "チャットごとのメモ",
		"_cmd_doc_note":   "[本文] — メモを保存、またはメッセージに返信",
		"_cmd_doc_notes":  "保存したメモを一覧表示",
		"_cmd_doc_delnote": "[番号] — メモを削除",

		"saved":     "📝 <b>#{n} として保存しました</b>",
		"empty":     "📭 <b>メモはまだありません</b>",
		"no_text":   "❌ <b>テキストを送るか、メッセージに返信してください</b>",
		"bad_index": "❌ <b>メモ番号を指定してください(.notes を参照)</b>",
		"deleted":   "🗑 <b>#{n} を削除しました</b>",
	}
}

4. コマンド

go
func (m *Notes) Commands() map[string]goroku.CommandHandler {
	return map[string]goroku.CommandHandler{
		"note":    m.NoteCmd,
		"notes":   m.NotesCmd,
		"delnote": m.DelNoteCmd,
	}
}

func (m *Notes) CommandMetas() map[string]goroku.CommandMeta {
	return map[string]goroku.CommandMeta{
		"note":    {Aliases: []string{"n"}, OnlyOwner: true},
		"notes":   {OnlyOwner: true},
		"delnote": {Aliases: []string{"dn"}, OnlyOwner: true},
	}
}

所有者専用にします。自分のストレージを触るコマンドだからです。

5. 保存

go
func (m *Notes) NoteCmd(msg *goroku.Message) error {
	text := msg.ArgsOrReply()
	if text == "" {
		return msg.Answer(m.T("no_text", "❌ <b>テキストを送るか、メッセージに返信してください</b>"))
	}

	notes := append(m.notesFor(msg.ChatID), text)
	if err := m.saveNotes(msg.ChatID, notes); err != nil {
		return err
	}

	tpl := m.T("saved", "📝 <b>#{n} として保存しました</b>")
	return msg.Answer(strings.Replace(tpl, "{n}", strconv.Itoa(len(notes)), 1))
}

ArgsOrReply のおかげで .note 牛乳を買う も、メッセージへの返信としての .note も、追加の手間なく動きます。

6. 一覧

go
func (m *Notes) NotesCmd(msg *goroku.Message) error {
	notes := m.notesFor(msg.ChatID)
	if len(notes) == 0 {
		return msg.Answer(m.T("empty", "📭 <b>メモはまだありません</b>"))
	}

	var b strings.Builder
	b.WriteString("📝 <b>メモ</b>\n\n")
	for i, note := range notes {
		fmt.Fprintf(&b, "%d. %s\n", i+1, utils.EscapeHTML(note))
	}
	return msg.Answer(b.String())
}

EscapeHTML が重要です。< を含むメモがあると、無ければメッセージが壊れます。

7. 削除

go
func (m *Notes) DelNoteCmd(msg *goroku.Message) error {
	notes := m.notesFor(msg.ChatID)
	if len(notes) == 0 {
		return msg.Answer(m.T("empty", "📭 <b>メモはまだありません</b>"))
	}

	n, err := strconv.Atoi(msg.Arg(0))
	if err != nil || n < 1 || n > len(notes) {
		return msg.Answer(m.T("bad_index", "❌ <b>メモ番号を指定してください(.notes を参照)</b>"))
	}

	notes = append(notes[:n-1], notes[n:]...)
	if err := m.saveNotes(msg.ChatID, notes); err != nil {
		return err
	}

	tpl := m.T("deleted", "🗑 <b>#{n} を削除しました</b>")
	return msg.Answer(strings.Replace(tpl, "{n}", strconv.Itoa(n), 1))
}

msg.Arg(0) は範囲外でも安全です。引数なしの .delnote"" になり、Atoi がそれを弾いて、利用者には分かりやすい文言が返ります。

8. 導入

ファイルを送り、返信して .loadmod。あとは:

.note 牛乳を買う
.note      (何かへの返信として)
.notes
.delnote 1

ここで扱ったこと

機能場所
読み・変更・書き戻しのストレージnotesFor / saveNotes
引数、または返信先のテキストArgsOrReply
範囲外でも安全な引数Arg(0)
エイリアスと所有者専用CommandMetas
既定値つきの翻訳可能な文言m.T
利用者データの HTML エスケープutils.EscapeHTML
エラーの文脈付けfmt.Errorf("...: %w", err)

次へ

Released under the GNU AGPL v3 License.