0
# Bot Framework
1
2
High-level IRC bot framework with automatic reconnection, channel management, and common bot functionality. Provides SingleServerIRCBot base class for easy bot development with built-in channel state tracking and reconnection strategies.
3
4
## Capabilities
5
6
### SingleServerIRCBot
7
8
Main IRC bot class that extends SimpleIRCClient with bot-specific functionality including automatic reconnection, channel management, and CTCP handling.
9
10
```python { .api }
11
class SingleServerIRCBot:
12
def __init__(self, server_list, nickname, realname,
13
_=None, recon=ExponentialBackoff(), **connect_params):
14
"""
15
Initialize IRC bot for single server connection.
16
17
Parameters:
18
- server_list: list, server specifications (ServerSpec objects or dicts)
19
- nickname: bot nickname
20
- realname: bot real name
21
- _: unused parameter (default: None)
22
- recon: ReconnectStrategy, reconnection strategy (default: ExponentialBackoff())
23
- **connect_params: additional connection parameters
24
"""
25
26
@property
27
def channels(self) -> dict:
28
"""Dictionary mapping channel names to Channel objects."""
29
30
@property
31
def servers(self) -> list:
32
"""List of server specifications."""
33
34
@property
35
def recon(self) -> ReconnectStrategy:
36
"""Current reconnection strategy."""
37
38
def start(self):
39
"""
40
Start bot operation and event processing.
41
42
Connects to server and begins event loop.
43
"""
44
45
def die(self, msg: str = "Bye, cruel world!"):
46
"""
47
Terminate bot permanently.
48
49
Parameters:
50
- msg: str, quit message
51
"""
52
53
def disconnect(self, msg: str = "I'll be back!"):
54
"""
55
Disconnect from server (will attempt reconnection).
56
57
Parameters:
58
- msg: str, quit message
59
"""
60
61
def jump_server(self, msg: str = "Changing servers"):
62
"""
63
Switch to next server in server list.
64
65
Parameters:
66
- msg: str, quit message for current server
67
"""
68
69
@staticmethod
70
def get_version() -> str:
71
"""Get IRC library version string."""
72
73
def on_ctcp(self, connection, event):
74
"""
75
Handle CTCP queries with standard responses.
76
77
Automatically responds to VERSION, PING, and TIME queries.
78
"""
79
80
def on_dccchat(self, connection, event):
81
"""
82
Handle DCC chat requests.
83
84
Override this method to implement custom DCC chat handling.
85
"""
86
```
87
88
### Channel Management
89
90
Channel class that tracks channel state including users, modes, and channel properties.
91
92
```python { .api }
93
class Channel:
94
@property
95
def user_modes(self) -> dict:
96
"""Dictionary mapping users to their modes in channel."""
97
98
@property
99
def mode_users(self) -> dict:
100
"""Dictionary mapping modes to users having those modes."""
101
102
@property
103
def modes(self) -> dict:
104
"""Dictionary of channel modes and their values."""
105
106
def users(self) -> list:
107
"""
108
Get list of all users in channel.
109
110
Returns:
111
List of user nicknames
112
"""
113
114
def add_user(self, nick: str):
115
"""
116
Add user to channel.
117
118
Parameters:
119
- nick: str, user nickname
120
"""
121
122
def remove_user(self, nick: str):
123
"""
124
Remove user from channel.
125
126
Parameters:
127
- nick: str, user nickname
128
"""
129
130
def change_nick(self, before: str, after: str):
131
"""
132
Handle user nickname change.
133
134
Parameters:
135
- before: str, old nickname
136
- after: str, new nickname
137
"""
138
139
def has_user(self, nick: str) -> bool:
140
"""
141
Check if user is in channel.
142
143
Parameters:
144
- nick: str, user nickname
145
146
Returns:
147
bool, True if user is in channel
148
"""
149
150
def opers(self) -> list:
151
"""Get list of channel operators."""
152
153
def voiced(self) -> list:
154
"""Get list of voiced users."""
155
156
def owners(self) -> list:
157
"""Get list of channel owners."""
158
159
def halfops(self) -> list:
160
"""Get list of half-operators."""
161
162
def admins(self) -> list:
163
"""Get list of channel admins."""
164
165
def is_oper(self, nick: str) -> bool:
166
"""
167
Check if user is channel operator.
168
169
Parameters:
170
- nick: str, user nickname
171
172
Returns:
173
bool, True if user is operator
174
"""
175
176
def is_voiced(self, nick: str) -> bool:
177
"""
178
Check if user is voiced.
179
180
Parameters:
181
- nick: str, user nickname
182
183
Returns:
184
bool, True if user is voiced
185
"""
186
187
def is_owner(self, nick: str) -> bool:
188
"""
189
Check if user is channel owner.
190
191
Parameters:
192
- nick: str, user nickname
193
194
Returns:
195
bool, True if user is owner
196
"""
197
198
def is_halfop(self, nick: str) -> bool:
199
"""
200
Check if user is half-operator.
201
202
Parameters:
203
- nick: str, user nickname
204
205
Returns:
206
bool, True if user is half-op
207
"""
208
209
def is_admin(self, nick: str) -> bool:
210
"""
211
Check if user is channel admin.
212
213
Parameters:
214
- nick: str, user nickname
215
216
Returns:
217
bool, True if user is admin
218
"""
219
220
def set_mode(self, mode: str, value=None):
221
"""
222
Set channel mode.
223
224
Parameters:
225
- mode: str, mode character
226
- value: optional mode value/parameter
227
"""
228
229
def clear_mode(self, mode: str, value=None):
230
"""
231
Clear channel mode.
232
233
Parameters:
234
- mode: str, mode character
235
- value: optional mode value/parameter
236
"""
237
238
def has_mode(self, mode: str) -> bool:
239
"""
240
Check if channel has mode set.
241
242
Parameters:
243
- mode: str, mode character
244
245
Returns:
246
bool, True if mode is set
247
"""
248
249
def is_moderated(self) -> bool:
250
"""Check if channel is moderated (+m)."""
251
252
def is_secret(self) -> bool:
253
"""Check if channel is secret (+s)."""
254
255
def is_protected(self) -> bool:
256
"""Check if channel is protected (+t topic lock)."""
257
258
def has_topic_lock(self) -> bool:
259
"""Check if channel has topic lock (+t)."""
260
261
def is_invite_only(self) -> bool:
262
"""Check if channel is invite-only (+i)."""
263
264
def has_allow_external_messages(self) -> bool:
265
"""Check if channel allows external messages (+n disabled)."""
266
267
def has_limit(self) -> bool:
268
"""Check if channel has user limit (+l)."""
269
270
def limit(self) -> int:
271
"""
272
Get channel user limit.
273
274
Returns:
275
int, user limit or None if not set
276
"""
277
278
def has_key(self) -> bool:
279
"""Check if channel has key/password (+k)."""
280
```
281
282
### Server Configuration
283
284
ServerSpec class for defining IRC server connection parameters.
285
286
```python { .api }
287
class ServerSpec:
288
def __init__(self, host: str, port: int = 6667, password: str = None):
289
"""
290
Initialize server specification.
291
292
Parameters:
293
- host: str, server hostname
294
- port: int, server port (default 6667)
295
- password: str, optional server password
296
"""
297
298
@property
299
def host(self) -> str:
300
"""Server hostname."""
301
302
@property
303
def port(self) -> int:
304
"""Server port."""
305
306
@property
307
def password(self) -> str:
308
"""Server password (may be None)."""
309
310
@classmethod
311
def ensure(cls, input):
312
"""
313
Ensure input is ServerSpec instance.
314
315
Parameters:
316
- input: ServerSpec, dict, or tuple to convert
317
318
Returns:
319
ServerSpec instance
320
"""
321
```
322
323
### Reconnection Strategies
324
325
Abstract base class and implementations for automatic reconnection handling.
326
327
```python { .api }
328
class ReconnectStrategy:
329
def run(self, bot):
330
"""
331
Execute reconnection strategy.
332
333
Parameters:
334
- bot: SingleServerIRCBot, bot instance to reconnect
335
"""
336
337
class ExponentialBackoff(ReconnectStrategy):
338
def __init__(self, min_interval: float = 1, max_interval: float = 300):
339
"""
340
Initialize exponential backoff reconnection strategy.
341
342
Parameters:
343
- min_interval: float, minimum wait time in seconds
344
- max_interval: float, maximum wait time in seconds
345
"""
346
347
@property
348
def min_interval(self) -> float:
349
"""Minimum reconnection interval."""
350
351
@property
352
def max_interval(self) -> float:
353
"""Maximum reconnection interval."""
354
355
@property
356
def attempt_count(self) -> int:
357
"""Number of reconnection attempts made."""
358
359
def run(self, bot):
360
"""
361
Execute exponential backoff reconnection.
362
363
Parameters:
364
- bot: SingleServerIRCBot, bot to reconnect
365
"""
366
367
def check(self):
368
"""Check if reconnection should proceed."""
369
```
370
371
## Usage Examples
372
373
### Basic IRC Bot
374
375
```python
376
from irc.bot import SingleServerIRCBot
377
378
class MyBot(SingleServerIRCBot):
379
def __init__(self, channels, nickname, server, port=6667):
380
# Server specification
381
server_spec = [{"host": server, "port": port}]
382
super().__init__(server_spec, nickname, nickname)
383
self.channels_to_join = channels
384
385
def on_welcome(self, connection, event):
386
"""Called when successfully connected to server."""
387
for channel in self.channels_to_join:
388
connection.join(channel)
389
print(f"Joining {channel}")
390
391
def on_pubmsg(self, connection, event):
392
"""Called when public message received in channel."""
393
channel = event.target
394
nick = event.source.nick
395
message = event.arguments[0]
396
397
print(f"[{channel}] <{nick}> {message}")
398
399
# Respond to commands
400
if message.startswith("!hello"):
401
connection.privmsg(channel, f"Hello {nick}!")
402
elif message.startswith("!users"):
403
users = self.channels[channel].users()
404
connection.privmsg(channel, f"Users in {channel}: {', '.join(users)}")
405
406
def on_privmsg(self, connection, event):
407
"""Called when private message received."""
408
nick = event.source.nick
409
message = event.arguments[0]
410
411
print(f"PM from {nick}: {message}")
412
connection.privmsg(nick, f"You said: {message}")
413
414
def on_join(self, connection, event):
415
"""Called when someone joins a channel."""
416
channel = event.target
417
nick = event.source.nick
418
419
if nick == connection.get_nickname():
420
print(f"Successfully joined {channel}")
421
else:
422
connection.privmsg(channel, f"Welcome {nick}!")
423
424
def on_part(self, connection, event):
425
"""Called when someone leaves a channel."""
426
channel = event.target
427
nick = event.source.nick
428
print(f"{nick} left {channel}")
429
430
# Create and start bot
431
bot = MyBot(["#test", "#botchannel"], "MyBot", "irc.libera.chat")
432
bot.start()
433
```
434
435
### Advanced Bot with Custom Reconnection
436
437
```python
438
from irc.bot import SingleServerIRCBot, ExponentialBackoff
439
import time
440
441
class AdvancedBot(SingleServerIRCBot):
442
def __init__(self, channels, nickname, servers):
443
# Custom reconnection strategy - faster initial reconnects
444
reconnect_strategy = ExponentialBackoff(min_interval=5, max_interval=60)
445
446
super().__init__(
447
servers,
448
nickname,
449
f"{nickname} IRC Bot",
450
recon=reconnect_strategy
451
)
452
self.channels_to_join = channels
453
self.start_time = time.time()
454
455
def on_welcome(self, connection, event):
456
for channel in self.channels_to_join:
457
connection.join(channel)
458
459
def on_pubmsg(self, connection, event):
460
message = event.arguments[0]
461
channel = event.target
462
nick = event.source.nick
463
464
if message.startswith("!uptime"):
465
uptime = int(time.time() - self.start_time)
466
hours, remainder = divmod(uptime, 3600)
467
minutes, seconds = divmod(remainder, 60)
468
connection.privmsg(channel, f"Uptime: {hours}h {minutes}m {seconds}s")
469
470
elif message.startswith("!channels"):
471
channels = list(self.channels.keys())
472
connection.privmsg(channel, f"I'm in: {', '.join(channels)}")
473
474
elif message.startswith("!ops"):
475
if channel in self.channels:
476
ops = self.channels[channel].opers()
477
connection.privmsg(channel, f"Operators: {', '.join(ops)}")
478
479
def on_disconnect(self, connection, event):
480
"""Called when disconnected from server."""
481
print("Disconnected from server, will attempt reconnection...")
482
483
def on_nicknameinuse(self, connection, event):
484
"""Called when nickname is already in use."""
485
connection.nick(connection.get_nickname() + "_")
486
487
# Multiple server configuration with fallbacks
488
servers = [
489
{"host": "irc.libera.chat", "port": 6667},
490
{"host": "irc.oftc.net", "port": 6667}
491
]
492
493
bot = AdvancedBot(["#test"], "AdvancedBot", servers)
494
bot.start()
495
```
496
497
### Bot with Channel Management
498
499
```python
500
from irc.bot import SingleServerIRCBot
501
502
class ChannelBot(SingleServerIRCBot):
503
def __init__(self, channels, nickname, server, port=6667):
504
server_spec = [{"host": server, "port": port}]
505
super().__init__(server_spec, nickname, nickname)
506
self.channels_to_join = channels
507
self.admin_users = {"admin_nick"} # Set of admin nicknames
508
509
def on_welcome(self, connection, event):
510
for channel in self.channels_to_join:
511
connection.join(channel)
512
513
def on_pubmsg(self, connection, event):
514
message = event.arguments[0]
515
channel = event.target
516
nick = event.source.nick
517
518
# Admin commands
519
if nick in self.admin_users:
520
if message.startswith("!kick "):
521
target = message.split()[1]
522
if len(message.split()) > 2:
523
reason = " ".join(message.split()[2:])
524
else:
525
reason = "Requested by admin"
526
connection.kick(channel, target, reason)
527
528
elif message.startswith("!mode "):
529
mode_command = message[6:] # Remove "!mode "
530
connection.mode(channel, mode_command)
531
532
elif message.startswith("!topic "):
533
new_topic = message[7:] # Remove "!topic "
534
connection.topic(channel, new_topic)
535
536
# Public commands
537
if message.startswith("!whoami"):
538
channel_obj = self.channels.get(channel)
539
if channel_obj:
540
modes = []
541
if channel_obj.is_oper(nick):
542
modes.append("operator")
543
if channel_obj.is_voiced(nick):
544
modes.append("voiced")
545
if channel_obj.is_owner(nick):
546
modes.append("owner")
547
548
if modes:
549
connection.privmsg(channel, f"{nick}: You are {', '.join(modes)}")
550
else:
551
connection.privmsg(channel, f"{nick}: You are a regular user")
552
553
elif message.startswith("!count"):
554
channel_obj = self.channels.get(channel)
555
if channel_obj:
556
user_count = len(channel_obj.users())
557
connection.privmsg(channel, f"Users in {channel}: {user_count}")
558
559
def on_mode(self, connection, event):
560
"""Called when channel or user mode changes."""
561
target = event.target
562
modes = event.arguments[0]
563
print(f"Mode change on {target}: {modes}")
564
565
def on_kick(self, connection, event):
566
"""Called when someone is kicked from channel."""
567
channel = event.target
568
kicked_nick = event.arguments[0]
569
kicker = event.source.nick
570
reason = event.arguments[1] if len(event.arguments) > 1 else "No reason"
571
572
print(f"{kicked_nick} was kicked from {channel} by {kicker}: {reason}")
573
574
# If we were kicked, try to rejoin
575
if kicked_nick == connection.get_nickname():
576
connection.join(channel)
577
578
bot = ChannelBot(["#mychannel"], "ChannelBot", "irc.libera.chat")
579
bot.start()
580
```
581
582
### Multi-Server Bot with State Persistence
583
584
```python
585
import json
586
import os
587
from irc.bot import SingleServerIRCBot, ExponentialBackoff
588
589
class PersistentBot(SingleServerIRCBot):
590
def __init__(self, channels, nickname, servers, state_file="bot_state.json"):
591
super().__init__(servers, nickname, nickname)
592
self.channels_to_join = channels
593
self.state_file = state_file
594
self.user_data = self.load_state()
595
596
def load_state(self):
597
"""Load bot state from file."""
598
if os.path.exists(self.state_file):
599
with open(self.state_file, 'r') as f:
600
return json.load(f)
601
return {}
602
603
def save_state(self):
604
"""Save bot state to file."""
605
with open(self.state_file, 'w') as f:
606
json.dump(self.user_data, f, indent=2)
607
608
def on_welcome(self, connection, event):
609
for channel in self.channels_to_join:
610
connection.join(channel)
611
612
def on_pubmsg(self, connection, event):
613
message = event.arguments[0]
614
channel = event.target
615
nick = event.source.nick
616
617
if message.startswith("!remember "):
618
key_value = message[10:].split("=", 1)
619
if len(key_value) == 2:
620
key, value = key_value
621
if nick not in self.user_data:
622
self.user_data[nick] = {}
623
self.user_data[nick][key.strip()] = value.strip()
624
self.save_state()
625
connection.privmsg(channel, f"{nick}: Remembered {key} = {value}")
626
627
elif message.startswith("!recall "):
628
key = message[8:].strip()
629
if nick in self.user_data and key in self.user_data[nick]:
630
value = self.user_data[nick][key]
631
connection.privmsg(channel, f"{nick}: {key} = {value}")
632
else:
633
connection.privmsg(channel, f"{nick}: I don't remember '{key}'")
634
635
elif message.startswith("!forget "):
636
key = message[8:].strip()
637
if nick in self.user_data and key in self.user_data[nick]:
638
del self.user_data[nick][key]
639
self.save_state()
640
connection.privmsg(channel, f"{nick}: Forgot '{key}'")
641
642
elif message.startswith("!list"):
643
if nick in self.user_data and self.user_data[nick]:
644
keys = list(self.user_data[nick].keys())
645
connection.privmsg(channel, f"{nick}: I remember: {', '.join(keys)}")
646
else:
647
connection.privmsg(channel, f"{nick}: I don't remember anything for you")
648
649
def on_disconnect(self, connection, event):
650
print("Disconnected, saving state...")
651
self.save_state()
652
653
# Multiple servers with SSL
654
servers = [
655
{"host": "irc.libera.chat", "port": 6697, "ssl": True},
656
{"host": "irc.oftc.net", "port": 6697, "ssl": True}
657
]
658
659
bot = PersistentBot(["#test", "#bots"], "PersistentBot", servers)
660
661
try:
662
bot.start()
663
except KeyboardInterrupt:
664
print("Bot shutting down...")
665
bot.save_state()
666
bot.die("Bot shutdown requested")
667
```