or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mddatabase.mdindex.mdirc-protocol.mdplugin-development.mdutilities.md

plugin-development.mddocs/

0

# Plugin Development

1

2

Sopel's plugin system is the core of its extensibility, providing a decorator-based approach for creating bot functionality. Plugins are Python modules that use decorators to register functions as commands, rules, events, and other bot behaviors.

3

4

## Capabilities

5

6

### Command Decorators

7

8

Register functions to respond to specific IRC commands triggered by a command prefix (typically `.` or `!`).

9

10

```python { .api }

11

def command(*command_names: str):

12

"""

13

Register a function as a command.

14

15

Args:

16

*command_names (str): One or more command names that trigger this function

17

18

Example:

19

@plugin.command('hello')

20

def hello_cmd(bot, trigger): ...

21

22

@plugin.command('hi', 'hello', 'hey')

23

def greet_cmd(bot, trigger): ...

24

"""

25

26

commands = command

27

"""

28

Alias to command() decorator.

29

30

This is an alias for the command() decorator, not a separate function.

31

Use @plugin.command() instead.

32

"""

33

34

def action_command(command_name: str):

35

"""

36

Register a function as an action command (CTCP ACTION).

37

38

Args:

39

command_name (str): The command name for ACTION messages

40

"""

41

42

def action_commands(*command_names: str):

43

"""

44

Register a function for multiple action commands.

45

46

Args:

47

*command_names (str): Multiple action command names

48

"""

49

50

def nickname_command(command_name: str):

51

"""

52

Register a function triggered when bot's nick is used as command.

53

54

Args:

55

command_name (str): The command name after bot's nickname

56

57

Example:

58

@plugin.nickname_command('hello')

59

def nick_hello(bot, trigger): ...

60

# Triggered by: "BotName: hello"

61

"""

62

63

def nickname_commands(*command_names: str):

64

"""

65

Register a function for multiple nickname commands.

66

67

Args:

68

*command_names (str): Multiple nickname command names

69

"""

70

```

71

72

### Rule-Based Pattern Matching

73

74

Register functions to respond to messages matching regular expression patterns.

75

76

```python { .api }

77

def rule(pattern: str):

78

"""

79

Register a function to trigger on regex pattern matches.

80

81

Args:

82

pattern (str): Regular expression pattern to match

83

84

Example:

85

@plugin.rule(r'.*\b(hello|hi)\b.*')

86

def greet_rule(bot, trigger): ...

87

"""

88

89

def rule_lazy(lazy_loader):

90

"""

91

Register a function with a lazy-loaded regex pattern.

92

93

Args:

94

lazy_loader (callable): Function returning list of regex patterns

95

"""

96

97

def find(pattern: str):

98

"""

99

Register a function to find pattern anywhere in message.

100

101

Args:

102

pattern (str): Pattern to search for in messages

103

"""

104

105

def find_lazy(lazy_loader):

106

"""

107

Register a function with lazy-loaded find patterns.

108

109

Args:

110

lazy_loader (callable): Function returning list of patterns

111

"""

112

113

def search(pattern: str):

114

"""

115

Register a function for pattern search with match groups.

116

117

Args:

118

pattern (str): Search pattern with capture groups

119

"""

120

121

def search_lazy(lazy_loader):

122

"""

123

Register a function with lazy-loaded search patterns.

124

125

Args:

126

lazy_loader (callable): Function returning list of search patterns

127

"""

128

```

129

130

### Event Handling

131

132

Register functions to respond to specific IRC events and protocol messages.

133

134

```python { .api }

135

def event(*event_types: str):

136

"""

137

Register a function to handle specific IRC events.

138

139

Args:

140

*event_types (str): IRC event types to handle (JOIN, PART, QUIT, etc.)

141

142

Example:

143

@plugin.event('JOIN')

144

def on_join(bot, trigger): ...

145

"""

146

147

def ctcp(ctcp_command: str):

148

"""

149

Register a function to handle CTCP commands.

150

151

Args:

152

ctcp_command (str): CTCP command to handle (VERSION, PING, etc.)

153

154

Example:

155

@plugin.ctcp('VERSION')

156

def ctcp_version(bot, trigger): ...

157

"""

158

```

159

160

### URL Handling

161

162

Register functions to process URLs in messages with automatic URL detection and processing.

163

164

```python { .api }

165

def url(url_pattern: str):

166

"""

167

Register a function to handle URLs matching a pattern.

168

169

Args:

170

url_pattern (str): Regex pattern for URL matching

171

172

Example:

173

@plugin.url(r'https?://example\.com/.*')

174

def handle_example_url(bot, trigger): ...

175

"""

176

177

def url_lazy(lazy_loader):

178

"""

179

Register a function with lazy-loaded URL patterns.

180

181

Args:

182

lazy_loader (callable): Function returning list of URL patterns

183

"""

184

```

