or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

asynchronous-client.mdbot-framework.mdconnection-management.mdevent-system.mdindex.mdprotocol-extensions.mdsynchronous-client.mdutilities.md

protocol-extensions.mddocs/

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

```