iT邦幫忙

2022 iThome 鐵人賽

DAY 9
3

Day 9 實作 In-Mem 儲存系統(以及 Word Count 模組)

https://i.imgur.com/mbMqB1q.png

就跟模組一樣,我希望 Bot 的後端儲存系統也是可抽換的,這樣就可以在 SQL、NoSQL、檔案系統之間切換,而不用改動太多程式碼。

所以我們必須要先定義一個較高級別的抽象,回顧一下昨天我們對於儲存系統的期望。

基本上我們的儲存系統會有:

  • 以 guild 為單位的儲存
  • 以 channel 為單位的儲存
  • 模組全域的鍵值儲存

想了一下後,我把最後一個項目名稱中的模組刪掉了,因為也許我們會需要一些跨模組的資料,一個全域的鍵值儲存應該是比較適合的。

在遇到每個事件時,我們可以依據事件解析出一個 StoreContext,這個 StoreContext 包含了對相關資料惰性求值的函式:

interface StoreContext {
    guild<T extends Record<string, unknown> = Record<string, unknown>>(): Promise<T | undefined>;
    channel<T extends Record<string, unknown> = Record<string, unknown>>(): Promise<T | undefined>;
    global<T extends Record<string, unknown> = Record<string, unknown>>(
        key: string,
    ): Promise<T | undefined>;
}

好了,那我們還需要一個可以做出 StoreContext 的東西對吧?

interface Store {
    ctx(...args: ClientEvents[Events]): StoreContext;
}

這個 Store 就應該是個抽象的儲存系統,我不管它內部怎麼處理的,反正實作一個 ctx 函示來接受事件參數,然後回傳一個 StoreContext 就好。

MemStore

為了簡單證明一下這個概念,我們先實作一個 MemStore,這個 MemStore 就是把所有資料都存在記憶體中,而且不會持久化。

class MemStore implements Store {
    private guild_store = new Map<string, unknown>();
    private channel_store = new Map<string, unknown>();
    private global_store = new Map<string, unknown>();

    private proxy<T>(map: Map<string, unknown>, key: string): T {
        const data = (map.get(key) || {}) as Record<string, unknown>;

        return new Proxy(data, {
            set: (target, prop, value) => {
                if (typeof prop === "string") {
                    target[prop] = value;
                    map.set(key, target);
                    return true;
                } else {
                    return false;
                }
            },
            get: (target, prop) => {
                if (typeof prop === "string") {
                    return target[prop];
                } else {
                    return undefined;
                }
            },
        }) as T;
    }

    ctx(...args: ClientEvents[Events]): StoreContext {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const self = this;

        const context: StoreContext = {
            async guild() {
                return undefined;
            },
            async channel() {
                return undefined;
            },
            async global<T>(key: string) {
                return self.global_store.get(key) as T | undefined;
            },
        };

        for (const arg of args) {
            if (arg instanceof Message) {
                context.guild = async <T>() => {
                    if (!arg.guild?.id) {
                        return undefined;
                    }

                    return self.proxy<T>(self.guild_store, arg.guild.id);
                };
                context.channel = async <T>() => {
                    return self.proxy<T>(self.channel_store, arg.channel.id);
                };
            } else if (arg instanceof BaseInteraction) {
                // ...
            } else if (arg instanceof Guild) {
                // ...
            } else if (arg instanceof GuildMember) {
                // ...
            }
        }

        return context;
    }
}

在這裡,我用了一些 Map 來儲存資料,然後用 Proxy 來做到更新 Map 內部資料的方法。

注意:如果機器人關閉,所有資料都會消失。

實作完之後,我發現比較麻煩的部分是如何依照事件參數解析出應該拿什麼,這個部分應該跟儲存後端並沒有直接關係,應該會找時間再把這部分獨立出來處理。

Word Count 模組

既然我們有了一個儲存系統,那我們就可以實作一個模組來驗證一下它了。

這個模組很簡單,我們在收到任何訊息時,就把訊息內容的字數加到相應 guildchannelword_count 裡面。

然後在收到 !wc 時,就把 guildchannelword_count 回傳給使用者。

class WordCount extends BaseModule implements Module {
    name = "wordcount";

    async messageCreate(
        [message]: [Message],
        ctx: StoreContext,
        next: CallNextModule,
    ): Promise<void> {
        if (message.content.trim() === "!wc") {
            const guild = await ctx.guild<{ word_count?: number }>();
            const channel = await ctx.channel<{ word_count?: number }>();

            const wc_guild = guild ? guild.word_count || 0 : 0;
            const wc_channel = channel ? channel.word_count || 0 : 0;

            await message.reply(`Guild word count: ${wc_guild}\nChannel word count: ${wc_channel}`);
        } else {
            const guild = await ctx.guild<{ word_count?: number }>();
            const channel = await ctx.channel<{ word_count?: number }>();

            if (guild) {
                guild.word_count = (guild.word_count || 0) + message.content.length;
            }
            if (channel) {
                channel.word_count = (channel.word_count || 0) + message.content.length;
            }

            await next();
        }
    }
}

https://i.imgur.com/DLZahxK.png

看來是成功了呢!如果是在 DM 裡面,則 Guild Word Count 將為 0。


每日鐵人賽熱門 Top 10 (2022-09-24)

以 2022/09/23 20:00 ~ 2022/09/24 20:00 文章觀看數增加值排名

誤差: 1 小時

  1. +1479 [Day 1] 工具從來不是問題,知識才是力量 ! Scrum 該懂的二三事 !
    • 作者: Darwin Watterson
    • 系列:工具從來不是問題,知識才是力量 ! Microsoft 365 照樣玩 Scrum !
  2. +422 「全端挑戰」製作動態網站第一步從了解useState與它的用法開始
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  3. +394 「全端挑戰」Scss與React Component的動態實作Navbar與Header
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  4. +368 「全端挑戰」熱門產品排行製作、了解react-router-dom、props與 ? : 的搭配
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  5. +361 「全端挑戰」使用useState製作彈跳視窗、製作Calendar與各種互動介面
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  6. +354 「全端挑戰」學習Mern全端開發概念與動態網站開發流程懶人包
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  7. +353 「全端挑戰」React props、Array.map應用與feature資訊主體設置
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  8. +339 Day 1 - 前言與內容大綱
    • 作者: Terry L.
    • 系列:重造會 Slide 的輪子!深入 JavaScript、CSS 模組化設計
  9. +307 「全端挑戰」了解Scss與React Component與首頁概念圖與UI實作
    • 作者: Ko
    • 系列:自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術
  10. +250 智慧屋展示
    • 作者: joulongleu
    • 系列:地圖物聯

Wow,突然有個 Scrum 文章冒出來了!


上一篇
Day 8 對所有事件標準化以及簡易儲存系統
下一篇
Day 10 MongoDB 儲存後端
系列文
Discord Bot with TypeScript: Framework, Database, and Modules30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言