1#!/usr/bin/env python
2
3###
4# Copyright (c) 2003-2004, Jeremiah Fincher
5# Copyright (c) 2009, James McCoy
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions are met:
10#
11#   * Redistributions of source code must retain the above copyright notice,
12#     this list of conditions, and the following disclaimer.
13#   * Redistributions in binary form must reproduce the above copyright notice,
14#     this list of conditions, and the following disclaimer in the
15#     documentation and/or other materials provided with the distribution.
16#   * Neither the name of the author of this software nor the name of
17#     contributors to this software may be used to endorse or promote products
18#     derived from this software without specific prior written consent.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30# POSSIBILITY OF SUCH DAMAGE.
31###
32
33from __future__ import print_function
34
35import os
36import sys
37
38def error(s):
39    sys.stderr.write(s)
40    if not s.endswith(os.linesep):
41        sys.stderr.write(os.linesep)
42    sys.exit(-1)
43
44if sys.version_info < (2, 6, 0):
45    error('This program requires Python >= 2.6.0')
46
47import supybot
48
49import re
50import time
51import pydoc
52import pprint
53import socket
54import logging
55import optparse
56
57try:
58    import supybot.i18n as i18n
59except ImportError:
60    sys.stderr.write("""Error:
61    You are running a mix of Limnoria and stock Supybot code. Although you run
62    one of Limnoria\'s executables, Python tries to load stock
63    Supybot\'s library. To fix this issue, uninstall Supybot
64    ("%s -m pip uninstall supybot" should do the job)
65    and install Limnoria again.
66    For your information, Supybot's libraries are installed here:
67    %s\n""" %
68    (sys.executable, '\n    '.join(supybot.__path__)))
69    exit(-1)
70
71import supybot.ansi as ansi
72import supybot.utils as utils
73import supybot.ircutils as ircutils
74import supybot.registry as registry
75# supybot.plugin, supybot.log, and supybot.conf will be imported later,
76# because we need to set a language before loading the conf
77
78import supybot.questions as questions
79from supybot.questions import output, yn, anything, something, expect, getpass
80
81def getPlugins(pluginDirs):
82    plugins = set([])
83    join = os.path.join
84    for pluginDir in pluginDirs:
85        try:
86            for filename in os.listdir(pluginDir):
87                fname = join(pluginDir, filename)
88                if (filename.endswith('.py') or os.path.isdir(fname)) \
89                   and filename[0].isupper():
90                    plugins.add(os.path.splitext(filename)[0])
91        except OSError:
92            continue
93    plugins.discard('Owner')
94    plugins = list(plugins)
95    plugins.sort()
96    return plugins
97
98def loadPlugin(name):
99    import supybot.plugin as plugin
100    try:
101        module = plugin.loadPluginModule(name)
102        if hasattr(module, 'Class'):
103            return module
104        else:
105            output("""That plugin loaded fine, but didn't seem to be a real
106            Supybot plugin; there was no Class variable to tell us what class
107            to load when we load the plugin.  We'll skip over it for now, but
108            you can always add it later.""")
109            return None
110    except Exception as e:
111        output("""We encountered a bit of trouble trying to load plugin %r.
112        Python told us %r.  We'll skip over it for now, you can always add it
113        later.""" % (name, utils.gen.exnToString(e)))
114        return None
115
116def describePlugin(module, showUsage):
117    if module.__doc__:
118        output(module.__doc__, unformatted=False)
119    elif hasattr(module.Class, '__doc__'):
120        output(module.Class.__doc__, unformatted=False)
121    else:
122        output("""Unfortunately, this plugin doesn't seem to have any
123        documentation.  Sorry about that.""")
124    if showUsage:
125        if hasattr(module, 'example'):
126            if yn('This plugin has a usage example.  '
127                  'Would you like to see it?', default=False):
128                pydoc.pager(module.example)
129        else:
130            output("""This plugin has no usage example.""")
131
132def clearLoadedPlugins(plugins, pluginRegistry):
133    for plugin in plugins:
134        try:
135            pluginKey = pluginRegistry.get(plugin)
136            if pluginKey():
137                plugins.remove(plugin)
138        except registry.NonExistentRegistryEntry:
139            continue
140
141_windowsVarRe = re.compile(r'%(\w+)%')
142def getDirectoryName(default, basedir=os.curdir, prompt=True):
143    done = False
144    while not done:
145        if prompt:
146            dir = something('What directory do you want to use?',
147                           default=os.path.join(basedir, default))
148        else:
149            dir = os.path.join(basedir, default)
150        orig_dir = dir
151        dir = os.path.expanduser(dir)
152        dir = _windowsVarRe.sub(r'$\1', dir)
153        dir = os.path.expandvars(dir)
154        dir = os.path.abspath(dir)
155        try:
156            os.makedirs(dir)
157            done = True
158        except OSError as e:
159            # 17 is File exists for Linux (and likely other POSIX systems)
160            # 183 is the same for Windows
161            if e.args[0] == 17 or (os.name == 'nt' and e.args[0] == 183):
162                done = True
163            else:
164                output("""Sorry, I couldn't make that directory for some
165                reason.  The Operating System told me %s.  You're going to
166                have to pick someplace else.""" % e)
167                prompt = True
168    return (dir, os.path.dirname(orig_dir))
169
170
171def main():
172    import supybot.version as version
173    parser = optparse.OptionParser(usage='Usage: %prog [options]',
174                                   version='Supybot %s' % version.version)
175    parser.add_option('', '--allow-root', action='store_true',
176                      dest='allowRoot',
177                      help='Determines whether the wizard will be allowed to '
178                           'run as root.  You don\'t want this.  Don\'t do it.'
179                           '  Even if you think you want it, you don\'t.  '
180                           'You\'re probably dumb if you do this.')
181    parser.add_option('', '--allow-home', action='store_true',
182                      dest='allowHome',
183                      help='Determines whether the wizard will be allowed to '
184                           'run directly in the HOME directory. '
185                           'You should not do this unless you want it to '
186                           'create multiple files in your HOME directory.')
187    parser.add_option('', '--allow-bin', action='store_true',
188                      dest='allowBin',
189                      help='Determines whether the wizard will be allowed to '
190                           'run directly in a directory for binaries (eg. '
191                           '/usr/bin, /usr/local/bin, or ~/.local/bin). '
192                           'You should not do this unless you want it to '
193                           'create multiple non-binary files in directory '
194                           'that should only contain binaries.')
195    parser.add_option('', '--no-network', action='store_false',
196                      dest='network',
197                      help='Determines whether the wizard will be allowed to '
198                           'run without a network connection.')
199    (options, args) = parser.parse_args()
200    if os.name == 'posix':
201        if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot:
202            error('Please, don\'t run this as root.')
203    if os.name == 'posix':
204        if (os.getcwd() == os.path.expanduser('~')) and not options.allowHome:
205            error('Please, don\'t run this in your HOME directory.')
206        if (os.path.split(os.getcwd())[-1] == 'bin') and not options.allowBin:
207            error('Please, don\'t run this in a "bin" directory.')
208    if os.path.isfile(os.path.join('scripts', 'supybot-wizard')) or \
209            os.path.isfile(os.path.join('..', 'scripts', 'supybot-wizard')):
210        print('')
211        print('+------------------------------------------------------------+')
212        print('| +--------------------------------------------------------+ |')
213        print('| | Warning: It looks like you are running the wizard from | |')
214        print('| | the Supybot source directory. This is not recommended. | |')
215        print('| | Please press Ctrl-C and change to another directory.   | |')
216        print('| +--------------------------------------------------------+ |')
217        print('+------------------------------------------------------------+')
218        print('')
219
220    if args:
221        parser.error('This program takes no non-option arguments.')
222    output("""This is a wizard to help you start running supybot.  What it
223    will do is create the necessary config files based on the options you
224    select here.  So hold on tight and be ready to be interrogated :)""")
225
226
227    output("""First of all, we can bold the questions you're asked so you can
228    easily distinguish the mostly useless blather (like this) from the
229    questions that you actually have to answer.""")
230    if yn('Would you like to try this bolding?', default=True):
231        questions.useBold = True
232        if not yn('Do you see this in bold?'):
233            output("""Sorry, it looks like your terminal isn't ANSI compliant.
234            Try again some other day, on some other terminal :)""")
235            questions.useBold = False
236        else:
237            output("""Great!""")
238
239    ###
240    # Preliminary questions.
241    ###
242    output("""We've got some preliminary things to get out of the way before
243    we can really start asking you questions that directly relate to what your
244    bot is going to be like.""")
245
246    # Advanced?
247    output("""We want to know if you consider yourself an advanced Supybot
248    user because some questions are just utterly boring and useless for new
249    users.  Others might not make sense unless you've used Supybot for some
250    time.""")
251    advanced = yn('Are you an advanced Supybot user?', default=False)
252
253    # Language?
254    output("""This version of Supybot (known as Limnoria) includes another
255    language. This can be changed at any time. You need to answer with a short
256    id for the language, such as 'en', 'fr', 'it' (without the quotes). If
257    you want to use English, just press enter.""")
258    language = something('What language do you want to use?', default='en')
259
260    class Empty:
261        """This is a hack to allow the i18n to get the current language, before
262        loading the conf module, before the conf module needs i18n to set the
263        default strings."""
264        def __call__(self):
265            return self.value
266    fakeConf = Empty()
267    fakeConf.supybot = Empty()
268    fakeConf.supybot.language = Empty()
269    fakeConf.supybot.language.value = language
270    i18n.conf = fakeConf
271    i18n.currentLocale = language
272    i18n.reloadLocales()
273    import supybot.conf as conf
274    i18n.import_conf() # It imports the real conf module
275
276    ### Directories.
277    # We set these variables in cache because otherwise conf and log will
278    # create directories for the default values, which might not be what the
279    # user wants.
280    if advanced:
281        output("""Now we've got to ask you some questions about where some of
282        your directories are (or, perhaps, will be :)).  If you're running this
283        wizard from the directory you'll actually be starting your bot from and
284        don't mind creating some directories in the current directory, then
285        just don't give answers to these questions and we'll create the
286        directories we need right here in this directory.""")
287
288        # conf.supybot.directories.log
289        output("""Your bot will need to put its logs somewhere.  Do you have
290        any specific place you'd like them?  If not, just press enter and we'll
291        make a directory named "logs" right here.""")
292        (logDir, basedir) = getDirectoryName('logs')
293        conf.supybot.directories.log.setValue(logDir)
294        import supybot.log as log
295        log._stdoutHandler.setLevel(100) # *Nothing* gets through this!
296
297        # conf.supybot.directories.data
298        output("""Your bot will need to put various data somewhere.  Things
299        like databases, downloaded files, etc.  Do you have any specific place
300        you'd like the bot to put these things?  If not, just press enter and
301        we'll make a directory named "data" right here.""")
302        (dataDir, basedir) = getDirectoryName('data', basedir=basedir)
303        conf.supybot.directories.data.setValue(dataDir)
304
305        # conf.supybot.directories.conf
306        output("""Your bot must know where to find its configuration files.
307        It'll probably only make one or two, but it's gotta have some place to
308        put them.  Where should that place be?  If you don't care, just press
309        enter and we'll make a directory right here named "conf" where it'll
310        store its stuff. """)
311        (confDir, basedir) = getDirectoryName('conf', basedir=basedir)
312        conf.supybot.directories.conf.setValue(confDir)
313
314        # conf.supybot.directories.backup
315        output("""Your bot must know where to place backups of its conf and
316        data files.  Where should that place be?  If you don't care, just press
317        enter and we'll make a directory right here named "backup" where it'll
318        store its stuff.""")
319        (backupDir, basedir) = getDirectoryName('backup', basedir=basedir)
320        conf.supybot.directories.backup.setValue(backupDir)
321
322        # conf.supybot.directories.data.tmp
323        output("""Your bot needs a directory to put temporary files (used
324        mainly to atomically update its configuration files).""")
325        (tmpDir, basedir) = getDirectoryName('tmp', basedir=basedir)
326        conf.supybot.directories.data.tmp.setValue(tmpDir)
327
328        # conf.supybot.directories.data.web
329        output("""Your bot needs a directory to put files related to the web
330        server (templates, CSS).""")
331        (webDir, basedir) = getDirectoryName('web', basedir=basedir)
332        conf.supybot.directories.data.web.setValue(webDir)
333
334
335        # imports callbacks, which imports ircdb, which requires
336        # directories.conf
337        import supybot.plugin as plugin
338
339        # pluginDirs
340        output("""Your bot will also need to know where to find its plugins at.
341        Of course, it already knows where the plugins that it came with are,
342        but your own personal plugins that you write for will probably be
343        somewhere else.""")
344        pluginDirs = conf.supybot.directories.plugins()
345        output("""Currently, the bot knows about the following directories:""")
346        output(format('%L', pluginDirs + [plugin._pluginsDir]))
347        while yn('Would you like to add another plugin directory?  '
348                 'Adding a local plugin directory is good style.',
349                 default=True):
350            (pluginDir, _) = getDirectoryName('plugins', basedir=basedir)
351            if pluginDir not in pluginDirs:
352                pluginDirs.append(pluginDir)
353        conf.supybot.directories.plugins.setValue(pluginDirs)
354    else:
355        output("""Your bot needs to create some directories in order to store
356        the various log, config, and data files.""")
357        basedir = something("""Where would you like to create these
358                            directories?""", default=os.curdir)
359
360        # conf.supybot.directories.log
361        (logDir, basedir) = getDirectoryName('logs',
362                                             basedir=basedir, prompt=False)
363        conf.supybot.directories.log.setValue(logDir)
364
365        # conf.supybot.directories.data
366        (dataDir, basedir) = getDirectoryName('data',
367                                              basedir=basedir, prompt=False)
368        conf.supybot.directories.data.setValue(dataDir)
369        (tmpDir, basedir) = getDirectoryName('tmp',
370                                             basedir=basedir, prompt=False)
371        conf.supybot.directories.data.tmp.setValue(tmpDir)
372        (webDir, basedir) = getDirectoryName('web',
373                                             basedir=basedir, prompt=False)
374        conf.supybot.directories.data.web.setValue(webDir)
375
376        # conf.supybot.directories.conf
377        (confDir, basedir) = getDirectoryName('conf',
378                                              basedir=basedir, prompt=False)
379        conf.supybot.directories.conf.setValue(confDir)
380
381        # conf.supybot.directories.backup
382        (backupDir, basedir) = getDirectoryName('backup',
383                                                basedir=basedir, prompt=False)
384        conf.supybot.directories.backup.setValue(backupDir)
385
386        # pluginDirs
387        pluginDirs = conf.supybot.directories.plugins()
388        (pluginDir, _) = getDirectoryName('plugins',
389                                          basedir=basedir, prompt=False)
390        if pluginDir not in pluginDirs:
391            pluginDirs.append(pluginDir)
392        conf.supybot.directories.plugins.setValue(pluginDirs)
393
394    import supybot.log as log
395    log._stdoutHandler.setLevel(100) # *Nothing* gets through this!
396    import supybot.plugin as plugin
397
398    output("Good!  We're done with the directory stuff.")
399
400    ###
401    # Bot stuff
402    ###
403    output("""Now we're going to ask you things that actually relate to the
404    bot you'll be running.""")
405
406    network = None
407    while not network:
408        output("""First, we need to know the name of the network you'd like to
409        connect to.  Not the server host, mind you, but the name of the
410        network.  If you plan to connect to chat.freenode.net, for instance,
411        you should answer this question with 'freenode' (without the quotes).
412        """)
413        network = something('What IRC network will you be connecting to?')
414        if '.' in network:
415            output("""There shouldn't be a '.' in the network name.  Remember,
416            this is the network name, not the actual server you plan to connect
417            to.""")
418            network = None
419        elif not registry.isValidRegistryName(network):
420            output("""That's not a valid name for one reason or another.  Please
421            pick a simpler name, one more likely to be valid.""")
422            network = None
423
424    conf.supybot.networks.setValue([network])
425    network = conf.registerNetwork(network)
426
427    defaultServer = None
428    server = None
429    ip = None
430    while not ip:
431        serverString = something('What server would you like to connect to?',
432                                 default=defaultServer)
433        if options.network:
434            try:
435                output("""Looking up %s...""" % serverString)
436                ip = socket.gethostbyname(serverString)
437            except:
438                output("""Sorry, I couldn't find that server.  Perhaps you
439                misspelled it?  Also, be sure not to put the port in the
440                server's name -- we'll ask you about that later.""")
441        else:
442            ip = 'no network available'
443
444    output("""Found %s (%s).""" % (serverString, ip))
445
446    # conf.supybot.networks.<network>.ssl
447    output("""Most networks allow you to use a secure connection via SSL.
448    If you are not sure whether this network supports SSL or not, check
449    its website.""")
450    use_ssl = yn('Do you want to use an SSL connection?', default=True)
451    network.ssl.setValue(use_ssl)
452
453    output("""IRC servers almost always accept connections on port
454    6697 (or 6667 when not using SSL).  They can, however, accept connections
455    anywhere their admins feel like they want to accept connections from.""")
456    if yn('Does this server require connection on a non-standard port?',
457          default=False):
458        port = 0
459        while not port:
460            port = something('What port is that?')
461            try:
462                i = int(port)
463                if not (0 < i < 65536):
464                    raise ValueError()
465            except ValueError:
466                output("""That's not a valid port.""")
467                port = 0
468    else:
469        if network.ssl.value:
470            port = 6697
471        else:
472            port = 6667
473    server = ':'.join([serverString, str(port)])
474    network.servers.setValue([server])
475
476    # conf.supybot.nick
477    # Force the user into specifying a nick if it didn't have one already
478    while True:
479        nick = something('What nick would you like your bot to use?',
480                         default=None)
481        try:
482            conf.supybot.nick.set(nick)
483            break
484        except registry.InvalidRegistryValue:
485            output("""That's not a valid nick.  Go ahead and pick another.""")
486
487    # conf.supybot.user
488    if advanced:
489        output("""If you've ever done a /whois on a person, you know that IRC
490        provides a way for users to show the world their full name.  What would
491        you like your bot's full name to be?  If you don't care, just press
492        enter and it'll be the same as your bot's nick.""")
493        user = ''
494        user = something('What would you like your bot\'s full name to be?',
495                         default=nick)
496        conf.supybot.user.set(user)
497    # conf.supybot.ident (if advanced)
498    defaultIdent = 'limnoria'
499    if advanced:
500        output("""IRC servers also allow you to set your ident, which they
501        might need if they can't find your identd server.  What would you like
502        your ident to be?  If you don't care, press enter and we'll use
503        'limnoria'.  In fact, we prefer that you do this, because it provides
504        free advertising for Supybot when users /whois your bot.  But, of
505        course, it's your call.""")
506        while True:
507            ident = something('What would you like your bot\'s ident to be?',
508                              default=defaultIdent)
509            try:
510                conf.supybot.ident.set(ident)
511                break
512            except registry.InvalidRegistryValue:
513                output("""That was not a valid ident.  Go ahead and pick
514                another.""")
515    else:
516        conf.supybot.ident.set(defaultIdent)
517
518    # conf.supybot.networks.<network>.password
519    output("""Some servers require a password to connect to them.  Most
520    public servers don't.  If you try to connect to a server and for some
521    reason it just won't work, it might be that you need to set a
522    password.""")
523    if yn('Do you want to set such a password?', default=False):
524        network.password.set(getpass())
525
526    # conf.supybot.networks.<network>.channels
527    output("""Of course, having an IRC bot isn't the most useful thing in the
528    world unless you can make that bot join some channels.""")
529    if yn('Do you want your bot to join some channels when it connects?',
530          default=True):
531        defaultChannels = ' '.join(network.channels())
532        output("""Separate channels with spaces.  If the channel is locked
533                  with a key, follow the channel name with the key separated
534                  by a comma. For example:
535                  #supybot-bots #mychannel,mykey #otherchannel""");
536        while True:
537            channels = something('What channels?', default=defaultChannels)
538            try:
539                network.channels.set(channels)
540                break
541            except registry.InvalidRegistryValue as e:
542                output(""""%s" is an invalid IRC channel.  Be sure to prefix
543                the channel with # (or +, or !, or &, but no one uses those
544                channels, really).  Be sure the channel key (if you are
545                supplying one) does not contain a comma.""" % e.channel)
546    else:
547        network.channels.setValue([])
548
549    ###
550    # Plugins
551    ###
552    def configurePlugin(module, advanced):
553        if hasattr(module, 'configure'):
554            output("""Beginning configuration for %s...""" %
555                   module.Class.__name__)
556            module.configure(advanced)
557            print() # Blank line :)
558            output("""Done!""")
559        else:
560            conf.registerPlugin(module.__name__, currentValue=True)
561
562    plugins = getPlugins(pluginDirs + [plugin._pluginsDir])
563    for s in ('Admin', 'User', 'Channel', 'Misc', 'Config', 'Utilities'):
564        m = loadPlugin(s)
565        if m is not None:
566            configurePlugin(m, advanced)
567        else:
568            error('There was an error loading one of the core plugins that '
569                  'under almost all circumstances are loaded.  Go ahead and '
570                  'fix that error and run this script again.')
571    clearLoadedPlugins(plugins, conf.supybot.plugins)
572
573    output("""Now we're going to run you through plugin configuration. There's
574           a variety of plugins in supybot by default, but you can create and
575           add your own, of course. We'll allow you to take a look at the known
576           plugins' descriptions and configure them
577           if you like what you see.""")
578
579    # bulk
580    addedBulk = False
581    if advanced and yn('Would you like to add plugins en masse first?'):
582        addedBulk = True
583        output(format("""The available plugins are: %L.""", plugins))
584        output("""What plugins would you like to add?  If you've changed your
585        mind and would rather not add plugins in bulk like this, just press
586        enter and we'll move on to the individual plugin configuration.
587        We suggest you to add Aka, Ctcp, Later, Network, Plugin, String,
588        and Utilities""")
589        massPlugins = anything('Separate plugin names by spaces or commas:')
590        for name in re.split(r',?\s+', massPlugins):
591            module = loadPlugin(name)
592            if module is not None:
593                configurePlugin(module, advanced)
594                clearLoadedPlugins(plugins, conf.supybot.plugins)
595
596    # individual
597    if yn('Would you like to look at plugins individually?'):
598        output("""Next comes your opportunity to learn more about the plugins
599        that are available and select some (or all!) of them to run in your
600        bot.  Before you have to make a decision, of course, you'll be able to
601        see a short description of the plugin and, if you choose, an example
602        session with the plugin.  Let's begin.""")
603        # until we get example strings again, this will default to false
604        #showUsage =yn('Would you like the option of seeing usage examples?')
605        showUsage = False
606        name = expect('What plugin would you like to look at?',
607                      plugins, acceptEmpty=True)
608        while name:
609            module = loadPlugin(name)
610            if module is not None:
611                describePlugin(module, showUsage)
612                if yn('Would you like to load this plugin?', default=True):
613                    configurePlugin(module, advanced)
614                    clearLoadedPlugins(plugins, conf.supybot.plugins)
615            if not yn('Would you like add another plugin?'):
616                break
617            name = expect('What plugin would you like to look at?', plugins)
618
619    ###
620    # Sundry
621    ###
622    output("""Although supybot offers a supybot-adduser script, with which
623    you can add users to your bot's user database, it's *very* important that
624    you have an owner user for you bot.""")
625    if yn('Would you like to add an owner user for your bot?', default=True):
626        import supybot.ircdb as ircdb
627        name = something('What should the owner\'s username be?')
628        try:
629            id = ircdb.users.getUserId(name)
630            u = ircdb.users.getUser(id)
631            if u._checkCapability('owner'):
632                output("""That user already exists, and has owner capabilities
633                already.  Perhaps you added it before? """)
634                if yn('Do you want to remove its owner capability?',
635                      default=False):
636                    u.removeCapability('owner')
637                    ircdb.users.setUser(id, u)
638            else:
639                output("""That user already exists, but doesn't have owner
640                capabilities.""")
641                if yn('Do you want to add to it owner capabilities?',
642                      default=False):
643                    u.addCapability('owner')
644                    ircdb.users.setUser(id, u)
645        except KeyError:
646            password = getpass('What should the owner\'s password be?')
647            u = ircdb.users.newUser()
648            u.name = name
649            u.setPassword(password)
650            u.addCapability('owner')
651            ircdb.users.setUser(u)
652
653    output("""Of course, when you're in an IRC channel you can address the bot
654    by its nick and it will respond, if you give it a valid command (it may or
655    may not respond, depending on what your config variable replyWhenNotCommand
656    is set to).  But your bot can also respond to a short "prefix character,"
657    so instead of saying "bot: do this," you can say, "@do this" and achieve
658    the same effect.  Of course, you don't *have* to have a prefix char, but
659    if the bot ends up participating significantly in your channel, it'll ease
660    things.""")
661    if yn('Would you like to set the prefix char(s) for your bot?  ',
662          default=True):
663        output("""Enter any characters you want here, but be careful: they
664        should be rare enough that people don't accidentally address the bot
665        (simply because they'll probably be annoyed if they do address the bot
666        on accident).  You can even have more than one.  I (jemfinch) am quite
667        partial to @, but that's because I've been using it since my ocamlbot
668        days.""")
669        import supybot.callbacks as callbacks
670        c = ''
671        while not c:
672            try:
673                c = anything('What would you like your bot\'s prefix '
674                             'character(s) to be?')
675                conf.supybot.reply.whenAddressedBy.chars.set(c)
676            except registry.InvalidRegistryValue as e:
677                output(str(e))
678                c = ''
679    else:
680        conf.supybot.reply.whenAddressedBy.chars.set('')
681
682    ###
683    # logging variables.
684    ###
685
686    if advanced:
687        # conf.supybot.log.stdout
688        output("""By default, your bot will log not only to files in the logs
689        directory you gave it, but also to stdout.  We find this useful for
690        debugging, and also just for the pretty output (it's colored!)""")
691        stdout = not yn('Would you like to turn off this logging to stdout?',
692                        default=False)
693        conf.supybot.log.stdout.setValue(stdout)
694        if conf.supybot.log.stdout():
695            # conf.something
696            output("""Some terminals may not be able to display the pretty
697            colors logged to stderr.  By default, though, we turn the colors
698            off for Windows machines and leave it on for *nix machines.""")
699            if os.name is not 'nt':
700                conf.supybot.log.stdout.colorized.setValue(
701                    not yn('Would you like to turn this colorization off?',
702                    default=False))
703
704        # conf.supybot.log.level
705        output("""Your bot can handle debug messages at several priorities,
706        CRITICAL, ERROR, WARNING, INFO, and DEBUG, in decreasing order of
707        priority. By default, your bot will log all of these priorities except
708        DEBUG.  You can, however, specify that it only log messages above a
709        certain priority level.""")
710        priority = str(conf.supybot.log.level)
711        logLevel = something('What would you like the minimum priority to be?'
712                             '  Just press enter to accept the default.',
713                             default=priority).lower()
714        while logLevel not in ['debug','info','warning','error','critical']:
715            output("""That's not a valid priority.  Valid priorities include
716            'DEBUG', 'INFO', 'WARNING', 'ERROR', and 'CRITICAL'""")
717            logLevel = something('What would you like the minimum priority to '
718                                 'be?  Just press enter to accept the default.',
719                                 default=priority).lower()
720        conf.supybot.log.level.set(logLevel)
721
722        # conf.supybot.databases.plugins.channelSpecific
723
724        output("""Many plugins in Supybot are channel-specific.  Their
725        databases, likewise, are specific to each channel the bot is in.  Many
726        people don't want this, so we have one central location in which to
727        say that you would prefer all databases for all channels to be shared.
728        This variable, supybot.databases.plugins.channelSpecific, is that
729        place.""")
730
731        conf.supybot.databases.plugins.channelSpecific.setValue(
732            not yn('Would you like plugin databases to be shared by all '
733                   'channels, rather than specific to each channel the '
734                   'bot is in?'))
735
736    output("""There are a lot of options we didn't ask you about simply
737              because we'd rather you get up and running and have time
738              left to play around with your bot.  But come back and see
739              us!  When you've played around with your bot enough to
740              know what you like, what you don't like, what you'd like
741              to change, then take a look at your configuration file
742              when your bot isn't running and read the comments,
743              tweaking values to your heart's desire.""")
744
745    # Let's make sure that src/ plugins are loaded.
746    conf.registerPlugin('Admin', True)
747    conf.registerPlugin('AutoMode', True)
748    conf.registerPlugin('Channel', True)
749    conf.registerPlugin('Config', True)
750    conf.registerPlugin('Misc', True)
751    conf.registerPlugin('Network', True)
752    conf.registerPlugin('NickAuth', True)
753    conf.registerPlugin('User', True)
754    conf.registerPlugin('Utilities', True)
755
756    ###
757    # Write the registry
758    ###
759
760    # We're going to need to do a darcs predist thing here.
761    #conf.supybot.debug.generated.setValue('...')
762
763    if advanced:
764        basedir = '.'
765    filename = os.path.join(basedir, '%s.conf')
766
767    filename = something("""In which file would you like to save
768                         this config?""", default=filename % nick)
769    if not filename.endswith('.conf'):
770        filename += '.conf'
771    registry.close(conf.supybot, os.path.expanduser(filename))
772
773    # Done!
774    output("""All done!  Your new bot configuration is %s.  If you're running
775    a *nix based OS, you can probably start your bot with the command line
776    "supybot %s".  If you're not running a *nix or similar machine, you'll
777    just have to start it like you start all your other Python scripts.""" % \
778                                                         (filename, filename))
779
780if __name__ == '__main__':
781    try:
782        main()
783    except KeyboardInterrupt:
784        # We may still be using bold text when exiting during a prompt
785        if questions.useBold:
786            import supybot.ansi as ansi
787            print(ansi.RESET)
788        print()
789        print()
790        output("""Well, it looks like you canceled out of the wizard before
791        it was done.  Unfortunately, I didn't get to write anything to file.
792        Please run the wizard again to completion.""")
793
794# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
795