Custom Context
Extend your bot with type-safe, domain-specific context types.
Custom contexts allow you to extend the base Context class with your own properties and methods. This enables you to create domain-specific functionality, inject dependencies, and maintain type safety throughout your bot.
Why Use Custom Context?
Custom contexts provide several key benefits:
- Type Safety: Leverage Dart's type system to catch errors at compile time
- Dependency Injection: Pass services like databases, loggers, or configuration to your handlers
- Code Organization: Keep related logic together in context methods
- Reusability: Use mixins to share functionality across different bot implementations
- Domain-Specific Logic: Add methods that make sense for your specific use case
Basic Custom Context
To create a custom context, extend the Context class and add your own methods and properties.
Defining Your Context
Your custom context must call the super constructor with update, api, and me parameters.
import 'package:televerse/televerse.dart';
// Define your custom context
class MyContext extends Context {
MyContext(super.update, super.api, super.me);
// Add custom methods
void doSomething() {
print('Doing something custom!');
print('Hello from MyContext! 👋');
}
// Add custom getters
String get userName => from?.firstName ?? 'Guest';
bool get isAdmin => from?.id == 123456789;
}Using Your Custom Context
Pass your context constructor to the Bot via the contextFactory parameter. The generic type parameter tells the bot what type of context to expect.
void main() async {
// Create bot with custom context type
final bot = Bot<MyContext>(
'YOUR_BOT_TOKEN',
contextFactory: MyContext.new,
);
// Handlers receive MyContext instances
bot.command('start', (ctx) async {
// ctx is MyContext, not just Context
await ctx.reply('Hello, ${ctx.userName}!');
// Call custom methods
ctx.doSomething();
});
await bot.start();
}Type Safety: Notice how ctx is typed as MyContext in the handler. This means you get full IDE autocomplete and compile-time type checking for all your custom methods!
Context with Dependencies
For more complex bots, you'll want to inject services like databases, loggers, or configuration into your context.
Defining Context with Dependencies
class MyContext extends Context {
final DatabaseService db;
final Logger logger;
MyContext(
super.update,
super.api,
super.me,
this.db,
this.logger,
);
// Helper method using injected dependencies
Future<User?> getUserFromDb() async {
if (from == null) return null;
return await db.getUser(from!.id);
}
}Creating a Factory Function
Since your context now has additional parameters, you need to create a factory function that captures these dependencies and returns a function matching the ContextFactory signature.
void main() async {
// Set up dependencies
final db = DatabaseService();
final logger = Logger();
// Create factory function that captures dependencies
MyContext createContext(Update update, RawAPI api, BotInfo me) {
return MyContext(update, api, me, db, logger);
}
// Pass factory to bot
final bot = Bot<MyContext>(
'YOUR_BOT_TOKEN',
contextFactory: createContext,
);
bot.command('profile', (ctx) async {
// Access injected dependencies
ctx.logger.info('Profile command called');
final user = await ctx.getUserFromDb();
if (user != null) {
await ctx.reply('Your profile: ${user.name}');
}
});
await bot.start();
}ContextFactory Type: The contextFactory parameter expects a function with this signature:CTX Function(Update update, RawAPI api, BotInfo me)
Using Mixins for Modularity
Dart's mixin system allows you to compose functionality from multiple sources. This is perfect for adding reusable features to your context.
Defining Mixins
Create mixins for common functionality like internationalization, logging, or session management.
// Define a mixin for internationalization
mixin I18nMixin {
String translate(String key, {String locale = 'en'}) {
final translations = {
'en': {
'hello': 'Hello',
'goodbye': 'Goodbye',
},
'es': {
'hello': 'Hola',
'goodbye': 'Adiós',
},
};
return translations[locale]?[key] ?? key;
}
}
// Define a mixin for logging
mixin LoggingMixin {
void logCommand(String command) {
print('[LOG] Command executed: $command');
}
}Applying Mixins to Context
Use the with keyword to apply mixins to your custom context.
// Use mixins with custom context
class MyContext extends Context with I18nMixin, LoggingMixin {
MyContext(super.update, super.api, super.me);
String get userLocale {
// Get user's language from Telegram
return from?.languageCode ?? 'en';
}
}Using Mixin Methods
void main() async {
final bot = Bot<MyContext>(
'YOUR_BOT_TOKEN',
contextFactory: MyContext.new,
);
bot.command('start', (ctx) async {
// Use mixin methods
ctx.logCommand('start');
// Translate based on user's locale
final greeting = ctx.translate('hello', locale: ctx.userLocale);
await ctx.reply(greeting);
});
await bot.start();
}Type Safety Benefits
The generic type system ensures that your context type is consistent throughout the middleware chain.
// This won't compile - type safety!
final bot = Bot<MyContext>(
'TOKEN',
// Error: Context is not MyContext
contextFactory: Context.new, // ❌ Type error
);
// This works - correct type
final bot = Bot<MyContext>(
'TOKEN',
contextFactory: MyContext.new, // ✅ Correct
);Advanced Patterns
Using Custom Context with Bot.fromAPI
You can also use custom contexts when creating a bot from an existing RawAPI instance.
void main() async {
// Create and configure API with transformers
final api = RawAPI('YOUR_BOT_TOKEN');
// Create bot from API with custom context
final bot = Bot.fromAPI<MyContext>(
api,
contextFactory: MyContext.new,
);
await bot.start();
}Best Practices
- Keep contexts focused: Don't turn your context into a dumping ground for unrelated functionality.
- Use mixins for reusability: Extract common patterns into mixins that can be shared across projects.
- Inject dependencies properly: Use factory functions to inject services rather than accessing globals.
- Leverage type safety: Let Dart's type system help you catch errors early.
- Document your context: Add dartdoc comments to explain what each property and method does.
- Consider async factories: If you need to fetch data during context creation, your factory can return
Future<CTX>.