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

utilities.mddocs/

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

```