├── pixie ├── __init__.py ├── plugins │ ├── __init__.py │ ├── moderation.py │ ├── info.py │ ├── owner.py │ ├── music.py │ └── weeb.py ├── utils │ ├── errors.py │ ├── __init__.py │ └── checks.py └── main.py ├── pixie.jpeg ├── setup_example.json ├── LICENSE ├── .gitignore └── README.md /pixie/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pixie/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pixie.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronvanstien/Pixie/HEAD/pixie.jpeg -------------------------------------------------------------------------------- /pixie/utils/errors.py: -------------------------------------------------------------------------------- 1 | class PixieException(Exception): 2 | pass 3 | 4 | 5 | class FailedHaste(PixieException): 6 | """ 7 | Raised when utils.hastebin fails to create a haste. 8 | """ 9 | pass 10 | -------------------------------------------------------------------------------- /setup_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "discord":{ 3 | "owner_id": "", 4 | "command_prefix": "", 5 | "token": "", 6 | "pixie_admins": [] 7 | }, 8 | 9 | "weeb":{ 10 | "ani_list": "", 11 | "hummingbird": "", 12 | "MAL": { 13 | "username":"", 14 | "password":"" 15 | } 16 | }, 17 | 18 | "games":{ 19 | "osu_api_key": "" 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /pixie/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | 3 | import aiohttp 4 | 5 | from .errors import FailedHaste 6 | 7 | # Our setup file 8 | with open('../setup.json') as file: 9 | setup_file = ujson.load(file) 10 | 11 | # Our user agent 12 | user_agent = "Pixie (https://github.com/GetRektByMe/Pixie)" 13 | 14 | 15 | async def hastebin(data: str): 16 | """ 17 | data (str): The data wanted in the hastebin document 18 | """ 19 | with aiohttp.ClientSession(headers={"User-Agent": user_agent}) as session: 20 | async with session.post("http://hastebin.com/documents", data=str(data)) as r: 21 | if r.status != 200: 22 | raise FailedHaste 23 | else: 24 | return "http://hastebin.com/{}.py".format(ujson.loads(await r.text())["key"]) 25 | -------------------------------------------------------------------------------- /pixie/utils/checks.py: -------------------------------------------------------------------------------- 1 | from . import setup_file 2 | 3 | 4 | def is_owner(ctx): 5 | return ctx.message.author.id == setup_file["discord"]['owner_id'] 6 | 7 | 8 | def pixie_admin(ctx): 9 | if is_owner(ctx): 10 | return True 11 | return ctx.message.author.id in setup_file["discord"]["pixie_admins"] 12 | 13 | 14 | def server_owner(ctx): 15 | if is_owner(ctx): 16 | return True 17 | if pixie_admin(ctx): 18 | return True 19 | return ctx.message.author == ctx.message.server.owner 20 | 21 | 22 | def server_admin(ctx): 23 | if is_owner(ctx): 24 | return True 25 | if pixie_admin(ctx): 26 | return True 27 | if server_owner(ctx): 28 | return True 29 | return ctx.message.author.server_permissions.administrator 30 | 31 | 32 | def server_moderator(ctx): 33 | if is_owner(ctx): 34 | return True 35 | if pixie_admin(ctx): 36 | return True 37 | if server_owner(ctx): 38 | return True 39 | if server_admin(ctx): 40 | return True 41 | return "pixie" in [role.name.lower() for role in ctx.message.author.roles] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Byron Vanstien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | setup.json 2 | 3 | ### Python template 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Pycharm 95 | .idea/ 96 | -------------------------------------------------------------------------------- /pixie/plugins/moderation.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from utils.checks import ( 5 | is_owner, 6 | pixie_admin, 7 | server_owner, 8 | server_admin, 9 | server_moderator 10 | ) 11 | 12 | 13 | # This needs to be rewritten entirely lol 14 | class Moderation: 15 | """Pixie's small set of moderation commands""" 16 | 17 | def __init__(self, bot): 18 | self.bot = bot 19 | 20 | @commands.check(server_moderator) 21 | @commands.command(pass_context=True, name="purge") 22 | async def purge(self, ctx, limit: int): 23 | try: 24 | await self.bot.purge_from(ctx.message.channel, limit=limit, before=ctx.message) 25 | await self.bot.say("```{0} messages deleted.```".format(limit)) 26 | except discord.errors.Forbidden: 27 | await self.bot.say("Sorry, I don't have the `manage messages` permission") 28 | 29 | @commands.check(server_moderator) 30 | @commands.command(pass_context=True, name="ban") 31 | async def ban_member(self, ctx, banned_for, days_to_delete: int = 1, *, reason: str): 32 | try: 33 | await self.bot.ban(ctx.message.mentions[0], delete_message_days=days_to_delete) 34 | await self.bot.say('```{0} has been banned for {1}```'.format(banned_for, reason)) 35 | except discord.errors.Forbidden: 36 | await self.bot.say("Sorry, I don't have the `ban` permission") 37 | 38 | @commands.check(server_moderator) 39 | @commands.command(pass_context=True, name="kick") 40 | async def kick_member(self, ctx, kicked_for, *, reason: str): 41 | try: 42 | await self.bot.kick(ctx.message.mentions[0]) 43 | await self.bot.say("```{0} has been kicked for {1}```".format(kicked_for, reason)) 44 | except discord.errors.Forbidden: 45 | await self.bot.say("Sorry, I don't have the `kick` permission") 46 | 47 | @commands.check(server_moderator) 48 | @commands.command(pass_context=True, name="softban") 49 | async def soft_ban(self, ctx, softbanned_for, days_to_delete: int = 1, *, reason: str): 50 | try: 51 | await self.bot.ban(ctx.message.mentions[0], delete_message_days=days_to_delete) 52 | await self.bot.unban(ctx.message.server, ctx.message.mentions[0]) 53 | await self.bot.say("```{0} has been softbanned for {1}```".format(softbanned_for, reason)) 54 | except discord.errors.Forbidden: 55 | await self.bot.say("Sorry I don't have the `ban` permission") 56 | 57 | 58 | def setup(bot): 59 | bot.add_cog(Moderation(bot)) 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS PROJECT IS NOW ARCHIVED 2 | See [here](https://github.com/WeebWare/Pixie) for the new iteration of Pixie, coming soon. 3 | 4 | # Pixie 5 | * An open-source Discord bot built for weebs by a weeb. 6 | * Here's the [invite link](https://discordapp.com/oauth2/authorize?client_id=175319652073734144&scope=bot&permissions=536083519) in case you want to add Pixie to your server. (Although she's not currently hosted by me), note that I am planning to host her quite soon now the addition of WeebMusic has been added! 7 | 8 | # Current Features 9 | * Owner 10 | * namechange (Changes the bots name) 11 | * gamechange (Changes the bots game) 12 | * avatar (Changes the bots avatar) 13 | * debug 14 | * REPL 15 | * Moderation 16 | * purge (Deletes large amounts of messages) (Requires manage message permission) 17 | * ban (Bans a user from a server) (Requires ban members permission) 18 | * kick (Kicks a user from a server) (Requires kick members permission) 19 | * Information 20 | * userinfo (Grabs information for a user) 21 | * serverinfo (Grabs general server information) 22 | * status (Shows some bot stats) 23 | * WeebMusic (Only supports streaming music from [listen.moe](https://listen.moe)) 24 | * music (a command group for using WeebMusic) 25 | * join (Joins voice channel and starts the stream) 26 | * pause (Pauses the stream) 27 | * resume (Resumes the stream) 28 | * volume (Sets the volume in the server you use the command in) 29 | * check_vol (Checks the volume of the server you're in) 30 | * disconnect (Leaves the voice channel and stops the stream) 31 | * Weeb 32 | * mal (a command group for interacting with myanimelist) 33 | * anisearch (Searches an anime) 34 | * mangasearch (Searches a manga) 35 | * novel (Searches a novel) 36 | 37 | 38 | # Planned Features 39 | * Weeb Features 40 | * Anime 41 | * Integration with MyAnimelist (being able to use your account from Pixie!) 42 | * Integration with Hummingbird (being able to use your account from Pixie!) 43 | * Integration with Anilist (being able to use your account from Pixie!) 44 | * Manga 45 | * Coming soon! (Tbh I have no idea what I can do for this, help is appreciated when it comes to ideas) 46 | * Custom server prefixes 47 | 48 | 49 | # Information 50 | * Pixie is built using [discord.py](https://github.com/Rapptz/discord.py) 51 | * Pixie is a reference to the character from [Mahouka Koukou no Rettousei](http://www.novelupdates.com/series/mahouka-koukou-no-rettousei/) light novels. 52 | 53 | # Setup 54 | 55 | But, now we're through all the boring stuff - here's what you need to get it running. This should be in the same folder as setup_example.json although it should be called setup.json 56 | ``` 57 | { 58 | "discord":{ 59 | "owner_id": "", 60 | "command_prefix": "", 61 | "token": "", 62 | "pixie_admins": [] 63 | }, 64 | 65 | "weeb":{ 66 | "ani_list": "", 67 | "hummingbird": "", 68 | "MAL": { 69 | "username":"", 70 | "password":"" 71 | } 72 | }, 73 | 74 | "games":{ 75 | "osu_api_key": "" 76 | }, 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /pixie/plugins/info.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | 5 | class Info: 6 | 7 | """Info is a class within Pixie that is only for accessing data from discords built in things (Although we add Pixie's status command here)""" 8 | 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | @commands.command(name="userinfo", pass_context=True) 13 | async def user_info(self, ctx, user: discord.Member = None): 14 | """Gets information about the desired user (defaults to the message sender)""" 15 | if not user: 16 | user = ctx.message.author 17 | msg = "```\n" 18 | msg += "User: %s\n" % user.name 19 | msg += "Nickname %s\n" % user.nick 20 | msg += "ID: %s\n" % user.id 21 | msg += "Created at: %s\n" % user.created_at 22 | msg += "Joined on: %s\n" % user.joined_at 23 | msg += "Game: %s\n" % user.game 24 | msg += "Roles: %s\n" % ", ".join([role.name for role in user.roles if role.name != "@everyone"]) 25 | msg += "```\n" 26 | msg += "Avatar: %s" % user.avatar_url 27 | await self.bot.send_message(ctx.message.channel, msg) 28 | 29 | @commands.command(name="guildinfo", pass_context=True) 30 | async def guild_info(self, ctx): 31 | """Gets information about the current server""" 32 | await self.bot.say("```xl\n" 33 | "Guild: {0}\n" 34 | "ID: {0.id}\n" 35 | "Region: {0.region}\n" 36 | "Member Count: {1}\n" 37 | "Owner: {0.owner}\n" 38 | "Icon: {0.icon_url}\n" 39 | "Roles: {2}" 40 | "```".format(ctx.message.server, sum(1 for x in ctx.message.server.members), 41 | ", ".join([x.name for x in ctx.message.server.roles]))) 42 | 43 | @commands.command(name="status") 44 | async def status(self): 45 | """Gives some general information about Pixie's current situation""" 46 | await self.bot.say("```xl\n" 47 | "I'm in {0} guilds\n" 48 | "I can currently see {1} people, {2} of which are unique\n" 49 | "I'm also in {3} voice channels" 50 | "```".format(len(self.bot.servers), 51 | sum(1 for x in self.bot.get_all_members()), 52 | len(set(self.bot.get_all_members())), 53 | len(self.bot.voice_clients))) 54 | 55 | @commands.command(name="info") 56 | async def info(self): 57 | await self.bot.say("```xl\n" 58 | "Hiya, I'm Pixie; I'm a bot built for weebs by Recchan.\n" 59 | "Check me out on Github, where you can see my codebase: https://github.com/GetRektByMe/Pixie\n" 60 | "Here's my invite link: https://discordapp.com/oauth2/authorize?client_id=175319652073734144&scope=bot&permissions=536083519```") 61 | 62 | 63 | def setup(bot): 64 | bot.add_cog(Info(bot)) 65 | -------------------------------------------------------------------------------- /pixie/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import traceback 4 | 5 | import logbook 6 | import discord 7 | from discord.ext import commands 8 | from logbook import Logger, StreamHandler 9 | from logbook.compat import redirect_logging 10 | from discord.ext.commands import Bot, when_mentioned_or 11 | 12 | from utils.checks import is_owner 13 | from utils.errors import FailedHaste 14 | from utils import setup_file, user_agent, hastebin 15 | 16 | 17 | # List of initial plugins to start up with (Loaded in Pixie.run()) 18 | plugins = ["plugins.weeb", "plugins.owner", "plugins.info", "plugins.moderation", "plugins.music"] 19 | 20 | 21 | class Pixie(Bot): 22 | 23 | def __init__(self, *args, **kwargs): 24 | super().__init__(command_prefix=when_mentioned_or(setup_file["discord"]["command_prefix"]), 25 | description="A bot for weebs programmed by Recchan") 26 | 27 | # Set a custom user agent for Pixie 28 | self.http.user_agent = user_agent 29 | 30 | # Logging setup 31 | redirect_logging() 32 | StreamHandler(sys.stderr).push_application() 33 | self.logger = Logger("Pixie") 34 | self.logger.level = getattr(logbook, setup_file.get("log_level", "INFO"), logbook.INFO) 35 | logging.root.setLevel(self.logger.level) 36 | 37 | async def on_ready(self): 38 | self.logger.info("Logged in as Bot Name: {0.user.name} Bot ID: {0.user.id}".format(self)) 39 | 40 | async def on_command_error(self, exception, ctx): 41 | print(exception) 42 | if isinstance(exception, commands.errors.CommandNotFound): 43 | return 44 | if isinstance(exception, commands.errors.CheckFailure): 45 | await self.send_message(ctx.message.channel, "You don't have the required permissions to run this command.") 46 | return 47 | # if is_owner(ctx): 48 | # try: 49 | # # Get a string of the traceback 50 | # trace = "".join(traceback.format_tb(exception.__traceback__)) 51 | # # Send that string as the data to hastebin 52 | # msg = await hastebin(trace) 53 | # # Send the link of the hastebin to discord 54 | # await self.send_message(ctx.message.channel, msg) 55 | # # Error raised when the hastebin fails 56 | # except FailedHaste: 57 | # await self.send_message(ctx.message.channel, "Failed to make hastebin.") 58 | 59 | async def on_member_join(self, member): 60 | # Auto roles people in the Mahouka (Onii-sama) server with the role "Member" 61 | if member.server.id == '209121677148160000': 62 | await bot.say("Hey {0.name}, welcome to {0.server.name}".format(member)) 63 | role = discord.utils.get(member.server.roles, name="Member") 64 | await bot.add_roles(member, role) 65 | 66 | async def on_voice_state_update(self, before, after): 67 | # If nothing changes just exit out of the function 68 | if before.voice.voice_channel == after.voice.voice_channel: 69 | return 70 | # Exit on channel being None as it errors if Pixie isn't in a voice channel 71 | if not after.server.me.voice_channel: 72 | return 73 | # Checks the length of the list of members in the voice channel 74 | if len(after.server.me.voice.voice_channel.voice_members) == 1: 75 | # Get the VoiceClient object 76 | voice = self.voice_client_in(after.server) 77 | # Disconnect the VoiceClient and close the stream 78 | await voice.disconnect() 79 | 80 | def run(self): 81 | # We load plugins in run rather than on_ready due to on_ready being able to be called multiple times 82 | for plugin in plugins: 83 | # We try to load the extension, and we account for if it fails 84 | try: 85 | self.load_extension(plugin) 86 | self.logger.info("{0} has been loaded".format(plugin)) 87 | # Except discord.ClientException so it doesn't fail to load all cogs when a cog doesn't have a setup function 88 | except discord.ClientException: 89 | self.logger.critical("{0} does not have a setup function!".format(plugin)) 90 | # Except import error (importlib raises this) so bot doesn't crash when it's raised 91 | except ImportError as IE: 92 | self.logger.critical(IE) 93 | # We check if discord.opus is loaded, despite it not having a reason to be 94 | if not discord.opus.is_loaded(): 95 | # Load discord.opus so we can use voice 96 | discord.opus.load_opus() 97 | self.logger.info("Opus has been loaded") 98 | super().run(setup_file["discord"]["token"]) 99 | 100 | 101 | if __name__ == "__main__": 102 | bot = Pixie() 103 | bot.run() 104 | -------------------------------------------------------------------------------- /pixie/plugins/owner.py: -------------------------------------------------------------------------------- 1 | import io 2 | import inspect 3 | import traceback 4 | from contextlib import redirect_stdout 5 | 6 | import discord 7 | from discord.ext import commands 8 | from utils.checks import is_owner 9 | 10 | 11 | class Owner: 12 | """A set of commands only for the owner of the bot""" 13 | 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.sessions = set() 17 | 18 | @commands.check(is_owner) 19 | @commands.command(name="namechange") 20 | async def name_change(self, *, name: str): 21 | """Lets the bot owner change the name of the bot""" 22 | await self.bot.edit_profile(username=name) 23 | await self.bot.say("```Bot name has been changed to: {}```".format(name)) 24 | 25 | @commands.check(is_owner) 26 | @commands.command(name="gamechange") 27 | async def game_change(self, *, game: str): 28 | """Lets the bot owner change the game of the bot""" 29 | await self.bot.change_status(discord.Game(name=game)) 30 | await self.bot.say('```Game has now been changed to: {}```'.format(game)) 31 | 32 | # TODO Change this to download a file and upload that instead 33 | @commands.check(is_owner) 34 | @commands.command(name="avatar") 35 | async def avatar(self, image: str): 36 | """Lets the bot owner change the avatar of the bot""" 37 | with open("{}.jpeg".format(image), 'rb') as image: 38 | image = image.read() 39 | await self.bot.edit_profile(avatar=image) 40 | await self.bot.say("My avatar has been changed!") 41 | 42 | @commands.check(is_owner) 43 | @commands.command(pass_context=True, hidden=True) 44 | async def debug(self, ctx, *, code: str): 45 | """Lets the bot owner evaluate code.""" 46 | code = code.strip('` ') 47 | python = '```py\n{}\n```' 48 | result = None 49 | 50 | env = { 51 | 'bot': self.bot, 52 | 'ctx': ctx, 53 | 'message': ctx.message, 54 | 'server': ctx.message.server, 55 | 'channel': ctx.message.channel, 56 | 'author': ctx.message.author 57 | } 58 | 59 | env.update(globals()) 60 | 61 | try: 62 | result = eval(code, env) 63 | if inspect.isawaitable(result): 64 | result = await result 65 | except Exception as e: 66 | await self.bot.say(python.format(type(e).__name__ + ': ' + str(e))) 67 | return 68 | 69 | await self.bot.say(python.format(result)) 70 | 71 | def cleanup_code(self, content): 72 | """Automatically removes code blocks from the code.""" 73 | # remove ```py\n``` 74 | if content.startswith('```') and content.endswith('```'): 75 | return '\n'.join(content.split('\n')[1:-1]) 76 | 77 | # remove `foo` 78 | return content.strip('` \n') 79 | 80 | def get_syntax_error(self, e): 81 | return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(e, '^', type(e).__name__) 82 | 83 | @commands.check(is_owner) 84 | @commands.command(pass_context=True, hidden=True) 85 | async def repl(self, ctx): 86 | msg = ctx.message 87 | 88 | variables = { 89 | 'ctx': ctx, 90 | 'bot': self.bot, 91 | 'message': msg, 92 | 'server': msg.server, 93 | 'channel': msg.channel, 94 | 'author': msg.author, 95 | 'last': None, 96 | } 97 | 98 | if msg.channel.id in self.sessions: 99 | await self.bot.say('Already running a REPL session in this channel. Exit it with `quit`.') 100 | return 101 | 102 | self.sessions.add(msg.channel.id) 103 | await self.bot.say('Enter code to execute or evaluate. `exit()` or `quit` to exit.') 104 | while True: 105 | response = await self.bot.wait_for_message(author=msg.author, channel=msg.channel, 106 | check=lambda m: m.content.startswith('`')) 107 | 108 | cleaned = self.cleanup_code(response.content) 109 | 110 | if cleaned in ('quit', 'exit', 'exit()'): 111 | await self.bot.say('Exiting.') 112 | self.sessions.remove(msg.channel.id) 113 | return 114 | 115 | executor = exec 116 | if cleaned.count('\n') == 0: 117 | # single statement, potentially 'eval' 118 | try: 119 | code = compile(cleaned, '', 'eval') 120 | except SyntaxError: 121 | pass 122 | else: 123 | executor = eval 124 | 125 | if executor is exec: 126 | try: 127 | code = compile(cleaned, '', 'exec') 128 | except SyntaxError as e: 129 | await self.bot.say(self.get_syntax_error(e)) 130 | continue 131 | 132 | variables['message'] = response 133 | 134 | fmt = None 135 | stdout = io.StringIO() 136 | 137 | try: 138 | with redirect_stdout(stdout): 139 | result = executor(code, variables) 140 | if inspect.isawaitable(result): 141 | result = await result 142 | except Exception as e: 143 | value = stdout.getvalue() 144 | fmt = '```py\n{}{}\n```'.format(value, traceback.format_exc()) 145 | else: 146 | value = stdout.getvalue() 147 | if result is not None: 148 | fmt = '```py\n{}{}\n```'.format(value, result) 149 | variables['last'] = result 150 | elif value: 151 | fmt = '```py\n{}\n```'.format(value) 152 | 153 | try: 154 | if fmt is not None: 155 | if len(fmt) > 2000: 156 | await self.bot.send_message(msg.channel, 'Content too big to be printed.') 157 | else: 158 | await self.bot.send_message(msg.channel, fmt) 159 | except discord.Forbidden: 160 | pass 161 | except discord.HTTPException as e: 162 | await self.bot.send_message(msg.channel, 'Unexpected error: `{}`'.format(e)) 163 | 164 | 165 | def setup(bot): 166 | bot.add_cog(Owner(bot)) 167 | -------------------------------------------------------------------------------- /pixie/plugins/music.py: -------------------------------------------------------------------------------- 1 | # Standard library imports 2 | import asyncio 3 | 4 | import discord 5 | from discord.ext import commands 6 | from discord.opus import OpusNotLoaded 7 | from utils import setup_file, user_agent 8 | 9 | 10 | class WeebMusic: 11 | 12 | """WeebMusic is a class within Pixie's plugins that's dedicated to creating a 13 | stream from listen.moe and allowing the users to listen to music from the stream""" 14 | 15 | def __init__(self, bot): 16 | self.bot = bot 17 | self.players = {} 18 | self.default_vol = 100 19 | 20 | @commands.group(pass_context=True) 21 | async def music(self, ctx): 22 | """A set of commands to play music from listen.moe""" 23 | # If a subcommand isn't called 24 | if ctx.invoked_subcommand is None: 25 | self.bot.say("Sorry, that's not a valid subcommand of WeebMusic") 26 | 27 | @music.command(name="join", pass_context=True) 28 | async def join_vc_and_play_stream(self, ctx, *, channel: discord.Channel = None): 29 | """Has the bot join a voice channel and also starts the stream from listen.moe""" 30 | try: 31 | # Because the bot needs a channel to join, if it's None we'll just return the function assuming they're not in a voice channel 32 | if channel is None: 33 | # Set it to the voice channel for the member who triggers the command 34 | channel = ctx.message.author.voice.voice_channel 35 | # Check if again None (If the command user isn't in a voice channel) 36 | if channel is None: 37 | # Tell the user they actually need to tell us what channel they want Pixie to join 38 | await self.bot.say("```xl\nSorry, I'm not too sure what channel you want me to join unless you tell me!```") 39 | # Exit out of the function so we don't try joining None 40 | return 41 | # Get the VoiceClient object 42 | voice_client = await self.bot.join_voice_channel(channel) 43 | # Set it to stereo and set sample rate to 48000 44 | voice_client.encoder_options(sample_rate=48000, channels=2) 45 | # Set the user agent and create the player 46 | player = voice_client.create_ffmpeg_player("http://listen.moe:9999/stream", headers={"User-Agent": user_agent}) 47 | # Set default player volume 48 | player.volume = self.default_vol / 100 49 | # Start the player 50 | player.start() 51 | # Be a tsun while telling the user that you joined the channel 52 | await self.bot.say("```xl\nI-I didn't join {0.channel} because you told me to... you b-b-baka! *hmph*```".format(voice_client)) 53 | # Add to the dict of server ids linked to objects 54 | self.players.update({ctx.message.server.id: player}) 55 | # Here we account for our bot not having enough perms or for the bot region being a bit dodgy 56 | except asyncio.TimeoutError: 57 | await self.bot.say("```xl\nSorry, I timed out trying to join!```") 58 | # This here pms the owner of the bot by the owner id in the setup file telling them if Opus isn't loaded 59 | except OpusNotLoaded: 60 | # Get the member object (here we assume the owner is in a server that the bot can see) 61 | member = discord.utils.get(self.bot.get_all_members(), id=setup_file["discord"]["owner_id"]) 62 | # Send a message to tell the owner that the Opus isn't loaded 63 | await self.bot.send_message(member, "```xl\nOpus library not loaded.```") 64 | # Account for if the bot is in a channel on the server already 65 | except discord.ClientException: 66 | await self.bot.say("```xl\nSorry, I'm already in a voice channel on this server!```") 67 | 68 | @music.command(name="pause", pass_context=True) 69 | async def pause_audio_stream(self, ctx): 70 | """Pauses the music""" 71 | # Get the player object from the dict using the server id as a key 72 | player = self.players[ctx.message.server.id] 73 | # Pause the bot's stream 74 | player.pause() 75 | # Tell the user who executed the command that the bot's stream is paused 76 | await self.bot.say("```xl\nStream has been paused```") 77 | 78 | @music.command(name="resume", pass_context=True) 79 | async def resume_audio_stream(self, ctx): 80 | """Unpauses the music""" 81 | # Get the player object from the dict using the server id as a key 82 | player = self.players[ctx.message.server.id] 83 | # Resume the bots stream 84 | player.resume() 85 | # Tell the user who executed the command that the stream is resumed 86 | await self.bot.say("```xl\nStream has been resumed```") 87 | 88 | @music.command(name="volume", pass_context=True) 89 | async def change_volume(self, ctx, volume: int = 100): 90 | """Allows the user to change the volume of the bot""" 91 | # Get the player object from the dict using the server id as a key 92 | player = self.players[ctx.message.server.id] 93 | # We divide volume by 100 because for some reason discord works on 1.0 as 100% 94 | player.volume = volume / 100 95 | # Check if the player volume is above 200 96 | if (player.volume * 100) > 200: 97 | # Tell the user their input isn't allowed 98 | await self.bot.say("```xl\nSorry, the max input is 200```") 99 | # Return the function as we don't want to set the volume 100 | return 101 | # Tell the user what the current volume now is 102 | await self.bot.say("```py\nVolume has been changed to: {}```".format(str(volume))) 103 | 104 | @music.command(name="check_vol", pass_context=True) 105 | async def check_volume(self, ctx): 106 | """Checks the volume for the servers voice channel that it's in""" 107 | # Get the player object from the dict using the server id as a key 108 | player = self.players[ctx.message.server.id] 109 | # Have the bot say the volume 110 | await self.bot.say("```xl\nThe current volume is: {}```".format(player.volume * 100)) 111 | 112 | @music.command(name="disconnect", pass_context=True) 113 | async def leave_vc(self, ctx): 114 | """Leaves the voice channel and stops the stream""" 115 | # Get the voice and player objects using the server id as a key 116 | voice = self.bot.voice_client_in(ctx.message.server) 117 | # Account for voice being None due to voice_client_in returning None if the bot isn't in a voice channel 118 | if voice is None: 119 | await self.bot.say("```xl\nSorry it doesn't seem like I'm in a voice channel in this server!```") 120 | return 121 | # Disconnect everything from the voice client object that the server is accessing 122 | await voice.disconnect() 123 | # Remove from the dictionaries since we no longer need to access this 124 | self.players.pop(ctx.message.server.id) 125 | 126 | 127 | def setup(bot): 128 | bot.add_cog(WeebMusic(bot)) 129 | -------------------------------------------------------------------------------- /pixie/plugins/weeb.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from Shosetsu import Shosetsu 3 | from discord.ext import commands 4 | from pyanimelist import PyAnimeList 5 | from Shosetsu.errors import VNDBOneResult, VNDBNoResults 6 | 7 | from utils import setup_file, user_agent 8 | 9 | 10 | class Weeb: 11 | """A set of commands for interacting with typical weeb things like MyAnimeList and NovelUpdates""" 12 | 13 | def __init__(self, bot): 14 | self.bot = bot 15 | # Our instance of pyanimelist, we pass a username and password here because it's needed for their (terrible) API 16 | self.pyanimelist = PyAnimeList( 17 | username=setup_file["weeb"]["MAL"]["username"], 18 | password=setup_file["weeb"]["MAL"]["password"], 19 | user_agent=user_agent 20 | ) 21 | 22 | self.shosetsu = Shosetsu() 23 | self.shosetsu.headers = {"User-Agent": user_agent} 24 | 25 | @commands.command(pass_context=True) 26 | async def vnsearch(self, ctx, *, vn_name: str): 27 | author = ctx.message.author 28 | avatar = author.avatar_url or author.default_avatar_url 29 | try: 30 | # List of result dictionaries 31 | vns = await self.shosetsu.search_vndb("v", vn_name) 32 | except VNDBOneResult: 33 | # Single dictionary 34 | vn = await self.shosetsu.get_novel(vn_name) 35 | except VNDBNoResults: 36 | await self.bot.send_message(ctx.message.channel, "VN not found.") 37 | else: 38 | # Put first 15 into a dictionary with ints as keys 39 | vns_ = dict(enumerate(vns[:15])) 40 | # Add all the anime names there to let the user select 41 | message = "```What visual novel would you like:\n" 42 | for vn_ in vns_.items(): 43 | message += "[{}] {}\n".format(str(vn_[0]), vn_[1]["name"]) 44 | message += "\nUse the number to the side of the vn as a key to select it!```" 45 | 46 | # Send the message for this 47 | await self.bot.send_message(ctx.message.channel, message) 48 | msg = await self.bot.wait_for_message(timeout=10.0, author=author) 49 | # If they don't send a message 50 | if not msg: 51 | return 52 | # Use this to get our dictionary 53 | key = int(msg.content) 54 | try: 55 | # Get the dictionary the user wants 56 | vn = vns_[key] 57 | # Get the ID that we have from searching 58 | id_ = vns_[key]["id"] 59 | except (ValueError, KeyError): 60 | await self.bot.send_message(ctx.message.channel, "Invalid key.") 61 | 62 | # Actually get the visual novel we want 63 | vn = await self.shosetsu.get_novel(vn["name"]) 64 | 65 | embed = discord.Embed( 66 | title=vn["titles"]["english"], 67 | colour=discord.Colour(0x7289da), 68 | url="https://vndb.org/" + id_ 69 | ) 70 | 71 | embed.set_author(name=author, icon_url=avatar) 72 | embed.set_image(url=vn["img"]) 73 | embed.add_field(name="Publishers", value=", ".join(vn["publishers"])) 74 | embed.add_field(name="Developers", value=", ".join(vn["developers"])) 75 | embed.add_field(name="Length", value=vn["length"] or "Not Stated") 76 | embed.add_field(name="Tags", value=", ".join({ 77 | _ for _ in ( 78 | vn["tags"]["content"] + vn["tags"]["technology"] + vn["tags"]["erotic"] 79 | ) 80 | }), inline=False) 81 | 82 | await self.bot.send_message(ctx.message.channel, embed=embed) 83 | 84 | @commands.command(pass_context=True) 85 | async def anisearch(self, ctx, *, anime_name: str): 86 | author = ctx.message.author 87 | avatar = author.avatar_url or author.default_avatar_url 88 | # List of anime objects 89 | animes = await self.pyanimelist.search_all_anime(anime_name) 90 | # Put the first 15 in a dictionary with ints as keys 91 | animes_ = dict(enumerate(animes[:15])) 92 | # Add all the anime names there to let the user select 93 | message = "```What anime would you like:\n" 94 | for anime in animes_.items(): 95 | message += "[{}] {}\n".format(str(anime[0]), anime[1].titles.jp) 96 | message += "\nUse the number to the side of the anime as a key to select it!```" 97 | await self.bot.send_message(ctx.message.channel, message) 98 | msg = await self.bot.wait_for_message(timeout=10.0, author=author) 99 | # If they don't send a message 100 | if not msg: 101 | return 102 | # Use this to get our anime object 103 | key = int(msg.content) 104 | try: 105 | # Get the anime object the user wants 106 | anime = animes_[key] 107 | except (ValueError, KeyError): 108 | await self.bot.send_message(ctx.message.channel, "Invalid key.") 109 | 110 | embed = discord.Embed( 111 | title=anime.titles.jp, 112 | colour=discord.Colour(0x7289da), 113 | url="https://myanimelist.net/anime/{0.id}/{0.titles.jp}".format(anime).replace(" ", "%20") 114 | ) 115 | 116 | embed.set_author(name=author.display_name, icon_url=avatar) 117 | embed.set_image(url=anime.cover) 118 | embed.add_field(name="Episode Count", value=str(anime.episode_count)) 119 | embed.add_field(name="Type", value=anime.type) 120 | embed.add_field(name="Status", value=anime.status) 121 | embed.add_field(name="Synopsis", value=anime.synopsis.split("\n\n", maxsplit=1)[0]) 122 | 123 | await self.bot.send_message(ctx.message.channel, embed=embed) 124 | 125 | @commands.command(pass_context=True) 126 | async def mangasearch(self, ctx, *, manga_name: str): 127 | """ 128 | Lets the user get data from a manga from myanimelist 129 | """ 130 | author = ctx.message.author 131 | avatar = author.avatar_url or author.default_avatar_url 132 | # List of manga objects 133 | mangas = await self.pyanimelist.search_all_manga(manga_name) 134 | # Put the first 15 in a dictionary with ints as keys 135 | mangas_ = dict(enumerate(mangas[:15])) 136 | # Add all the anime names there to let the user select 137 | message = "```What manga would you like:\n" 138 | for manga in mangas_.items(): 139 | message += "[{}] {}\n".format(str(manga[0]), manga[1].titles.jp) 140 | message += "\nUse the number to the side of the manga as a key to select it!```" 141 | await self.bot.send_message(ctx.message.channel, message) 142 | msg = await self.bot.wait_for_message(timeout=10.0, author=ctx.message.author) 143 | # If they don't send a message 144 | if not msg: 145 | return 146 | # Use this to get our manga object 147 | key = int(msg.content) 148 | try: 149 | # Get the manga object the user wants 150 | manga = mangas_[key] 151 | except (ValueError, KeyError): 152 | await self.bot.send_message(ctx.message.channel, "Invalid key.") 153 | 154 | embed = discord.Embed( 155 | title=manga.titles.jp, 156 | colour=discord.Colour(0x7289da), 157 | url="https://myanimelist.net/manga/{0.id}/{0.titles.jp}".format(manga).replace(" ", "%20") 158 | ) 159 | 160 | embed.set_author(name=author.display_name, icon_url=avatar) 161 | embed.set_image(url=manga.cover) 162 | embed.add_field(name="Volume Count", value=str(manga.volumes)) 163 | embed.add_field(name="Type", value=manga.type) 164 | embed.add_field(name="Status", value=manga.status) 165 | embed.add_field(name="Synopsis", value=manga.synopsis.split("\n\n", maxsplit=1)[0]) 166 | 167 | await self.bot.send_message(ctx.message.channel, embed=embed) 168 | 169 | 170 | class NSFW: 171 | """ 172 | A class for interacting with sites like Gelbooru 173 | """ 174 | def __init__(self, bot): 175 | pass 176 | 177 | 178 | def setup(bot): 179 | bot.add_cog(Weeb(bot)) 180 | bot.add_cog(NSFW(bot)) 181 | --------------------------------------------------------------------------------