就跟模組一樣,我希望 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
就是把所有資料都存在記憶體中,而且不會持久化。
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 內部資料的方法。
注意:如果機器人關閉,所有資料都會消失。
實作完之後,我發現比較麻煩的部分是如何依照事件參數解析出應該拿什麼,這個部分應該跟儲存後端並沒有直接關係,應該會找時間再把這部分獨立出來處理。
既然我們有了一個儲存系統,那我們就可以實作一個模組來驗證一下它了。
這個模組很簡單,我們在收到任何訊息時,就把訊息內容的字數加到相應 guild
及 channel
的 word_count
裡面。
然後在收到 !wc
時,就把 guild
及 channel
的 word_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();
}
}
}
看來是成功了呢!如果是在 DM 裡面,則 Guild Word Count 將為 0。
以 2022/09/23 20:00 ~ 2022/09/24 20:00 文章觀看數增加值排名
誤差: 1 小時
+1479
[Day 1] 工具從來不是問題,知識才是力量 ! Scrum 該懂的二三事 !
+422
「全端挑戰」製作動態網站第一步從了解useState與它的用法開始
+394
「全端挑戰」Scss與React Component的動態實作Navbar與Header
+368
「全端挑戰」熱門產品排行製作、了解react-router-dom、props與 ? :
的搭配
+361
「全端挑戰」使用useState製作彈跳視窗、製作Calendar與各種互動介面
+354
「全端挑戰」學習Mern全端開發概念與動態網站開發流程懶人包
+353
「全端挑戰」React props、Array.map應用與feature資訊主體設置
+339
Day 1 - 前言與內容大綱
+307
「全端挑戰」了解Scss與React Component與首頁概念圖與UI實作
+250
智慧屋展示
Wow,突然有個 Scrum 文章冒出來了!