YouTube Member Bot Module
A YouTube-only bot module that welcomes new members and handles Super Chat events. Demonstrates YouTube-specific event types and member-only commands.
Project structure
youtube-member-bot/
├── lumio.config.json
├── server/
│ ├── schema.ts
│ └── functions.ts
├── package.json
└── tsconfig.json
lumio.config.json
{
"$schema": "https://lumio.vision/schemas/lumio.config.schema.json",
"extension_id": "ext_placeholder",
"name": "YouTube Member Bot",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["youtube"],
"server": true,
"permissions": ["chat:send", "chat:read", "events:read"],
"triggers": {
"commands": [
{
"name": "memberinfo",
"description": "Show your membership info",
"cooldown_global": 5,
"cooldown_user": 15,
"min_role": "subscriber"
},
{
"name": "topdonors",
"description": "Show top Super Chat donors",
"cooldown_global": 15,
"cooldown_user": 60,
"min_role": "everyone"
}
],
"events": [
"youtube:member",
"youtube:superchat",
"youtube:supersticker",
"youtube:gift_membership"
]
},
"config_schema": [
{
"key": "welcomeMessage",
"type": "string",
"label": "Member welcome message template",
"default_value": "Welcome to the crew, {name}!",
"required": true
},
{
"key": "superChatThreshold",
"type": "number",
"label": "Minimum Super Chat amount for special thank-you (USD cents)",
"default_value": 500,
"required": true
},
{
"key": "topDonorsCount",
"type": "number",
"label": "Number of donors to show in !topdonors",
"default_value": 5,
"required": true
}
]
}
server/schema.ts
import { defineSchema, defineTable, column } from "@zaflun/lumio-sdk/server";
export default defineSchema({
members: defineTable({
user_id: column.string(),
user_name: column.string(),
joined_at: column.number(),
membership_months: column.number(),
}),
donations: defineTable({
user_id: column.string(),
user_name: column.string(),
amount_cents: column.number(),
currency: column.string(),
message: column.string(),
type: column.string(),
timestamp: column.number(),
}),
});
server/functions.ts
import { command, event } from "@zaflun/lumio-sdk/server";
// Event handler — new YouTube member
export const memberWelcome = event("youtube:member", async (ctx, evt) => {
const userName = evt.user_name as string;
const months = (evt.membership_months as number) ?? 1;
// Store member record in background
ctx.defer(async () => {
const existing = await ctx.db.get("members", evt.user_id as string);
if (existing) {
await ctx.db.patch("members", evt.user_id as string, {
membership_months: months,
});
} else {
await ctx.db.insert("members", {
user_id: evt.user_id,
user_name: userName,
joined_at: Date.now(),
membership_months: months,
});
}
});
// Build welcome message from config template
const template = (ctx.config.welcomeMessage as string) ?? "Welcome, {name}!";
const message = template
.replace("{name}", userName)
.replace("{months}", String(months));
if (months > 1) {
return { reply: `${message} ${months} months strong!` };
}
return { reply: message };
});
// Event handler — Super Chat
export const superChatHandler = event("youtube:superchat", async (ctx, evt) => {
const userName = evt.user_name as string;
const amountCents = (evt.amount_micros as number ?? 0) / 10000; // micros to cents
const currency = (evt.currency as string) ?? "USD";
const chatMessage = (evt.message as string) ?? "";
const threshold = (ctx.config.superChatThreshold as number) ?? 500;
// Store donation in background
ctx.defer(async () => {
await ctx.db.insert("donations", {
user_id: evt.user_id,
user_name: userName,
amount_cents: amountCents,
currency,
message: chatMessage,
type: "superchat",
timestamp: Date.now(),
});
// Update cached leaderboard
const currentTotal = ((await ctx.cache.get(`donor:${evt.user_id}`)) as number) ?? 0;
await ctx.cache.set(`donor:${evt.user_id}`, currentTotal + amountCents, 86400);
});
const amountFormatted = (amountCents / 100).toFixed(2);
if (amountCents >= threshold) {
return {
reply: `HUGE Super Chat from ${userName}: $${amountFormatted} ${currency}! Thank you so much!`,
};
}
return {
reply: `Thank you ${userName} for the $${amountFormatted} ${currency} Super Chat!`,
};
});
// Event handler — Super Sticker
export const superStickerHandler = event("youtube:supersticker", async (ctx, evt) => {
const userName = evt.user_name as string;
const amountCents = (evt.amount_micros as number ?? 0) / 10000;
const currency = (evt.currency as string) ?? "USD";
ctx.defer(async () => {
await ctx.db.insert("donations", {
user_id: evt.user_id,
user_name: userName,
amount_cents: amountCents,
currency,
message: "",
type: "supersticker",
timestamp: Date.now(),
});
});
const amountFormatted = (amountCents / 100).toFixed(2);
return {
reply: `${userName} sent a Super Sticker ($${amountFormatted} ${currency})! Thank you!`,
};
});
// Event handler — Gift Membership
export const giftMembershipHandler = event("youtube:gift_membership", async (ctx, evt) => {
const userName = evt.user_name as string;
const giftCount = (evt.gift_count as number) ?? 1;
ctx.defer(async () => {
await ctx.db.insert("donations", {
user_id: evt.user_id,
user_name: userName,
amount_cents: 0,
currency: "",
message: `Gifted ${giftCount} membership(s)`,
type: "gift_membership",
timestamp: Date.now(),
});
});
if (giftCount > 1) {
return { reply: `${userName} just gifted ${giftCount} memberships! Incredible!` };
}
return { reply: `${userName} gifted a membership! Thank you!` };
});
// !memberinfo — Show membership info (members only)
export const memberinfo = command("memberinfo", async (ctx, args) => {
const member = await ctx.db.get("members", ctx.user.id);
if (!member) {
return { reply: `${ctx.user.name}, no membership record found.` };
}
const months = member.membership_months as number;
const joinedDate = new Date(member.joined_at as number).toLocaleDateString();
return {
reply: `${ctx.user.displayName}: Member since ${joinedDate}, ${months} month(s)`,
};
});
// !topdonors — Show top Super Chat donors
export const topdonors = command("topdonors", async (ctx, args) => {
const count = (ctx.config.topDonorsCount as number) ?? 5;
const allDonations = await ctx.db.list("donations");
if (allDonations.length === 0) {
return { reply: "No donations recorded yet!" };
}
// Aggregate totals by user
const totals: Record<string, { name: string; total: number }> = {};
for (const d of allDonations) {
const uid = d.user_id as string;
if (!totals[uid]) {
totals[uid] = { name: d.user_name as string, total: 0 };
}
totals[uid].total += (d.amount_cents as number) ?? 0;
}
// Sort and take top N
const sorted = Object.values(totals)
.filter((t) => t.total > 0)
.sort((a, b) => b.total - a.total)
.slice(0, count);
if (sorted.length === 0) {
return { reply: "No monetary donations recorded yet!" };
}
const list = sorted
.map((t, i) => `${i + 1}. ${t.name}: $${(t.total / 100).toFixed(2)}`)
.join(" | ");
return { reply: `Top donors: ${list}` };
});
Key concepts demonstrated
- YouTube-specific events —
youtube:member,youtube:superchat,youtube:supersticker,youtube:gift_membership - Single-platform module —
platforms: ["youtube"]restricts this to YouTube - Config templates — welcome message uses
\{name\}and\{months\}placeholders replaced at runtime - Member-only commands —
!memberinfousesmin_role: "subscriber"to restrict access to members - Background data persistence — all event data is stored via
ctx.defer()to keep sync handlers fast - Aggregated leaderboard —
!topdonorsaggregates donation data from the database