-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a574900
Showing
8 changed files
with
469 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
__pycache__ | ||
data/ | ||
.python-version | ||
*.log | ||
todo.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
## ;; | ||
|
||
> v0.1.0 | ||
This is the repository for the *new* version of `;;`, a nice Discord bot with currently no features. | ||
[Old version here.](http://github.com/Zeroji/semicold) | ||
|
||
If you want to add features, feel free to [write a cog](https://github.com/Zeroji/semicolon/blob/master/docs/cogs.md)! | ||
|
||
> *A side note about the `data` folder which is ignored* | ||
`data/secret/token` contains the bot's token, without newline | ||
`data/admins` and `data/banned` contain newline-separated IDs | ||
`data/master` contains the owner's ID, without newline |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
"""Importing all cogs.""" | ||
COGS = {} | ||
|
||
def load(name): | ||
import importlib | ||
import logging | ||
mod = None | ||
try: | ||
mod = importlib.import_module(__name__ + '.' + name) | ||
except Exception as exc: # Yes I know it's too general. Just wanna catch 'em all. | ||
logging.critical("Error while loading '%s': %s", name, exc) | ||
else: | ||
logging.info("Loaded module '%s'.", name) | ||
COGS[name] = mod | ||
|
||
def cog(name): | ||
"""Returns a Cog object given its name.""" | ||
if not name in COGS: | ||
return None | ||
return COGS[name].cog | ||
|
||
def command(cmd): | ||
"""Finds a command given its name.""" | ||
matches = [] | ||
for name, _cog in COGS.items(): | ||
if _cog.cog.has(cmd): | ||
matches.append((name, _cog.cog.get(cmd))) | ||
if not matches: | ||
return None | ||
if len(matches) == 1: | ||
# Return the Command object | ||
return matches[0][1] | ||
else: | ||
# Return the names of the cogs containing that command | ||
return [name for name, _ in matches] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""Example cog.""" | ||
import gearbox | ||
cog = gearbox.Cog('example') | ||
|
||
@cog.command | ||
@cog.alias('hi') | ||
def hello(author): | ||
"""Say hello.""" | ||
return 'Hello, %s!' % author.name |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
#!/usr/bin/env python | ||
"""Bot core.""" | ||
import asyncio | ||
import importlib | ||
import logging | ||
import os.path | ||
import time | ||
import discord | ||
import cogs | ||
import gearbox | ||
|
||
|
||
class Bot(discord.Client): | ||
"""Client wrapper.""" | ||
|
||
def __init__(self, master='', admins=(), banned=()): | ||
"""Magic method docstring.""" | ||
super(Bot, self).__init__() | ||
self.master = master | ||
self.admins = admins | ||
self.banned = banned | ||
self.cogs = {} | ||
self.last_update = time.time() | ||
|
||
def run(self, *args): | ||
"""Start client.""" | ||
super(Bot, self).run(*args) | ||
|
||
async def on_message(self, message): | ||
"""Handle messages.""" | ||
# Avoid non-dev servers [TEMP] (Imgur ARGs & Nightcore Reality) | ||
if message.channel.is_private or message.server.id in \ | ||
('133648084671528961', '91460936186990592', '211982476745113601'): | ||
return | ||
# Avoid replying to self [TEMP] | ||
if message.author == self.user: | ||
return | ||
|
||
# Detecting and stripping prefixes | ||
prefixes = [';'] | ||
prefixes.append(self.user.mention) | ||
breaker = '|' | ||
text = message.content | ||
if not message.channel.is_private: | ||
text, is_command = gearbox.strip_prefix(text, prefixes) | ||
if is_command: | ||
commands = (text,) | ||
else: | ||
if breaker * 2 in text: | ||
text = text[text.find(breaker * 2) + 2:].lstrip() | ||
text, is_command = gearbox.strip_prefix(text, prefixes) | ||
if is_command: | ||
commands = (text,) | ||
elif breaker in text: | ||
parts = [part.strip() for part in text.split(breaker)] | ||
commands = [] | ||
for part in parts: | ||
part, is_command = gearbox.strip_prefix(part, prefixes) | ||
if is_command: | ||
commands.append(part) | ||
is_command = len(parts) > 0 | ||
if not is_command: | ||
return | ||
else: | ||
commands = (gearbox.strip_prefix(text, prefixes)[0],) | ||
|
||
for text in commands: | ||
# Getting command arguments (or not) | ||
if ' ' in text: | ||
command, arguments = text.split(' ', 1) | ||
else: | ||
command, arguments = text, '' | ||
|
||
if '.' in command: | ||
# Getting command from cog when using cog.command | ||
cog, cmd = command.split('.') | ||
cog = cogs.cog(cog) | ||
if not cog: | ||
return | ||
func = cog.get(cmd) | ||
else: | ||
# Checking for command existence / possible duplicates | ||
func = cogs.command(command) | ||
if isinstance(func, list): | ||
output = ("The command `%s` was found in multiple cogs: %s. Use <cog>.%s to specify." % | ||
(command, gearbox.pretty(func, '`%s`'), command)) | ||
await self.send_message(message.channel, output) | ||
if isinstance(func, gearbox.Command): | ||
await func.call(self, message, arguments) | ||
|
||
async def wheel(self): # They see me loading | ||
logging.info('Wheel rolling.') | ||
while True: | ||
for name, cog in cogs.COGS.items(): | ||
if os.path.getmtime(cog.__file__) > self.last_update: | ||
try: | ||
importlib.reload(cog) | ||
except Exception as exc: | ||
logging.error("Error while reloading '%s': %s", name, exc) | ||
else: | ||
logging.info("Reloaded '%s'.", name) | ||
self.last_update = time.time() | ||
for name in [f[:-3] for f in os.listdir('cogs') if f.endswith('.py')]: | ||
if name not in cogs.COGS and gearbox.is_valid(name): | ||
cogs.load(name) # They're addin' | ||
await asyncio.sleep(2) | ||
|
||
|
||
async def on_ready(self): | ||
"""Initialization.""" | ||
self.loop.create_task(self.wheel()) | ||
await super(Bot, self).change_status(idle=True) | ||
logging.info('Client started.') | ||
|
||
|
||
def main(): | ||
"""Load authentication data and run the bot.""" | ||
logging.basicConfig(filename='run.log', level=logging.DEBUG) | ||
logging.info('Starting...') | ||
token = open('data/secret/token', 'r').read().strip() | ||
|
||
master = open('data/master', 'r').read().strip() | ||
admins = open('data/admins', 'r').read().splitlines() | ||
banned = open('data/banned', 'r').read().splitlines() | ||
|
||
bot = Bot(master, admins, banned) | ||
bot.run(token) | ||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
## How to write your cog | ||
|
||
`;;` draws its main features from modules, named "cogs". | ||
Writing one is rather straightforward, but a couple rules have to be respected. | ||
|
||
### Match `[a-z][a-z_0-9]*\.py` | ||
|
||
Don't run away ~~yet~~! This simply means that the name of your file must be **full lowercase** and it has to start by a letter (any file starting with `_` or a digit will be ignored). Once you have that file, just drop it in the `cogs` folder and that's all. | ||
|
||
### Don't forget your tools | ||
|
||
Every cog must contain a `cog` variable, which has to be an instance of `gearbox.Cog`. Here's what a standard cog header looks like: | ||
```python | ||
import gearbox | ||
cog = gearbox.Cog() | ||
``` | ||
> By default, your cog's name will be the file name, minus the `.py` part. | ||
To change this, simply pass a new name as an argument to `gearbox.Cog()`. | ||
|
||
### Creating a command | ||
|
||
#### The basics | ||
|
||
If you're familiar with `discord.py`, then you probably know about `async`, `await` and all that stuff. If not, don't worry! You don't need that to write stuff. | ||
|
||
Every command must be "decorated" by placing `@cog.command` before its definition. After that step, it'll be recognized by `;;` as a command - as long as it has a valid name (see above). Here's a really simple command: | ||
|
||
```python | ||
@cog.command | ||
def hello(): | ||
return 'Hello, world!' | ||
``` | ||
|
||
Straightforward, right? Your command just have to return something, and `;;` will send it in the good channel. If you return nothing... Well, nothing happens. | ||
But what if you want it to greet someone specifically? | ||
|
||
#### Special arguments | ||
|
||
Greeting a user can be done very simply: | ||
|
||
```python | ||
@cog.command | ||
def hello(author): | ||
return 'Hello, %s!' % author.name | ||
``` | ||
|
||
> If you really aren't familiar with `discord.py`, have a look at [their documentation](http://discordpy.readthedocs.io/en/latest/). For very simple usage, you can get a user/channel/server name with `.name`. | ||
> Wondering what this `%` thing does? Basically, it's the same as `'Hello, ' + author.name + '!'` but shorter and fancier. | ||
[Learn more here](https://docs.python.org/3.5/library/stdtypes.html#printf-style-string-formatting) | ||
|
||
As you can see, simply putting `author` in the function definition will give you the corresponding object. Why? Because `;;` is made in such a way that it'll look at what you want, and attempt to provide it to you so you don't need to write extra pieces of code. Here's a list of those "special" arguments: *(as of v0.1.0)* | ||
|
||
|Argument | Description | ||
|- | ||
|`client` | The application's `discord.Client()` | ||
|`message` | The Message object which was sent - don't use like a string! | ||
|`author` | Shortcut for `message.author` | ||
|`channel` | Shortcut for `message.channel` | ||
|`server` | Shortcut for `message.server` | ||
|
||
*Remember that using those will give you special values, which might not correspond to your expectations.* | ||
|
||
#### Normal arguments | ||
|
||
Now maybe you simply want to write a `repeat` command, but you don't know how to get the text? Just ask for it! | ||
|
||
```python | ||
@cog.command | ||
def repeat(what_they_said): | ||
return what_they_said | ||
``` | ||
|
||
When sending arguments to commands, `;;` will take care of special arguments, then send the rest of the message to the other arguments. If you need multiple arguments, just define them! | ||
|
||
```python | ||
@cog.command | ||
def add(number_a, number_b): | ||
return str(int(number_a) + int(number_b)) | ||
``` | ||
|
||
> *May change in future versions* | ||
If the user doesn't provide the arguments you need, for example if they type `add 4`, `;;` will send an empty string (`''`) for each missing arguments. | ||
|
||
> If the user sends too many arguments, for example by typing `add 1 2 3`, then your last argument will receive the extra information. Here, `number_a` would contain `'1'` but `number_b` would contain `'2 3'`. You can discard unwanted information by adding a third argument which will take it: | ||
```python | ||
def add(number_a, number_b, trash): | ||
``` | ||
|
||
#### More arguments! | ||
|
||
Let's say you want to have a command that takes a string, then a series of strings, and inserts that first string between all the others, i.e. `, 1 2 3` would give `1,2,3` - wow, that's just like `str.join()`! | ||
You'll want to have the first argument, then "all the rest". Of course, you could get away with using `def join(my_string, all_the_rest):` and then use `.split()`, but ;; can do that for you! Simply add `*` before your last argument, and it'll receive a nice little list of whatever was sent: | ||
|
||
```python | ||
@cog.command | ||
def join(my_string, *all_the_rest): | ||
return my_string.join(all_the_rest) | ||
``` | ||
|
||
#### About `async` and `await` | ||
|
||
What if you're an advanced user and you know all that async stuff already and just want to add your tasks to the event loop while awaiting coroutines? | ||
|
||
```python | ||
async def command(client): | ||
``` | ||
|
||
It's that simple. If your command is a coroutine, then `;;` will simply `await` it (if you want to send a message, do it yourself!); and the `client` argument will give you access to the main client. Hint, the loop is at `client.loop` | ||
|
||
### Decorating your command | ||
|
||
No, this isn't about adding a cute little ribbon onto it. *(as of v0.1.0)* | ||
|
||
You've already used the decorator `@cog.command` to indicate that your function was a `;;` command and not a random function. | ||
You can do a bit more, here, have a list: | ||
|
||
##### `@cog.rename(name)` | ||
This will change the name of your command. It's useful, for example, if you want your command to be called `str` but you can't because of Python's `str()` function. Just call your function `asdf` and put `@cog.rename('str')` before it. | ||
|
||
##### `@cog.alias(alias, ...)` | ||
This creates aliases for your command. Let's say you find `encrypt` is a quite long name, just add `@cog.alias('en')` and you'll be able to call your command with `encrypt` *and* `en`. | ||
|
||
##### `@cog.init` | ||
This doesn't apply to a command, but to a regular function - it marks it, so it will be called when the cog is loaded. You can only have one init function. | ||
> *Not yet implemented as of v0.1.0* | ||
##### `@cog.exit` | ||
This doesn't apply to a command, but to a regular function - it marks it, so it will be called when the cog is unloaded. You can only have one exit function. | ||
> *Not yet implemented as of v0.1.0* | ||
### Using your cog | ||
|
||
As written above, you just need to drop it in the `cogs` folder! | ||
If `;;` is running, it'll automatically load it within a couple of seconds, and reload it when you edit it. Don't worry, if you break stuff, it'll keep running the old code until the new one is working. | ||
If you have name conflicts with another module, call your commands with `cog_name.command_name` to avoid collisions. |
Oops, something went wrong.