Skip to main content

Clip Logger Bot Module

A bot module that uses regex pattern matching to detect Twitch clip URLs in chat, stores them in the extension database, and provides a !clips command to display recent clips.

Project structure

clip-logger/
├── 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": "Clip Logger",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["twitch"],
"server": true,
"permissions": ["chat:send", "chat:read"],
"triggers": {
"commands": [
{
"name": "clips",
"description": "Show the most recent clips shared in chat",
"cooldown_global": 10,
"cooldown_user": 30,
"min_role": "everyone"
},
{
"name": "clipcount",
"description": "Show total number of clips logged",
"cooldown_global": 5,
"cooldown_user": 15,
"min_role": "everyone"
}
],
"patterns": [
"https?://clips\\.twitch\\.tv/\\S+",
"https?://www\\.twitch\\.tv/\\S+/clip/\\S+"
]
},
"config_schema": [
{
"key": "maxClipsShown",
"type": "number",
"label": "Number of clips to show with !clips",
"default_value": 5,
"required": true
},
{
"key": "announceClips",
"type": "boolean",
"label": "Announce when a clip is detected",
"default_value": true,
"required": true
}
]
}

server/schema.ts

import { defineSchema, defineTable, column } from "@zaflun/lumio-sdk/server";

export default defineSchema({
clips: defineTable({
url: column.string(),
shared_by: column.string(),
shared_by_name: column.string(),
platform: column.string(),
timestamp: column.number(),
}),
});

server/functions.ts

import { command, pattern } from "@zaflun/lumio-sdk/server";

// Pattern handler — fires when a Twitch clip URL is detected
export const clipDetector = pattern(
"https?://clips\\.twitch\\.tv/\\S+",
async (ctx, message, matched) => {
// Check for duplicate (same URL in the last hour)
const recentClips = await ctx.db.list("clips");
const oneHourAgo = Date.now() - 3600000;
const isDuplicate = recentClips.some(
(clip) => clip.url === matched && (clip.timestamp as number) > oneHourAgo
);

if (isDuplicate) {
return null; // Silently skip duplicates
}

// Store the clip
ctx.defer(async () => {
await ctx.db.insert("clips", {
url: matched,
shared_by: ctx.user.id,
shared_by_name: ctx.user.displayName,
platform: ctx.user.platform,
timestamp: Date.now(),
});
});

// Increment clip counter in cache
await ctx.cache.increment("clip_total", 1);

const announceClips = (ctx.config.announceClips as boolean) ?? true;
if (announceClips) {
return { reply: `Clip logged from ${ctx.user.displayName}!` };
}
return null;
}
);

// Also catch the alternate Twitch clip URL format
export const clipDetectorAlt = pattern(
"https?://www\\.twitch\\.tv/\\S+/clip/\\S+",
async (ctx, message, matched) => {
ctx.defer(async () => {
await ctx.db.insert("clips", {
url: matched,
shared_by: ctx.user.id,
shared_by_name: ctx.user.displayName,
platform: ctx.user.platform,
timestamp: Date.now(),
});
});

await ctx.cache.increment("clip_total", 1);

const announceClips = (ctx.config.announceClips as boolean) ?? true;
if (announceClips) {
return { reply: `Clip logged from ${ctx.user.displayName}!` };
}
return null;
}
);

// !clips — Show recent clips
export const clips = command("clips", async (ctx, args) => {
const maxShown = (ctx.config.maxClipsShown as number) ?? 5;
const allClips = await ctx.db.list("clips");

if (allClips.length === 0) {
return { reply: "No clips have been shared yet!" };
}

// Sort by timestamp descending and take the most recent
const recent = allClips
.sort((a, b) => (b.timestamp as number) - (a.timestamp as number))
.slice(0, maxShown);

const clipList = recent
.map((clip, i) => `${i + 1}. ${clip.url} (by ${clip.shared_by_name})`)
.join(" | ");

return { reply: `Recent clips: ${clipList}` };
});

// !clipcount — Show total clips logged
export const clipcount = command("clipcount", async (ctx, args) => {
const total = (await ctx.cache.get("clip_total")) as number | null;
if (total === null) {
// Cache miss — count from DB
const allClips = await ctx.db.list("clips");
await ctx.cache.set("clip_total", allClips.length, 3600);
return { reply: `Total clips logged: ${allClips.length}` };
}
return { reply: `Total clips logged: ${total}` };
});

Key concepts demonstrated

  • Regex pattern triggers — two patterns catch both Twitch clip URL formats
  • matched parameter — the handler receives the exact URL that matched the pattern
  • Duplicate detection — checks recent clips to avoid logging the same URL twice
  • Background persistencectx.defer() stores clips in the database after responding
  • Cache countersctx.cache.increment() for fast total clip counts
  • Config-driven announcements — the announceClips config lets users toggle clip detection messages