Second commit:
* Adding an 8ball game * Adding an admin manager * Creating the bot shell
This commit is contained in:
commit
7f259bac35
8 changed files with 395 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Elijah Lazkani
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
18
README.md
Normal file
18
README.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# boots
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This project is an attempt to write a fully featured IRC bot. I am learning to write code in python and get better at it.
|
||||||
|
If you are reading this then you are welcome to contribute code, reviews are a part of learning programming =)
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
This code only requires one dependency; [bottom](https://github.com/numberoverzero/bottom).
|
||||||
|
|
||||||
|
## 8ball
|
||||||
|
|
||||||
|
The `Eightball` class gives the bot the ability to support the iconic 8Ball game.
|
||||||
|
It will only answer to the `keyword` provided to the `Eightball` object.
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
|
||||||
|
The `Admin` module provides the ability to add bot administrators who can request some destructive actions that might put the bot itself being abused like `join`, `part` and `quit`.
|
0
boots/__init__.py
Normal file
0
boots/__init__.py
Normal file
122
boots/admin.py
Normal file
122
boots/admin.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import types
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import robot
|
||||||
|
|
||||||
|
|
||||||
|
class Admin:
|
||||||
|
"""
|
||||||
|
Admin class that will keep user access to the bot
|
||||||
|
"""
|
||||||
|
LOGIN = re.compile(r'^LOGIN\s+([^\s]+)\s+([^\s]+)\s*$')
|
||||||
|
|
||||||
|
def __init__(self, client: robot.Bot):
|
||||||
|
self.client = client
|
||||||
|
self.admins = {}
|
||||||
|
self.services = {}
|
||||||
|
self.logger = logging.getLogger()
|
||||||
|
self.logger.debug("Initializing...")
|
||||||
|
client.on("privmsg")(self._handle)
|
||||||
|
|
||||||
|
def add_admin(self, user: str, password: str) -> None:
|
||||||
|
"""
|
||||||
|
Method to add an admin to the list of admins
|
||||||
|
|
||||||
|
:param user str: the user of the admin to be added
|
||||||
|
:param password str: the password to be added
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.admins[user] = {}
|
||||||
|
self.admins[user]['password'] = password
|
||||||
|
self.admins[user]['logged_in'] = False
|
||||||
|
self.admins[user]['logged_in_hostname'] = None
|
||||||
|
self.admins[user]['logged_in_nick'] = None
|
||||||
|
self.admins[user]['LOGIN'] = re.compile(r'^LOGIN\s+({})\s+({})\s*$'.format(user, password))
|
||||||
|
|
||||||
|
def _handle(self, nick: str, target: str, message: str, **kwargs: dict) -> None:
|
||||||
|
"""
|
||||||
|
client callback on event trigger
|
||||||
|
|
||||||
|
:param nick str: nickname of the caller
|
||||||
|
:param target str: nickname to reply to
|
||||||
|
:param message str: event hooks
|
||||||
|
:param kwargs dict: for API compatibility
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.logger.debug("nick={} target={} message={}".format(nick, target, message))
|
||||||
|
self.logger.debug("{}".format(kwargs))
|
||||||
|
_user = kwargs['user']
|
||||||
|
_host = kwargs['host']
|
||||||
|
match = self.LOGIN.match(message)
|
||||||
|
self.logger.debug("match={}".format(match))
|
||||||
|
if match and target == self.client.nick:
|
||||||
|
user = self.admins.get(match.group(1), None)
|
||||||
|
if user:
|
||||||
|
_login = user.get('LOGIN', None)
|
||||||
|
if _login:
|
||||||
|
login_match = _login.match(message)
|
||||||
|
if login_match:
|
||||||
|
self.admins[match.group(1)]['logged_in'] = True
|
||||||
|
self.admins[match.group(1)]['logged_in_nick'] = str(_user)
|
||||||
|
self.admins[match.group(1)]['logged_in_hostname'] = str(_host)
|
||||||
|
kwargs['target'] = nick
|
||||||
|
kwargs['message'] = "{}, you are logged in successfully".format(nick)
|
||||||
|
self.logger.debug("kwargs={}".format(kwargs))
|
||||||
|
self.client.send("PRIVMSG", **kwargs)
|
||||||
|
return
|
||||||
|
|
||||||
|
for regex, (func, command) in self.services.items():
|
||||||
|
match = regex.match(message)
|
||||||
|
if match:
|
||||||
|
if self.is_admin(_user, _host):
|
||||||
|
kwargs['channel'] = match.group(1)
|
||||||
|
self.logger.debug("command={} kwargs={}".format(command, kwargs))
|
||||||
|
self.client.loop.create_task(
|
||||||
|
func(command, **kwargs))
|
||||||
|
|
||||||
|
def on_prvmsg(self,
|
||||||
|
special_char: chr,
|
||||||
|
command: str,
|
||||||
|
func: types.FunctionType = None,
|
||||||
|
**kwargs):
|
||||||
|
"""
|
||||||
|
Decorator function for the admin plugin
|
||||||
|
|
||||||
|
:param command str: the command to answer to
|
||||||
|
:param func function: the function being decorated
|
||||||
|
:param kwargs dict: for API compatibility
|
||||||
|
:return: function the function that called it
|
||||||
|
"""
|
||||||
|
self.logger.debug("sepcial_char={} command={} kwargs={}".format(special_char, command, kwargs))
|
||||||
|
if func is None:
|
||||||
|
return functools.partial(self.on_prvmsg, special_char, command)
|
||||||
|
|
||||||
|
wrapped = func
|
||||||
|
if not asyncio.iscoroutinefunction(wrapped):
|
||||||
|
wrapped = asyncio.coroutine(wrapped)
|
||||||
|
|
||||||
|
commands = command.split("|")
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
compiled = re.compile(r'^{}{}\s*(.*)$'.format(re.escape(special_char), command))
|
||||||
|
self.services[compiled] = (wrapped, command)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
def is_admin(self, user: str, host: str):
|
||||||
|
"""
|
||||||
|
Method to check if the current user is an admin
|
||||||
|
|
||||||
|
:param user str: the user to check against
|
||||||
|
:param host str: the host to check against
|
||||||
|
:return bool: if the user is an admin
|
||||||
|
"""
|
||||||
|
for nick, value in self.admins.items():
|
||||||
|
if value.get('logged_in', None) and \
|
||||||
|
value.get('logged_in_nick', None) == user and \
|
||||||
|
value.get('logged_in_hostname', None) == host:
|
||||||
|
self.logger.debug("{} is an admin".format(nick))
|
||||||
|
return True
|
||||||
|
return False
|
43
boots/boots.py
Normal file
43
boots/boots.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import logging
|
||||||
|
import robot
|
||||||
|
import eightball
|
||||||
|
import admin
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
host = 'eu.undernet.org'
|
||||||
|
port = 6667
|
||||||
|
ssl = False
|
||||||
|
|
||||||
|
nick = "LuckyBoots"
|
||||||
|
channel = "#msaytbeh"
|
||||||
|
|
||||||
|
bot = robot.Bot(host=host, port=port, ssl=ssl, nick=nick, channels=[channel])
|
||||||
|
eightball = eightball.EightBall(bot)
|
||||||
|
|
||||||
|
@eightball.on_keyword
|
||||||
|
async def ball(target, message, **kwargs):
|
||||||
|
bot.send("PRIVMSG", target=target, message=message)
|
||||||
|
|
||||||
|
admin = admin.Admin(bot)
|
||||||
|
admin.add_admin("Armageddon", "12345")
|
||||||
|
|
||||||
|
@_admin.on_prvmsg(".", "join|part|quit")
|
||||||
|
async def login(command, **kwargs):
|
||||||
|
logger.debug("command={} kwargs={}".format(command, kwargs))
|
||||||
|
if command:
|
||||||
|
bot.send(command, **kwargs)
|
||||||
|
|
||||||
|
logger.info("Connecting to {}:{} as {}".format(host, port, nick))
|
||||||
|
bot.loop.create_task(bot.connect())
|
||||||
|
|
||||||
|
bot.loop.run_forever()
|
||||||
|
bot.loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
127
boots/eightball.py
Normal file
127
boots/eightball.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import types
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import functools
|
||||||
|
import asyncio
|
||||||
|
import robot
|
||||||
|
|
||||||
|
class EightBall:
|
||||||
|
"""
|
||||||
|
EightBall Game for the IRC bot
|
||||||
|
"""
|
||||||
|
ANSWERS = [
|
||||||
|
"It is certain",
|
||||||
|
"It is decidedly so",
|
||||||
|
"Without a doubt",
|
||||||
|
"Yes definitely",
|
||||||
|
"You may rely on it",
|
||||||
|
"As I see it, yes",
|
||||||
|
"Most likely",
|
||||||
|
"Outlook good",
|
||||||
|
"Yes",
|
||||||
|
"Signs point to yes",
|
||||||
|
"Reply hazy try again",
|
||||||
|
"Ask again later",
|
||||||
|
"Better not tell you now",
|
||||||
|
"Cannot predict now",
|
||||||
|
"Concentrate and ask again",
|
||||||
|
"Don't count on it",
|
||||||
|
"My reply is no",
|
||||||
|
"My sources say no",
|
||||||
|
"Outlook not so good",
|
||||||
|
"Very doubtful"
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, client: robot.Bot, keyword: str = ".8ball"):
|
||||||
|
self.client = client
|
||||||
|
self.questions = {}
|
||||||
|
self.keyword_pattern = r'^{}\s+(.+)$'.format(keyword)
|
||||||
|
self.logger = logging.getLogger()
|
||||||
|
self.logger.debug("Initializing...")
|
||||||
|
client.on("privmsg")(self._handle)
|
||||||
|
|
||||||
|
def _handle(self, nick, target, message, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
client callback on event trigger
|
||||||
|
|
||||||
|
:param nick str: nickname of the caller
|
||||||
|
:param target str: nickname to reply to
|
||||||
|
:param message str: the question asked
|
||||||
|
:param kwargs dict:
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
nick, target = self._prefix(nick, target)
|
||||||
|
|
||||||
|
# Check that it did not call itself
|
||||||
|
if not nick and not target:
|
||||||
|
return
|
||||||
|
|
||||||
|
for regex, (func, pattern) in self.questions.items():
|
||||||
|
match = regex.match(message)
|
||||||
|
if match:
|
||||||
|
if is_question(match.group(1)):
|
||||||
|
message = "{}, {}".format(nick, self._get_answer())
|
||||||
|
else:
|
||||||
|
message = \
|
||||||
|
"{}, I did not detect a question for me to answer".format(nick)
|
||||||
|
self.logger.debug("PRIVMSG {}".format(message))
|
||||||
|
self.client.loop.create_task(
|
||||||
|
func(target, message, **kwargs))
|
||||||
|
|
||||||
|
def _prefix(self, nick: str, target: str) -> tuple:
|
||||||
|
"""
|
||||||
|
Private method to check the prefix
|
||||||
|
|
||||||
|
This will make sure that it did not call itself
|
||||||
|
and that the nick is set to prefix the 8ball
|
||||||
|
message properly addressing the user calling it
|
||||||
|
|
||||||
|
:param nick str: the nickname calling the game
|
||||||
|
:param target str: the target we aim to reply to
|
||||||
|
:return tuple: the nick and target in a tuple
|
||||||
|
"""
|
||||||
|
if nick == self.client.nick:
|
||||||
|
return None, None
|
||||||
|
if target == self.client.nick:
|
||||||
|
target = nick
|
||||||
|
return nick, target
|
||||||
|
|
||||||
|
def _get_answer(self):
|
||||||
|
return random.choice(self.ANSWERS)
|
||||||
|
|
||||||
|
def on_keyword(self,
|
||||||
|
func: types.FunctionType = None,
|
||||||
|
**kwargs: dict):
|
||||||
|
"""
|
||||||
|
Decorator function for the 8ball game
|
||||||
|
|
||||||
|
:param keyword str: the keyword to activate the 8ball game
|
||||||
|
:param func types.FunctionType: the function being decorated
|
||||||
|
:param kwargs dict: for API compatibility
|
||||||
|
:return: function the function that called it
|
||||||
|
"""
|
||||||
|
if func is None:
|
||||||
|
return functools.partial(self.on_keyword, self.keyword_pattern)
|
||||||
|
|
||||||
|
wrapped = func
|
||||||
|
if not asyncio.iscoroutinefunction(wrapped):
|
||||||
|
wrapped = asyncio.coroutine(wrapped)
|
||||||
|
|
||||||
|
compiled = re.compile(self.keyword_pattern)
|
||||||
|
self.questions[compiled] = (wrapped, self.keyword_pattern)
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def is_question(phrase: str) -> bool:
|
||||||
|
"""
|
||||||
|
Method to check if a string is a question
|
||||||
|
|
||||||
|
:param phrase str: the phrase to check
|
||||||
|
:return: bool if the phrase is a question or not
|
||||||
|
"""
|
||||||
|
compiled = re.compile(r'^[^\?]+\?\s*$')
|
||||||
|
if compiled.match(phrase):
|
||||||
|
return True
|
||||||
|
return False
|
63
boots/robot.py
Normal file
63
boots/robot.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import bottom
|
||||||
|
|
||||||
|
|
||||||
|
class Bot(bottom.Client):
|
||||||
|
"""
|
||||||
|
The Bot class adds features to the bottom.Client class
|
||||||
|
"""
|
||||||
|
def __init__(self,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
ssl: bool,
|
||||||
|
nick: str,
|
||||||
|
user: str = None,
|
||||||
|
realname: str = None,
|
||||||
|
channels: list = None) -> None:
|
||||||
|
super().__init__(host=host, port=port, ssl=ssl)
|
||||||
|
self.logger = logging.getLogger()
|
||||||
|
self.nick = nick
|
||||||
|
self.user = user or self.nick
|
||||||
|
self.realname = realname or self.nick
|
||||||
|
self.channels = channels or []
|
||||||
|
self.on("ping", self.keepalive)
|
||||||
|
self.on("CLIENT_CONNECT", self.on_connect)
|
||||||
|
|
||||||
|
async def keepalive(self, message: str = None, **kwargs: dict) -> None:
|
||||||
|
"""
|
||||||
|
Essential keepalive method to pong the server back
|
||||||
|
automagically everytime it pings
|
||||||
|
|
||||||
|
:param message str: the ping parameters
|
||||||
|
:param kwargs dict: for API compatibility
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
message = message or ""
|
||||||
|
self.logger.debug("PONG {}".format(message))
|
||||||
|
self.send("pong", message=message)
|
||||||
|
|
||||||
|
async def on_connect(self, **kwargs: dict) -> None:
|
||||||
|
"""
|
||||||
|
Essential user information to send at the
|
||||||
|
beginning of the connection with the server
|
||||||
|
|
||||||
|
:param kwargs dict: for API compatibility
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.send('NICK', nick=self.nick)
|
||||||
|
self.send('USER', user=self.user,
|
||||||
|
realname=self.realname)
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
[self.wait("RPL_ENDOFMOTD"),
|
||||||
|
self.wait("ERR_NOMOTD")],
|
||||||
|
loop=self.loop,
|
||||||
|
return_when=asyncio.FIRST_COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cancel whichever waiter's event didn't come in
|
||||||
|
for future in pending:
|
||||||
|
future.cancel()
|
||||||
|
for channel in self.channels:
|
||||||
|
self.send('JOIN', channel=channel)
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
bottom
|
Loading…
Reference in a new issue