Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeroji committed Aug 11, 2016
0 parents commit a574900
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__
data/
.python-version
*.log
todo.md
13 changes: 13 additions & 0 deletions README.md
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
35 changes: 35 additions & 0 deletions cogs/__init__.py
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]
9 changes: 9 additions & 0 deletions cogs/example.py
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
130 changes: 130 additions & 0 deletions core.py
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()
136 changes: 136 additions & 0 deletions docs/cogs.md
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.
Loading

0 comments on commit a574900

Please sign in to comment.