185

186

### Scheduled Tasks

187

188

Register functions to run at regular intervals or specific times.

189

190

```python { .api }

191

def interval(seconds: int):

192

"""

193

Register a function to run at regular intervals.

194

195

Args:

196

seconds (int): Interval between executions in seconds

197

198

Example:

199

@plugin.interval(300) # Every 5 minutes

200

def periodic_task(bot): ...

201

"""

202

```

203

204

### Access Control

205

206

Decorators to restrict plugin functions based on user privileges and message context.

207

208

```python { .api }

209

def require_privmsg():

210

"""

211

Restrict function to private messages only.

212

213

Example:

214

@plugin.require_privmsg()

215

@plugin.command('secret')

216

def secret_cmd(bot, trigger): ...

217

"""

218

219

def require_chanmsg():

220

"""

221

Restrict function to channel messages only.

222

"""

223

224

def require_account():

225

"""

226

Require user to be authenticated with services.

227

"""

228

229

def require_admin():

230

"""

231

Require user to be a bot admin.

232

"""

233

234

def require_owner():

235

"""

236

Require user to be a bot owner.

237

"""

238

239

def require_privilege(level: 'AccessLevel'):

240

"""

241

Require specific channel privilege level.

242

243

Args:

244

level (AccessLevel): Minimum required privilege level

245

246

Example:

247

@plugin.require_privilege(plugin.OP)

248

@plugin.command('kick')

249

def kick_cmd(bot, trigger): ...

250

"""

251

252

def require_bot_privilege(level: 'AccessLevel'):

253

"""

254

Require bot to have specific channel privileges.

255

256

Args:

257

level (AccessLevel): Required bot privilege level

258

"""

259

```

260

261

### Rate Limiting

262

263

Control how frequently plugin functions can be triggered to prevent spam and abuse.

264

265

```python { .api }

266

def rate(user: int = 0, channel: int = 0, server: int = 0, *, message: Optional[str] = None):

267

"""

268

Apply rate limiting to plugin function.

269

270

Args:

271

user (int): Seconds between triggers per user (0 = no limit)

272

channel (int): Seconds between triggers per channel (0 = no limit)

273

server (int): Seconds between any triggers (0 = no limit)

274

message (str): Optional notice message when rate limit is reached

275

276

Example:

277

@plugin.rate(user=10, channel=30, message='Please wait!')

278

@plugin.command('expensive')

279

def expensive_cmd(bot, trigger): ...

280

"""

281

282

def rate_user(seconds: int):

283

"""

284

Rate limit per user.

285

286

Args:

287

seconds (int): Seconds between triggers per user

288

"""

289

290

def rate_channel(seconds: int):

291

"""

292

Rate limit per channel.

293

294

Args:

295

seconds (int): Seconds between triggers per channel

296

"""

297

298

def rate_global(seconds: int):

299

"""

300

Global rate limit.

301

302

Args:

303

seconds (int): Seconds between any triggers

304

"""

305

```

306

307

### Plugin Metadata and Behavior

308

309

Decorators to add metadata and control plugin function behavior.

310

311

```python { .api }

312

def label(label_name: str):

313

"""

314

Add a label to a plugin function for identification.

315

316

Args:

317

label_name (str): Label for the function

318

"""

319

320

class example:

321

"""

322

Add usage example to plugin function documentation with testing support.

323

324

Args:

325

msg (str): Example command or message

326

result (str or list, optional): Expected output for testing

327

privmsg (bool, optional): If True, test as private message

328

admin (bool, optional): If True, test as admin user

329

owner (bool, optional): If True, test as owner user

330

repeat (int, optional): Number of times to repeat test

331

re (bool, optional): Use regex matching for result

332

ignore (list, optional): Patterns to ignore in output

333

user_help (bool, optional): Include in user help output

334

online (bool, optional): Mark as online test

335

vcr (bool, optional): Record HTTP requests for testing

336

337

Example:

338

@plugin.example('.weather London', 'Weather in London: 15°C')

339

@plugin.command('weather')

340

def weather_cmd(bot, trigger): ...

341

"""

342

343

def output_prefix(prefix: str):

344

"""

345

Set output prefix for plugin function responses.

346

347

Args:

348

prefix (str): Prefix string for bot responses

349

"""

350

351

def priority(level: str):

352

"""

353

Set execution priority for plugin function.

354

355

Args:

356

level (str): Priority level ('low', 'medium', 'high')

357

"""

358

359

def thread(threaded: bool = True):

360

"""

361

Control whether function runs in separate thread.

362

363

Args:

364

threaded (bool): True to run in thread, False for main thread

365

"""

366

367

def echo():

368

"""

369

Allow function to respond to bot's own messages.

370

"""

371

372

def unblockable():

373

"""

374

Prevent function from being blocked by ignore lists.

375

"""

376

377

def allow_bots():

378

"""

379

Allow function to respond to other bots.

380

"""

381

```

