0
# Utilities and Helpers
1
2
IRC-specific string handling, nickname parsing, channel detection, mode parsing, and scheduling utilities for IRC applications. These utilities provide essential functionality for working with IRC protocol data.
3
4
## Capabilities
5
6
### Nickname and Mask Parsing
7
8
Parse and manipulate IRC nicknames and user masks.
9
10
```python { .api }
11
class NickMask(str):
12
"""IRC nickname mask parsing and manipulation."""
13
14
@property
15
def nick(self) -> str:
16
"""
17
Extract nickname from mask.
18
19
Returns:
20
str, nickname portion of mask
21
"""
22
23
@property
24
def user(self) -> str:
25
"""
26
Extract username from mask.
27
28
Returns:
29
str, username portion of mask (after !)
30
"""
31
32
@property
33
def host(self) -> str:
34
"""
35
Extract hostname from mask.
36
37
Returns:
38
str, hostname portion of mask (after @)
39
"""
40
41
@property
42
def userhost(self) -> str:
43
"""
44
Get user@host portion of mask.
45
46
Returns:
47
str, user@host portion
48
"""
49
50
@classmethod
51
def from_params(cls, nick: str, user: str, host: str):
52
"""
53
Create NickMask from individual components.
54
55
Parameters:
56
- nick: str, nickname
57
- user: str, username
58
- host: str, hostname
59
60
Returns:
61
NickMask, constructed mask (nick!user@host)
62
"""
63
64
@classmethod
65
def from_group(cls, group):
66
"""
67
Create NickMask from regex match group.
68
69
Parameters:
70
- group: regex match group containing mask components
71
72
Returns:
73
NickMask instance
74
"""
75
```
76
77
### Channel and Message Utilities
78
79
Utilities for working with IRC channels and messages.
80
81
```python { .api }
82
def is_channel(string: str) -> bool:
83
"""
84
Check if string is a channel name.
85
86
Checks if the string starts with valid channel prefixes
87
according to IRC standards (#, &, +, !).
88
89
Parameters:
90
- string: str, string to check
91
92
Returns:
93
bool, True if string appears to be a channel name
94
"""
95
96
def ip_numstr_to_quad(num: str) -> str:
97
"""
98
Convert IP address from numeric string to dotted quad format.
99
100
Converts DCC IP address format to standard dotted decimal.
101
102
Parameters:
103
- num: str, numeric IP address string
104
105
Returns:
106
str, dotted quad IP address (e.g., "192.168.1.1")
107
"""
108
109
def ip_quad_to_numstr(quad: str) -> str:
110
"""
111
Convert IP address from dotted quad to numeric string format.
112
113
Converts standard dotted decimal to DCC numeric format.
114
115
Parameters:
116
- quad: str, dotted quad IP address
117
118
Returns:
119
str, numeric IP address string
120
"""
121
```
122
123
### IRC String Handling
124
125
IRC-specific string processing and case folding.
126
127
```python { .api }
128
class IRCFoldedCase(str):
129
"""IRC-compliant case folding for nicknames and channels."""
130
131
@property
132
def translation(self) -> dict:
133
"""Character translation table for IRC case folding."""
134
135
def lower(self) -> str:
136
"""
137
Convert to lowercase using IRC rules.
138
139
Uses IRC-specific case folding where [ ] \ are lowercase
140
equivalents of { } |.
141
142
Returns:
143
str, lowercase string according to IRC rules
144
"""
145
146
def casefold(self) -> str:
147
"""
148
Case-fold string for IRC comparison.
149
150
Returns:
151
str, case-folded string
152
"""
153
154
def __setattr__(self, key, val):
155
"""Prevent modification of immutable string attributes."""
156
157
def lower(string: str) -> str:
158
"""
159
IRC-compliant string lowercasing.
160
161
Converts string to lowercase using IRC case folding rules
162
where ASCII brackets are equivalent to braces.
163
164
Parameters:
165
- string: str, string to convert
166
167
Returns:
168
str, lowercase string using IRC rules
169
"""
170
```
171
172
### IRC Dictionary
173
174
Case-insensitive dictionary implementation using IRC string rules.
175
176
```python { .api }
177
class IRCDict(dict):
178
"""Case-insensitive dictionary using IRC case folding rules."""
179
180
@staticmethod
181
def transform_key(key: str) -> str:
182
"""
183
Transform key using IRC case folding.
184
185
Applies IRC-specific case folding to dictionary keys,
186
ensuring case-insensitive lookups follow IRC standards.
187
188
Parameters:
189
- key: str, dictionary key
190
191
Returns:
192
str, transformed key
193
"""
194
```
195
196
### Mode Parsing
197
198
Parse IRC user and channel mode strings.
199
200
```python { .api }
201
def parse_nick_modes(mode_string: str) -> list:
202
"""
203
Parse user mode string into list of mode changes.
204
205
Parses MODE commands targeting users, handling both
206
setting (+) and unsetting (-) of modes.
207
208
Parameters:
209
- mode_string: str, mode string (e.g., "+iwx", "-o+v")
210
211
Returns:
212
list, list of (action, mode) tuples where action is '+' or '-'
213
"""
214
215
def parse_channel_modes(mode_string: str) -> list:
216
"""
217
Parse channel mode string into list of mode changes.
218
219
Parses MODE commands targeting channels, handling modes
220
with and without parameters.
221
222
Parameters:
223
- mode_string: str, channel mode string
224
225
Returns:
226
list, list of (action, mode, parameter) tuples
227
"""
228
229
def _parse_modes(mode_string: str, unary_modes: str = "") -> list:
230
"""
231
Generic mode parser for IRC mode strings.
232
233
Internal function used by specific mode parsers.
234
235
Parameters:
236
- mode_string: str, raw mode string
237
- unary_modes: str, modes that don't take parameters
238
239
Returns:
240
list, parsed mode changes
241
"""
242
```
243
244
### Event Scheduling
245
246
Task scheduling utilities for IRC bots and clients.
247
248
```python { .api }
249
class IScheduler:
250
"""Abstract interface for event scheduling."""
251
252
def execute_every(self, period, func):
253
"""
254
Execute function periodically.
255
256
Parameters:
257
- period: time period between executions
258
- func: callable, function to execute
259
"""
260
261
def execute_at(self, when, func):
262
"""
263
Execute function at specific time.
264
265
Parameters:
266
- when: datetime, when to execute
267
- func: callable, function to execute
268
"""
269
270
def execute_after(self, delay, func):
271
"""
272
Execute function after delay.
273
274
Parameters:
275
- delay: time delay before execution
276
- func: callable, function to execute
277
"""
278
279
def run_pending(self):
280
"""Run any pending scheduled tasks."""
281
282
class DefaultScheduler(IScheduler):
283
"""Default scheduler implementation using the schedule library."""
284
285
def execute_every(self, period, func):
286
"""
287
Schedule function to run every period.
288
289
Parameters:
290
- period: int/float, seconds between executions
291
- func: callable, function to execute
292
"""
293
294
def execute_at(self, when, func):
295
"""
296
Schedule function to run at specific time.
297
298
Parameters:
299
- when: datetime, execution time
300
- func: callable, function to execute
301
"""
302
303
def execute_after(self, delay, func):
304
"""
305
Schedule function to run after delay.
306
307
Parameters:
308
- delay: int/float, delay in seconds
309
- func: callable, function to execute
310
"""
311
```
312
313
## Usage Examples
314
315
### Nickname Mask Parsing
316
317
```python
318
from irc.client import NickMask
319
320
# Parse nickname mask from IRC message
321
mask = NickMask("alice!alice@example.com")
322
print(f"Nick: {mask.nick}") # alice
323
print(f"User: {mask.user}") # alice
324
print(f"Host: {mask.host}") # example.com
325
print(f"Userhost: {mask.userhost}") # alice@example.com
326
327
# Create mask from components
328
mask2 = NickMask.from_params("bob", "robert", "isp.net")
329
print(mask2) # bob!robert@isp.net
330
331
# Parse server messages
332
def on_join(connection, event):
333
user_mask = NickMask(event.source)
334
print(f"{user_mask.nick} joined from {user_mask.host}")
335
336
# Use in bot
337
import irc.client
338
339
client = irc.client.SimpleIRCClient()
340
client.connection.add_global_handler("join", on_join)
341
client.connect("irc.libera.chat", 6667, "maskbot")
342
client.start()
343
```
344
345
### Channel Detection and Validation
346
347
```python
348
from irc.client import is_channel
349
350
# Check if strings are channels
351
channels = ["#general", "&local", "+private", "!unique", "user", "server.name"]
352
353
for item in channels:
354
if is_channel(item):
355
print(f"{item} is a channel")
356
else:
357
print(f"{item} is not a channel")
358
359
# Use in message handler
360
def on_pubmsg(connection, event):
361
target = event.target
362
message = event.arguments[0]
363
364
if message.startswith("!join "):
365
channel_name = message[6:]
366
if is_channel(channel_name):
367
connection.join(channel_name)
368
else:
369
connection.privmsg(target, f"'{channel_name}' is not a valid channel name")
370
```
371
372
### IRC String Handling
373
374
```python
375
from irc.strings import lower, IRCFoldedCase
376
from irc.dict import IRCDict
377
378
# IRC case folding
379
nick1 = "Alice[Bot]"
380
nick2 = "alice{bot}"
381
382
# These are equivalent in IRC
383
print(lower(nick1)) # alice{bot}
384
print(lower(nick2)) # alice{bot}
385
print(lower(nick1) == lower(nick2)) # True
386
387
# Use IRCDict for case-insensitive storage
388
users = IRCDict()
389
users["Alice[Bot]"] = {"level": "admin"}
390
users["Bob^Away"] = {"level": "user"}
391
392
# Lookups are case-insensitive
393
print(users["alice{bot}"]) # {'level': 'admin'}
394
print(users["bob~away"]) # {'level': 'user'}
395
396
# Practical example: user database
397
class UserDatabase:
398
def __init__(self):
399
self.users = IRCDict()
400
401
def add_user(self, nick, info):
402
self.users[nick] = info
403
404
def get_user(self, nick):
405
return self.users.get(nick)
406
407
def is_admin(self, nick):
408
user = self.get_user(nick)
409
return user and user.get("level") == "admin"
410
411
# Usage in bot
412
db = UserDatabase()
413
db.add_user("Alice[Admin]", {"level": "admin"})
414
415
def on_pubmsg(connection, event):
416
nick = event.source.nick
417
message = event.arguments[0]
418
419
if message.startswith("!kick") and db.is_admin(nick):
420
# Admin command - nick comparison is case-insensitive
421
target = message.split()[1]
422
connection.kick(event.target, target)
423
```
424
425
### Mode Parsing
426
427
```python
428
from irc.modes import parse_nick_modes, parse_channel_modes
429
430
# Parse user modes
431
user_modes = parse_nick_modes("+iwx-o")
432
print(user_modes) # [('+', 'i'), ('+', 'w'), ('+', 'x'), ('-', 'o')]
433
434
# Parse channel modes
435
channel_modes = parse_channel_modes("+nt-k+l")
436
print(channel_modes) # [('+', 'n'), ('+', 't'), ('-', 'k'), ('+', 'l')]
437
438
# Use in mode event handler
439
def on_mode(connection, event):
440
target = event.target
441
mode_string = event.arguments[0]
442
mode_args = event.arguments[1:] if len(event.arguments) > 1 else []
443
444
if is_channel(target):
445
modes = parse_channel_modes(mode_string)
446
print(f"Channel {target} mode changes: {modes}")
447
448
for i, (action, mode) in enumerate(modes):
449
if mode in "ovh": # User privilege modes
450
if i < len(mode_args):
451
user = mode_args[i]
452
print(f"{action}{mode} {user}")
453
else:
454
modes = parse_nick_modes(mode_string)
455
print(f"User {target} mode changes: {modes}")
456
```
457
458
### IP Address Conversion
459
460
```python
461
from irc.client import ip_numstr_to_quad, ip_quad_to_numstr
462
463
# Convert between DCC IP formats
464
numeric_ip = "3232235777" # DCC format
465
dotted_ip = ip_numstr_to_quad(numeric_ip)
466
print(dotted_ip) # 192.168.1.1
467
468
# Convert back
469
numeric_again = ip_quad_to_numstr(dotted_ip)
470
print(numeric_again) # 3232235777
471
472
# Use in DCC handling
473
def on_pubmsg(connection, event):
474
message = event.arguments[0]
475
nick = event.source.nick
476
477
if message.startswith("!dcc"):
478
# Create DCC connection
479
dcc = connection.dcc("chat")
480
dcc.listen()
481
482
# Get connection info
483
address = dcc.socket.getsockname()
484
host_ip = "192.168.1.100" # Your external IP
485
port = address[1]
486
487
# Convert IP to DCC format
488
numeric_ip = ip_quad_to_numstr(host_ip)
489
490
# Send DCC offer
491
dcc_msg = f"\x01DCC CHAT chat {numeric_ip} {port}\x01"
492
connection.privmsg(nick, dcc_msg)
493
```
494
495
### Event Scheduling
496
497
```python
498
from irc.schedule import DefaultScheduler
499
import irc.client
500
import datetime
501
502
class ScheduledBot:
503
def __init__(self):
504
self.client = irc.client.SimpleIRCClient()
505
self.scheduler = DefaultScheduler()
506
self.setup_handlers()
507
self.setup_scheduled_tasks()
508
509
def setup_handlers(self):
510
def on_connect(connection, event):
511
connection.join("#scheduled")
512
513
def on_pubmsg(connection, event):
514
message = event.arguments[0]
515
channel = event.target
516
517
if message.startswith("!remind "):
518
# Parse reminder: !remind 30 Take a break
519
parts = message[8:].split(" ", 1)
520
if len(parts) == 2:
521
try:
522
delay = int(parts[0])
523
reminder_text = parts[1]
524
525
# Schedule reminder
526
self.scheduler.execute_after(
527
delay,
528
lambda: connection.privmsg(channel, f"Reminder: {reminder_text}")
529
)
530
connection.privmsg(channel, f"Reminder set for {delay} seconds")
531
except ValueError:
532
connection.privmsg(channel, "Invalid delay time")
533
534
elif message == "!time":
535
# Schedule time announcement in 5 seconds
536
self.scheduler.execute_after(
537
5,
538
lambda: connection.privmsg(channel, f"Time: {datetime.datetime.now()}")
539
)
540
541
self.client.connection.add_global_handler("welcome", on_connect)
542
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
543
544
def setup_scheduled_tasks(self):
545
"""Set up recurring scheduled tasks."""
546
547
def hourly_announcement():
548
if self.client.connection.is_connected():
549
self.client.connection.privmsg("#scheduled", "Hourly ping!")
550
551
def daily_stats():
552
if self.client.connection.is_connected():
553
uptime = "Bot has been running for some time"
554
self.client.connection.privmsg("#scheduled", f"Daily stats: {uptime}")
555
556
# Schedule recurring tasks
557
self.scheduler.execute_every(3600, hourly_announcement) # Every hour
558
self.scheduler.execute_every(86400, daily_stats) # Every day
559
560
# Schedule one-time task
561
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
562
self.scheduler.execute_at(tomorrow, lambda: print("Tomorrow arrived!"))
563
564
def start(self):
565
"""Start bot with scheduler integration."""
566
self.client.connect("irc.libera.chat", 6667, "scheduledbot")
567
568
# Override the event loop to include scheduler
569
while True:
570
try:
571
self.client.reactor.process_once(timeout=1.0)
572
self.scheduler.run_pending()
573
except KeyboardInterrupt:
574
break
575
576
self.client.connection.quit("Scheduler bot shutting down")
577
578
# Usage
579
bot = ScheduledBot()
580
bot.start()
581
```
582
583
### Comprehensive Utility Bot
584
585
```python
586
import irc.client
587
from irc.client import NickMask, is_channel, ip_quad_to_numstr
588
from irc.strings import lower
589
from irc.dict import IRCDict
590
from irc.modes import parse_channel_modes
591
import datetime
592
import json
593
594
class UtilityBot:
595
def __init__(self):
596
self.client = irc.client.SimpleIRCClient()
597
self.user_data = IRCDict() # Case-insensitive user storage
598
self.channel_stats = IRCDict() # Case-insensitive channel storage
599
self.setup_handlers()
600
601
def setup_handlers(self):
602
def on_connect(connection, event):
603
connection.join("#utilities")
604
print("UtilityBot connected and ready!")
605
606
def on_join(connection, event):
607
nick = event.source.nick
608
channel = event.target
609
610
# Track channel stats
611
if channel not in self.channel_stats:
612
self.channel_stats[channel] = {"joins": 0, "messages": 0}
613
self.channel_stats[channel]["joins"] += 1
614
615
# Welcome message with user info
616
mask = NickMask(event.source)
617
connection.privmsg(channel, f"Welcome {nick} from {mask.host}!")
618
619
def on_pubmsg(connection, event):
620
message = event.arguments[0]
621
channel = event.target
622
nick = event.source.nick
623
mask = NickMask(event.source)
624
625
# Track message stats
626
if channel in self.channel_stats:
627
self.channel_stats[channel]["messages"] += 1
628
629
# Store user info
630
self.user_data[nick] = {
631
"host": mask.host,
632
"last_message": message,
633
"last_seen": datetime.datetime.now().isoformat()
634
}
635
636
# Handle commands
637
if message.startswith("!"):
638
self.handle_command(connection, channel, nick, message)
639
640
def on_mode(connection, event):
641
target = event.target
642
mode_string = event.arguments[0]
643
644
if is_channel(target):
645
modes = parse_channel_modes(mode_string)
646
print(f"Mode change in {target}: {modes}")
647
648
self.client.connection.add_global_handler("welcome", on_connect)
649
self.client.connection.add_global_handler("join", on_join)
650
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
651
self.client.connection.add_global_handler("mode", on_mode)
652
653
def handle_command(self, connection, channel, nick, message):
654
"""Handle bot commands."""
655
cmd_parts = message[1:].split()
656
command = cmd_parts[0].lower()
657
658
if command == "userinfo":
659
target_nick = cmd_parts[1] if len(cmd_parts) > 1 else nick
660
user_info = self.user_data.get(target_nick)
661
662
if user_info:
663
response = f"{target_nick}: Host={user_info['host']}, Last seen={user_info['last_seen']}"
664
connection.privmsg(channel, response)
665
else:
666
connection.privmsg(channel, f"No info available for {target_nick}")
667
668
elif command == "stats":
669
if channel in self.channel_stats:
670
stats = self.channel_stats[channel]
671
response = f"{channel}: {stats['joins']} joins, {stats['messages']} messages"
672
connection.privmsg(channel, response)
673
674
elif command == "ischannel":
675
if len(cmd_parts) > 1:
676
test_string = cmd_parts[1]
677
result = "is" if is_channel(test_string) else "is not"
678
connection.privmsg(channel, f"'{test_string}' {result} a channel")
679
680
elif command == "lower":
681
if len(cmd_parts) > 1:
682
test_string = " ".join(cmd_parts[1:])
683
lowered = lower(test_string)
684
connection.privmsg(channel, f"IRC lowercase: '{lowered}'")
685
686
elif command == "ip":
687
if len(cmd_parts) > 1:
688
try:
689
ip_addr = cmd_parts[1]
690
numeric = ip_quad_to_numstr(ip_addr)
691
connection.privmsg(channel, f"{ip_addr} = {numeric} (DCC format)")
692
except:
693
connection.privmsg(channel, "Invalid IP address format")
694
695
elif command == "export":
696
# Export user data as JSON
697
data = dict(self.user_data) # Convert IRCDict to regular dict
698
json_data = json.dumps(data, indent=2)
699
700
# Send in private to avoid spam
701
connection.privmsg(nick, "User data export:")
702
for line in json_data.split('\n')[:20]: # Limit lines
703
connection.privmsg(nick, line)
704
705
elif command == "help":
706
help_text = [
707
"Available commands:",
708
"!userinfo [nick] - Show user information",
709
"!stats - Show channel statistics",
710
"!ischannel <string> - Test if string is channel name",
711
"!lower <text> - Convert to IRC lowercase",
712
"!ip <address> - Convert IP to DCC format",
713
"!export - Export user data (private message)",
714
"!help - Show this help"
715
]
716
717
for line in help_text:
718
connection.privmsg(nick, line)
719
720
def start(self):
721
"""Start the utility bot."""
722
self.client.connect("irc.libera.chat", 6667, "utilitybot")
723
self.client.start()
724
725
# Usage
726
bot = UtilityBot()
727
bot.start()
728
```