forked from progval/Limnoria
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconf.py
1516 lines (1337 loc) · 65.9 KB
/
conf.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
###
# Copyright (c) 2002-2005, Jeremiah Fincher
# Copyright (c) 2008-2009,2011, James McCoy
# Copyright (c) 2010-2021, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import os
import sys
import time
import socket
import random
from . import ircutils, registry, utils
from .utils import minisix
from .utils.net import isSocketAddress
from .version import version
from .i18n import PluginInternationalization
_ = PluginInternationalization()
if minisix.PY2:
from urllib2 import build_opener, install_opener, ProxyHandler
else:
from urllib.request import build_opener, install_opener, ProxyHandler
###
# *** The following variables are affected by command-line options. They are
# not registry variables for a specific reason. Do *not* change these to
# registry variables without first consulting people smarter than yourself.
###
###
# daemonized: This determines whether or not the bot has been daemonized
# (i.e., set to run in the background). Obviously, this defaults
# to False. A command-line option for obvious reasons.
###
daemonized = False
###
# allowDefaultOwner: True if supybot.capabilities is allowed not to include
# '-owner' -- that is, if all users should be automatically
# recognized as owners. That would suck, hence we require a
# command-line option to allow this stupidity.
###
allowDefaultOwner = False
###
# Here we replace values in other modules as appropriate.
###
utils.web.defaultHeaders['User-agent'] = \
'Mozilla/5.0 (Compatible; Limnoria %s)' % version
###
# The standard registry.
###
supybot = registry.Group()
supybot.setName('supybot')
def registerGroup(Group, name, group=None, **kwargs):
if kwargs:
group = registry.Group(**kwargs)
return Group.register(name, group)
def registerGlobalValue(group, name, value):
value._networkValue = False
value._channelValue = False
return group.register(name, value)
def registerNetworkValue(group, name, value):
value._supplyDefault = True
value._networkValue = True
value._channelValue = False
g = group.register(name, value)
gname = g._name.lower()
for name in registry._cache.keys():
if name.lower().startswith(gname) and len(gname) < len(name):
name = name[len(gname)+1:] # +1 for .
parts = registry.split(name)
if len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]):
# This gets the network values so they always persist.
g.get(parts[0])()
return g
def registerChannelValue(group, name, value, opSettable=True):
value._supplyDefault = True
value._networkValue = True
value._channelValue = True
value._opSettable = opSettable
g = group.register(name, value)
gname = g._name.lower()
for name in registry._cache.keys():
if name.lower().startswith(gname) and len(gname) < len(name):
name = name[len(gname)+1:] # +1 for .
parts = registry.split(name)
if len(parts) == 2 and parts[0] and parts[0].startswith(':') \
and parts[1] and ircutils.isChannel(parts[1]):
# This gets the network+channel values so they always persist.
g.get(parts[0])()
g.get(parts[0]).get(parts[1])()
elif len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]):
# Old-style variant of the above, without a network
g.get(parts[0])()
return g
def registerPlugin(name, currentValue=None, public=True):
group = registerGlobalValue(supybot.plugins, name,
registry.Boolean(False, _("""Determines whether this plugin is loaded
by default."""), showDefault=False))
supybot.plugins().add(name)
registerGlobalValue(group, 'public',
registry.Boolean(public, _("""Determines whether this plugin is
publicly visible.""")))
if currentValue is not None:
supybot.plugins.get(name).setValue(currentValue)
registerGroup(users.plugins, name)
return group
def get(group, channel=None, network=None):
return group.getSpecific(channel=channel, network=network)()
###
# The user info registry.
###
users = registry.Group()
users.setName('users')
registerGroup(users, 'plugins', orderAlphabetically=True)
def registerUserValue(group, name, value):
assert group._name.startswith('users')
value._supplyDefault = True
group.register(name, value)
class ValidNick(registry.String):
"""Value must be a valid IRC nick."""
__slots__ = ()
def setValue(self, v):
if not ircutils.isNick(v):
self.error()
else:
registry.String.setValue(self, v)
class ValidNickOrEmpty(ValidNick):
"""Value must be a valid IRC nick or empty."""
__slots__ = ()
def setValue(self, v):
if v != '' and not ircutils.isNick(v):
self.error()
else:
registry.String.setValue(self, v)
class ValidNicks(registry.SpaceSeparatedListOf):
__slots__ = ()
Value = ValidNick
class ValidNickAllowingPercentS(ValidNick):
"""Value must be a valid IRC nick, with the possible exception of a %s
in it."""
__slots__ = ()
def setValue(self, v):
# If this works, it's a valid nick, aside from the %s.
try:
ValidNick.setValue(self, v.replace('%s', ''))
# It's valid aside from the %s, we'll let it through.
registry.String.setValue(self, v)
except registry.InvalidRegistryValue:
self.error()
class ValidNicksAllowingPercentS(ValidNicks):
__slots__ = ()
Value = ValidNickAllowingPercentS
class ValidChannel(registry.String):
"""Value must be a valid IRC channel name."""
__slots__ = ('channel',)
def setValue(self, v):
self.channel = v
if ',' in v:
# To prevent stupid users from: a) trying to add a channel key
# with a comma in it, b) trying to add channels separated by
# commas instead of spaces
try:
(channel, _) = v.split(',')
except ValueError:
self.error()
else:
channel = v
if not ircutils.isChannel(channel):
self.error()
else:
registry.String.setValue(self, v)
def error(self):
try:
super(ValidChannel, self).error()
except registry.InvalidRegistryValue as e:
e.channel = self.channel
raise e
class ValidHostmask(registry.String):
"""Value must be a valid user hostmask."""
__slots__ = ()
def setValue(self, v):
if not ircutils.isUserHostmask(v):
self.error()
super(ValidHostmask, self).setValue(v)
registerGlobalValue(supybot, 'nick',
ValidNick('supybot', _("""Determines the bot's default nick.""")))
registerGlobalValue(supybot.nick, 'alternates',
ValidNicksAllowingPercentS(['%s`', '%s_'], _("""Determines what alternative
nicks will be used if the primary nick (supybot.nick) isn't available. A
%s in this nick is replaced by the value of supybot.nick when used. If no
alternates are given, or if all are used, the supybot.nick will be perturbed
appropriately until an unused nick is found.""")))
registerGlobalValue(supybot, 'ident',
ValidNick('limnoria', _("""Determines the bot's ident string, if the server
doesn't provide one by default.""")))
# Although empty version strings are theoretically allowed by the RFC,
# popular IRCds do not.
# So, we keep replacing the empty string by the current version for
# bots which are migrated from Supybot or an old version of Limnoria
# (whose default value of supybot.user is the empty string).
class VersionIfEmpty(registry.String):
__slots__ = ()
def __call__(self):
ret = registry.String.__call__(self)
if not ret:
ret = 'Limnoria $version'
return ret
registerGlobalValue(supybot, 'user',
VersionIfEmpty('Limnoria $version', _("""Determines the real name which the bot sends to
the server. A standard real name using the current version of the bot
will be generated if this is left empty.""")))
class Networks(registry.SpaceSeparatedSetOfStrings):
__slots__ = ()
List = ircutils.IrcSet
registerGlobalValue(supybot, 'networks',
Networks([], _("""Determines what networks the bot will connect to."""),
orderAlphabetically=True))
class Servers(registry.SpaceSeparatedListOfStrings):
__slots__ = ()
def normalize(self, s):
if ':' not in s:
s += ':6667'
return s
def convert(self, s):
from .drivers import Server
s = self.normalize(s)
(hostname, port) = s.rsplit(':', 1)
# support for `[ipv6]:port` format
if hostname.startswith("[") and hostname.endswith("]"):
hostname = hostname[1:-1]
port = int(port)
return Server(hostname, port, None, force_tls_verification=False)
def __call__(self):
L = registry.SpaceSeparatedListOfStrings.__call__(self)
return list(map(self.convert, L))
def __str__(self):
return ' '.join(registry.SpaceSeparatedListOfStrings.__call__(self))
def append(self, s):
L = registry.SpaceSeparatedListOfStrings.__call__(self)
L.append(s)
class SocksProxy(registry.String):
"""Value must be a valid hostname:port string."""
__slots__ = ()
def setValue(self, v):
# TODO: improve checks
if ':' not in v:
self.error()
try:
int(v.rsplit(':', 1)[1])
except ValueError:
self.error()
super(SocksProxy, self).setValue(v)
class SpaceSeparatedSetOfChannels(registry.SpaceSeparatedListOf):
__slots__ = ()
sorted = True
List = ircutils.IrcSet
Value = ValidChannel
def join(self, channel):
from . import ircmsgs # Don't put this globally! It's recursive.
key = self.key.get(channel)()
if key:
return ircmsgs.join(channel, key)
else:
return ircmsgs.join(channel)
def joins(self):
from . import ircmsgs # Don't put this globally! It's recursive.
channels = []
channels_with_key = []
keys = []
old = None
msgs = []
msg = None
for channel in self():
key = self.key.get(channel)()
if key:
keys.append(key)
channels_with_key.append(channel)
else:
channels.append(channel)
msg = ircmsgs.joins(channels_with_key + channels, keys)
if len(str(msg)) > 512:
# Use previous short enough join message
msgs.append(old)
# Reset and construct a new join message using the current
# channel.
keys = []
channels_with_key = []
channels = []
if key:
keys.append(key)
channels_with_key.append(channel)
else:
channels.append(channel)
msg = ircmsgs.joins(channels_with_key + channels, keys)
old = msg
if msg:
msgs.append(msg)
return msgs
else:
# Let's be explicit about it
return None
class ValidSaslMechanism(registry.OnlySomeStrings):
__slots__ = ()
validStrings = ('ecdsa-nist256p-challenge', 'external', 'plain',
'scram-sha-256')
class SpaceSeparatedListOfSaslMechanisms(registry.SpaceSeparatedListOf):
__slots__ = ()
Value = ValidSaslMechanism
def registerNetwork(name, password='', ssl=True, sasl_username='',
sasl_password=''):
network = registerGroup(supybot.networks, name)
registerGlobalValue(network, 'password', registry.String(password,
_("""Determines what password will be used on %s. Yes, we know that
technically passwords are server-specific and not network-specific,
but this is the best we can do right now.""") % name, private=True))
registerGlobalValue(network, 'servers', Servers([],
_("""Space-separated list of servers the bot will connect to for %s.
Each will be tried in order, wrapping back to the first when the cycle
is completed.""") % name))
registerGlobalValue(network, 'channels', SpaceSeparatedSetOfChannels([],
_("""Space-separated list of channels the bot will join only on %s.""")
% name, private=True))
registerGlobalValue(network, 'ssl', registry.Boolean(ssl,
_("""Determines whether the bot will attempt to connect with SSL
sockets to %s.""") % name))
registerGlobalValue(network.ssl, 'serverFingerprints',
registry.SpaceSeparatedSetOfStrings([], format(_("""Space-separated list
of fingerprints of trusted certificates for this network.
Supported hash algorithms are: %L.
If non-empty, Certification Authority signatures will not be used to
verify certificates."""), utils.net.FINGERPRINT_ALGORITHMS)))
registerGlobalValue(network.ssl, 'authorityCertificate',
registry.String('', _("""A certificate that is trusted to verify
certificates of this network (aka. Certificate Authority).""")))
registerGlobalValue(network, 'requireStarttls', registry.Boolean(False,
_("""Deprecated config value, keep it to False.""")))
registerGlobalValue(network, 'certfile', registry.String('',
_("""Determines what certificate file (if any) the bot will use to
connect with SSL sockets to %s.""") % name))
registerChannelValue(network.channels, 'key', registry.String('',
_("""Determines what key (if any) will be used to join the
channel."""), private=True))
registerGlobalValue(network, 'nick', ValidNickOrEmpty('', _("""Determines
what nick the bot will use on this network. If empty, defaults to
supybot.nick.""")))
registerGlobalValue(network, 'ident', ValidNickOrEmpty('', _("""Determines
the bot's ident string, if the server doesn't provide one by default.
If empty, defaults to supybot.ident.""")))
registerGlobalValue(network, 'user', registry.String('', _("""Determines
the real name which the bot sends to the server. If empty, defaults to
supybot.user""")))
registerGlobalValue(network, 'umodes',
registry.String('', _("""Determines what user modes the bot will request
from the server when it first connects. If empty, defaults to
supybot.protocols.irc.umodes""")))
registerGlobalValue(network, 'vhost',
registry.String('', _("""Determines what vhost the bot will bind to before
connecting a server (IRC, HTTP, ...) via IPv4. If empty, defaults to
supybot.protocols.irc.vhost""")))
registerGlobalValue(network, 'vhostv6',
registry.String('', _("""Determines what vhost the bot will bind to before
connecting a server (IRC, HTTP, ...) via IPv6. If empty, defaults to
supybot.protocols.irc.vhostv6""")))
sasl = registerGroup(network, 'sasl')
registerGlobalValue(sasl, 'username', registry.String(sasl_username,
_("""Determines what SASL username will be used on %s. This should
be the bot's account name.""") % name, private=False))
registerGlobalValue(sasl, 'password', registry.String(sasl_password,
_("""Determines what SASL password will be used on %s.""") \
% name, private=True))
registerGlobalValue(sasl, 'ecdsa_key', registry.String('',
_("""Determines what SASL ECDSA key (if any) will be used on %s.
The public key must be registered with NickServ for SASL
ECDSA-NIST256P-CHALLENGE to work.""") % name, private=False))
registerGlobalValue(sasl, 'mechanisms', SpaceSeparatedListOfSaslMechanisms(
['scram-sha-256', 'external', 'ecdsa-nist256p-challenge', 'plain'],
_("""Determines what SASL mechanisms will be tried and in which order.
""")))
registerGlobalValue(sasl, 'required', registry.Boolean(False,
_("""Determines whether the bot will abort the connection if the
none of the enabled SASL mechanism succeeded.""")))
registerGlobalValue(network, 'socksproxy', registry.String('',
_("""If not empty, determines the hostname:port of the socks proxy that
will be used to connect to this network.""")))
return network
# Let's fill our networks.
for (name, s) in registry._cache.items():
if name.startswith('supybot.networks.'):
parts = name.split('.')
name = parts[2]
if name != 'default':
registerNetwork(name)
###
# Reply/error tweaking.
###
registerGroup(supybot, 'reply')
registerGroup(supybot.reply, 'format')
registerChannelValue(supybot.reply.format, 'url',
registry.String('<%s>', _("""Determines how urls should be formatted.""")))
def url(s):
if s:
return supybot.reply.format.url() % s
else:
return ''
utils.str.url = url
registerChannelValue(supybot.reply.format, 'time',
registry.String('%Y-%m-%dT%H:%M:%S%z', _("""Determines how timestamps
printed for human reading should be formatted. Refer to the Python
documentation for the time module to see valid formatting characters for
time formats.""")))
def timestamp(t):
if t is None:
t = time.time()
if isinstance(t, float) or isinstance(t, int):
t = time.localtime(t)
format = get(supybot.reply.format.time, dynamic.channel)
return time.strftime(format, t)
utils.str.timestamp = timestamp
registerGroup(supybot.reply.format.time, 'elapsed')
registerChannelValue(supybot.reply.format.time.elapsed, 'short',
registry.Boolean(False, _("""Determines whether elapsed times will be given
as "1 day, 2 hours, 3 minutes, and 15 seconds" or as "1d 2h 3m 15s".""")))
originalTimeElapsed = utils.timeElapsed
def timeElapsed(*args, **kwargs):
kwargs['short'] = supybot.reply.format.time.elapsed.short()
return originalTimeElapsed(*args, **kwargs)
utils.timeElapsed = timeElapsed
registerGroup(supybot.reply.format, 'list')
registerChannelValue(supybot.reply.format.list, 'maximumItems',
registry.NonNegativeInteger(0, _("""Maximum number of items in a list
before the end is replaced with 'and others'. Set to 0 to always
show the entire list.""")))
originalCommaAndify = utils.str.commaAndify
def commaAndify(seq, *args, **kwargs):
maximum_items = supybot.reply.format.list.maximumItems.getSpecific(
channel=dynamic.channel,
network=getattr(dynamic.irc, 'network', None))()
if maximum_items:
seq = list(seq)
initial_length = len(seq)
if len(seq) > maximum_items:
seq = seq[:maximum_items]
nb_skipped = initial_length - maximum_items + 1
# Even though nb_skipped is always >= 2, some languages require
# nItems for proper pluralization.
seq[-1] = utils.str.nItems(nb_skipped, _('other'))
return originalCommaAndify(seq, *args, **kwargs)
utils.str.commaAndify = commaAndify
registerGlobalValue(supybot.reply, 'maximumLength',
registry.Integer(512*256, _("""Determines the absolute maximum length of
the bot's reply -- no reply will be passed through the bot with a length
greater than this.""")))
registerChannelValue(supybot.reply, 'mores',
registry.Boolean(True, _("""Determines whether the bot will break up long
messages into chunks and allow users to use the 'more' command to get the
remaining chunks.""")))
registerChannelValue(supybot.reply.mores, 'maximum',
registry.PositiveInteger(50, _("""Determines what the maximum number of
chunks (for use with the 'more' command) will be.""")))
registerChannelValue(supybot.reply.mores, 'length',
registry.NonNegativeInteger(0, _("""Determines how long individual chunks
will be. If set to 0, uses our super-tweaked,
get-the-most-out-of-an-individual-message default.""")))
registerChannelValue(supybot.reply.mores, 'instant',
registry.PositiveInteger(1, _("""Determines how many mores will be sent
instantly (i.e., without the use of the more command, immediately when
they are formed). Defaults to 1, which means that a more command will be
required for all but the first chunk.""")))
registerChannelValue(supybot.reply, 'oneToOne',
registry.Boolean(True, _("""Determines whether the bot will send
multi-message replies in a single message. This defaults to True
in order to prevent the bot from flooding. If this is set to False
the bot will send multi-message replies on multiple lines.""")))
registerChannelValue(supybot.reply, 'whenNotCommand',
registry.Boolean(True, _("""Determines whether the bot will reply with an
error message when it is addressed but not given a valid command. If this
value is False, the bot will remain silent, as long as no other plugins
override the normal behavior.""")))
registerGroup(supybot.reply, 'error')
registerGlobalValue(supybot.reply.error, 'detailed',
registry.Boolean(False, _("""Determines whether error messages that result
from bugs in the bot will show a detailed error message (the uncaught
exception) or a generic error message.""")))
registerChannelValue(supybot.reply.error, 'inPrivate',
registry.Boolean(False, _("""Determines whether the bot will send error
messages to users in private. You might want to do this in order to keep
channel traffic to minimum. This can be used in combination with
supybot.reply.error.withNotice.""")))
registerChannelValue(supybot.reply.error, 'withNotice',
registry.Boolean(False, _("""Determines whether the bot will send error
messages to users via NOTICE instead of PRIVMSG. You might want to do this
so users can ignore NOTICEs from the bot and not have to see error
messages; or you might want to use it in combination with
supybot.reply.error.inPrivate so private errors don't open a query window
in most IRC clients.""")))
registerChannelValue(supybot.reply.error, 'noCapability',
registry.Boolean(False, _("""Determines whether the bot will *not* provide
details in the error
message to users who attempt to call a command for which they do not have
the necessary capability. You may wish to make this True if you don't want
users to understand the underlying security system preventing them from
running certain commands.""")))
registerChannelValue(supybot.reply, 'inPrivate',
registry.Boolean(False, _("""Determines whether the bot will reply
privately when replying in a channel, rather than replying to the whole
channel.""")))
registerChannelValue(supybot.reply, 'withNotice',
registry.Boolean(False, _("""Determines whether the bot will reply with a
notice when replying in a channel, rather than replying with a privmsg as
normal.""")))
# XXX: User value.
registerGlobalValue(supybot.reply, 'withNoticeWhenPrivate',
registry.Boolean(True, _("""Determines whether the bot will reply with a
notice when it is sending a private message, in order not to open a /query
window in clients.""")))
registerChannelValue(supybot.reply, 'withNickPrefix',
registry.Boolean(True, _("""Determines whether the bot will always prefix
the user's nick to its reply to that user's command.""")))
registerChannelValue(supybot.reply, 'whenNotAddressed',
registry.Boolean(False, _("""Determines whether the bot should attempt to
reply to all messages even if they don't address it (either via its nick
or a prefix character). If you set this to True, you almost certainly want
to set supybot.reply.whenNotCommand to False.""")))
registerChannelValue(supybot.reply, 'requireChannelCommandsToBeSentInChannel',
registry.Boolean(False, _("""Determines whether the bot will allow you to
send channel-related commands outside of that channel. Sometimes people
find it confusing if a channel-related command (like Filter.outfilter)
changes the behavior of the channel but was sent outside the channel
itself.""")))
registerGlobalValue(supybot, 'followIdentificationThroughNickChanges',
registry.Boolean(False, _("""Determines whether the bot will unidentify
someone when that person changes their nick. Setting this to True
will cause the bot to track such changes. It defaults to False for a
little greater security.""")))
registerChannelValue(supybot, 'alwaysJoinOnInvite',
registry.Boolean(False, _("""Determines whether the bot will always join a
channel when it's invited. If this value is False, the bot will only join
a channel if the user inviting it has the 'admin' capability (or if it's
explicitly told to join the channel using the Admin.join command).""")))
registerChannelValue(supybot.reply, 'showSimpleSyntax',
registry.Boolean(False, _("""Supybot normally replies with the full help
whenever a user misuses a command. If this value is set to True, the bot
will only reply with the syntax of the command (the first line of the
help) rather than the full help.""")))
class ValidPrefixChars(registry.String):
"""Value must contain only ~!@#$%^&*()_-+=[{}]\\|'\";:,<.>/?"""
__slots__ = ()
def setValue(self, v):
if any([x not in '`~!@#$%^&*()_-+=[{}]\\|\'";:,<.>/?' for x in v]):
self.error()
registry.String.setValue(self, v)
registerGroup(supybot.reply, 'whenAddressedBy')
registerChannelValue(supybot.reply.whenAddressedBy, 'chars',
ValidPrefixChars('', _("""Determines what prefix characters the bot will
reply to. A prefix character is a single character that the bot will use
to determine what messages are addressed to it; when there are no prefix
characters set, it just uses its nick. Each character in this string is
interpreted individually; you can have multiple prefix chars
simultaneously, and if any one of them is used as a prefix the bot will
assume it is being addressed.""")))
registerChannelValue(supybot.reply.whenAddressedBy, 'strings',
registry.SpaceSeparatedSetOfStrings([], _("""Determines what strings the
bot will reply to when they are at the beginning of the message. Whereas
prefix.chars can only be one character (although there can be many of
them), this variable is a space-separated list of strings, so you can
set something like '@@ ??' and the bot will reply when a message is
prefixed by either @@ or ??.""")))
registerChannelValue(supybot.reply.whenAddressedBy, 'nick',
registry.Boolean(True, _("""Determines whether the bot will reply when
people address it by its nick, rather than with a prefix character.""")))
registerChannelValue(supybot.reply.whenAddressedBy.nick, 'atEnd',
registry.Boolean(False, _("""Determines whether the bot will reply when
people address it by its nick at the end of the message, rather than at
the beginning.""")))
registerChannelValue(supybot.reply.whenAddressedBy, 'nicks',
registry.SpaceSeparatedSetOfStrings([], _("""Determines what extra nicks
the bot will always respond to when addressed by, even if its current nick
is something else.""")))
###
# Replies
###
registerGroup(supybot, 'replies')
registerChannelValue(supybot.replies, 'success',
registry.NormalizedString(_("""The operation succeeded."""),
_("""Determines what message the bot replies with when a command succeeded.
If this configuration variable is empty, no success message will be
sent.""")))
registerChannelValue(supybot.replies, 'error',
registry.NormalizedString(_("""An error has occurred and has been logged.
Please contact this bot's administrator for more information.
If this configuration variable is empty, no generic error message will be sent."""),
_("""Determines what error message the bot gives when it wants to be
ambiguous.""")))
registerChannelValue(supybot.replies, 'errorOwner',
registry.NormalizedString(_("""An error has occurred and has been logged.
Check the logs for more information."""), _("""Determines what error
message the bot gives to the owner when it wants to be ambiguous.""")))
registerChannelValue(supybot.replies, 'incorrectAuthentication',
registry.NormalizedString(_("""Your hostmask doesn't match or your password
is wrong."""), _("""Determines what message the bot replies with when
someone tries to use a command that requires being identified or having a
password and neither credential is correct.""")))
# XXX: This should eventually check that there's one and only one %s here.
registerChannelValue(supybot.replies, 'noUser',
registry.NormalizedString(_("""I can't find %s in my user
database. If you didn't give a user name, then I might not know what your
user is, and you'll need to identify before this command might work."""),
_("""Determines what error message the bot replies with when someone tries
to accessing some information on a user the bot doesn't know about.""")))
registerChannelValue(supybot.replies, 'notRegistered',
registry.NormalizedString(_("""You must be registered to use this command.
If you are already registered, you must either identify (using the identify
command) or add a hostmask matching your current hostmask (using the
"hostmask add" command)."""), _("""Determines what error message the bot
replies with when someone tries to do something that requires them to be
registered but they're not currently recognized.""")))
registerChannelValue(supybot.replies, 'noCapability',
registry.NormalizedString(_("""You don't have the %s capability. If you
think that you should have this capability, be sure that you are identified
before trying again. The 'whoami' command can tell you if you're
identified."""), _("""Determines what error message is given when the bot
is telling someone they aren't cool enough to use the command they tried to
use.""")))
registerChannelValue(supybot.replies, 'genericNoCapability',
registry.NormalizedString(_("""You're missing some capability you need.
This could be because you actually possess the anti-capability for the
capability that's required of you, or because the channel provides that
anti-capability by default, or because the global capabilities include
that anti-capability. Or, it could be because the channel or
supybot.capabilities.default is set to False, meaning that no commands are
allowed unless explicitly in your capabilities. Either way, you can't do
what you want to do."""),
_("""Determines what generic error message is given when the bot is telling
someone that they aren't cool enough to use the command they tried to use,
and the author of the code calling errorNoCapability didn't provide an
explicit capability for whatever reason.""")))
registerChannelValue(supybot.replies, 'requiresPrivacy',
registry.NormalizedString(_("""That operation cannot be done in a
channel."""), _("""Determines what error messages the bot sends to people
who try to do things in a channel that really should be done in
private.""")))
registerChannelValue(supybot.replies, 'possibleBug',
registry.NormalizedString(_("""This may be a bug. If you think it is,
please file a bug report at
<https://github.com/progval/Limnoria/issues>."""),
_("""Determines what message the bot sends when it thinks you've
encountered a bug that the developers don't know about.""")))
class DatabaseRecordTemplatedString(registry.TemplatedString):
requiredTemplates = ['text']
registerChannelValue(supybot.replies, 'databaseRecord',
DatabaseRecordTemplatedString(_('$Type #$id: $text (added by $username at $at)'),
_("""Format used by generic database plugins (Lart, Dunno, Prase, Success,
Quote, ...) to show an entry. You can use the following variables:
$type/$types/$Type/$Types (plugin name and variants), $id, $text,
$at (creation time), $userid/$username/$nick (author).""")))
###
# End supybot.replies.
###
registerGlobalValue(supybot, 'snarfThrottle',
registry.Float(10.0, _("""A floating point number of seconds to throttle
snarfed URLs, in order to prevent loops between two bots snarfing the same
URLs and having the snarfed URL in the output of the snarf message.""")))
registerGlobalValue(supybot, 'upkeepInterval',
registry.PositiveInteger(3600, _("""Determines the number of seconds
between running the upkeep function that flushes (commits) open databases,
collects garbage, and records some useful statistics at the debugging
level.""")))
registerGlobalValue(supybot, 'flush',
registry.Boolean(True, _("""Determines whether the bot will periodically
flush data and configuration files to disk. Generally, the only time
you'll want to set this to False is when you want to modify those
configuration files by hand and don't want the bot to flush its current
version over your modifications. Do note that if you change this to False
inside the bot, your changes won't be flushed. To make this change
permanent, you must edit the registry yourself.""")))
###
# supybot.commands. For stuff relating to commands.
###
registerGroup(supybot, 'commands')
class ValidQuotes(registry.Value):
"""Value must consist solely of \", ', and ` characters."""
__slots__ = ()
def setValue(self, v):
if [c for c in v if c not in '"`\'']:
self.error()
super(ValidQuotes, self).setValue(v)
def __str__(self):
return str(self())
registerChannelValue(supybot.commands, 'quotes',
ValidQuotes('"', _("""Determines what characters are valid for quoting
arguments to commands in order to prevent them from being tokenized.
""")))
# This is a GlobalValue because bot owners should be able to say, "There will
# be no nesting at all on this bot." Individual channels can just set their
# brackets to the empty string.
registerGlobalValue(supybot.commands, 'nested',
registry.Boolean(True, _("""Determines whether the bot will allow nested
commands, which rule. You definitely should keep this on.""")))
registerGlobalValue(supybot.commands.nested, 'maximum',
registry.PositiveInteger(10, _("""Determines what the maximum number of
nested commands will be; users will receive an error if they attempt
commands more nested than this.""")))
class ValidBrackets(registry.OnlySomeStrings):
__slots__ = ()
validStrings = ('', '[]', '<>', '{}', '()')
registerChannelValue(supybot.commands.nested, 'brackets',
ValidBrackets('[]', _("""Supybot allows you to specify what brackets
are used for your nested commands. Valid sets of brackets include
[], <>, {}, and (). [] has strong historical motivation, but <> or
() might be slightly superior because they cannot occur in a nick.
If this string is empty, nested commands will not be allowed in this
channel.""")))
registerChannelValue(supybot.commands.nested, 'pipeSyntax',
registry.Boolean(False, _("""Supybot allows nested commands. Enabling this
option will allow nested commands with a syntax similar to UNIX pipes, for
example: 'bot: foo | bar'.""")))
registerGroup(supybot.commands, 'defaultPlugins',
orderAlphabetically=True, help=_("""Determines what commands have default
plugins set, and which plugins are set to be the default for each of those
commands."""))
registerGlobalValue(supybot.commands.defaultPlugins, 'importantPlugins',
registry.SpaceSeparatedSetOfStrings(
['Admin', 'Channel', 'Config', 'Misc', 'Owner', 'User'],
_("""Determines what plugins automatically get precedence over all
other plugins when selecting a default plugin for a command. By
default, this includes the standard loaded plugins. You probably
shouldn't change this if you don't know what you're doing; if you do
know what you're doing, then also know that this set is
case-sensitive.""")))
# For this config variable to make sense, it must no be writable via IRC.
# Make sure it is always blacklisted from the Config plugin.
registerGlobalValue(supybot.commands, 'allowShell',
registry.Boolean(True, _("""Allows this bot's owner user to use commands
that grants them shell access. This config variable exists in case you want
to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)
from gaining shell access to the bot's server by impersonating the owner.
Setting this to False also disables plugins and commands that can be
used to indirectly gain shell access.""")))
# supybot.commands.disabled moved to callbacks for canonicalName.
###
# supybot.abuse. For stuff relating to abuse of the bot.
###
registerGroup(supybot, 'abuse')
registerGroup(supybot.abuse, 'flood')
registerGlobalValue(supybot.abuse.flood, 'interval',
registry.PositiveInteger(60, _("""Determines the interval used for
the history storage.""")))
registerGlobalValue(supybot.abuse.flood, 'command',
registry.Boolean(True, _("""Determines whether the bot will defend itself
against command-flooding.""")))
registerGlobalValue(supybot.abuse.flood.command, 'maximum',
registry.PositiveInteger(12, _("""Determines how many commands users are
allowed per minute. If a user sends more than this many commands in any
60 second period, they will be ignored for
supybot.abuse.flood.command.punishment seconds.""")))
registerGlobalValue(supybot.abuse.flood.command, 'punishment',
registry.PositiveInteger(300, _("""Determines how many seconds the bot
will ignore users who flood it with commands.""")))
registerGlobalValue(supybot.abuse.flood.command, 'notify',
registry.Boolean(True, _("""Determines whether the bot will notify people
that they're being ignored for command flooding.""")))
registerGlobalValue(supybot.abuse.flood.command, 'invalid',
registry.Boolean(True, _("""Determines whether the bot will defend itself
against invalid command-flooding.""")))
registerGlobalValue(supybot.abuse.flood.command.invalid, 'maximum',
registry.PositiveInteger(5, _("""Determines how many invalid commands users
are allowed per minute. If a user sends more than this many invalid
commands in any 60 second period, they will be ignored for
supybot.abuse.flood.command.invalid.punishment seconds. Typically, this
value is lower than supybot.abuse.flood.command.maximum, since it's far
less likely (and far more annoying) for users to flood with invalid
commands than for them to flood with valid commands.""")))
registerGlobalValue(supybot.abuse.flood.command.invalid, 'punishment',
registry.PositiveInteger(600, _("""Determines how many seconds the bot
will ignore users who flood it with invalid commands. Typically, this
value is higher than supybot.abuse.flood.command.punishment, since it's far
less likely (and far more annoying) for users to flood with invalid
commands than for them to flood with valid commands.""")))
registerGlobalValue(supybot.abuse.flood.command.invalid, 'notify',
registry.Boolean(True, _("""Determines whether the bot will notify people
that they're being ignored for invalid command flooding.""")))
###
# supybot.drivers. For stuff relating to Supybot's drivers (duh!)
###
registerGroup(supybot, 'drivers')
registerGlobalValue(supybot.drivers, 'poll',
registry.PositiveFloat(1.0, _("""Determines the default length of time a
driver should block waiting for input.""")))
class ValidDriverModule(registry.OnlySomeStrings):
__slots__ = ()
validStrings = ('default', 'Socket')
registerGlobalValue(supybot.drivers, 'module',
ValidDriverModule('default', _("""Determines what driver module the
bot will use. Current, the only (and default) driver is Socket.""")))
registerGlobalValue(supybot.drivers, 'minReconnectWait',
registry.PositiveFloat(10.0, _("""Determines the minimum time the bot will
wait before attempting to reconnect to an IRC server.""")))
registerGlobalValue(supybot.drivers, 'maxReconnectWait',
registry.PositiveFloat(300.0, _("""Determines the maximum time the bot will
wait before attempting to reconnect to an IRC server. The bot may, of
course, reconnect earlier if possible.""")))
###
# supybot.directories, for stuff relating to directories.
###
# XXX This shouldn't make directories willy-nilly. As it is now, if it's
# configured, it'll still make the default directories, I think.
class Directory(registry.String):
__slots__ = ()
def __call__(self):
# ??? Should we perhaps always return an absolute path here?
v = super(Directory, self).__call__()
if not os.path.exists(v):
os.mkdir(v)
return v
def dirize(self, filename):
myself = self()
if os.path.isabs(filename):
filename = os.path.abspath(filename)
selfAbs = os.path.abspath(myself)
commonPrefix = os.path.commonpath([selfAbs, filename])
filename = filename[len(commonPrefix):]
elif not os.path.isabs(myself):
if filename.startswith(myself):
filename = filename[len(myself):]
filename = filename.lstrip(os.path.sep) # Stupid os.path.join!
return os.path.join(myself, filename)
class DataFilename(registry.String):
__slots__ = ()
def __call__(self):
v = super(DataFilename, self).__call__()
dataDir = supybot.directories.data()
if not v.startswith("/") and not v.startswith(dataDir):
v = os.path.basename(v)
v = os.path.join(dataDir, v)
self.setValue(v)
return v
class DataFilenameDirectory(DataFilename, Directory):
__slots__ = ()
def __call__(self):
v = DataFilename.__call__(self)
v = Directory.__call__(self)
return v
registerGroup(supybot, 'directories')
registerGlobalValue(supybot.directories, 'conf',
Directory('conf', _("""Determines what directory configuration data is
put into.""")))
registerGlobalValue(supybot.directories, 'data',
Directory('data', _("""Determines what directory data is put into.""")))
registerGlobalValue(supybot.directories, 'backup',
Directory('backup', _("""Determines what directory backup data is put
into. Set it to /dev/null to disable backup (it is a special value,
so it also works on Windows and systems without /dev/null).""")))
registerGlobalValue(supybot.directories, 'log',
Directory('logs', """Determines what directory the bot will store its
logfiles in."""))
registerGlobalValue(supybot.directories.data, 'tmp',
DataFilenameDirectory('tmp', _("""Determines what directory temporary files
are put into.""")))
registerGlobalValue(supybot.directories.data, 'web',
DataFilenameDirectory('web', _("""Determines what directory files of the
web server (templates, custom images, ...) are put into.""")))
def _update_tmp():
utils.file.AtomicFile.default.tmpDir = supybot.directories.data.tmp