382

383

### Capability Negotiation

384

385

Handle IRC capability negotiation for advanced protocol features.

386

387

```python { .api }

388

def capability(*capabilities: str):

389

"""

390

Register IRC capabilities that the plugin uses.

391

392

Args:

393

*capabilities (str): IRC capability names

394

395

Example:

396

@plugin.capability('account-tag')

397

@plugin.command('whoami')

398

def whoami_cmd(bot, trigger): ...

399

"""

400

```

401

402

## Usage Examples

403

404

### Basic Command Plugin

405

406

```python

407

from sopel import plugin

408

409

@plugin.command('hello')

410

@plugin.example('.hello')

411

def hello_command(bot, trigger):

412

"""Greet a user."""

413

bot.reply(f"Hello, {trigger.nick}!")

414

415

@plugin.command('roll')

416

@plugin.example('.roll 6')

417

@plugin.rate_user(5) # 5 second cooldown per user

418

def roll_dice(bot, trigger):

419

"""Roll a die."""

420

import random

421

sides = 6

422

if trigger.group(2):

423

try:

424

sides = int(trigger.group(2))

425

except ValueError:

426

bot.reply("Please provide a valid number.")

427

return

428

429

result = random.randint(1, sides)

430

bot.reply(f"🎲 Rolled a {result} (1-{sides})")

431

```

432

433

### Advanced Plugin with Configuration and Database

434

435

```python

436

from sopel import plugin, config, db

437

438

class WeatherSection(config.types.StaticSection):

439

api_key = config.types.ValidatedAttribute('api_key')

440

default_location = config.types.ValidatedAttribute('default_location', default='London')

441

442

@plugin.command('weather')

443

@plugin.example('.weather London')

444

@plugin.rate_channel(30) # 30 second channel cooldown

445

def weather_command(bot, trigger):

446

"""Get weather information."""

447

location = trigger.group(2) or bot.settings.weather.default_location

448

449

# Store user's last location query

450

bot.db.set_nick_value(trigger.nick, 'last_weather_location', location)

451

452

# Weather API call would go here

453

bot.reply(f"Weather for {location}: Sunny, 25°C")

454

455

@plugin.rule(r'.*\b(hot|cold|weather)\b.*')

456

@plugin.require_chanmsg()

457

def weather_mention(bot, trigger):

458

"""Respond to weather mentions."""

459

last_location = bot.db.get_nick_value(trigger.nick, 'last_weather_location')

460

if last_location:

461

bot.say(f"The weather in {last_location} is still nice!")

462

```

463

464

### Event Handler Plugin

465

466

```python

467

from sopel import plugin

468

469

@plugin.event('JOIN')

470

def welcome_user(bot, trigger):

471

"""Welcome new users to the channel."""

472

if trigger.nick != bot.nick: # Don't welcome ourselves

473

bot.say(f"Welcome to {trigger.sender}, {trigger.nick}!")

474

475

@plugin.event('PART', 'QUIT')

476

def goodbye_user(bot, trigger):

477

"""Say goodbye to leaving users."""

478

if trigger.nick != bot.nick:

479

bot.say(f"Goodbye, {trigger.nick}!")

480

```

481

482

## Types

483

484

```python { .api }

485

from typing import Optional

486

```

487

488

### Plugin Function Parameters

489

490

```python { .api }

491

# Standard plugin function signature

492

def plugin_function(bot: SopelWrapper, trigger: Trigger):

493

"""

494

Standard plugin function.

495

496

Args:

497

bot (SopelWrapper): Bot interface for sending messages and accessing data

498

trigger (Trigger): Context information about the triggering message

499

"""

500

501

# Interval function signature (no trigger)

502

def interval_function(bot: SopelWrapper):

503

"""

504

Interval function for scheduled tasks.

505

506

Args:

507

bot (SopelWrapper): Bot interface

508

"""

509

```

510

511

### Constants

512

513

```python { .api }

514

# Rate limiting bypass constant

515

NOLIMIT: int = 1

516

517

# Access level constants (imported from sopel.privileges)

518

VOICE: AccessLevel

519

HALFOP: AccessLevel

520

OP: AccessLevel

521

ADMIN: AccessLevel

522

OWNER: AccessLevel

523

OPER: AccessLevel

524

525

# Capability negotiation options

526

class CapabilityNegotiation(enum.Enum):

527

NONE: str = "none"

528

OPTIONAL: str = "optional"

529

REQUIRED: str = "required"

530

```