Compare commits
114 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
004632537b | ||
|
d528213c5f | ||
|
c3613f196a | ||
|
fdca6da624 | ||
|
5c9278bc63 | ||
|
35f5bb684b | ||
|
7c62e59c3a | ||
|
b995b866ec | ||
|
9f62c6c67e | ||
|
855413a093 | ||
|
cea5e6f855 | ||
|
e7b7a5b832 | ||
|
0417dddf2f | ||
|
a0378e09c9 | ||
|
5b241eee13 | ||
|
0e510cc8b6 | ||
|
1c4fdb8d9e | ||
|
8c71f7bae0 | ||
|
9faa59a41d | ||
|
787a69757f | ||
|
23355bb699 | ||
|
9af3abd4a5 | ||
|
4da89fa815 | ||
|
ff3520f8fd | ||
|
a98c82b332 | ||
|
8d01f70a5c | ||
|
80dcc75a58 | ||
|
0bb6eb6b6a | ||
|
5b6ec72d0b | ||
|
0b8bbb4213 | ||
|
9960209c47 | ||
|
74a028dfca | ||
|
cbc9550f97 | ||
|
9797f731a7 | ||
|
1aa17eebfb | ||
|
0da04acc4e | ||
|
4ae8753095 | ||
|
fdb16287ce | ||
|
bb7ff3fdea | ||
|
ff8a825846 | ||
|
9e68e8e8a2 | ||
|
29d34320f1 | ||
|
05c83a50ec | ||
|
6eb4325f3f | ||
|
3a93b9e639 | ||
|
93cb2784ee | ||
|
abfc6189d3 | ||
|
2063072cbb | ||
|
eee71ad774 | ||
|
fcd456ea52 | ||
|
da0dc3576e | ||
|
6449f17864 | ||
|
abc0dc8c70 | ||
|
dd8f02618b | ||
|
3b07ed99b2 | ||
|
23f5c76f13 | ||
|
709d05da73 | ||
|
ac2491c005 | ||
|
07fe4d1c6e | ||
|
73c3de9c99 | ||
|
81af6ba8bd | ||
|
836a696dc5 | ||
|
8bcf296f2f | ||
|
d8cdaae7b3 | ||
|
5e46ab940d | ||
|
a182e55a65 | ||
|
3cc712b835 | ||
|
2118cc26eb | ||
|
bf984bacd0 | ||
|
7c7d92948a | ||
|
39be6ea582 | ||
|
c0fc471a8f | ||
|
479d009346 | ||
|
4fdb5d19e6 | ||
|
7a28374d7a | ||
|
67e36fca81 | ||
|
6c87e6d9f0 | ||
|
334ed442fd | ||
|
1523cfaf3a | ||
|
7d0844e156 | ||
|
34590dfa32 | ||
|
f989deb0d9 | ||
|
6df2cb9d79 | ||
|
f65248f797 | ||
|
86eddea996 | ||
|
764e88a222 | ||
|
df7007efd1 | ||
|
6cf5e4bacc | ||
|
eb2e59656e | ||
|
b8aed568f4 | ||
|
101bd91718 | ||
|
9ea10b18f1 | ||
|
966ac38603 | ||
|
2bc2c70d1b | ||
|
b91731e4ca | ||
|
3287b20c43 | ||
|
dfd2cd76af | ||
|
08a98febca | ||
|
0a66ed4fb8 | ||
|
03bad6ff1c | ||
|
7faea22fe1 | ||
|
0476ddd5df | ||
|
95513d21a1 | ||
|
9bc48c3de9 | ||
|
541001d3c8 | ||
|
2d337313f2 | ||
|
8adff14dbb | ||
|
708d5146e8 | ||
|
afb9cc7f12 | ||
|
f6c4078d09 | ||
|
8e88b21516 | ||
|
3cf1c6d02a | ||
|
97316c750b | ||
|
311457148e |
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
github: archangelic
|
||||
ko_fi: archangelic
|
||||
liberapay: archangelic
|
||||
custom: ["https://paypal.me/archangelic", "https://cash.app/$archangelic"]
|
30
.github/workflows/python-publish.yml
vendored
Normal file
30
.github/workflows/python-publish.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# This workflows will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py upload
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -99,3 +99,6 @@ ENV/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# etc
|
||||
.vscode/
|
@ -1 +1 @@
|
||||
include README.rst
|
||||
include README.md
|
||||
|
163
README.md
163
README.md
@ -1,19 +1,84 @@
|
||||
# pinhook
|
||||
a pluggable irc bot framework in python
|
||||
|
||||
## Tutorial
|
||||
### Installation
|
||||
```
|
||||
$ pip install pinhook
|
||||
[](https://pypi.org/project/pinhook) [](https://github.com/archangelic/pinhook/blob/master/LICENSE) [](https://pypi.org/project/pinhook) [](https://pypi.org/project/pinhook) [](https://tilde.town)
|
||||
|
||||
The pluggable python framework for IRC bots and Twitch bots
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Creating an IRC Bot](#creating-an-irc-bot)
|
||||
* [From Config File](#from-config-file)
|
||||
* [From Python File](#from-python-file)
|
||||
* [Creating a Twitch Bot](#creating-a-twitch-bot)
|
||||
* [Creating plugins](#creating-plugins)
|
||||
* [Examples](#examples)
|
||||
|
||||
## Installation
|
||||
|
||||
Pinhook can be installed from PyPI:
|
||||
|
||||
``` bash
|
||||
pip install pinhook
|
||||
```
|
||||
|
||||
### Creating the Bot
|
||||
## Creating an IRC Bot
|
||||
|
||||
A pinhook bot can be initialized using the command line tool `pinhook` with a config file, or by importing it into a python file to extend the base class.
|
||||
|
||||
### From Config File
|
||||
|
||||
Pinhook supports configuration files in YAML, TOML, and JSON formats.
|
||||
|
||||
Example YAML config:
|
||||
|
||||
```YAML
|
||||
nickname: "ph-bot"
|
||||
server: "irc.somewhere.net"
|
||||
channels:
|
||||
- "#foo"
|
||||
- "#bar"
|
||||
```
|
||||
|
||||
Required configuration keys:
|
||||
|
||||
* `nickname`: (string) nickname for your bot
|
||||
* `server`: (string) server for the bot to connect
|
||||
* `channels`: (array of strings) list of channels to connect to once connected
|
||||
|
||||
Optional keys:
|
||||
|
||||
* `port`: (default: `6667`) choose a custom port to connect to the server
|
||||
* `ops`: (default: empty list) list of operators who can do things like make the bot join other channels or quit
|
||||
* `plugin_dir`: (default: `"plugins"`) directory where the bot should look for plugins
|
||||
* `log_level`: (default: `"info"`) string indicating logging level. Logging can be disabled by setting this to `"off"`
|
||||
* `ns_pass`: this is the password to identify with nickserv
|
||||
* `server_pass`: password for the server
|
||||
* `ssl_required`: (default: `False`) boolean to turn ssl on or off
|
||||
|
||||
Once you have your configuration file ready and your plugins in place, you can start your bot from the command line:
|
||||
|
||||
```bash
|
||||
pinhook config.yaml
|
||||
```
|
||||
|
||||
Pinhook will try to detect the config format from the file extension, but the format can also be supplied using the `--format` option.
|
||||
|
||||
```bash
|
||||
$ pinhook --help
|
||||
Usage: pinhook [OPTIONS] CONFIG
|
||||
|
||||
Options:
|
||||
-f, --format [json|yaml|toml]
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
### From Python File
|
||||
|
||||
To create the bot, just create a python file with the following:
|
||||
|
||||
```python
|
||||
import pinhook.bot
|
||||
from pinhook.bot import Bot
|
||||
|
||||
bot = pinhook.bot.Bot(
|
||||
bot = Bot(
|
||||
channels=['#foo', '#bar'],
|
||||
nickname='ph-bot',
|
||||
server='irc.freenode.net'
|
||||
@ -24,19 +89,52 @@ bot.start()
|
||||
This will start a basic bot and look for plugins in the 'plugins' directory to add functionality.
|
||||
|
||||
Optional arguments are:
|
||||
* `port`: choose a custom port to connect to the server (default: 6667)
|
||||
* `ops`: list of operators who can do things like make the bot join other channels or quit (default: empty list)
|
||||
* `plugin_dir`: directory where the bot should look for plugins (default: "plugins")
|
||||
* `log_level`: string indicating logging level. Logging can be disabled by setting this to "off". (default: "info")
|
||||
|
||||
### Creating plugins
|
||||
In your chosen plugins directory ("plugins" by default) make a python file with a function. You can use the `@pinhook.plugin.register` decorator to tell the bot the command to activate the function.
|
||||
* `port`: (default: `6667`) choose a custom port to connect to the server
|
||||
* `ops`: (default: empty list) list of operators who can do things like make the bot join other channels or quit
|
||||
* `plugin_dir`: (default: `"plugins"`) directory where the bot should look for plugins
|
||||
* `log_level`: (default: `"info"`) string indicating logging level. Logging can be disabled by setting this to `"off"`
|
||||
* `ns_pass`: this is the password to identify with nickserv
|
||||
* `server_pass`: password for the server
|
||||
* `ssl_required`: (default: `False`) boolean to turn ssl on or off
|
||||
|
||||
## Creating a Twitch Bot
|
||||
|
||||
Pinhook has a baked in way to connect directly to a twitch channel
|
||||
|
||||
```python
|
||||
from pinhook.bot import TwitchBot
|
||||
|
||||
bot = TwitchBot(
|
||||
nickname='ph-bot',
|
||||
channel='#channel',
|
||||
token='super-secret-oauth-token'
|
||||
)
|
||||
bot.start()
|
||||
```
|
||||
|
||||
This function has far less options, as the server, port, and ssl are already handled by twitch.
|
||||
|
||||
Optional aguments are:
|
||||
|
||||
* `ops`
|
||||
* `plugin_dir`
|
||||
* `log_level`
|
||||
|
||||
These options are the same for both IRC and Twitch
|
||||
|
||||
## Creating plugins
|
||||
|
||||
There are two types of plugins, commands and listeners. Commands only activate if a message starts with the command word, while listeners receive all messages and are parsed by the plugin for maximum flexibility.
|
||||
|
||||
In your chosen plugins directory ("plugins" by default) make a python file with a function. You use the `@pinhook.plugin.command` decorator to create command plugins, or `@pinhook.plugin.listener` to create listeners.
|
||||
|
||||
The function will need to be structured as such:
|
||||
|
||||
```python
|
||||
import pinhook.plugin
|
||||
|
||||
@pinhook.plugin.register('!test')
|
||||
@pinhook.plugin.command('!test')
|
||||
def test_plugin(msg):
|
||||
message = '{}: this is a test!'.format(msg.nick)
|
||||
return pinhook.plugin.message(message)
|
||||
@ -45,19 +143,46 @@ def test_plugin(msg):
|
||||
The function will need to accept a single argument in order to accept a `Message` object from the bot.
|
||||
|
||||
The `Message` object has the following attributes:
|
||||
* `cmd`: the command that triggered the function
|
||||
|
||||
* `cmd`: (for command plugins) the command that triggered the function
|
||||
* `nick`: the user who triggered the command
|
||||
* `arg`: all the trailing text after the command. This is what you will use to get optional information for the command
|
||||
* `arg`: (for command plugins) all the trailing text after the command. This is what you will use to get optional information for the command
|
||||
* `text`: (for listener plugins) the entire text of the message
|
||||
* `channel`: the channel where the command was initiated
|
||||
* `ops`: the list of bot operators
|
||||
* `botnick`: the nickname of the bot
|
||||
* `logger`: instance of `Bot`'s logger
|
||||
* `datetime`: aware `datetime.datetime` object when the `Message` object was created
|
||||
* `timestamp`: float for the unix timestamp when the `Message` object was created
|
||||
* `bot`: the initialized Bot class
|
||||
|
||||
It also contains the following IRC functions:
|
||||
|
||||
* `privmsg`: send a message to an arbitrary channel or user
|
||||
* `action`: same as privmsg, but does a CTCP action. (i.e., `/me does a thing`)
|
||||
* `notice`: send a notice
|
||||
|
||||
You can optionally set a command to be used only by ops
|
||||
|
||||
The function will need to be structured as such:
|
||||
|
||||
```python
|
||||
@pinhook.plugin.command('!test', ops=True, ops_msg='This command can only be run by an op')
|
||||
def test_plugin(msg):
|
||||
return pinhook.plugin.message('This was run by an op!')
|
||||
```
|
||||
|
||||
The plugin function can return one of the following in order to give a response to the command:
|
||||
|
||||
The plugin function **must** return one of the following in order to give a response to the command:
|
||||
* `pinhook.plugin.message`: basic message in channel where command was triggered
|
||||
* `pinhook.plugin.action`: CTCP action in the channel where command was triggered (basically like using `/me does a thing`)
|
||||
|
||||
## Examples
|
||||
|
||||
There are some basic examples in the `examples` directory in this repository.
|
||||
|
||||
For a live and maintained bot running the current version of pinhook see [pinhook-tilde](https://github.com/archangelic/pinhook-tilde).
|
||||
Here is a list of live bots using pinhook:
|
||||
|
||||
* [pinhook-tilde](https://github.com/archangelic/pinhook-tilde) - fun bot for tilde.town
|
||||
* [adminbot](https://github.com/tildetown/adminbot) - admin helper bot for tilde.town, featuring some of the ways you can change the Bot class to suit your needs
|
||||
* [lucibot](https://github.com/Lucidiot/lucibot)
|
||||
|
93
README.rst
93
README.rst
@ -1,93 +0,0 @@
|
||||
pinhook
|
||||
=======
|
||||
|
||||
a pluggable irc bot framework in python
|
||||
|
||||
Tutorial
|
||||
--------
|
||||
|
||||
Installation
|
||||
~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
$ pip install pinhook
|
||||
|
||||
Creating the Bot
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
To create the bot, just create a python file with the following:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pinhook.bot
|
||||
|
||||
bot = pinhook.bot.Bot(
|
||||
channels=['#foo', '#bar'],
|
||||
nickname='ph-bot',
|
||||
server='irc.freenode.net'
|
||||
)
|
||||
bot.start()
|
||||
|
||||
This will start a basic bot and look for plugins in the 'plugins'
|
||||
directory to add functionality.
|
||||
|
||||
Optional arguments are:
|
||||
|
||||
- ``port``: choose a custom port to connect to the server (default:
|
||||
6667)
|
||||
- ``ops``: list of operators who can do things like make the bot join
|
||||
other channels or quit (default: empty list)
|
||||
- ``plugin_dir``: directory where the bot should look for plugins
|
||||
(default: "plugins")
|
||||
- ``log_level``: string indicating logging level. Logging can be
|
||||
disabled by setting this to "off". (default: "info")
|
||||
|
||||
Creating plugins
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
In your chosen plugins directory ("plugins" by default) make a python
|
||||
file with a function. You can use the ``@pinhook.plugin.register``
|
||||
decorator to tell the bot the command to activate the function.
|
||||
|
||||
The function will need to be structured as such:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pinhook.plugin
|
||||
|
||||
@pinhook.plugin.register('!test')
|
||||
def test_plugin(msg):
|
||||
message = '{}: this is a test!'.format(msg.nick)
|
||||
return pinhook.plugin.message(message)
|
||||
|
||||
The function will need to accept a single argument in order to accept a
|
||||
``Message`` object from the bot.
|
||||
|
||||
The ``Message`` object has the following attributes:
|
||||
|
||||
- ``cmd``: the command that triggered the function
|
||||
- ``nick``: the user who triggered the command
|
||||
- ``arg``: all the trailing text after the command. This is what you
|
||||
will use to get optional information for the command
|
||||
- ``channel``: the channel where the command was initiated
|
||||
- ``ops``: the list of bot operators
|
||||
- ``botnick``: the nickname of the bot
|
||||
- ``logger``: instance of ``Bot``'s logger
|
||||
|
||||
The plugin function **must** return one of the following in order to
|
||||
give a response to the command:
|
||||
|
||||
- ``pinhook.plugin.message``: basic message in channel where command
|
||||
was triggered
|
||||
- ``pinhook.plugin.action``: CTCP action in the channel where command
|
||||
was triggered (basically like using ``/me does a thing``)
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
There are some basic examples in the ``examples`` directory in this
|
||||
repository.
|
||||
|
||||
For a live and maintained bot running the current version of pinhook see
|
||||
`pinhook-tilde <https://github.com/archangelic/pinhook-tilde>`__.
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@ -0,0 +1 @@
|
||||
theme: jekyll-theme-minimal
|
11
examples/irc/dicebot.py
Executable file
11
examples/irc/dicebot.py
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from pinhook.bot import Bot
|
||||
|
||||
ph = Bot(
|
||||
channels=['#dicechannel'],
|
||||
nickname='dicebot',
|
||||
server='irc.freenode.net',
|
||||
ops=['archangelic']
|
||||
)
|
||||
ph.start()
|
29
examples/irc/plugins/dice.py
Normal file
29
examples/irc/plugins/dice.py
Normal file
@ -0,0 +1,29 @@
|
||||
import random
|
||||
import re
|
||||
|
||||
import pinhook.plugin
|
||||
|
||||
dicepattern = re.compile('(?P<amount>\d+)d(?P<sides>\d+)\+?(?P<modifier>\d+)?')
|
||||
|
||||
def build_output(rolls, modifier):
|
||||
if len(rolls) == 1:
|
||||
start = str(sum(rolls))
|
||||
else:
|
||||
all_rolls = ''.join([str(i)+', ' for i in rolls]).strip(', ')
|
||||
start = '{} = {}'.format(all_rolls, sum(rolls))
|
||||
if modifier:
|
||||
output = start + ' + {} = {}'.format(modifier, sum(rolls) + int(modifier))
|
||||
else:
|
||||
output = start
|
||||
return output
|
||||
|
||||
@pinhook.plugin.command('!roll')
|
||||
def roll(msg):
|
||||
matches = dicepattern.match(msg.arg)
|
||||
if matches:
|
||||
msg.logger.info('Valid dice roll: {}'.format(msg.arg))
|
||||
rolls = [random.randrange(1, int(matches.group('sides'))+1) for i in range(int(matches.group('amount')))]
|
||||
output = build_output(rolls, matches.group('modifier'))
|
||||
else:
|
||||
output = '{}: improper format, should be NdN+N'.format(msg.nick)
|
||||
return pinhook.plugin.message(output)
|
@ -1,4 +0,0 @@
|
||||
import pinhook.bot
|
||||
|
||||
ph = pinhook.bot.Bot(['#arch-dev'], 'ph-bot', 'localhost', ops=['archangelic'])
|
||||
ph.start()
|
@ -1,7 +0,0 @@
|
||||
import pinhook.plugin
|
||||
|
||||
@pinhook.plugin.register('!test')
|
||||
def test(msg):
|
||||
msg.logger.info('This is test log output')
|
||||
return pinhook.plugin.message("{}: Test".format(msg.nick))
|
||||
|
10
examples/twitch/dicebot.py
Executable file
10
examples/twitch/dicebot.py
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from pinhook.bot import TwitchBot
|
||||
|
||||
bot = TwitchBot(
|
||||
nickname='dicebot',
|
||||
channel='#dicechannel',
|
||||
token='supersecrettokenhere'
|
||||
)
|
||||
bot.start()
|
29
examples/twitch/plugins/dice.py
Normal file
29
examples/twitch/plugins/dice.py
Normal file
@ -0,0 +1,29 @@
|
||||
import random
|
||||
import re
|
||||
|
||||
import pinhook.plugin
|
||||
|
||||
dicepattern = re.compile('(?P<amount>\d+)d(?P<sides>\d+)\+?(?P<modifier>\d+)?')
|
||||
|
||||
def build_output(rolls, modifier):
|
||||
if len(rolls) == 1:
|
||||
start = str(sum(rolls))
|
||||
else:
|
||||
all_rolls = ''.join([str(i)+', ' for i in rolls]).strip(', ')
|
||||
start = '{} = {}'.format(all_rolls, sum(rolls))
|
||||
if modifier:
|
||||
output = start + ' + {} = {}'.format(modifier, sum(rolls) + int(modifier))
|
||||
else:
|
||||
output = start
|
||||
return output
|
||||
|
||||
@pinhook.plugin.command('!roll')
|
||||
def roll(msg):
|
||||
matches = dicepattern.match(msg.arg)
|
||||
if matches:
|
||||
msg.logger.info('Valid dice roll: {}'.format(msg.arg))
|
||||
rolls = [random.randrange(1, int(matches.group('sides'))+1) for i in range(int(matches.group('amount')))]
|
||||
output = build_output(rolls, matches.group('modifier'))
|
||||
else:
|
||||
output = '{}: improper format, should be NdN+N'.format(msg.nick)
|
||||
return pinhook.plugin.message(output)
|
1
pinhook/__version__.py
Normal file
1
pinhook/__version__.py
Normal file
@ -0,0 +1 @@
|
||||
__version__ = '1.9.7'
|
438
pinhook/bot.py
438
pinhook/bot.py
@ -1,9 +1,11 @@
|
||||
import imp
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import time
|
||||
import pinhook.plugin
|
||||
|
||||
from . import log
|
||||
from . import plugin
|
||||
|
||||
import irc.bot
|
||||
|
||||
@ -11,130 +13,260 @@ import irc.bot
|
||||
irc.client.ServerConnection.buffer_class.errors = 'replace'
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(self, channel, nick, botnick, ops, logger, cmd=None, arg=None, text=None, nick_list=None):
|
||||
self.channel = channel
|
||||
self.nick = nick
|
||||
self.nick_list = nick_list
|
||||
self.botnick = botnick
|
||||
self.ops = ops
|
||||
self.logger = logger
|
||||
if cmd:
|
||||
self.cmd = cmd
|
||||
self.arg = arg
|
||||
if text:
|
||||
self.text = text
|
||||
if not (cmd or text):
|
||||
print('Please pass Message a command or text!')
|
||||
|
||||
|
||||
class Bot(irc.bot.SingleServerIRCBot):
|
||||
internal_commands = {
|
||||
'join': 'join a channel',
|
||||
'quit': 'force the bot to quit',
|
||||
'reload': 'force bot to reload all plugins',
|
||||
'enable': 'enable a plugin',
|
||||
'disable': 'disable a plugin',
|
||||
'op': 'add a user as bot operator',
|
||||
'deop': 'remove a user as bot operator',
|
||||
'ops': 'list all ops',
|
||||
'ban': 'ban a user from using the bot',
|
||||
'unban': 'remove bot ban for user',
|
||||
'banlist': 'currently banned nicks'
|
||||
}
|
||||
|
||||
def __init__(self, channels, nickname, server, **kwargs):
|
||||
self.set_kwargs(**kwargs)
|
||||
self.port = kwargs.get('port', 6667)
|
||||
self.ops = kwargs.get('ops', [])
|
||||
self.plugin_dir = kwargs.get('plugin_dir', 'plugins')
|
||||
self.ssl_required = kwargs.get('ssl_required', False)
|
||||
self.ns_pass = kwargs.get('ns_pass', None)
|
||||
self.nickserv = kwargs.get('nickserv', 'NickServ')
|
||||
self.log_level = kwargs.get('log_level', 'info')
|
||||
self.log_file = kwargs.get('log_file', None)
|
||||
self.server_pass = kwargs.get('server_pass', None)
|
||||
self.cmd_prefix = kwargs.get('cmd_prefix', '!')
|
||||
self.use_prefix_for_plugins = kwargs.get('use_prefix_for_plugins', False)
|
||||
self.disable_help = kwargs.get('disable_help', False)
|
||||
self.banned_users = kwargs.get('banned_users', [])
|
||||
if self.ssl_required:
|
||||
factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
|
||||
irc.bot.SingleServerIRCBot.__init__(self, [(server, self.port)], nickname, nickname, connect_factory=factory)
|
||||
irc.bot.SingleServerIRCBot.__init__(self, [(server, self.port, self.server_pass)], nickname, nickname, connect_factory=factory)
|
||||
else:
|
||||
irc.bot.SingleServerIRCBot.__init__(self, [(server, self.port)], nickname, nickname)
|
||||
irc.bot.SingleServerIRCBot.__init__(self, [(server, self.port, self.server_pass)], nickname, nickname)
|
||||
self.chanlist = channels
|
||||
self.bot_nick = nickname
|
||||
self.start_logging(self.log_level)
|
||||
self.load_plugins()
|
||||
self.start_logging()
|
||||
self.internal_commands = {self.cmd_prefix + k: v for k,v in self.internal_commands.items()}
|
||||
plugin.load_plugins(self.plugin_dir, use_prefix=self.use_prefix_for_plugins, cmd_prefix=self.cmd_prefix)
|
||||
|
||||
def set_kwargs(self, **kwargs):
|
||||
kwarguments = {
|
||||
'port': 6667,
|
||||
'ops': [],
|
||||
'plugin_dir': 'plugins',
|
||||
'ssl_required': False,
|
||||
'ns_pass': None,
|
||||
'nickserv': 'NickServ',
|
||||
'log_level': 'info',
|
||||
}
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
for a in kwarguments:
|
||||
if a not in kwargs:
|
||||
setattr(self, a, kwarguments[a])
|
||||
class Message:
|
||||
def __init__(self, bot, channel, nick, botnick, ops, logger, action, privmsg, notice, msg_type, cmd=None, arg=None, text=None, nick_list=None):
|
||||
self.bot = bot
|
||||
self.datetime = datetime.now(timezone.utc)
|
||||
self.timestamp = self.datetime.timestamp()
|
||||
self.channel = channel
|
||||
self.nick = nick
|
||||
self.nick_list = nick_list
|
||||
self.botnick = botnick
|
||||
self.ops = ops
|
||||
self.logger = logger
|
||||
self.action = action
|
||||
self.privmsg = privmsg
|
||||
self.notice = notice
|
||||
self.msg_type = msg_type
|
||||
if cmd:
|
||||
self.cmd = cmd
|
||||
self.arg = arg
|
||||
if text != None:
|
||||
self.text = text
|
||||
if not (cmd==None or text==None):
|
||||
raise TypeError('missing cmd or text parameter')
|
||||
|
||||
def start_logging(self, level):
|
||||
if level == 'error':
|
||||
def start_logging(self):
|
||||
self.logger = log.logger
|
||||
if self.log_file:
|
||||
log.set_log_file(self.log_file)
|
||||
else:
|
||||
log.set_log_file('{}.log'.format(self.bot_nick))
|
||||
if self.log_level == 'error':
|
||||
level = logging.ERROR
|
||||
elif level == 'warning':
|
||||
elif self.log_level == 'warning':
|
||||
level = logging.WARNING
|
||||
elif level == 'info':
|
||||
elif self.log_level == 'info':
|
||||
level = logging.INFO
|
||||
elif level == 'debug':
|
||||
elif self.log_level == 'debug':
|
||||
level = logging.DEBUG
|
||||
self.logger = logging.getLogger(self.bot_nick)
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
||||
# Set console logger
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
# Set file logger
|
||||
fh = logging.FileHandler('{}.log'.format(self.bot_nick))
|
||||
fh.setFormatter(formatter)
|
||||
# Set levels
|
||||
if level != "off":
|
||||
if self.log_level != "off":
|
||||
self.logger.setLevel(level)
|
||||
ch.setLevel(level)
|
||||
fh.setLevel(level)
|
||||
# Add handlers
|
||||
self.logger.addHandler(ch)
|
||||
self.logger.addHandler(fh)
|
||||
self.logger.info('Logging started!')
|
||||
|
||||
def load_plugins(self):
|
||||
# clear plugin list to ensure no old plugins remain
|
||||
self.logger.info('clearing plugin cache')
|
||||
pinhook.plugin.clear_plugins()
|
||||
# ensure plugin folder exists
|
||||
self.logger.info('checking plugin directory')
|
||||
if not os.path.exists(self.plugin_dir):
|
||||
self.logger.info('plugin directory {} not found, creating'.format(self.plugin_dir))
|
||||
os.makedirs(self.plugin_dir)
|
||||
# load all plugins
|
||||
for m in os.listdir(self.plugin_dir):
|
||||
if m.endswith('.py'):
|
||||
try:
|
||||
name = m[:-3]
|
||||
self.logger.info('loading plugin {}'.format(name))
|
||||
fp, pathname, description = imp.find_module(name, [self.plugin_dir])
|
||||
imp.load_module(name, fp, pathname, description)
|
||||
except Exception as e:
|
||||
self.logger.exception('could not load plugin')
|
||||
# gather all commands and listeners
|
||||
self.cmds = {}
|
||||
self.lstnrs = {}
|
||||
for cmd in pinhook.plugin.cmds:
|
||||
self.logger.debug('adding command {}'.format(cmd['cmd']))
|
||||
self.cmds[cmd['cmd']] = cmd['func']
|
||||
for lstnr in pinhook.plugin.lstnrs:
|
||||
self.logger.debug('adding listener {}'.format(lstnr['lstn']))
|
||||
self.lstnrs[lstnr['lstn']] = lstnr['func']
|
||||
|
||||
def on_welcome(self, c, e):
|
||||
if self.ns_pass:
|
||||
self.logger.info('identifying with nickserv')
|
||||
c.privmsg(self.nickserv, 'identify {}'.format(self.ns_pass))
|
||||
for channel in self.chanlist:
|
||||
self.logger.info('joining channel {}'.format(channel))
|
||||
c.join(channel)
|
||||
self.logger.info('joining channel {}'.format(channel.split()[0]))
|
||||
c.join(*channel.split())
|
||||
|
||||
def on_pubmsg(self, c, e):
|
||||
self.process_command(c, e)
|
||||
self.process_event(c, e)
|
||||
|
||||
def on_privmsg(self, c, e):
|
||||
self.process_command(c, e)
|
||||
self.process_event(c, e)
|
||||
|
||||
def process_command(self, c, e):
|
||||
def on_action(self, c, e):
|
||||
self.process_event(c, e)
|
||||
|
||||
def call_help(self, nick, op):
|
||||
cmds = {k:v.help_text for k,v in plugin.cmds.items() if not plugin.cmds[k].ops}
|
||||
cmds.update({self.cmd_prefix + 'help': 'returns this output to private message'})
|
||||
if op:
|
||||
cmds.update({k:v.help_text for k,v in plugin.cmds.items() if plugin.cmds[k].ops})
|
||||
cmds.update({k:v for k,v in self.internal_commands.items()})
|
||||
helpout = OrderedDict(sorted(cmds.items()))
|
||||
for h in helpout:
|
||||
self.connection.privmsg(nick, '{} -- {}'.format(h, helpout[h]))
|
||||
time.sleep(.1)
|
||||
self.connection.privmsg(nick, 'List of listeners: {}'.format(', '.join([l for l in plugin.lstnrs])))
|
||||
return None
|
||||
|
||||
def call_internal_commands(self, channel, nick, cmd, text, arg, c):
|
||||
if not cmd.startswith(self.cmd_prefix):
|
||||
return None
|
||||
else:
|
||||
cmd = cmd[len(self.cmd_prefix):]
|
||||
output = None
|
||||
if nick in self.ops:
|
||||
op = True
|
||||
else:
|
||||
op = False
|
||||
if cmd == 'join' and op:
|
||||
try:
|
||||
c.join(*arg.split())
|
||||
self.logger.info('joining {} per request of {}'.format(arg, nick))
|
||||
output = plugin.message('{}: joined {}'.format(nick, arg.split()[0]))
|
||||
except:
|
||||
self.logger.exception('issue with join command: {}join #channel <channel key>'.format(self.cmd_prefix))
|
||||
elif cmd == 'quit' and op:
|
||||
self.logger.info('quitting per request of {}'.format(nick))
|
||||
if not arg:
|
||||
arg = "See y'all later!"
|
||||
c.quit(arg)
|
||||
quit()
|
||||
elif cmd == 'help' and not self.disable_help:
|
||||
self.call_help(nick, op)
|
||||
elif cmd == 'reload' and op:
|
||||
self.logger.info('reloading plugins per request of {}'.format(nick))
|
||||
plugin.load_plugins(self.plugin_dir, use_prefix=self.use_prefix_for_plugins, cmd_prefix=self.cmd_prefix)
|
||||
output = plugin.message('Plugins reloaded')
|
||||
elif cmd == 'enable' and op:
|
||||
if arg in plugin.plugins:
|
||||
if plugin.plugins[arg].enabled:
|
||||
output = plugin.message("{}: '{}' already enabled".format(nick, arg))
|
||||
else:
|
||||
plugin.plugins[arg].enable()
|
||||
output = plugin.message("{}: '{}' enabled!".format(nick, arg))
|
||||
else:
|
||||
output = plugin.message("{}: '{}' not found".format(nick, arg))
|
||||
elif cmd == 'disable' and op:
|
||||
if arg in plugin.plugins:
|
||||
if not plugin.plugins[arg].enabled:
|
||||
output = plugin.message("{}: '{}' already disabled".format(nick, arg))
|
||||
else:
|
||||
plugin.plugins[arg].disable()
|
||||
output = plugin.message("{}: '{}' disabled!".format(nick, arg))
|
||||
elif cmd == 'op' and op:
|
||||
for o in arg.split(' '):
|
||||
self.ops.append(o)
|
||||
output = plugin.message('{}: {} added as op'.format(nick, arg))
|
||||
elif cmd == 'deop' and op:
|
||||
for o in arg.split(' '):
|
||||
self.ops = [i for i in self.ops if i != o]
|
||||
output = plugin.message('{}: {} removed as op'.format(nick, arg))
|
||||
elif cmd == 'ops' and op:
|
||||
output = plugin.message('current ops: {}'.format(', '.join(self.ops)))
|
||||
elif cmd == 'ban' and op:
|
||||
for o in arg.split(' '):
|
||||
self.banned_users.append(o)
|
||||
output = plugin.message('{}: banned {}'.format(nick, arg))
|
||||
elif cmd == 'unban' and op:
|
||||
for o in arg.split(' '):
|
||||
self.banned_users = [i for i in self.banned_users if i != o]
|
||||
output = plugin.message('{}: removed ban for {}'.format(nick, arg))
|
||||
elif cmd == 'banlist':
|
||||
output = plugin.message('currently banned: {}'.format(', '.join(self.banned_users)))
|
||||
return output
|
||||
|
||||
def call_plugins(self, privmsg, action, notice, chan, cmd, text, nick_list, nick, arg, msg_type):
|
||||
output = None
|
||||
if cmd in plugin.cmds:
|
||||
try:
|
||||
if plugin.cmds[cmd].ops and nick not in self.ops:
|
||||
if plugin.cmds[cmd].ops_msg:
|
||||
output = plugin.message(plugin.cmds[cmd].ops_msg)
|
||||
elif plugin.cmds[cmd].enabled:
|
||||
self.logger.debug('executing {}'.format(cmd))
|
||||
output = plugin.cmds[cmd].run(self.Message(
|
||||
bot=self,
|
||||
channel=chan,
|
||||
cmd=cmd,
|
||||
nick_list=nick_list,
|
||||
nick=nick,
|
||||
arg=arg,
|
||||
privmsg=privmsg,
|
||||
action=action,
|
||||
notice=notice,
|
||||
botnick=self.bot_nick,
|
||||
ops=self.ops,
|
||||
logger=self.logger,
|
||||
msg_type=msg_type
|
||||
))
|
||||
except Exception:
|
||||
self.logger.exception('issue with command {}'.format(cmd))
|
||||
else:
|
||||
for lstnr in plugin.lstnrs:
|
||||
if plugin.lstnrs[lstnr].enabled:
|
||||
try:
|
||||
self.logger.debug('whispering to listener: {}'.format(lstnr))
|
||||
listen_output = plugin.lstnrs[lstnr].run(self.Message(
|
||||
bot=self,
|
||||
channel=chan,
|
||||
text=text,
|
||||
nick_list=nick_list,
|
||||
nick=nick,
|
||||
privmsg=privmsg,
|
||||
action=action,
|
||||
notice=notice,
|
||||
botnick=self.bot_nick,
|
||||
ops=self.ops,
|
||||
logger=self.logger,
|
||||
msg_type=msg_type
|
||||
))
|
||||
if listen_output:
|
||||
output = listen_output
|
||||
except Exception:
|
||||
self.logger.exception('issue with listener {}'.format(lstnr))
|
||||
if output:
|
||||
self.logger.debug(f'returning output: {output.msg}')
|
||||
return output
|
||||
|
||||
def process_event(self, c, e):
|
||||
nick = e.source.nick
|
||||
text = e.arguments[0]
|
||||
if nick == self.bot_nick:
|
||||
pass
|
||||
if e.arguments:
|
||||
text = e.arguments[0]
|
||||
else:
|
||||
text = ''
|
||||
if e.type == 'privmsg' or e.type == 'pubmsg':
|
||||
msg_type = 'message'
|
||||
else:
|
||||
msg_type = e.type
|
||||
if e.target == self.bot_nick:
|
||||
chan = nick
|
||||
nick_list = [nick]
|
||||
else:
|
||||
chan = e.target
|
||||
cmd = text.split(' ')[0]
|
||||
nick_list = list(self.channels[chan].users())
|
||||
if e.type == 'action':
|
||||
cmd = ''
|
||||
else:
|
||||
cmd = text.split(' ')[0]
|
||||
self.logger.debug(
|
||||
'Message info: channel: {}, nick: {}, cmd: {}, text: {}'.format(chan, nick, cmd, text)
|
||||
)
|
||||
@ -142,62 +274,72 @@ class Bot(irc.bot.SingleServerIRCBot):
|
||||
arg = ''.join([i + ' ' for i in text.split(' ')[1:]]).strip()
|
||||
else:
|
||||
arg = ''
|
||||
output = None
|
||||
if cmd == '!join' and nick in self.ops:
|
||||
self.logger.info('joining {} per request of {}'.format(arg, nick))
|
||||
c.join(arg)
|
||||
c.privmsg(chan, '{}: joined {}'.format(nick, arg))
|
||||
elif cmd == '!quit' and nick in self.ops:
|
||||
self.logger.info('quitting per request of {}'.format(nick))
|
||||
c.quit("See y'all later!")
|
||||
quit()
|
||||
elif cmd == '!help':
|
||||
helplist = sorted([i for i in self.cmds])
|
||||
msg = ', '.join(helplist)
|
||||
c.privmsg(chan, 'Available commands: {}'.format(msg))
|
||||
elif cmd == '!reload' and nick in self.ops:
|
||||
self.logger.info('reloading plugins per request of {}'.format(nick))
|
||||
self.load_plugins()
|
||||
c.privmsg(chan, 'Plugins reloaded')
|
||||
elif cmd in self.cmds:
|
||||
try:
|
||||
output = self.cmds[cmd](Message(
|
||||
channel=chan,
|
||||
cmd=cmd,
|
||||
nick_list=list(self.channels[chan].users()),
|
||||
nick=nick,
|
||||
arg=arg,
|
||||
botnick=self.bot_nick,
|
||||
ops=self.ops,
|
||||
logger=self.logger
|
||||
))
|
||||
if output:
|
||||
self.process_output(c, chan, output)
|
||||
except Exception as e:
|
||||
self.logger.exception('issue with command {}'.format(cmd))
|
||||
else:
|
||||
for lstnr in self.lstnrs:
|
||||
try:
|
||||
output = self.lstnrs[lstnr](Message(
|
||||
channel=chan,
|
||||
text=text,
|
||||
nick_list=list(self.channels[chan].users()),
|
||||
nick=nick,
|
||||
botnick=self.bot_nick,
|
||||
ops=self.ops,
|
||||
logger=self.logger
|
||||
))
|
||||
if output:
|
||||
self.process_output(c, chan, output)
|
||||
except Exception as e:
|
||||
self.logger.exception('issue with listener {}'.format(lstnr))
|
||||
output = self.call_internal_commands(chan, nick, cmd, text, arg, c)
|
||||
if not output:
|
||||
plugin_info = {
|
||||
'chan': chan,
|
||||
'cmd': cmd,
|
||||
'text': text,
|
||||
'nick_list': nick_list,
|
||||
'nick': nick,
|
||||
'arg': arg,
|
||||
'privmsg': c.privmsg,
|
||||
'action': c.action,
|
||||
'notice': c.notice,
|
||||
'msg_type': msg_type
|
||||
}
|
||||
output = self.call_plugins(**plugin_info)
|
||||
if output:
|
||||
self.logger.debug(f'sending output: {output.msg}')
|
||||
self.process_output(c, chan, output)
|
||||
|
||||
def process_output(self, c, chan, output):
|
||||
if not output.msg:
|
||||
return
|
||||
for msg in output.msg:
|
||||
if output.msg_type == 'message':
|
||||
if output.msg_type == plugin.OutputType.Message:
|
||||
self.logger.debug('output message: {}'.format(msg))
|
||||
c.privmsg(chan, msg)
|
||||
elif output.msg_type == 'action':
|
||||
try:
|
||||
c.privmsg(chan, msg)
|
||||
except irc.client.MessageTooLong:
|
||||
self.logger.error('output message too long: {}'.format(msg))
|
||||
break
|
||||
elif output.msg_type == plugin.OutputType.Action:
|
||||
self.logger.debug('output action: {}'.format(msg))
|
||||
c.action(chan, msg)
|
||||
try:
|
||||
c.action(chan, msg)
|
||||
except irc.client.MessageTooLong:
|
||||
self.logger.error('output message too long: {}'.format(msg))
|
||||
break
|
||||
else:
|
||||
self.logger.warning("Unsupported output type '{}'".format(output.msg_type))
|
||||
time.sleep(.5)
|
||||
|
||||
|
||||
class TwitchBot(Bot):
|
||||
def __init__(self, nickname, channel, token, **kwargs):
|
||||
self.port = kwargs.get('port', 6667)
|
||||
self.ops = kwargs.get('ops', [])
|
||||
self.plugin_dir = kwargs.get('plugin_dir', 'plugins')
|
||||
self.log_level = kwargs.get('log_level', 'info')
|
||||
self.log_file = kwargs.get('log_file', None)
|
||||
self.server_pass = kwargs.get('server_pass', None)
|
||||
self.cmd_prefix = kwargs.get('cmd_prefix', '!')
|
||||
self.use_prefix_for_plugins = kwargs.get('use_prefix_for_plugins', False)
|
||||
self.disable_help = kwargs.get('disable_help', False)
|
||||
self.banned_users = kwargs.get('banned_users', [])
|
||||
self.bot_nick = nickname
|
||||
self.start_logging()
|
||||
self.channel = channel
|
||||
self.logger.info('Joining Twitch Server')
|
||||
irc.bot.SingleServerIRCBot.__init__(self, [('irc.twitch.tv', 6667, 'oauth:'+token)], nickname, nickname)
|
||||
self.internal_commands = {self.cmd_prefix + k: v for k,v in self.internal_commands.items()}
|
||||
plugin.load_plugins(self.plugin_dir, use_prefix=self.use_prefix_for_plugins, cmd_prefix=self.cmd_prefix)
|
||||
|
||||
def on_welcome(self, c, e):
|
||||
self.logger.info('requesting permissions')
|
||||
c.cap('REQ', ':twitch.tv/membership')
|
||||
c.cap('REQ', ':twitch.tv/tags')
|
||||
c.cap('REQ', ':twitch.tv/commands')
|
||||
self.logger.info('Joining channel ' + self.channel)
|
||||
c.join(self.channel)
|
||||
|
60
pinhook/cli.py
Normal file
60
pinhook/cli.py
Normal file
@ -0,0 +1,60 @@
|
||||
import click
|
||||
from .bot import Bot
|
||||
from marshmallow import Schema, fields, validate, INCLUDE
|
||||
|
||||
class Config(Schema):
|
||||
nickname = fields.Str(required=True)
|
||||
channels = fields.List(fields.Str(), required=True)
|
||||
server = fields.Str(required=True)
|
||||
port = fields.Int()
|
||||
ops = fields.List(fields.Str())
|
||||
ssl_required = fields.Bool()
|
||||
plugin_dir = fields.Str()
|
||||
ns_pass = fields.Str()
|
||||
log_level = fields.Str(validate=validate.OneOf(['debug', 'warn', 'info', 'off', 'error']))
|
||||
server_pass = fields.Str()
|
||||
|
||||
class Meta:
|
||||
unknown = INCLUDE
|
||||
|
||||
def read_conf(config, conf_format):
|
||||
schema = Config()
|
||||
if not conf_format:
|
||||
if config.name.endswith('.json'):
|
||||
conf_format = 'json'
|
||||
elif config.name.endswith(('.yaml', '.yml')):
|
||||
conf_format = 'yaml'
|
||||
elif config.name.endswith(('.toml', '.tml')):
|
||||
conf_format = 'toml'
|
||||
else:
|
||||
raise click.ClickException('Could not detect file format, please supply using --format option')
|
||||
if conf_format == 'json':
|
||||
import json
|
||||
to_json = json.loads(config.read())
|
||||
output = schema.load(to_json)
|
||||
elif conf_format == 'yaml':
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
raise click.ClickException('yaml not installed, please use `pip3 install pinhook[yaml]` to install')
|
||||
else:
|
||||
to_yaml = yaml.load(config.read(), Loader=yaml.FullLoader)
|
||||
output = schema.load(to_yaml)
|
||||
elif conf_format == 'toml':
|
||||
try:
|
||||
import toml
|
||||
except ImportError:
|
||||
raise click.ClicKException('toml not installed, please use `pip3 install pinhook[toml]` to install')
|
||||
else:
|
||||
to_toml = toml.load(config.name)
|
||||
output = schema.load(to_toml)
|
||||
return output
|
||||
|
||||
@click.command()
|
||||
@click.argument('config', type=click.File('rb'))
|
||||
@click.option('--format', '-f', 'conf_format', type=click.Choice(['json', 'yaml', 'toml']))
|
||||
def cli(config, conf_format):
|
||||
config = read_conf(config, conf_format)
|
||||
bot = Bot(**config)
|
||||
bot.start()
|
||||
|
14
pinhook/log.py
Normal file
14
pinhook/log.py
Normal file
@ -0,0 +1,14 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('bot')
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
||||
# Set console logger
|
||||
streamhandler = logging.StreamHandler()
|
||||
streamhandler.setFormatter(formatter)
|
||||
logger.addHandler(streamhandler)
|
||||
|
||||
def set_log_file(filename):
|
||||
# Set file logger
|
||||
filehandler = logging.FileHandler(filename)
|
||||
filehandler.setFormatter(formatter)
|
||||
logger.addHandler(filehandler)
|
@ -1,5 +1,17 @@
|
||||
cmds = []
|
||||
lstnrs = []
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from .log import logger
|
||||
|
||||
plugins = {}
|
||||
cmds = {}
|
||||
lstnrs = {}
|
||||
|
||||
class OutputType(Enum):
|
||||
Message = 'message'
|
||||
Action = 'action'
|
||||
|
||||
|
||||
class Output:
|
||||
@ -14,36 +26,150 @@ class Output:
|
||||
return msg
|
||||
|
||||
|
||||
def action(msg):
|
||||
return Output('action', msg)
|
||||
class _BasePlugin:
|
||||
enabled = True
|
||||
logger = logger
|
||||
|
||||
def enable(self):
|
||||
self.enabled = True
|
||||
|
||||
def disable(self):
|
||||
self.enabled = False
|
||||
|
||||
|
||||
class Listener(_BasePlugin):
|
||||
def __init__(self, name, run=None):
|
||||
self.name = name
|
||||
if run:
|
||||
self.run = run
|
||||
self._add_listener()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
def _add_listener(self):
|
||||
lstnrs[self.name] = self
|
||||
plugins[self.name] = self
|
||||
|
||||
|
||||
class Command(_BasePlugin):
|
||||
def __init__(self, cmd, **kwargs):
|
||||
self.cmd = cmd
|
||||
self.help_text = kwargs.get('help_text', 'N/A')
|
||||
self.ops = kwargs.get('ops', False)
|
||||
self.ops_msg = kwargs.get('ops_msg', '')
|
||||
self.run = kwargs.get('run', self.run)
|
||||
self._add_command()
|
||||
|
||||
def __str__(self):
|
||||
return self.cmd
|
||||
|
||||
def run(self, msg):
|
||||
pass
|
||||
|
||||
def _enable_ops(self, ops_msg):
|
||||
self.ops = True
|
||||
self.ops_msg = ops_msg
|
||||
|
||||
def _update_plugin(self, **kwargs):
|
||||
self.help_text = kwargs.get('help_text', 'N/A')
|
||||
self.run = kwargs.get('run', self.run)
|
||||
|
||||
def _add_command(self):
|
||||
cmds[self.cmd] = self
|
||||
plugins[self.cmd] = self
|
||||
|
||||
|
||||
def action(msg):
|
||||
return Output(OutputType.Action, msg)
|
||||
|
||||
def message(msg):
|
||||
return Output('message', msg)
|
||||
return Output(OutputType.Message, msg)
|
||||
|
||||
def _add_command(command, help_text, func, ops=False, ops_msg=''):
|
||||
if command not in cmds:
|
||||
Command(command, help_text=help_text, ops=ops, ops_msg=ops_msg, run=func)
|
||||
else:
|
||||
cmds[command]._update_plugin(help_text=help_text, run=func)
|
||||
|
||||
def add_plugin(command, func):
|
||||
cmds.append({'cmd': command, 'func': func})
|
||||
def _ops_plugin(command, ops_msg, func):
|
||||
if command not in cmds:
|
||||
Command(command, ops=True, ops_msg=ops_msg)
|
||||
else:
|
||||
cmds[command]._enable_ops(ops_msg)
|
||||
|
||||
def _add_listener(name, func):
|
||||
Listener(name, run=func)
|
||||
|
||||
def clear_plugins():
|
||||
cmds.clear()
|
||||
lstnrs.clear()
|
||||
|
||||
def load_plugins(plugin_dir, use_prefix=False, cmd_prefix='!'):
|
||||
# i'm not sure why i need this but i do
|
||||
global cmds
|
||||
global plugins
|
||||
global lstnrs
|
||||
#check for all the disabled plugins so that we don't re-enable them
|
||||
disabled_plugins = [i for i in plugins if not plugins[i].enabled]
|
||||
logger.debug(disabled_plugins)
|
||||
# clear plugin list to ensure no old plugins remain
|
||||
logger.info('clearing plugin cache')
|
||||
clear_plugins()
|
||||
# ensure plugin folder exists
|
||||
logger.info('checking plugin directory')
|
||||
if not os.path.exists(plugin_dir):
|
||||
logger.info('plugin directory {} not found, creating'.format(plugin_dir))
|
||||
os.makedirs(plugin_dir)
|
||||
# load all plugins
|
||||
for m in os.listdir(plugin_dir):
|
||||
if m.endswith('.py'):
|
||||
try:
|
||||
name = m[:-3]
|
||||
logger.info('loading plugin {}'.format(name))
|
||||
spec = importlib.machinery.PathFinder().find_spec(name, [plugin_dir])
|
||||
spec.loader.load_module()
|
||||
except Exception:
|
||||
logger.exception('could not load plugin')
|
||||
# gather all commands and listeners
|
||||
if use_prefix: # use prefixes if needed
|
||||
cmds = {cmd_prefix + k: v for k,v in cmds.items()}
|
||||
for p in plugins:
|
||||
if p in disabled_plugins:
|
||||
plugins[p].disable()
|
||||
for cmd in cmds:
|
||||
logger.debug('adding command {}'.format(cmd))
|
||||
for lstnr in lstnrs:
|
||||
logger.debug('adding listener {}'.format(lstnr))
|
||||
|
||||
def add_listener(name, func):
|
||||
lstnrs.append({'lstn': name, 'func': func})
|
||||
|
||||
|
||||
def register(command):
|
||||
def command(command, help_text='N/A', ops=False, ops_msg=''):
|
||||
@wraps(command)
|
||||
def register_for_command(func):
|
||||
add_plugin(command, func)
|
||||
_add_command(command, help_text, func, ops=ops, ops_msg=ops_msg)
|
||||
return func
|
||||
return register_for_command
|
||||
|
||||
def register(command, help_text='N/A'):
|
||||
logger.warn('@register decorator has been deprecated in favor of @command. This will cause errors in future versions.')
|
||||
@wraps(command)
|
||||
def register_for_command(func):
|
||||
_add_command(command, help_text, func)
|
||||
return func
|
||||
return register_for_command
|
||||
|
||||
def listener(name):
|
||||
def register_as_listener(func):
|
||||
add_listener(name, func)
|
||||
_add_listener(name, func)
|
||||
return func
|
||||
return register_as_listener
|
||||
|
||||
def ops(command, msg=None):
|
||||
logger.warn('use of the @ops decorator has been deprecated in favor of using the @command decorator with the ops and ops_msg options. Use will cause errors in future versions.')
|
||||
@wraps(command)
|
||||
def register_ops_command(func):
|
||||
_ops_plugin(command, msg, func)
|
||||
return func
|
||||
return register_ops_command
|
||||
|
35
setup.py
35
setup.py
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Note: To use the 'upload' functionality of this file, you must:
|
||||
@ -17,12 +17,21 @@ DESCRIPTION = 'a pluggable irc bot framework in python'
|
||||
URL = 'https://github.com/archangelic/pinhook'
|
||||
EMAIL = 'mhancock@archangelic.space'
|
||||
AUTHOR = 'M. Hancock'
|
||||
VERSION = None
|
||||
|
||||
# What packages are required for this module to be executed?
|
||||
REQUIRED = [
|
||||
'irc',
|
||||
'enum34',
|
||||
'click',
|
||||
'marshmallow',
|
||||
]
|
||||
|
||||
EXTRAS = {
|
||||
'toml': ['toml'],
|
||||
'yaml': ['pyyaml']
|
||||
}
|
||||
|
||||
# The rest you shouldn't have to touch too much :)
|
||||
# ------------------------------------------------
|
||||
# Except, perhaps the License and Trove Classifiers!
|
||||
@ -32,9 +41,18 @@ here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# Import the README and use it as the long-description.
|
||||
# Note: this will only work if 'README.rst' is present in your MANIFEST.in file!
|
||||
with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||
with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
|
||||
long_description = '\n' + f.read()
|
||||
|
||||
# Load the package's __version__.py module as a dictionary.
|
||||
about = {}
|
||||
if not VERSION:
|
||||
project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
|
||||
with open(os.path.join(here, project_slug, '__version__.py')) as f:
|
||||
exec(f.read(), about)
|
||||
else:
|
||||
about['__version__'] = VERSION
|
||||
|
||||
|
||||
class UploadCommand(Command):
|
||||
"""Support setup.py upload."""
|
||||
@ -72,28 +90,33 @@ class UploadCommand(Command):
|
||||
# Where the magic happens:
|
||||
setup(
|
||||
name=NAME,
|
||||
version='1.3.3',
|
||||
version=about['__version__'],
|
||||
description=DESCRIPTION,
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
author=AUTHOR,
|
||||
author_email=EMAIL,
|
||||
url=URL,
|
||||
packages=['pinhook'],
|
||||
entry_points={
|
||||
'console_scripts':
|
||||
['pinhook=pinhook.cli:cli']
|
||||
},
|
||||
install_requires=REQUIRED,
|
||||
extras_require=EXTRAS,
|
||||
include_package_data=True,
|
||||
license='MIT',
|
||||
classifiers=[
|
||||
# Trove classifiers
|
||||
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
],
|
||||
# $ setup.py publish support.
|
||||
cmdclass={
|
||||
|
Loading…
x
Reference in New Issue
Block a user