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