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

bot-framework.mddocs/

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

```