0
# Utilities and Tools
1
2
Sopel provides comprehensive utility functions and classes for IRC formatting, time handling, web operations, mathematical calculations, logging, and identifier management. These tools simplify common bot development tasks and provide robust functionality for plugin developers.
3
4
## Capabilities
5
6
### IRC Text Formatting
7
8
Functions and constants for applying IRC formatting codes to text messages.
9
10
```python { .api }
11
# Formatting functions
12
def bold(text: str) -> str:
13
"""
14
Apply bold formatting to text.
15
16
Args:
17
text (str): Text to make bold
18
19
Returns:
20
Text with bold formatting codes
21
"""
22
23
def italic(text: str) -> str:
24
"""
25
Apply italic formatting to text.
26
27
Args:
28
text (str): Text to make italic
29
30
Returns:
31
Text with italic formatting codes
32
"""
33
34
def underline(text: str) -> str:
35
"""
36
Apply underline formatting to text.
37
38
Args:
39
text (str): Text to underline
40
41
Returns:
42
Text with underline formatting codes
43
"""
44
45
def strikethrough(text: str) -> str:
46
"""
47
Apply strikethrough formatting to text.
48
49
Args:
50
text (str): Text to strikethrough
51
52
Returns:
53
Text with strikethrough formatting codes
54
"""
55
56
def monospace(text: str) -> str:
57
"""
58
Apply monospace formatting to text.
59
60
Args:
61
text (str): Text to make monospace
62
63
Returns:
64
Text with monospace formatting codes
65
"""
66
67
def reverse(text: str) -> str:
68
"""
69
Apply reverse formatting to text.
70
71
Args:
72
text (str): Text to reverse colors
73
74
Returns:
75
Text with reverse formatting codes
76
"""
77
78
def color(text: str, fg: int = None, bg: int = None) -> str:
79
"""
80
Apply color formatting to text.
81
82
Args:
83
text (str): Text to colorize
84
fg (int): Foreground color code (0-15)
85
bg (int): Background color code (0-15)
86
87
Returns:
88
Text with color formatting codes
89
"""
90
91
def hex_color(text: str, fg: str = None, bg: str = None) -> str:
92
"""
93
Apply hexadecimal color formatting to text.
94
95
Args:
96
text (str): Text to colorize
97
fg (str): Foreground hex color (e.g., "#FF0000")
98
bg (str): Background hex color
99
100
Returns:
101
Text with hex color formatting codes
102
"""
103
104
def plain(text: str) -> str:
105
"""
106
Remove all formatting codes from text.
107
108
Args:
109
text (str): Text to strip formatting from
110
111
Returns:
112
Plain text without formatting codes
113
"""
114
115
def hex_color(text: str, fg: str = None, bg: str = None) -> str:
116
"""
117
Apply hex color formatting to text.
118
119
Args:
120
text (str): Text to color
121
fg (str, optional): Foreground hex color (e.g., '#FF0000')
122
bg (str, optional): Background hex color (e.g., '#00FF00')
123
124
Returns:
125
Text with hex color formatting codes
126
"""
127
128
def strikethrough(text: str) -> str:
129
"""
130
Apply strikethrough formatting to text.
131
132
Args:
133
text (str): Text to strike through
134
135
Returns:
136
Text with strikethrough formatting codes
137
"""
138
139
def monospace(text: str) -> str:
140
"""
141
Apply monospace formatting to text.
142
143
Args:
144
text (str): Text to make monospace
145
146
Returns:
147
Text with monospace formatting codes
148
"""
149
150
def reverse(text: str) -> str:
151
"""
152
Apply reverse-color formatting to text.
153
154
Args:
155
text (str): Text to reverse colors
156
157
Returns:
158
Text with reverse-color formatting codes
159
"""
160
161
# Formatting constants
162
CONTROL_NORMAL: str # Reset all formatting
163
CONTROL_COLOR: str # Color control code
164
CONTROL_HEX_COLOR: str # Hex color control code
165
CONTROL_BOLD: str # Bold control code
166
CONTROL_ITALIC: str # Italic control code
167
CONTROL_UNDERLINE: str # Underline control code
168
CONTROL_STRIKETHROUGH: str # Strikethrough control code
169
CONTROL_MONOSPACE: str # Monospace control code
170
CONTROL_REVERSE: str # Reverse control code
171
172
# Color enumeration
173
class colors(enum.Enum):
174
"""Standard IRC color codes."""
175
WHITE: int = 0
176
BLACK: int = 1
177
BLUE: int = 2
178
GREEN: int = 3
179
RED: int = 4
180
BROWN: int = 5
181
PURPLE: int = 6
182
ORANGE: int = 7
183
YELLOW: int = 8
184
LIGHT_GREEN: int = 9
185
TEAL: int = 10
186
LIGHT_CYAN: int = 11
187
LIGHT_BLUE: int = 12
188
PINK: int = 13
189
GREY: int = 14
190
LIGHT_GREY: int = 15
191
```
192
193
### Time and Duration Utilities
194
195
Functions for time formatting, timezone handling, and duration calculations.
196
197
```python { .api }
198
def validate_timezone(zone: str) -> str:
199
"""
200
Validate and normalize timezone string.
201
202
Args:
203
zone (str): Timezone identifier
204
205
Returns:
206
Normalized timezone string
207
208
Raises:
209
ValueError: If timezone is invalid
210
"""
211
212
def validate_format(tformat: str) -> str:
213
"""
214
Validate time format string.
215
216
Args:
217
tformat (str): Time format string
218
219
Returns:
220
Validated format string
221
222
Raises:
223
ValueError: If format is invalid
224
"""
225
226
def get_nick_timezone(db: 'SopelDB', nick: str) -> str | None:
227
"""
228
Get timezone preference for a user.
229
230
Args:
231
db (SopelDB): Database connection
232
nick (str): User nickname
233
234
Returns:
235
User's timezone or None if not set
236
"""
237
238
def get_channel_timezone(db: 'SopelDB', channel: str) -> str | None:
239
"""
240
Get timezone preference for a channel.
241
242
Args:
243
db (SopelDB): Database connection
244
channel (str): Channel name
245
246
Returns:
247
Channel's timezone or None if not set
248
"""
249
250
def get_timezone(db: 'SopelDB' = None, config: 'Config' = None, zone: str = None,
251
nick: str = None, channel: str = None) -> str:
252
"""
253
Get timezone from various sources with fallback logic.
254
255
Args:
256
db (SopelDB): Database connection
257
config (Config): Bot configuration
258
zone (str): Explicit timezone
259
nick (str): User nickname to check
260
channel (str): Channel name to check
261
262
Returns:
263
Resolved timezone string
264
"""
265
266
def format_time(dt=None, zone=None, format=None) -> str:
267
"""
268
Format datetime with timezone and format options.
269
270
Args:
271
dt: Datetime object (defaults to now)
272
zone (str): Timezone for formatting
273
format (str): Time format string
274
275
Returns:
276
Formatted time string
277
"""
278
279
def seconds_to_split(seconds: int) -> 'Duration':
280
"""
281
Convert seconds to Duration namedtuple.
282
283
Args:
284
seconds (int): Number of seconds
285
286
Returns:
287
Duration with years, days, hours, minutes, seconds
288
"""
289
290
def get_time_unit(unit: str) -> int:
291
"""
292
Get number of seconds in a time unit.
293
294
Args:
295
unit (str): Time unit name (second, minute, hour, day, week, etc.)
296
297
Returns:
298
Number of seconds in the unit
299
"""
300
301
def seconds_to_human(seconds: int, precision: int = 2) -> str:
302
"""
303
Convert seconds to human-readable duration string.
304
305
Args:
306
seconds (int): Number of seconds
307
precision (int): Number of time units to include
308
309
Returns:
310
Human-readable duration (e.g., "2 hours, 30 minutes")
311
"""
312
313
# Duration namedtuple
314
Duration = namedtuple('Duration', ['years', 'days', 'hours', 'minutes', 'seconds'])
315
```
316
317
### Web Utilities
318
319
Functions for web requests, user agent management, and HTTP operations.
320
321
```python { .api }
322
def get_user_agent() -> str:
323
"""
324
Get default User-Agent string for web requests.
325
326
Returns:
327
User-Agent string identifying Sopel
328
"""
329
330
def get_session() -> 'requests.Session':
331
"""
332
Get configured requests session with appropriate headers.
333
334
Returns:
335
Requests session object with Sopel User-Agent
336
"""
337
338
# Additional web utilities available in sopel.tools.web module
339
```
340
341
### Mathematical Calculations
342
343
Safe mathematical expression evaluation and utility functions.
344
345
```python { .api }
346
def eval_equation(equation: str) -> float:
347
"""
348
Safely evaluate mathematical expression.
349
350
Args:
351
equation (str): Mathematical expression to evaluate
352
353
Returns:
354
Result of the calculation
355
356
Raises:
357
ValueError: If expression is invalid or unsafe
358
"""
359
360
def guarded_mul(left: float, right: float) -> float:
361
"""
362
Multiply two numbers with overflow protection.
363
364
Args:
365
left (float): First number
366
right (float): Second number
367
368
Returns:
369
Product of the numbers
370
371
Raises:
372
ValueError: If result would overflow
373
"""
374
375
def guarded_pow(num: float, exp: float) -> float:
376
"""
377
Raise number to power with complexity protection.
378
379
Args:
380
num (float): Base number
381
exp (float): Exponent
382
383
Returns:
384
Result of num ** exp
385
386
Raises:
387
ValueError: If operation is too complex
388
"""
389
390
def pow_complexity(num: int, exp: int) -> float:
391
"""
392
Calculate complexity score for power operation.
393
394
Args:
395
num (int): Base number
396
exp (int): Exponent
397
398
Returns:
399
Complexity score for the operation
400
"""
401
```
402
403
### Identifier Handling
404
405
IRC identifier management with proper case-insensitive comparison.
406
407
```python { .api }
408
class Identifier(str):
409
"""
410
IRC identifier with RFC1459 case-insensitive comparison.
411
412
Handles IRC nickname and channel name comparison properly.
413
"""
414
415
def __new__(cls, identifier: str) -> 'Identifier':
416
"""Create new identifier instance."""
417
418
def lower(self) -> str:
419
"""Get RFC1459 lowercase version of identifier."""
420
421
class IdentifierFactory:
422
"""Factory for creating Identifier instances."""
423
424
def __init__(self, case_mapping: str = 'rfc1459'):
425
"""
426
Initialize identifier factory.
427
428
Args:
429
case_mapping (str): Case mapping to use ('rfc1459' or 'ascii')
430
"""
431
432
def __call__(self, identifier: str) -> Identifier:
433
"""Create identifier using this factory's case mapping."""
434
435
def ascii_lower(text: str) -> str:
436
"""
437
Convert text to lowercase using ASCII rules.
438
439
Args:
440
text (str): Text to convert
441
442
Returns:
443
Lowercase text using ASCII case mapping
444
"""
445
```
446
447
### Memory Storage Classes
448
449
Utility classes for storing data with identifier-based access.
450
451
```python { .api }
452
class SopelMemory(dict):
453
"""
454
Dictionary subclass for storing bot data.
455
456
Provides additional utility methods for data management.
457
"""
458
459
def __init__(self):
460
"""Initialize empty memory storage."""
461
462
def lock(self, key: str) -> 'threading.Lock':
463
"""
464
Get thread lock for a specific key.
465
466
Args:
467
key (str): Key to get lock for
468
469
Returns:
470
Threading lock object
471
"""
472
473
class SopelIdentifierMemory(SopelMemory):
474
"""
475
Memory storage using IRC identifiers as keys.
476
477
Provides case-insensitive access using IRC identifier rules.
478
"""
479
480
def __init__(self, identifier_factory: IdentifierFactory = None):
481
"""
482
Initialize identifier memory.
483
484
Args:
485
identifier_factory (IdentifierFactory): Factory for creating identifiers
486
"""
487
488
class SopelMemoryWithDefault(SopelMemory):
489
"""
490
Memory storage with default value factory.
491
492
Automatically creates default values for missing keys.
493
"""
494
495
def __init__(self, default_factory: callable = None):
496
"""
497
Initialize memory with default factory.
498
499
Args:
500
default_factory (callable): Function to create default values
501
"""
502
```
503
504
### Logging Utilities
505
506
Functions for plugin logging and message output.
507
508
```python { .api }
509
def get_logger(plugin_name: str) -> 'logging.Logger':
510
"""
511
Get logger instance for a plugin.
512
513
Args:
514
plugin_name (str): Name of the plugin
515
516
Returns:
517
Logger configured for the plugin
518
"""
519
520
def get_sendable_message(text: str, max_length: int = 400) -> tuple[str, str]:
521
"""
522
Split text into sendable message and excess.
523
524
Args:
525
text (str): Text to split
526
max_length (int): Maximum message length in bytes
527
528
Returns:
529
Tuple of (sendable_text, excess_text)
530
"""
531
532
def get_hostmask_regex(mask: str) -> 'Pattern':
533
"""
534
Create regex pattern for IRC hostmask matching.
535
536
Args:
537
mask (str): Hostmask pattern with wildcards
538
539
Returns:
540
Compiled regex pattern for matching
541
"""
542
543
def chain_loaders(*lazy_loaders) -> callable:
544
"""
545
Chain multiple lazy loader functions together.
546
547
Args:
548
*lazy_loaders: Lazy loader functions
549
550
Returns:
551
Combined lazy loader function
552
"""
553
```
554
555
## Usage Examples
556
557
### IRC Text Formatting
558
559
```python
560
from sopel import plugin
561
from sopel.formatting import bold, color, italic, plain
562
563
@plugin.command('format')
564
@plugin.example('.format Hello World')
565
def format_example(bot, trigger):
566
"""Demonstrate text formatting."""
567
text = trigger.group(2) or "Sample Text"
568
569
# Apply various formatting
570
formatted_examples = [
571
bold(text),
572
italic(text),
573
color(text, colors.RED),
574
color(text, colors.WHITE, colors.BLUE),
575
bold(color(text, colors.GREEN))
576
]
577
578
for example in formatted_examples:
579
bot.say(example)
580
581
# Show plain text version
582
bot.say(f"Plain: {plain(formatted_examples[0])}")
583
584
@plugin.command('rainbow')
585
def rainbow_text(bot, trigger):
586
"""Create rainbow-colored text."""
587
text = trigger.group(2) or "RAINBOW"
588
rainbow_colors = [colors.RED, colors.ORANGE, colors.YELLOW,
589
colors.GREEN, colors.BLUE, colors.PURPLE]
590
591
colored_chars = []
592
for i, char in enumerate(text):
593
if char != ' ':
594
color_code = rainbow_colors[i % len(rainbow_colors)]
595
colored_chars.append(color(char, color_code))
596
else:
597
colored_chars.append(char)
598
599
bot.say(''.join(colored_chars))
600
```
601
602
### Time and Duration Utilities
603
604
```python
605
from sopel import plugin
606
from sopel.tools.time import format_time, seconds_to_human, get_time_unit
607
608
@plugin.command('time')
609
@plugin.example('.time UTC')
610
def time_command(bot, trigger):
611
"""Show current time in specified timezone."""
612
zone = trigger.group(2) or 'UTC'
613
614
try:
615
current_time = format_time(zone=zone, format='%Y-%m-%d %H:%M:%S %Z')
616
bot.reply(f"Current time in {zone}: {current_time}")
617
except ValueError as e:
618
bot.reply(f"Invalid timezone: {e}")
619
620
@plugin.command('uptime')
621
def uptime_command(bot, trigger):
622
"""Show bot uptime."""
623
import time
624
625
# Calculate uptime (this would need to be tracked elsewhere)
626
uptime_seconds = int(time.time() - bot.start_time) # Hypothetical
627
uptime_human = seconds_to_human(uptime_seconds)
628
629
bot.reply(f"Bot uptime: {uptime_human}")
630
631
@plugin.command('remind')
632
@plugin.example('.remind 30m Check the logs')
633
def remind_command(bot, trigger):
634
"""Set a reminder with time parsing."""
635
args = trigger.group(2)
636
if not args:
637
bot.reply("Usage: .remind <time> <message>")
638
return
639
640
parts = args.split(' ', 1)
641
if len(parts) < 2:
642
bot.reply("Usage: .remind <time> <message>")
643
return
644
645
time_str, message = parts
646
647
# Parse time string (simplified)
648
try:
649
if time_str.endswith('m'):
650
minutes = int(time_str[:-1])
651
seconds = minutes * get_time_unit('minute')
652
elif time_str.endswith('h'):
653
hours = int(time_str[:-1])
654
seconds = hours * get_time_unit('hour')
655
else:
656
seconds = int(time_str)
657
658
# Schedule reminder (would need actual scheduling)
659
bot.reply(f"Reminder set for {seconds_to_human(seconds)}: {message}")
660
661
except ValueError:
662
bot.reply("Invalid time format. Use: 30m, 2h, or seconds")
663
```
664
665
### Mathematical Calculations
666
667
```python
668
from sopel import plugin
669
from sopel.tools.calculation import eval_equation
670
671
@plugin.command('calc')
672
@plugin.example('.calc 2 + 2 * 3')
673
def calc_command(bot, trigger):
674
"""Safely calculate mathematical expressions."""
675
expression = trigger.group(2)
676
if not expression:
677
bot.reply("Usage: .calc <expression>")
678
return
679
680
try:
681
result = eval_equation(expression)
682
bot.reply(f"{expression} = {result}")
683
except ValueError as e:
684
bot.reply(f"Calculation error: {e}")
685
except Exception as e:
686
bot.reply(f"Invalid expression: {e}")
687
688
@plugin.command('convert')
689
@plugin.example('.convert 100 F to C')
690
def convert_command(bot, trigger):
691
"""Temperature conversion with calculations."""
692
args = trigger.group(2)
693
if not args:
694
bot.reply("Usage: .convert <temp> <F|C> to <C|F>")
695
return
696
697
parts = args.split()
698
if len(parts) != 4 or parts[2].lower() != 'to':
699
bot.reply("Usage: .convert <temp> <F|C> to <C|F>")
700
return
701
702
try:
703
temp = float(parts[0])
704
from_unit = parts[1].upper()
705
to_unit = parts[3].upper()
706
707
if from_unit == 'F' and to_unit == 'C':
708
result = (temp - 32) * 5/9
709
bot.reply(f"{temp}°F = {result:.2f}°C")
710
elif from_unit == 'C' and to_unit == 'F':
711
result = temp * 9/5 + 32
712
bot.reply(f"{temp}°C = {result:.2f}°F")
713
else:
714
bot.reply("Supported conversions: F to C, C to F")
715
except ValueError:
716
bot.reply("Invalid temperature value")
717
```
718
719
### Identifier and Memory Usage
720
721
```python
722
from sopel import plugin
723
from sopel.tools import Identifier, SopelMemory
724
725
# Plugin-level memory storage
726
user_scores = SopelMemory()
727
728
@plugin.command('score')
729
def score_command(bot, trigger):
730
"""Show or modify user scores."""
731
args = trigger.group(2)
732
733
if not args:
734
# Show current user's score
735
nick = Identifier(trigger.nick)
736
score = user_scores.get(nick, 0)
737
bot.reply(f"Your score: {score}")
738
return
739
740
parts = args.split()
741
if len(parts) == 1:
742
# Show specified user's score
743
nick = Identifier(parts[0])
744
score = user_scores.get(nick, 0)
745
bot.reply(f"Score for {nick}: {score}")
746
elif len(parts) == 2:
747
# Set score (admin only)
748
if trigger.nick not in bot.settings.core.admins:
749
bot.reply("Only admins can set scores")
750
return
751
752
nick = Identifier(parts[0])
753
try:
754
new_score = int(parts[1])
755
user_scores[nick] = new_score
756
bot.reply(f"Set score for {nick} to {new_score}")
757
except ValueError:
758
bot.reply("Score must be a number")
759
760
@plugin.command('leaderboard')
761
def leaderboard_command(bot, trigger):
762
"""Show score leaderboard."""
763
if not user_scores:
764
bot.reply("No scores recorded yet")
765
return
766
767
# Sort by score (descending)
768
sorted_scores = sorted(user_scores.items(), key=lambda x: x[1], reverse=True)
769
top_5 = sorted_scores[:5]
770
771
leaderboard = ["🏆 Leaderboard:"]
772
for i, (nick, score) in enumerate(top_5, 1):
773
leaderboard.append(f"{i}. {nick}: {score}")
774
775
bot.reply(" | ".join(leaderboard))
776
```
777
778
### Logging and Debugging
779
780
```python
781
from sopel import plugin
782
from sopel.tools import get_logger
783
784
# Get plugin-specific logger
785
LOGGER = get_logger('my_plugin')
786
787
@plugin.command('debug')
788
@plugin.require_admin()
789
def debug_command(bot, trigger):
790
"""Show debug information."""
791
args = trigger.group(2)
792
793
if args == 'memory':
794
# Show memory usage information
795
import psutil
796
process = psutil.Process()
797
memory_mb = process.memory_info().rss / 1024 / 1024
798
bot.reply(f"Memory usage: {memory_mb:.1f} MB")
799
800
elif args == 'channels':
801
# Show channel information
802
channel_count = len(bot.channels)
803
channel_list = list(bot.channels.keys())[:5] # First 5
804
bot.reply(f"In {channel_count} channels: {', '.join(channel_list)}")
805
806
elif args == 'users':
807
# Show user information
808
user_count = len(bot.users)
809
bot.reply(f"Tracking {user_count} users")
810
811
else:
812
bot.reply("Debug options: memory, channels, users")
813
814
# Log debug information
815
LOGGER.info(f"Debug command used by {trigger.nick}: {args}")
816
817
@plugin.command('log')
818
@plugin.require_owner()
819
def log_test(bot, trigger):
820
"""Test logging at different levels."""
821
message = trigger.group(2) or "Test message"
822
823
LOGGER.debug(f"Debug: {message}")
824
LOGGER.info(f"Info: {message}")
825
LOGGER.warning(f"Warning: {message}")
826
LOGGER.error(f"Error: {message}")
827
828
bot.reply("Log messages sent at all levels")
829
```
830
831
## Types
832
833
### Formatting Types
834
835
```python { .api }
836
# Control character constants
837
CONTROL_FORMATTING: list # List of all formatting control chars
838
CONTROL_NON_PRINTING: list # List of non-printing control chars
839
```
840
841
### Time Types
842
843
```python { .api }
844
Duration = namedtuple('Duration', ['years', 'days', 'hours', 'minutes', 'seconds'])
845
```
846
847
### Memory Types
848
849
```python { .api }
850
# Type aliases for memory storage
851
IdentifierMemory = SopelIdentifierMemory
852
Memory = SopelMemory
853
MemoryWithDefault = SopelMemoryWithDefault
854
```