0
# Protocol Extensions
1
2
Support for IRC protocol extensions including CTCP (Client-To-Client Protocol), DCC (Direct Client-to-Client), IRCv3 message tags, and server capability detection. These extensions provide enhanced functionality beyond basic IRC protocol.
3
4
## Capabilities
5
6
### CTCP (Client-To-Client Protocol)
7
8
CTCP enables direct client-to-client communication through specially formatted messages embedded in PRIVMSG and NOTICE commands.
9
10
```python { .api }
11
# CTCP constants
12
LOW_LEVEL_QUOTE = "\x10" # Low-level quote character
13
LEVEL_QUOTE = "\\" # Level quote character
14
DELIMITER = "\x01" # CTCP delimiter character
15
16
# Character mapping for low-level quoting
17
low_level_mapping = {
18
"\x10": "\x10\x10", # Quote -> Quote Quote
19
"\x00": "\x10\x30", # NUL -> Quote 0
20
"\x0a": "\x10\x6e", # LF -> Quote n
21
"\x0d": "\x10\x72" # CR -> Quote r
22
}
23
24
def dequote(message: str) -> list:
25
"""
26
Dequote CTCP message according to CTCP specifications.
27
28
Processes CTCP quoting and extracts CTCP commands from IRC messages.
29
30
Parameters:
31
- message: str, raw CTCP message with quoting
32
33
Returns:
34
list, dequoted CTCP commands
35
"""
36
```
37
38
### DCC (Direct Client-to-Client)
39
40
DCC provides direct TCP connections between IRC clients, bypassing the IRC server for file transfers and chat.
41
42
```python { .api }
43
class DCCConnection:
44
"""Direct client-to-client connection for file transfers and chat."""
45
46
def connect(self, address: tuple, port: int):
47
"""
48
Connect to DCC peer.
49
50
Parameters:
51
- address: tuple, (hostname, port) of peer
52
- port: int, port number for connection
53
"""
54
55
def listen(self, addr=None):
56
"""
57
Listen for DCC connections.
58
59
Parameters:
60
- addr: tuple, optional bind address (default: all interfaces)
61
"""
62
63
def disconnect(self, message: str = ""):
64
"""
65
Disconnect DCC connection.
66
67
Parameters:
68
- message: str, optional disconnect message
69
"""
70
71
def privmsg(self, text: str):
72
"""
73
Send private message over DCC chat.
74
75
Parameters:
76
- text: str, message text to send
77
"""
78
79
def send_bytes(self, bytes_data: bytes):
80
"""
81
Send raw bytes over DCC connection.
82
83
Parameters:
84
- bytes_data: bytes, raw data to send
85
"""
86
87
@property
88
def connected(self) -> bool:
89
"""Whether DCC connection is established."""
90
91
@property
92
def socket(self):
93
"""Underlying socket object."""
94
```
95
96
### Server Features (ISUPPORT)
97
98
Server capability detection and management based on IRC ISUPPORT (005) messages.
99
100
```python { .api }
101
class FeatureSet:
102
"""Manages IRC server features and capabilities from ISUPPORT."""
103
104
def __init__(self):
105
"""Initialize empty feature set."""
106
107
def set(self, name: str, value=True):
108
"""
109
Set server feature value.
110
111
Parameters:
112
- name: str, feature name (e.g., "CHANTYPES", "NICKLEN")
113
- value: feature value (True for boolean features, str/int for valued features)
114
"""
115
116
def remove(self, feature_name: str):
117
"""
118
Remove server feature.
119
120
Parameters:
121
- feature_name: str, name of feature to remove
122
"""
123
124
def load(self, arguments: list):
125
"""
126
Load features from ISUPPORT message arguments.
127
128
Parameters:
129
- arguments: list, ISUPPORT message arguments
130
"""
131
132
def load_feature(self, feature: str):
133
"""
134
Load individual feature string.
135
136
Parameters:
137
- feature: str, feature string (e.g., "CHANTYPES=#&", "NICKLEN=30")
138
"""
139
140
def _parse_PREFIX(self) -> dict:
141
"""Parse PREFIX feature (channel user modes)."""
142
143
def _parse_CHANMODES(self) -> dict:
144
"""Parse CHANMODES feature (channel mode types)."""
145
146
def _parse_TARGMAX(self) -> dict:
147
"""Parse TARGMAX feature (maximum targets per command)."""
148
149
def _parse_CHANLIMIT(self) -> dict:
150
"""Parse CHANLIMIT feature (channel limits by type)."""
151
152
def _parse_MAXLIST(self) -> dict:
153
"""Parse MAXLIST feature (maximum list entries)."""
154
155
def _parse_other(self) -> dict:
156
"""Parse other miscellaneous features."""
157
158
def string_int_pair(target: str, sep: str = ":") -> tuple:
159
"""
160
Parse string:integer pair from server features.
161
162
Parameters:
163
- target: str, string to parse (e.g., "#:120")
164
- sep: str, separator character (default ":")
165
166
Returns:
167
tuple, (string, integer) pair
168
"""
169
```
170
171
### IRCv3 Message Tags
172
173
Support for IRCv3 message tags that provide metadata and extended functionality.
174
175
```python { .api }
176
class Tag:
177
"""IRCv3 message tag parsing and handling."""
178
179
@staticmethod
180
def parse(item: str) -> dict:
181
"""
182
Parse IRCv3 tag string into key-value pairs.
183
184
Parameters:
185
- item: str, tag string (e.g., "key=value;key2=value2")
186
187
Returns:
188
dict, parsed tags
189
"""
190
191
@classmethod
192
def from_group(cls, group):
193
"""
194
Create Tag from regex match group.
195
196
Parameters:
197
- group: regex match group containing tag data
198
199
Returns:
200
Tag instance
201
"""
202
203
# Tag unescaping for IRCv3 compliance
204
_TAG_UNESCAPE_MAP = {
205
"\\\\": "\\", # Backslash
206
"\\_": "_", # Underscore
207
"\\:": ";", # Semicolon
208
"\\s": " ", # Space
209
"\\r": "\r", # Carriage return
210
"\\n": "\n" # Line feed
211
}
212
```
213
214
### Message Parsing
215
216
IRC message parsing utilities for handling protocol messages and arguments.
217
218
```python { .api }
219
class Arguments(list):
220
"""IRC command arguments with special parsing rules."""
221
222
@staticmethod
223
def from_group(group) -> list:
224
"""
225
Parse IRC command arguments from regex match group.
226
227
Parameters:
228
- group: regex match group containing arguments
229
230
Returns:
231
list, parsed arguments
232
"""
233
```
234
235
## Usage Examples
236
237
### CTCP Handler
238
239
```python
240
import irc.client
241
import irc.ctcp
242
243
def handle_ctcp(connection, event):
244
"""Handle CTCP queries and provide standard responses."""
245
ctcp_command = event.arguments[0]
246
nick = event.source.nick
247
248
if ctcp_command == "VERSION":
249
version_info = f"Python IRC Bot 1.0 using python-irc library"
250
connection.ctcp_reply(nick, f"VERSION {version_info}")
251
252
elif ctcp_command == "TIME":
253
import datetime
254
current_time = datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y")
255
connection.ctcp_reply(nick, f"TIME {current_time}")
256
257
elif ctcp_command.startswith("PING"):
258
# Echo back the ping data
259
ping_data = ctcp_command[5:] if len(ctcp_command) > 5 else ""
260
connection.ctcp_reply(nick, f"PING {ping_data}")
261
262
elif ctcp_command == "CLIENTINFO":
263
supported_commands = "VERSION TIME PING CLIENTINFO"
264
connection.ctcp_reply(nick, f"CLIENTINFO {supported_commands}")
265
266
else:
267
print(f"Unknown CTCP command from {nick}: {ctcp_command}")
268
269
def handle_ctcp_reply(connection, event):
270
"""Handle CTCP replies."""
271
reply = event.arguments[0]
272
nick = event.source.nick
273
print(f"CTCP reply from {nick}: {reply}")
274
275
def on_connect(connection, event):
276
connection.join("#test")
277
278
# Set up CTCP handling
279
client = irc.client.SimpleIRCClient()
280
client.connection.add_global_handler("welcome", on_connect)
281
client.connection.add_global_handler("ctcp", handle_ctcp)
282
client.connection.add_global_handler("ctcpreply", handle_ctcp_reply)
283
284
client.connect("irc.libera.chat", 6667, "ctcpbot")
285
client.start()
286
```
287
288
### DCC Chat Example
289
290
```python
291
import irc.client
292
import threading
293
294
class DCCChatBot:
295
def __init__(self):
296
self.client = irc.client.SimpleIRCClient()
297
self.dcc_connections = {}
298
self.setup_handlers()
299
300
def setup_handlers(self):
301
"""Set up IRC and DCC event handlers."""
302
def on_connect(connection, event):
303
connection.join("#dcctest")
304
305
def on_pubmsg(connection, event):
306
message = event.arguments[0]
307
nick = event.source.nick
308
309
if message.startswith("!dcc"):
310
# Offer DCC chat
311
dcc = self.client.dcc("chat")
312
dcc.listen()
313
314
# Get listening address and port
315
address = dcc.socket.getsockname()
316
host_ip = "127.0.0.1" # Use actual external IP
317
port = address[1]
318
319
# Send DCC CHAT request
320
dcc_msg = f"\x01DCC CHAT chat {self._ip_to_int(host_ip)} {port}\x01"
321
connection.privmsg(nick, dcc_msg)
322
323
self.dcc_connections[nick] = dcc
324
325
def on_dcc_connect(connection, event):
326
"""Handle DCC connection established."""
327
nick = event.source.nick if event.source else "unknown"
328
print(f"DCC chat connected with {nick}")
329
330
# Send welcome message
331
connection.privmsg("Welcome to DCC chat!")
332
333
def on_dccmsg(connection, event):
334
"""Handle DCC chat messages."""
335
nick = event.source.nick if event.source else "unknown"
336
message = event.arguments[0]
337
print(f"DCC <{nick}> {message}")
338
339
# Echo message back
340
connection.privmsg(f"Echo: {message}")
341
342
def on_dcc_disconnect(connection, event):
343
"""Handle DCC disconnection."""
344
print("DCC chat disconnected")
345
346
self.client.connection.add_global_handler("welcome", on_connect)
347
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
348
self.client.connection.add_global_handler("dcc_connect", on_dcc_connect)
349
self.client.connection.add_global_handler("dccmsg", on_dccmsg)
350
self.client.connection.add_global_handler("dcc_disconnect", on_dcc_disconnect)
351
352
def _ip_to_int(self, ip_str):
353
"""Convert IP address string to integer."""
354
parts = ip_str.split('.')
355
return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])
356
357
def start(self):
358
"""Start the bot."""
359
self.client.connect("irc.libera.chat", 6667, "dccbot")
360
self.client.start()
361
362
# Usage
363
bot = DCCChatBot()
364
bot.start()
365
```
366
367
### Server Features Detection
368
369
```python
370
import irc.client
371
372
class FeatureAwareBot:
373
def __init__(self):
374
self.client = irc.client.SimpleIRCClient()
375
self.server_features = None
376
self.setup_handlers()
377
378
def setup_handlers(self):
379
"""Set up event handlers."""
380
def on_connect(connection, event):
381
self.server_features = connection.features
382
self.analyze_features()
383
connection.join("#test")
384
385
def on_isupport(connection, event):
386
"""Handle server feature announcements."""
387
features = event.arguments
388
print(f"Server features: {features}")
389
390
# Features are automatically parsed into connection.features
391
self.analyze_features()
392
393
def on_pubmsg(connection, event):
394
message = event.arguments[0]
395
channel = event.target
396
397
if message == "!features":
398
self.show_features(connection, channel)
399
elif message == "!limits":
400
self.show_limits(connection, channel)
401
402
self.client.connection.add_global_handler("welcome", on_connect)
403
self.client.connection.add_global_handler("isupport", on_isupport)
404
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
405
406
def analyze_features(self):
407
"""Analyze server features and adapt behavior."""
408
if not self.server_features:
409
return
410
411
features = self.server_features
412
413
# Check nickname length limit
414
if hasattr(features, 'NICKLEN'):
415
print(f"Maximum nickname length: {features.NICKLEN}")
416
417
# Check channel types
418
if hasattr(features, 'CHANTYPES'):
419
print(f"Supported channel types: {features.CHANTYPES}")
420
421
# Check channel modes
422
if hasattr(features, 'CHANMODES'):
423
print(f"Channel modes: {features.CHANMODES}")
424
425
# Check prefix modes (op, voice, etc.)
426
if hasattr(features, 'PREFIX'):
427
print(f"User prefix modes: {features.PREFIX}")
428
429
# Check maximum targets per command
430
if hasattr(features, 'TARGMAX'):
431
print(f"Target maximums: {features.TARGMAX}")
432
433
def show_features(self, connection, channel):
434
"""Show server features in channel."""
435
if not self.server_features:
436
connection.privmsg(channel, "Server features not available")
437
return
438
439
features = []
440
for attr in dir(self.server_features):
441
if not attr.startswith('_'):
442
value = getattr(self.server_features, attr)
443
if value is not None:
444
features.append(f"{attr}={value}")
445
446
# Send features in chunks to avoid message length limits
447
chunk_size = 5
448
for i in range(0, len(features), chunk_size):
449
chunk = features[i:i+chunk_size]
450
connection.privmsg(channel, " | ".join(chunk))
451
452
def show_limits(self, connection, channel):
453
"""Show server limits."""
454
limits = []
455
456
if hasattr(self.server_features, 'NICKLEN'):
457
limits.append(f"Nick: {self.server_features.NICKLEN}")
458
459
if hasattr(self.server_features, 'CHANNELLEN'):
460
limits.append(f"Channel: {self.server_features.CHANNELLEN}")
461
462
if hasattr(self.server_features, 'TOPICLEN'):
463
limits.append(f"Topic: {self.server_features.TOPICLEN}")
464
465
if hasattr(self.server_features, 'KICKLEN'):
466
limits.append(f"Kick: {self.server_features.KICKLEN}")
467
468
if limits:
469
connection.privmsg(channel, f"Server limits: {' | '.join(limits)}")
470
else:
471
connection.privmsg(channel, "No length limits announced")
472
473
def start(self):
474
"""Start the bot."""
475
self.client.connect("irc.libera.chat", 6667, "featurebot")
476
self.client.start()
477
478
# Usage
479
bot = FeatureAwareBot()
480
bot.start()
481
```
482
483
### IRCv3 Tags Handler
484
485
```python
486
import irc.client
487
import irc.message
488
489
class IRCv3Bot:
490
def __init__(self):
491
self.client = irc.client.SimpleIRCClient()
492
self.capabilities = set()
493
self.setup_handlers()
494
495
def setup_handlers(self):
496
"""Set up event handlers."""
497
def on_cap_ls(connection, event):
498
"""Handle capability list."""
499
available_caps = event.arguments[1].split()
500
print(f"Available capabilities: {available_caps}")
501
502
# Request desired capabilities
503
desired_caps = ["message-tags", "server-time", "account-tag", "batch"]
504
to_request = [cap for cap in desired_caps if cap in available_caps]
505
506
if to_request:
507
connection.send_raw(f"CAP REQ :{' '.join(to_request)}")
508
connection.send_raw("CAP END")
509
510
def on_cap_ack(connection, event):
511
"""Handle capability acknowledgment."""
512
acked_caps = event.arguments[1].split()
513
self.capabilities.update(acked_caps)
514
print(f"Enabled capabilities: {acked_caps}")
515
516
def on_connect(connection, event):
517
"""Handle connection."""
518
# Request capabilities before registration
519
connection.send_raw("CAP LS 302")
520
connection.join("#ircv3test")
521
522
def on_pubmsg(connection, event):
523
"""Handle public messages with IRCv3 tags."""
524
self.handle_tagged_message(connection, event)
525
526
def on_privmsg(connection, event):
527
"""Handle private messages with IRCv3 tags."""
528
self.handle_tagged_message(connection, event)
529
530
# Set up handlers
531
self.client.connection.add_global_handler("cap", self.handle_cap)
532
self.client.connection.add_global_handler("welcome", on_connect)
533
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
534
self.client.connection.add_global_handler("privmsg", on_privmsg)
535
536
def handle_cap(self, connection, event):
537
"""Handle all CAP subcommands."""
538
cap_command = event.arguments[0]
539
540
if cap_command == "LS":
541
self.on_cap_ls(connection, event)
542
elif cap_command == "ACK":
543
self.on_cap_ack(connection, event)
544
elif cap_command == "NAK":
545
rejected_caps = event.arguments[1].split()
546
print(f"Rejected capabilities: {rejected_caps}")
547
connection.send_raw("CAP END")
548
549
def handle_tagged_message(self, connection, event):
550
"""Handle messages with IRCv3 tags."""
551
tags = event.tags or {}
552
nick = event.source.nick if event.source else "server"
553
message = event.arguments[0] if event.arguments else ""
554
555
print(f"<{nick}> {message}")
556
557
# Process specific tags
558
if "account" in tags:
559
print(f" Account: {tags['account']}")
560
561
if "server-time" in tags:
562
print(f" Time: {tags['server-time']}")
563
564
if "batch" in tags:
565
print(f" Batch: {tags['batch']}")
566
567
if "reply" in tags:
568
print(f" Reply to: {tags['reply']}")
569
570
# Respond to tagged commands
571
if message.startswith("!tag"):
572
response_tags = {}
573
if "msgid" in tags:
574
response_tags["reply"] = tags["msgid"]
575
576
# Send response with tags (if server supports message-tags)
577
if "message-tags" in self.capabilities and response_tags:
578
tag_string = ";".join(f"{k}={v}" for k, v in response_tags.items())
579
connection.send_raw(f"@{tag_string} PRIVMSG {event.target} :Tagged response!")
580
else:
581
connection.privmsg(event.target, "Tagged response!")
582
583
def start(self):
584
"""Start the bot."""
585
self.client.connect("irc.libera.chat", 6667, "ircv3bot")
586
self.client.start()
587
588
# Usage
589
bot = IRCv3Bot()
590
bot.start()
591
```
592
593
### Multi-Protocol Extension Bot
594
595
```python
596
import irc.client
597
import irc.ctcp
598
import time
599
600
class ExtensionBot:
601
def __init__(self):
602
self.client = irc.client.SimpleIRCClient()
603
self.dcc_connections = {}
604
self.batch_buffer = {}
605
self.setup_handlers()
606
607
def setup_handlers(self):
608
"""Set up handlers for all protocol extensions."""
609
# Basic connection
610
def on_connect(connection, event):
611
connection.send_raw("CAP LS 302") # Request IRCv3 capabilities
612
connection.join("#extensions")
613
614
# CTCP handling
615
def on_ctcp(connection, event):
616
self.handle_ctcp(connection, event)
617
618
# DCC handling
619
def on_dcc_connect(connection, event):
620
print("DCC connection established")
621
connection.privmsg("Welcome to DCC chat! Type 'help' for commands.")
622
623
def on_dccmsg(connection, event):
624
message = event.arguments[0].strip()
625
if message == "help":
626
connection.privmsg("Commands: time, quit, echo <text>")
627
elif message == "time":
628
connection.privmsg(f"Current time: {time.ctime()}")
629
elif message == "quit":
630
connection.disconnect("Goodbye!")
631
elif message.startswith("echo "):
632
connection.privmsg(f"Echo: {message[5:]}")
633
634
# IRCv3 batch handling
635
def on_batch(connection, event):
636
batch_id = event.arguments[0]
637
batch_type = event.arguments[1] if len(event.arguments) > 1 else "unknown"
638
639
if batch_id.startswith("+"):
640
# Start of batch
641
self.batch_buffer[batch_id[1:]] = {"type": batch_type, "messages": []}
642
print(f"Started batch {batch_id[1:]} of type {batch_type}")
643
elif batch_id.startswith("-"):
644
# End of batch
645
batch_data = self.batch_buffer.pop(batch_id[1:], None)
646
if batch_data:
647
print(f"Completed batch {batch_id[1:]} with {len(batch_data['messages'])} messages")
648
649
# Main message handler
650
def on_pubmsg(connection, event):
651
message = event.arguments[0]
652
channel = event.target
653
nick = event.source.nick
654
655
# Handle batch messages
656
if event.tags and "batch" in event.tags:
657
batch_id = event.tags["batch"]
658
if batch_id in self.batch_buffer:
659
self.batch_buffer[batch_id]["messages"].append(event)
660
return # Don't process batched messages immediately
661
662
# Regular message processing
663
if message.startswith("!dcc"):
664
self.offer_dcc_chat(connection, nick)
665
elif message.startswith("!ctcp "):
666
target = message.split()[1]
667
ctcp_cmd = " ".join(message.split()[2:])
668
connection.send_raw(f"PRIVMSG {target} :\x01{ctcp_cmd}\x01")
669
elif message.startswith("!extensions"):
670
self.show_extensions(connection, channel)
671
672
# Set up all handlers
673
self.client.connection.add_global_handler("welcome", on_connect)
674
self.client.connection.add_global_handler("ctcp", on_ctcp)
675
self.client.connection.add_global_handler("dcc_connect", on_dcc_connect)
676
self.client.connection.add_global_handler("dccmsg", on_dccmsg)
677
self.client.connection.add_global_handler("batch", on_batch)
678
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
679
680
def handle_ctcp(self, connection, event):
681
"""Handle CTCP queries."""
682
ctcp_command = event.arguments[0]
683
nick = event.source.nick
684
685
responses = {
686
"VERSION": "ExtensionBot 1.0 - IRC Protocol Extension Demo",
687
"TIME": time.ctime(),
688
"CLIENTINFO": "VERSION TIME PING CLIENTINFO SOURCE"
689
}
690
691
if ctcp_command in responses:
692
connection.ctcp_reply(nick, f"{ctcp_command} {responses[ctcp_command]}")
693
elif ctcp_command.startswith("PING"):
694
ping_data = ctcp_command[5:] if len(ctcp_command) > 5 else ""
695
connection.ctcp_reply(nick, f"PING {ping_data}")
696
elif ctcp_command == "SOURCE":
697
connection.ctcp_reply(nick, "SOURCE https://github.com/jaraco/irc")
698
699
def offer_dcc_chat(self, connection, nick):
700
"""Offer DCC chat to user."""
701
try:
702
dcc = self.client.dcc("chat")
703
dcc.listen()
704
705
# Get connection details
706
address = dcc.socket.getsockname()
707
host_ip = "127.0.0.1" # Use actual external IP in real implementation
708
port = address[1]
709
710
# Convert IP to integer format
711
ip_parts = host_ip.split('.')
712
ip_int = (int(ip_parts[0]) << 24) + (int(ip_parts[1]) << 16) + \
713
(int(ip_parts[2]) << 8) + int(ip_parts[3])
714
715
# Send DCC CHAT offer
716
dcc_msg = f"\x01DCC CHAT chat {ip_int} {port}\x01"
717
connection.privmsg(nick, dcc_msg)
718
719
self.dcc_connections[nick] = dcc
720
print(f"Offered DCC chat to {nick} on port {port}")
721
722
except Exception as e:
723
print(f"Failed to offer DCC chat: {e}")
724
connection.privmsg(nick, "Sorry, DCC chat is not available right now.")
725
726
def show_extensions(self, connection, channel):
727
"""Show supported protocol extensions."""
728
extensions = [
729
"CTCP (Client-To-Client Protocol)",
730
"DCC Chat (Direct Client-to-Client)",
731
"IRCv3 Message Tags",
732
"IRCv3 Batches",
733
"Server Feature Detection (ISUPPORT)"
734
]
735
736
connection.privmsg(channel, "Supported extensions:")
737
for ext in extensions:
738
connection.privmsg(channel, f" • {ext}")
739
740
def start(self):
741
"""Start the bot."""
742
self.client.connect("irc.libera.chat", 6667, "extensionbot")
743
self.client.start()
744
745
# Usage
746
bot = ExtensionBot()
747
bot.start()
748
```