making bot frameworks in my spare time
interested in irc, go, python
the pluggable python framework for IRC bots and Twitch bots
Home Page: https://archangelic.github.io/pinhook/
License: MIT License
basically create a way to log to an irc channel, and pass that channel name to the bot.
Currently adding an ops command requires 2 decorators, but this option could be added using a options in the command decorator now that plugins are objects.
@pinhook.plugin.command('!command', ops=True, ops_msg='this is an ops message')
Instead of hardcoded 'action'
and 'message'
strings here, use a Python Enum. The enum34
can provide backporting to Python versions before 3.4.
This a really easy issue, feel free to assign it to me if you want. :p
Add timestamp
to the Message object that gets passed to plugin handlers and listeners
Implement class-based plugins for all features going forward.
Spec for class-based plugins:
The bot should be logging events based on a level when the bot is initialized. Plugins should also be able to add arbitrary information to the log as well.
By default, a '!help' command is registered for all bots that enumerate all the plugins that they respond to. I would like a way to either disable or override the default '!help' command.
This is useful for the cases where I do not want all my plugin functionality made public or do not want my bot to respond to a !help
tilde.town now has profanity
installed, and that made me think about having yet another backend on pinhook!
!help should bring up help info for a command, if any is provided
this would need to be implemented on both the plugin side and on the bot side
Some users would like the ability to simultaneously have one bot connect to multiple servers.
When plugins handle text and send it to pinhook.plugin.message
it is vulnerable to command injection.
for instance text i want\r\nQUIT :injected quit message
The bot runs this without validation, which is a security concern when dealing with untrusted text.
One thing I've seen some bot creators do is create commands that can "chain" or "pipe" into each other. This would use the output of one command as the input for the next one.
As an example, a bot may support the commands "!rainbow" and "!superhero". Chaining the two commands together (like "!superhero | !rainbow") would result in a random super hero printed in a rainbow color; the output from "!superhero" would be used to call "!rainbow ".
The decorator would go on any plugin that is allowed to be "piped" into another. In this case, it would be on "!superhero" and the decorator would mean that only the user input up to the first pipe '|' would be sent to that command.
I'd be interested in doing this at some point in the future!
MSN is not dead! An MSN server called Escargot is being developed on GitLab (official website here). It supports Windows Messenger 1.0 up to WLM 2009 and Yahoo! Messenger 5.0 to 5.5 currently, and plans are made for bridges to XMPP and Matrix. I am currently submitting this issue while logged on on WLM 8.5.
There are some MSNP implementations in Python, but obviously we're talking about obsolete code: msnp.py was last updated in 2004, and I found a bot implementation made in 2010 that uses a module last updated in 2001. But it does not look that hard to start maintaining that code again, and I might just get started on that if I ever have the time, and Escargot is designed to work with any version of the protocol, so that helps.
listener plugins should be able to read the text when a ctcp action happens in the channel. currently they are not called to respond at all.
The bot will crash if you return pinhook.plugin.message(None)
from a registered command
When creating command-line scripts, the cool way to parse command-line arguments is to use the argparse
module. That could automatically generate help text (like for #19) with details about arguments. This should be easy to do with class-based plugins (#27) by using a way similar to how Django commands are designed:
import pinhook.plugin
class HelloPlugin(pinhook.plugin.BasePlugin):
help = 'Say hello to someone!'
def add_arguments(self, parser):
parser.add_argument(
'nick',
nargs='?',
default=None,
help='Nickname of the user to say hello to',
)
def handle(self, msg, **options):
message = 'Hello {}!'.format(options.get('nick', msg.nick))
return pinhook.plugin.message(message)
I treat my plugin files as thin shims that just import util methods. Running a !reload against bot commands will reload the plugin file but not plugin dependencies.
Eg. I might have "./plugins/foo.py" that contains from util import doFoo
. Running a !reload will reload "foo.py" but not my "doFoo" module. In order to reload the "doFoo" module, I need to do a full bot restart.
Cheers!
So far, all I have is a poorly made tutorial. This should be expanded with more examples and possibly technical details of how plugins and bot initialization work.
If I remove a plugin (or the plugin can't be imported after a change) and !reload
all plugins, the old version of the plugin is still available.
So, I guess, the easiest fix would be to clear the whole cmds list and refilled it again on reload.
events. like on join, on kick, on leave, on ban, op, on deop etc etc
Plugin loading should be the domain of pinhook.plugin. This should be a function to call in that module, further removing the need to have the plugins and bot be linked too heavily.
Should also come with implementing #6
Pinhook is designed to be extended by using command and listener plugins. Sometimes, however, it is natural to extend the Bot class in other ways. Currently I am struggling with appending a natural language processing model to the Bot class.
My current attempt involves creating a new class that instantiates a Pinhook Bot
and a Model
instance during init; it looks something like this:
from pinhook.bot import Bot
from mybot.nlp.model import Model
class Mybot:
def __init__(self, **config):
self.config = config
self.model = Model()
self.bot = Bot(
use_prefix_for_plugins=True,
**self.config
)
self.bot.start()
But now I want to create a listener which has access to the model
attribute, and it is not clear how. I considered trying instead to extend the Bot
class directly, but plugins do not have access to self
anyway so this doesn't help me.
Any ideas on how I could work around this sort of issue?
Many channels have anti flooding settings that will cause an overly-chatty bot to get kicked. Eg, 5 lines within 10 seconds.
Add either an optional delay timer or max msg lines or rate limit or something to avoid getting kicked for flooding.
Eg Setting flood_limit 5
and flood_time 10
will cause the bot to rate limit itself whenever pinhook.plugin.message receives more than 5 lines in the array. It could print the first 5 lines then wait 10 seconds or only print a message every 2 seconds.
Allow a plugin to be registered as only runnable by the bot's op. it would add an automatic check of if msg.nick not in msg.ops; return
before calling the plugin. maybe something like @pinhook.plugin.ops($message_what_gets_printed_if_a_non_op_tries = "")
where None will result in no message.
These op commands should probably also not be included in !help
(maybe unless called by an op?)
Using the following code:
bot = TwitchBot(
nickname="pyllybot",
channel="#siinacutie",
token=secret
).start()
I receive the error:
Traceback (most recent call last):
File "./pyllybot.py", line 18, in <module>
bot = TwitchBot(
File "/opt/virtual_env/pyllybot/lib/python3.8/site-packages/pinhook/bot.py", line 319, in __init__
self.start_logging()
File "/opt/virtual_env/pyllybot/lib/python3.8/site-packages/pinhook/bot.py", line 82, in start_logging
if self.log_file:
AttributeError: 'TwitchBot' object has no attribute 'log_file'
The title says it all! It is possible to handle KeyboardInterrupt
just like any other Python exception, so it must be possible to let the bot do a /quit with a predefined message (maybe another keyword argument in the IRCBot
class)
If you do !join #foo password but more
for example, the bot will crash. This should fail and give a message as to why and not crash.
Create way for settings for plugins to be changed from within IRC
for example we would use a reserved command !set
to change the setting for the dice plugin
<archangelic> !set dice.max_size 100
<pinhook> archangelic: setting dice.max_size changed to 100
Possible ways to implement this:
If multiple pinhook bots sharing ops are in the same room, conflicts could arise if commands are shared. While plugin command prefixes are controlled by the user, the prefixes for the builtins !join
, !help
, !reload
and most critically !quit
are hardcoded.
Doing something like:
class Bot(irc.bot.SingleServerIRCBot):
def __init__(self, channels, nickname, server, cmd_prefix='!', **kwargs):
...
self.cmd_prefix = cmd_prefix
...
def call_internal_commands(self, channel, nick, cmd, text, arg, c):
output = None
if nick in self.ops:
op = True
else:
op = False
if cmd == self.cmd_prefix + 'join' and op:
c.join(*arg.split())
self.logger.info('joining {} per request of {}'.format(arg, nick))
output = self.output_message('{}: joined {}'.format(nick, arg.split()[0]))
elif cmd == self.cmd_prefix + 'quit' and op:
self.logger.info('quitting per request of {}'.format(nick))
c.quit("See y'all later!")
quit()
elif cmd == self.cmd_prefix + 'help':
output = self.call_help()
elif cmd == self.cmd_prefix + 'reload' and op:
self.logger.info('reloading plugins per request of {}'.format(nick))
self.load_plugins()
output = self.output_message('Plugins reloaded')
return output
This would resolve this at least for the builtins, but I am not sure how you would like to handle plugin commands. Ideally, this prefix would be applied to them as well, but of course this change is not backwards compatible with existing plugins (which is why I didn't submit a PR).
Add Shields.io badges to indicate the package is on PyPI and supports specific versions. This is very helpful to anyone looking for a bot framework, finding pinhook and not knowing anything about it.
[![Supported Python versions](https://img.shields.io/pypi/pyversions/pinhook.svg)](https://pypi.org/project/pinhook) [![Package License](https://img.shields.io/pypi/l/pinhook.svg)](https://github.com/archangelic/pinhook/blob/master/LICENSE) [![PyPI package format](https://img.shields.io/pypi/format/pinhook.svg)](https://pypi.org/project/pinhook) [![Package development status](https://img.shields.io/pypi/status/pinhook.svg)](https://pypi.org/project/pinhook) [![With love from tilde.town](https://img.shields.io/badge/with%20love%20from-tilde%20town-e0b0ff.svg)](https://tilde.town)
The Status badge will need a development status classifier in setup.py, one of these:
Development Status :: 1 - Planning
Development Status :: 2 - Pre-Alpha
Development Status :: 3 - Alpha
Development Status :: 4 - Beta
Development Status :: 5 - Production/Stable
Development Status :: 6 - Mature
Development Status :: 7 - Inactive
I feel like, in order for this to be a solid project, it will need some tests. I am way out of my depth with writing tests. So any help would be greatly appreciated!
There should be an internal command that ops can add a user by nick that cannot interact with a bot (commands and messages to listeners should be ignored)
Should be paired with an unban command.
Instead of having IRCBot
and TwitchBot
classes, have a single Bot
class that takes a backend as a parameter. That could allow to split pinhook into multiple packages, like pinhook-irc
, pinhook-twitch
, etc.
The backend could be a class or a string pointing to a class (to help with import troubles). A backend would be initialized with the Bot instance as a parameter, and started as soon as the bot is ready to accept events; then, the backend deals with everything and fires on_*
events on the bot.
It could be possible to use Abstract Base Classes to enforce implementing specific methods if needed.
Messages would probably need to become even more abstract (or have more possibilities than just action
and message
) to provide support for backends from different paradigms (like toots or facebook privacy settings).
Sometimes a plugin will return a message over 512 bytes. Need to find a good solution to figure out length of bytestring and split it if necessary into multiple blocks to send back to the message sending function.
What I need to implement:
This is all the framework should handle. Plugins should be responsible for cleaning up messages sent to irc.
The project should eventually have a website, and github pages should be totally fine for this.
Currently there is no way to disable or enable specific plugins. The bot loads literally every plugin in the folder.
So, in order to do this, plugins might need to declare names and we will need to set an op-only command for enabling and disabling. As well as some checks to make sure code is not executed when a plugin is disabled.
I have two bots using the same bot account on Twitch, with different OAuth tokens.
When pinhook sends messages, it does not trigger its own listener, which is good. If the other bot sends a message using the same nick, it causes pinhook to trigger listeners and commands even when pinhook's bot name is added to banned_users
:
I can work around this by adding the following code, but it is tedious to do it for every plugin:
if msg.nick != "pyllybot":
# plugin code here
Ideally, the bot should always be ignoring its own nick.
Note: pinhook has the command !ping
and the other bot has !ding
Here is a log generate for this issue:
2020-05-22 15:36:40,393 - INFO - bot - Logging started!
2020-05-22 15:36:40,393 - INFO - bot - Joining Twitch Server
2020-05-22 15:36:40,393 - DEBUG - plugin - []
2020-05-22 15:36:40,393 - INFO - plugin - clearing plugin cache
2020-05-22 15:36:40,393 - INFO - plugin - checking plugin directory
2020-05-22 15:36:40,393 - INFO - plugin - loading plugin test_plugin
2020-05-22 15:36:40,393 - DEBUG - plugin - adding command !ping
2020-05-22 15:36:40,393 - DEBUG - plugin - adding listener echo
2020-05-22 15:36:40,393 - DEBUG - pyllybot - Banned users: pyllybot, buttsbot
2020-05-22 15:36:41,034 - INFO - bot - requesting permissions
2020-05-22 15:36:41,034 - INFO - bot - Joining channel #siinacutie
2020-05-22 15:36:45,228 - DEBUG - bot - Message info: channel: #siinacutie, nick: siinacutie, cmd: !ping, text: !ping
2020-05-22 15:36:45,228 - DEBUG - bot - executing !ping
2020-05-22 15:36:45,228 - DEBUG - bot - returning output: ['is pinged!']
2020-05-22 15:36:45,228 - DEBUG - bot - sending output: ['is pinged!']
2020-05-22 15:36:45,228 - DEBUG - bot - output action: is pinged!
2020-05-22 15:36:49,415 - DEBUG - bot - Message info: channel: #siinacutie, nick: siinacutie, cmd: !ding, text: !ding
2020-05-22 15:36:49,415 - DEBUG - bot - whispering to listener: echo
2020-05-22 15:36:49,614 - DEBUG - bot - Message info: channel: #siinacutie, nick: pyllybot, cmd: Dong!, text: Dong!
2020-05-22 15:36:49,615 - DEBUG - bot - whispering to listener: echo
2020-05-22 15:36:49,615 - INFO - test_plugin - Echoing user: pyllybot
2020-05-22 15:36:49,615 - DEBUG - bot - returning output: ['Dong!']
2020-05-22 15:36:49,615 - DEBUG - bot - sending output: ['Dong!']
2020-05-22 15:36:49,615 - DEBUG - bot - output message: Dong!
Create an executable that will allow people to write a config file with basic info about the bot and then start it.
The bot needs persistent storage, but not enough to justify a database (see #51). I believe the solution is to have a config file passed to the Bot object that it can read and write to. This can also help in doing config-only init (#39)
So the only decision i need to make is to standardize the format for config:
Any thoughts?
This just needs to be documented.
Actually a lot of authentication stuff is probably not well implemented, but I really want to make a guide to using this for twitch bots and I can't without this functionality.
In order to implement #31, the Message class sent to plugins needs to be more generic. Currently, the properties are very irc-centric.
What properties are necessary and what should they be named?
First thoughts:
msg.nick -> msg.who
imp has been deprecated in favor of importlib. The plugin loading should be changed to use this.
Pass an 'event' value to Message handlers to allow listeners to identify when a user has performed a /me
action.
I've already played with this locally so you can bop the issue back to me if you like the idea.
Could fix #65 and also makes base class much neater. Also allows pinhook.plugin and pinhook.cli to log.
Currently this relies on the irc library, which is very nice, but a bit much for what we need in a simple bot framework. Eventually, i would like to replace this dependency with the bare minimum necessary to get a bot connected to irc.
When I use the following:
import pinhook.bot
bot = pinhook.bot.Bot(
channels=['#channel'],
nickname='pin-test',
server='my.server',
port=6667,
ops='SinaCutie',
)
bot.start()
I receive:
Traceback (most recent call last):
File "bot.py", line 9, in <module>
ops='SinaCutie',
File "/home/sina/.virtualenvs/pinhook/lib/python3.5/site-packages/pinhook-1.1.0-py3.5.egg/pinhook/bot.py", line 29, in __init__
File "/home/sina/.virtualenvs/pinhook/lib/python3.5/site-packages/pinhook-1.1.0-py3.5.egg/pinhook/bot.py", line 50, in load_plugins
FileNotFoundError: [Errno 2] No such file or directory: 'plugins'
Running mkdir plugins
alleviates the problem.
Should pinhook be creating the directory or gracefully exiting with an explanation that plugins are needed?
Right now pinhook can issue PRIVMSG, NOTICE, and /me actions.
Could we also have support for arbitrary IRC commands such as JOIN, KICK, TOPIC, etc? https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.