1# -*- coding: utf-8 -*-
2
3"""
4The MIT License (MIT)
5
6Copyright (c) 2015-present Rapptz
7
8Permission is hereby granted, free of charge, to any person obtaining a
9copy of this software and associated documentation files (the "Software"),
10to deal in the Software without restriction, including without limitation
11the rights to use, copy, modify, merge, publish, distribute, sublicense,
12and/or sell copies of the Software, and to permit persons to whom the
13Software is furnished to do so, subject to the following conditions:
14
15The above copyright notice and this permission notice shall be included in
16all copies or substantial portions of the Software.
17
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24DEALINGS IN THE SOFTWARE.
25"""
26
27import argparse
28import sys
29from pathlib import Path
30
31import discord
32import pkg_resources
33import aiohttp
34import platform
35
36def show_version():
37    entries = []
38
39    entries.append('- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(sys.version_info))
40    version_info = discord.version_info
41    entries.append('- discord.py v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(version_info))
42    if version_info.releaselevel != 'final':
43        pkg = pkg_resources.get_distribution('discord.py')
44        if pkg:
45            entries.append('    - discord.py pkg_resources: v{0}'.format(pkg.version))
46
47    entries.append('- aiohttp v{0.__version__}'.format(aiohttp))
48    uname = platform.uname()
49    entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
50    print('\n'.join(entries))
51
52def core(parser, args):
53    if args.version:
54        show_version()
55
56bot_template = """#!/usr/bin/env python3
57# -*- coding: utf-8 -*-
58
59from discord.ext import commands
60import discord
61import config
62
63class Bot(commands.{base}):
64    def __init__(self, **kwargs):
65        super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs)
66        for cog in config.cogs:
67            try:
68                self.load_extension(cog)
69            except Exception as exc:
70                print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
71
72    async def on_ready(self):
73        print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
74
75
76bot = Bot()
77
78# write general commands here
79
80bot.run(config.token)
81"""
82
83gitignore_template = """# Byte-compiled / optimized / DLL files
84__pycache__/
85*.py[cod]
86*$py.class
87
88# C extensions
89*.so
90
91# Distribution / packaging
92.Python
93env/
94build/
95develop-eggs/
96dist/
97downloads/
98eggs/
99.eggs/
100lib/
101lib64/
102parts/
103sdist/
104var/
105*.egg-info/
106.installed.cfg
107*.egg
108
109# Our configuration files
110config.py
111"""
112
113cog_template = '''# -*- coding: utf-8 -*-
114
115from discord.ext import commands
116import discord
117
118class {name}(commands.Cog{attrs}):
119    """The description for {name} goes here."""
120
121    def __init__(self, bot):
122        self.bot = bot
123{extra}
124def setup(bot):
125    bot.add_cog({name}(bot))
126'''
127
128cog_extras = '''
129    def cog_unload(self):
130        # clean up logic goes here
131        pass
132
133    async def cog_check(self, ctx):
134        # checks that apply to every command in here
135        return True
136
137    async def bot_check(self, ctx):
138        # checks that apply to every command to the bot
139        return True
140
141    async def bot_check_once(self, ctx):
142        # check that apply to every command but is guaranteed to be called only once
143        return True
144
145    async def cog_command_error(self, ctx, error):
146        # error handling to every command in here
147        pass
148
149    async def cog_before_invoke(self, ctx):
150        # called before a command is called here
151        pass
152
153    async def cog_after_invoke(self, ctx):
154        # called after a command is called here
155        pass
156
157'''
158
159
160# certain file names and directory names are forbidden
161# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
162# although some of this doesn't apply to Linux, we might as well be consistent
163_base_table = {
164    '<': '-',
165    '>': '-',
166    ':': '-',
167    '"': '-',
168    # '/': '-', these are fine
169    # '\\': '-',
170    '|': '-',
171    '?': '-',
172    '*': '-',
173}
174
175# NUL (0) and 1-31 are disallowed
176_base_table.update((chr(i), None) for i in range(32))
177
178translation_table = str.maketrans(_base_table)
179
180def to_path(parser, name, *, replace_spaces=False):
181    if isinstance(name, Path):
182        return name
183
184    if sys.platform == 'win32':
185        forbidden = ('CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', \
186                     'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9')
187        if len(name) <= 4 and name.upper() in forbidden:
188            parser.error('invalid directory name given, use a different one')
189
190    name = name.translate(translation_table)
191    if replace_spaces:
192        name = name.replace(' ', '-')
193    return Path(name)
194
195def newbot(parser, args):
196    new_directory = to_path(parser, args.directory) / to_path(parser, args.name)
197
198    # as a note exist_ok for Path is a 3.5+ only feature
199    # since we already checked above that we're >3.5
200    try:
201        new_directory.mkdir(exist_ok=True, parents=True)
202    except OSError as exc:
203        parser.error('could not create our bot directory ({})'.format(exc))
204
205    cogs = new_directory / 'cogs'
206
207    try:
208        cogs.mkdir(exist_ok=True)
209        init = cogs / '__init__.py'
210        init.touch()
211    except OSError as exc:
212        print('warning: could not create cogs directory ({})'.format(exc))
213
214    try:
215        with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp:
216            fp.write('token = "place your token here"\ncogs = []\n')
217    except OSError as exc:
218        parser.error('could not create config file ({})'.format(exc))
219
220    try:
221        with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp:
222            base = 'Bot' if not args.sharded else 'AutoShardedBot'
223            fp.write(bot_template.format(base=base, prefix=args.prefix))
224    except OSError as exc:
225        parser.error('could not create bot file ({})'.format(exc))
226
227    if not args.no_git:
228        try:
229            with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp:
230                fp.write(gitignore_template)
231        except OSError as exc:
232            print('warning: could not create .gitignore file ({})'.format(exc))
233
234    print('successfully made bot at', new_directory)
235
236def newcog(parser, args):
237    cog_dir = to_path(parser, args.directory)
238    try:
239        cog_dir.mkdir(exist_ok=True)
240    except OSError as exc:
241        print('warning: could not create cogs directory ({})'.format(exc))
242
243    directory = cog_dir / to_path(parser, args.name)
244    directory = directory.with_suffix('.py')
245    try:
246        with open(str(directory), 'w', encoding='utf-8') as fp:
247            attrs = ''
248            extra = cog_extras if args.full else ''
249            if args.class_name:
250                name = args.class_name
251            else:
252                name = str(directory.stem)
253                if '-' in name or '_' in name:
254                    translation = str.maketrans('-_', '  ')
255                    name = name.translate(translation).title().replace(' ', '')
256                else:
257                    name = name.title()
258
259            if args.display_name:
260                attrs += ', name="{}"'.format(args.display_name)
261            if args.hide_commands:
262                attrs += ', command_attrs=dict(hidden=True)'
263            fp.write(cog_template.format(name=name, extra=extra, attrs=attrs))
264    except OSError as exc:
265        parser.error('could not create cog file ({})'.format(exc))
266    else:
267        print('successfully made cog at', directory)
268
269def add_newbot_args(subparser):
270    parser = subparser.add_parser('newbot', help='creates a command bot project quickly')
271    parser.set_defaults(func=newbot)
272
273    parser.add_argument('name', help='the bot project name')
274    parser.add_argument('directory', help='the directory to place it in (default: .)', nargs='?', default=Path.cwd())
275    parser.add_argument('--prefix', help='the bot prefix (default: $)', default='$', metavar='<prefix>')
276    parser.add_argument('--sharded', help='whether to use AutoShardedBot', action='store_true')
277    parser.add_argument('--no-git', help='do not create a .gitignore file', action='store_true', dest='no_git')
278
279def add_newcog_args(subparser):
280    parser = subparser.add_parser('newcog', help='creates a new cog template quickly')
281    parser.set_defaults(func=newcog)
282
283    parser.add_argument('name', help='the cog name')
284    parser.add_argument('directory', help='the directory to place it in (default: cogs)', nargs='?', default=Path('cogs'))
285    parser.add_argument('--class-name', help='the class name of the cog (default: <name>)', dest='class_name')
286    parser.add_argument('--display-name', help='the cog name (default: <name>)')
287    parser.add_argument('--hide-commands', help='whether to hide all commands in the cog', action='store_true')
288    parser.add_argument('--full', help='add all special methods as well', action='store_true')
289
290def parse_args():
291    parser = argparse.ArgumentParser(prog='discord', description='Tools for helping with discord.py')
292    parser.add_argument('-v', '--version', action='store_true', help='shows the library version')
293    parser.set_defaults(func=core)
294
295    subparser = parser.add_subparsers(dest='subcommand', title='subcommands')
296    add_newbot_args(subparser)
297    add_newcog_args(subparser)
298    return parser, parser.parse_args()
299
300def main():
301    parser, args = parse_args()
302    args.func(parser, args)
303
304if __name__ == '__main__':
305    main()
306