Skip to main content

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 eventsyoutube:member, youtube:superchat, youtube:supersticker, youtube:gift_membership
  • Single-platform moduleplatforms: ["youtube"] restricts this to YouTube
  • Config templates — welcome message uses \{name\} and \{months\} placeholders replaced at runtime
  • Member-only commands!memberinfo uses min_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!topdonors aggregates donation data from the database