diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ee64372
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+*.pyc
diff --git a/README.md b/README.md
index 4330d8e..5ffd023 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,25 @@
# Fox-V3
+Cog Function
+| Name | Status | Description (Click to see full status)
+| --- | --- | --- |
+| ccrole | **Beta** | Create custom commands that also assign roles
May have some bugs, please create an issue if you find any |
+| chatter | **Alpha** | Chat-bot trained to talk like your guild
Missing some key features, but currently functional |
+| coglint | **Alpha** | Error check code in python syntax posted to discord
Works, but probably needs more turning to work for cogs |
+| fight | **Incomplete** | Organize bracket tournaments within discord
Still in-progress, a massive project |
+| flag | **Incomplete** | Create temporary marks on users that expire after specified time
Not yet ported to v3 |
+| hangman | **Alpha** | Play a game of hangman
Some visual glitches and needs more customization |
+| howdoi | **Incomplete** | Create temporary marks on users that expire after specified time
Not yet ported to v3 |
+| leaver | **Incomplete** | Send a message in a channel when a user leaves the server
Not yet ported to v3 |
+| lseen | **Alpha** | Track when a member was last online
Alpha release, please report bugs |
+| reactrestrict | **Alpha** | Removes reactions by role per channel
A bit clunky, but functional |
+| sayurl | **Alpha** | Convert any URL into text and post to discord
No error checking and pretty spammy |
+| secrethitler | **Incomplete** | Play the Secret Hitler game
Concept, no work done yet |
+| stealemoji | **Alpha** | Steals any custom emoji it sees in a reaction
Some planned upgrades for server generation |
+| werewolf | **Alpha** | Play the classic party game Werewolf within discord
Another massive project currently being developed, will be fully customizable |
-Cog Status
- - ccrole: **Incomplete**
- - challonge: **Incomplete** - Challonge integration with discord
- - fight: **Incomplete** Still in-progress, a massive project
- - flag: **Incomplete** Not yet ported to v3
- - hangman: **Incomplete** v2 version is functional at [Fox-Cogs v2](https://github.com/bobloy/Fox-Cogs)
- - immortal: **Private** Designed for a specific server, not recommended to install
- - leaver: **Incomplete** Have not begun conversion yet
- - reactrestrict: **Beta** - Removes reactions by role per channel
- - stealemoji: **Incomplete** - Steals any custom emoji it sees
\ No newline at end of file
+Check out my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox-Cogs)
+
+Get support on the [Third Party Cog Server](https://discord.gg/GET4DVk)
diff --git a/ccrole/__init__.py b/ccrole/__init__.py
new file mode 100644
index 0000000..a22b3f1
--- /dev/null
+++ b/ccrole/__init__.py
@@ -0,0 +1,5 @@
+from .ccrole import CCRole
+
+
+def setup(bot):
+ bot.add_cog(CCRole(bot))
diff --git a/ccrole/ccrole.py b/ccrole/ccrole.py
index f60b26c..2459b6e 100644
--- a/ccrole/ccrole.py
+++ b/ccrole/ccrole.py
@@ -1,178 +1,318 @@
-from discord.ext import commands
-from .utils.dataIO import dataIO
-from .utils import checks
-from .utils.chat_formatting import pagify, box
-import os
+import asyncio
import re
+import discord
+from discord.ext import commands
+from redbot.core import Config, checks
+from redbot.core.utils.chat_formatting import pagify, box
+
class CCRole:
- """Custom commands
- Creates commands used to display text"""
+ """
+ Custom commands
+ Creates commands used to display text and adjust roles
+ """
def __init__(self, bot):
self.bot = bot
- self.file_path = "data/ccrole/commands.json"
- self.c_commands = dataIO.load_json(self.file_path)
+ self.config = Config.get_conf(self, identifier=9999114111108101)
+ default_guild = {
+ "cmdlist": {},
+ "settings": {}
+ }
+
+ self.config.register_guild(**default_guild)
- @commands.group(pass_context=True, no_pm=True)
+ @commands.group(no_pm=True)
async def ccrole(self, ctx):
- """Custom commands management"""
- if ctx.invoked_subcommand is None:
- await self.bot.send_cmd_help(ctx)
+ """Custom commands management with roles
- @ccrole.command(name="add", pass_context=True)
+ Highly customizable custom commands with role management."""
+ if not ctx.invoked_subcommand:
+ await ctx.send_help()
+
+ @ccrole.command(name="add")
@checks.mod_or_permissions(administrator=True)
- async def ccrole_add(self, ctx, command : str):
- """Adds a custom command with roles"""
-
- server = ctx.message.server
- author = ctx.message.author
- msg = 'What roles should it add? (Must be comma separated) Example:\n\n'
- for c, m in enumerate(self.settings[server.id]["GREETING"]):
- msg += " {}. {}\n".format(c, m)
- for page in pagify(msg, ['\n', ' '], shorten_by=20):
- await self.bot.say("```\n{}\n```".format(page))
- answer = await self.bot.wait_for_message(timeout=120, author=author)
+ async def ccrole_add(self, ctx, command: str):
+ """Adds a custom command with roles
+
+ When adding text, put arguments in `{}` to eval them
+ Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`"""
+ command = command.lower()
+ if command in self.bot.all_commands:
+ await ctx.send("That command is already a standard command.")
+ return
+
+ guild = ctx.guild
+ author = ctx.author
+ channel = ctx.channel
+
+ cmd_list = self.config.guild(guild).cmdlist
+
+ if await cmd_list.get_raw(command, default=None):
+ await ctx.send("This command already exists. Delete it with `{}ccrole delete` first.".format(ctx.prefix))
+ return
+
+ # Roles to add
+ await ctx.send('What roles should it add? (Must be **comma separated**)\nSay `None` to skip adding roles')
+
+ def check(m):
+ return m.author == author and m.channel == channel
+
try:
- num = int(answer.content)
- choice = self.settings[server.id]["GREETING"].pop(num)
- except:
- await self.bot.say("That's not a number in the list :/")
+ answer = await self.bot.wait_for('message', timeout=120, check=check)
+ except asyncio.TimeoutError:
+ await ctx.send("Timed out, canceling")
return
- server = ctx.message.server
- command = command.lower()
- if command in self.bot.commands:
- await self.bot.say("That command is already a standard command.")
+ arole_list = []
+ if answer.content.upper() != "NONE":
+ arole_list = await self._get_roles_from_content(ctx, answer.content)
+ if arole_list is None:
+ await ctx.send("Invalid answer, canceling")
+ return
+
+ # Roles to remove
+ await ctx.send('What roles should it remove? (Must be comma separated)\nSay `None` to skip removing roles')
+ try:
+ answer = await self.bot.wait_for('message', timeout=120, check=check)
+ except asyncio.TimeoutError:
+ await ctx.send("Timed out, canceling")
return
- if server.id not in self.c_commands:
- self.c_commands[server.id] = {}
- cmdlist = self.c_commands[server.id]
- if command not in cmdlist:
- cmdlist[command] = text
- self.c_commands[server.id] = cmdlist
- dataIO.save_json(self.file_path, self.c_commands)
- await self.bot.say("Custom command successfully added.")
- else:
- await self.bot.say("This command already exists. Use "
- "`{}customcom edit` to edit it."
- "".format(ctx.prefix))
- @customcom.command(name="edit", pass_context=True)
- @checks.mod_or_permissions(administrator=True)
- async def cc_edit(self, ctx, command : str, *, text):
- """Edits a custom command
- Example:
- [p]customcom edit yourcommand Text you want
- """
- server = ctx.message.server
- command = command.lower()
- if server.id in self.c_commands:
- cmdlist = self.c_commands[server.id]
- if command in cmdlist:
- cmdlist[command] = text
- self.c_commands[server.id] = cmdlist
- dataIO.save_json(self.file_path, self.c_commands)
- await self.bot.say("Custom command successfully edited.")
- else:
- await self.bot.say("That command doesn't exist. Use "
- "`{}customcom add` to add it."
- "".format(ctx.prefix))
+ rrole_list = []
+ if answer.content.upper() != "NONE":
+ rrole_list = await self._get_roles_from_content(ctx, answer.content)
+ if rrole_list is None:
+ await ctx.send("Invalid answer, canceling")
+ return
+
+ # Roles to use
+ await ctx.send(
+ 'What roles are allowed to use this command? (Must be comma separated)\nSay `None` to allow all roles')
+
+ try:
+ answer = await self.bot.wait_for('message', timeout=120, check=check)
+ except asyncio.TimeoutError:
+ await ctx.send("Timed out, canceling")
+ return
+
+ prole_list = []
+ if answer.content.upper() != "NONE":
+ prole_list = await self._get_roles_from_content(ctx, answer.content)
+ if prole_list is None:
+ await ctx.send("Invalid answer, canceling")
+ return
+
+ # Selfrole
+ await ctx.send('Is this a targeted command?(yes/no)\nNo will make this a selfrole command')
+
+ try:
+ answer = await self.bot.wait_for('message', timeout=120, check=check)
+ except asyncio.TimeoutError:
+ await ctx.send("Timed out, canceling")
+ return
+
+ if answer.content.upper() in ["Y", "YES"]:
+ targeted = True
+ await ctx.send("This command will be **`targeted`**")
else:
- await self.bot.say("There are no custom commands in this server."
- " Use `{}customcom add` to start adding some."
- "".format(ctx.prefix))
+ targeted = False
+ await ctx.send("This command will be **`selfrole`**")
+
+ # Message to send
+ await ctx.send(
+ 'What message should the bot say when using this command?\n'
+ 'Say `None` to send the default `Success!` message\n'
+ 'Eval Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`\n'
+ 'For example: `Welcome {target.mention} to {server.name}!`')
+
+ try:
+ answer = await self.bot.wait_for('message', timeout=120, check=check)
+ except asyncio.TimeoutError:
+ await ctx.send("Timed out, canceling")
+ return
- @customcom.command(name="delete", pass_context=True)
+ text = "Success!"
+ if answer.content.upper() != "NONE":
+ text = answer.content
+
+ # Save the command
+
+ out = {'text': text, 'aroles': arole_list, 'rroles': rrole_list, "proles": prole_list, "targeted": targeted}
+
+ await cmd_list.set_raw(command, value=out)
+
+ await ctx.send("Custom Command **`{}`** successfully added".format(command))
+
+ @ccrole.command(name="delete")
@checks.mod_or_permissions(administrator=True)
- async def cc_delete(self, ctx, command : str):
+ async def ccrole_delete(self, ctx, command: str):
"""Deletes a custom command
+
Example:
- [p]customcom delete yourcommand"""
- server = ctx.message.server
+ `[p]ccrole delete yourcommand`"""
+ guild = ctx.guild
command = command.lower()
- if server.id in self.c_commands:
- cmdlist = self.c_commands[server.id]
- if command in cmdlist:
- cmdlist.pop(command, None)
- self.c_commands[server.id] = cmdlist
- dataIO.save_json(self.file_path, self.c_commands)
- await self.bot.say("Custom command successfully deleted.")
- else:
- await self.bot.say("That command doesn't exist.")
+ if not await self.config.guild(guild).cmdlist.get_raw(command, default=None):
+ await ctx.send("That command doesn't exist")
else:
- await self.bot.say("There are no custom commands in this server."
- " Use `{}customcom add` to start adding some."
- "".format(ctx.prefix))
+ await self.config.guild(guild).cmdlist.set_raw(command, value=None)
+ await ctx.send("Custom command successfully deleted.")
- @customcom.command(name="list", pass_context=True)
- async def cc_list(self, ctx):
- """Shows custom commands list"""
- server = ctx.message.server
- commands = self.c_commands.get(server.id, {})
+ @ccrole.command(name="details")
+ async def ccrole_details(self, ctx, command: str):
+ """Provide details about passed custom command"""
+ guild = ctx.guild
+ command = command.lower()
+ cmd = await self.config.guild(guild).cmdlist.get_raw(command, default=None)
+ if cmd is None:
+ await ctx.send("That command doesn't exist")
+ return
+
+ embed = discord.Embed(title=command,
+ description="{} custom command".format("Targeted" if cmd['targeted'] else "Non-Targeted"))
- if not commands:
- await self.bot.say("There are no custom commands in this server."
- " Use `{}customcom add` to start adding some."
- "".format(ctx.prefix))
+ def process_roles(role_list):
+ if not role_list:
+ return "None"
+ return ", ".join([discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list])
+
+ embed.add_field(name="Text", value="```{}```".format(cmd['text']))
+ embed.add_field(name="Adds Roles", value=process_roles(cmd['aroles']), inline=True)
+ embed.add_field(name="Removes Roles", value=process_roles(cmd['rroles']), inline=True)
+ embed.add_field(name="Role Restrictions", value=process_roles(cmd['proles']), inline=True)
+
+ await ctx.send(embed=embed)
+
+ @ccrole.command(name="list")
+ async def ccrole_list(self, ctx):
+ """Shows custom commands list"""
+ guild = ctx.guild
+ cmd_list = await self.config.guild(guild).cmdlist()
+ cmd_list = {k: v for k,v in cmd_list.items() if v}
+ if not cmd_list:
+ await ctx.send(
+ "There are no custom commands in this server. Use `{}ccrole add` to start adding some.".format(
+ ctx.prefix))
return
- commands = ", ".join([ctx.prefix + c for c in sorted(commands)])
- commands = "Custom commands:\n\n" + commands
+ cmd_list = ", ".join([ctx.prefix + c for c in sorted(cmd_list.keys())])
+ cmd_list = "Custom commands:\n\n" + cmd_list
- if len(commands) < 1500:
- await self.bot.say(box(commands))
+ if len(cmd_list) < 1500: # I'm allowed to have arbitrary numbers for when it's too much to dm dammit
+ await ctx.send(box(cmd_list))
else:
- for page in pagify(commands, delims=[" ", "\n"]):
- await self.bot.whisper(box(page))
+ for page in pagify(cmd_list, delims=[" ", "\n"]):
+ await ctx.author.send(box(page))
+ await ctx.send("Command list DM'd")
async def on_message(self, message):
- if len(message.content) < 2 or message.channel.is_private:
+ if len(message.content) < 2 or message.guild is None:
return
- server = message.server
- prefix = self.get_prefix(message)
-
- if not prefix:
+ guild = message.guild
+ try:
+ prefix = await self.get_prefix(message)
+ except ValueError:
return
- if server.id in self.c_commands and self.bot.user_allowed(message):
- cmdlist = self.c_commands[server.id]
- cmd = message.content[len(prefix):]
- if cmd in cmdlist:
- cmd = cmdlist[cmd]
- cmd = self.format_cc(cmd, message)
- await self.bot.send_message(message.channel, cmd)
- elif cmd.lower() in cmdlist:
- cmd = cmdlist[cmd.lower()]
- cmd = self.format_cc(cmd, message)
- await self.bot.send_message(message.channel, cmd)
-
- def get_prefix(self, message):
- for p in self.bot.settings.get_prefixes(message.server):
- if message.content.startswith(p):
+ cmdlist = self.config.guild(guild).cmdlist
+ cmd = message.content[len(prefix):].split()[0].lower()
+ cmd = await cmdlist.get_raw(cmd, default=None)
+
+ if cmd is not None:
+ await self.eval_cc(cmd, message)
+
+ async def _get_roles_from_content(self, ctx, content):
+ content_list = content.split(",")
+ try:
+ role_list = [discord.utils.get(ctx.guild.roles, name=role.strip(' ')).id for role in content_list]
+ except (discord.HTTPException, AttributeError): # None.id is attribute error
+ return None
+ else:
+ return role_list
+
+ async def get_prefix(self, message: discord.Message) -> str:
+ """
+ Borrowed from alias cog
+ Tries to determine what prefix is used in a message object.
+ Looks to identify from longest prefix to smallest.
+
+ Will raise ValueError if no prefix is found.
+ :param message: Message object
+ :return:
+ """
+ content = message.content
+ prefix_list = await self.bot.command_prefix(self.bot, message)
+ prefixes = sorted(prefix_list,
+ key=lambda pfx: len(pfx),
+ reverse=True)
+ for p in prefixes:
+ if content.startswith(p):
return p
- return False
+ raise ValueError
+
+ async def eval_cc(self, cmd, message):
+ """Does all the work"""
+ if cmd['proles'] and not (set(role.id for role in message.author.roles) & set(cmd['proles'])):
+ return # Not authorized, do nothing
+
+ if cmd['targeted']:
+ try:
+ target = discord.utils.get(message.guild.members, mention=message.content.split()[1])
+ except IndexError: # .split() return list of len<2
+ target = None
+
+ if not target:
+ out_message = "This custom command is targeted! @mention a target\n`{} `".format(
+ message.content.split()[0])
+ await message.channel.send(out_message)
+ return
+ else:
+ target = message.author
- def format_cc(self, command, message):
- results = re.findall("\{([^}]+)\}", command)
+ if cmd['aroles']:
+ arole_list = [discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd['aroles']]
+ # await self.bot.send_message(message.channel, "Adding: "+str([str(arole) for arole in arole_list]))
+ try:
+ await target.add_roles(*arole_list)
+ except discord.Forbidden:
+ await message.channel.send("Permission error: Unable to add roles")
+ await asyncio.sleep(1)
+
+ if cmd['rroles']:
+ rrole_list = [discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd['rroles']]
+ # await self.bot.send_message(message.channel, "Removing: "+str([str(rrole) for rrole in rrole_list]))
+ try:
+ await target.remove_roles(*rrole_list)
+ except discord.Forbidden:
+ await message.channel.send("Permission error: Unable to remove roles")
+
+ out_message = self.format_cc(cmd, message, target)
+ await message.channel.send(out_message)
+
+ def format_cc(self, cmd, message, target):
+ out = cmd['text']
+ results = re.findall("{([^}]+)\}", out)
for result in results:
- param = self.transform_parameter(result, message)
- command = command.replace("{" + result + "}", param)
- return command
+ param = self.transform_parameter(result, message, target)
+ out = out.replace("{" + result + "}", param)
+ return out
- def transform_parameter(self, result, message):
+ def transform_parameter(self, result, message, target):
"""
For security reasons only specific objects are allowed
Internals are ignored
"""
raw_result = "{" + result + "}"
objects = {
- "message" : message,
- "author" : message.author,
- "channel" : message.channel,
- "server" : message.server
+ "message": message,
+ "author": message.author,
+ "channel": message.channel,
+ "server": message.guild,
+ "guild": message.guild,
+ "target": target
}
if result in objects:
return str(objects[result])
@@ -185,22 +325,3 @@ class CCRole:
else:
return raw_result
return str(getattr(first, second, raw_result))
-
-
-def check_folders():
- if not os.path.exists("data/customcom"):
- print("Creating data/customcom folder...")
- os.makedirs("data/customcom")
-
-
-def check_files():
- f = "data/customcom/commands.json"
- if not dataIO.is_valid_json(f):
- print("Creating empty commands.json...")
- dataIO.save_json(f, {})
-
-
-def setup(bot):
- check_folders()
- check_files()
- bot.add_cog(CCRole(bot))
\ No newline at end of file
diff --git a/ccrole/info.json b/ccrole/info.json
index c2be724..73a1f79 100644
--- a/ccrole/info.json
+++ b/ccrole/info.json
@@ -1,9 +1,10 @@
{
- "AUTHOR" : "Bobloy",
- "INSTALL_MSG" : "Thank you for installing Custom Commands w/ Roles",
- "NAME" : "CCRole",
- "SHORT" : "Creates commands that adjust roles",
- "DESCRIPTION" : "Creates custom commands to adjust roles and send custom messages",
- "TAGS" : ["fox", "bobloy", "utilities", "tools", "roles"],
- "HIDDEN" : false
+ "author" : ["Bobloy"],
+ "bot_version" : [3,0,0],
+ "description" : "[Incomplete] Creates custom commands to adjust roles and send custom messages",
+ "hidden" : false,
+ "install_msg" : "Thank you for installing Custom Commands w/ Roles.",
+ "requirements" : [],
+ "short" : "[Incomplete] Creates commands that adjust roles",
+ "tags" : ["fox", "bobloy", "utility", "tools", "roles"]
}
\ No newline at end of file
diff --git a/challonge/__init__.py b/challonge/__init__.py
deleted file mode 100644
index a1e7e01..0000000
--- a/challonge/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .challonge import Challonge
-
-def setup(bot):
- n = Challonge(bot)
- bot.add_cog(n)
\ No newline at end of file
diff --git a/challonge/challonge.py b/challonge/challonge.py
deleted file mode 100644
index 3d468db..0000000
--- a/challonge/challonge.py
+++ /dev/null
@@ -1,134 +0,0 @@
-import os
-
-import challonge
-
-import discord
-from discord.ext import commands
-
-from redbot.core.utils.chat_formatting import pagify
-from redbot.core.utils.chat_formatting import box
-from redbot.core import Config
-from redbot.core import checks
-
-
-
-
-class Challonge:
- """Cog for organizing Challonge tourneys"""
-
- def __init__(self, bot):
- self.bot = bot
- self.config = Config.get_conf(self, identifier=6710497108108111110103101)
- default_global = {
- "username": None,
- "apikey": None
- }
- default_guild = {
- "reportchannel": None,
- "announcechannel": None
- }
-
- self.config.register_global(**default_global)
- self.config.register_guild(**default_guild)
-
- await self._set_credentials()
-
-# ************************Challonge command group start************************
-
- @commands.group()
- @commands.guild_only()
- async def challonge(self, ctx):
- """Challonge command base"""
- if ctx.invoked_subcommand is None:
- await ctx.send_help()
- # await ctx.send("I can do stuff!")
-
-
- @challonge.command(name="apikey")
- async def c_apikey(self, ctx, username, apikey):
- """Sets challonge username and apikey"""
- await self.config.username.set(username)
- await self.config.apikey.set(apikey)
- await self._set_credentials()
- await ctx.send("Success!")
-
- @challonge.command(name="report")
- async def c_report(self, ctx, channel: discord.TextChannel=None):
- """Set the channel for self-reporting matches"""
- if channel is None:
- channel = ctx.channel
-
- await self.config.guild(ctx.guild).reportchnnl.set(channel.id)
-
- channel = (await self._get_reportchnnl(ctx.guild))
- await ctx.send("Self-Reporting Channel is now set to: " + channel.mention)
-
- @challonge.command(name="announce")
- async def c_announce(self, ctx, channel: discord.TextChannel=None):
- """Set the channel for tournament announcements"""
- if channel is None:
- channel = ctx.channel
-
- await self.config.guild(ctx.guild).announcechnnl.set(channel.id)
-
- channel = (await self._get_announcechnnl(ctx.guild))
- await ctx.send("Announcement Channel is now set to: " + channel.mention)
-
-# ************************Private command group start************************
- async def _print_tourney(self, guild: discord.Guild, tID: int):
- channel = (await self._get_announcechnnl(ctx.guild))
-
- await channel.send()
-
- async def _set_credentials(self):
- username = await self.config.username
- apikey = await self.config.apikey
- if username and apikey:
- challonge.set_credentials(username, apikey)
- return True
- return False
-
- async def _get_message_from_id(self, guild: discord.Guild, message_id: int):
- """
- Tries to find a message by ID in the current guild context.
- :param ctx:
- :param message_id:
- :return:
- """
- for channel in guild.text_channels:
- try:
- return await channel.get_message(message_id)
- except discord.NotFound:
- pass
- except AttributeError: # VoiceChannel object has no attribute 'get_message'
- pass
-
- return None
-
- async def _get_announcechnnl(self, guild: discord.Guild):
- channelid = await self.config.guild(guild).announcechnnl()
- channel = self._get_channel_from_id(channelid)
- return channel
-
- async def _get_reportchnnl(self, guild: discord.Guild):
- channelid = await self.config.guild(guild).reportchnnl()
- channel = self._get_channel_from_id(channelid)
- return channel
-
- def _get_channel_from_id(self, channelid):
- return self.bot.get_channel(channelid)
-
- def _get_user_from_id(self, userid):
- # guild = self._get_guild_from_id(guildID)
- # return discord.utils.get(guild.members, id=userid)
- return self.bot.get_user(userid)
-
- def _get_guild_from_id(self, guildID):
- return self.bot.get_guild(guildID)
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/challonge/info.json b/challonge/info.json
deleted file mode 100644
index 6dac0ca..0000000
--- a/challonge/info.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "author" : ["Bobloy"],
- "bot_version" : [3,0,0],
- "description" : "[Incomplete] Cog to organize tournaments within Discord using Challonge",
- "hidden" : false,
- "install_msg" : "Thank you for installing the Challonge Cog.",
- "requirements" : ["iso8601", "challonge"],
- "short" : "[Incomplete] Cog to organize Challonge tournaments",
- "tags" : ["game", "fun", "fight", "tournament", "tourney", "challonge", "elimination", "bracket", "bobloy"]
-}
\ No newline at end of file
diff --git a/chatter/__init__.py b/chatter/__init__.py
new file mode 100644
index 0000000..cc101b7
--- /dev/null
+++ b/chatter/__init__.py
@@ -0,0 +1,11 @@
+from . import chatterbot
+from .chat import Chatter
+
+
+def setup(bot):
+ bot.add_cog(Chatter(bot))
+
+
+__all__ = (
+ 'chatterbot'
+)
diff --git a/chatter/chat.py b/chatter/chat.py
new file mode 100644
index 0000000..32d83a3
--- /dev/null
+++ b/chatter/chat.py
@@ -0,0 +1,157 @@
+import asyncio
+from datetime import datetime, timedelta
+
+import discord
+from discord.ext import commands
+from redbot.core import Config
+
+from chatter.chatterbot import ChatBot
+from chatter.chatterbot.trainers import ListTrainer
+
+
+
+class Chatter:
+ """
+ This cog trains a chatbot that will talk like members of your Guild
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+ self.config = Config.get_conf(self, identifier=6710497116116101114)
+ default_global = {}
+ default_guild = {
+ "whitelist": None,
+ "days": 1
+ }
+
+ self.chatbot = ChatBot(
+ "ChatterBot",
+ storage_adapter='chatter.chatterbot.storage.SQLStorageAdapter',
+ database='./database.sqlite3'
+ )
+ self.chatbot.set_trainer(ListTrainer)
+
+ self.config.register_global(**default_global)
+ self.config.register_guild(**default_guild)
+
+ self.loop = asyncio.get_event_loop()
+
+ async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None):
+ """
+ Compiles all conversation in the Guild this bot can get it's hands on
+ Currently takes a stupid long time
+ Returns a list of text
+ """
+ out = []
+ after = datetime.today() - timedelta(days=(await self.config.guild(ctx.guild).days()))
+
+ for channel in ctx.guild.text_channels:
+ if in_channel:
+ channel = in_channel
+ await ctx.send("Gathering {}".format(channel.mention))
+ user = None
+ try:
+ async for message in channel.history(limit=None, reverse=True, after=after):
+ if user == message.author:
+ out[-1] += "\n" + message.clean_content
+ else:
+ user = message.author
+ out.append(message.clean_content)
+ except discord.Forbidden:
+ pass
+ except discord.HTTPException:
+ pass
+
+ if in_channel:
+ break
+
+ return out
+
+ def _train(self, data):
+ try:
+ self.chatbot.train(data)
+ except:
+ return False
+ return True
+
+ @commands.group()
+ async def chatter(self, ctx: RedContext):
+ """
+ Base command for this cog. Check help for the commands list.
+ """
+ if ctx.invoked_subcommand is None:
+ await ctx.send_help()
+
+ @chatter.command()
+ async def age(self, ctx: RedContext, days: int):
+ """
+ Sets the number of days to look back
+ Will train on 1 day otherwise
+ """
+
+ await self.config.guild(ctx.guild).days.set(days)
+ await ctx.send("Success")
+
+ @chatter.command()
+ async def backup(self, ctx, backupname):
+ """
+ Backup your training data to a json for later use
+ """
+ await ctx.send("Backing up data, this may take a while")
+ future = await self.loop.run_in_executor(None, self.chatbot.trainer.export_for_training, './{}.json'.format(backupname))
+
+ if future:
+ await ctx.send("Backup successful!")
+ else:
+ await ctx.send("Error occurred :(")
+
+ @chatter.command()
+ async def train(self, ctx: commands.Context, channel: discord.TextChannel):
+ """
+ Trains the bot based on language in this guild
+ """
+
+ conversation = await self._get_conversation(ctx, channel)
+
+ if not conversation:
+ await ctx.send("Failed to gather training data")
+ return
+
+ await ctx.send("Gather successful! Training begins now\n(**This will take a long time, be patient**)")
+ embed = discord.Embed(title="Loading")
+ embed.set_image(url="http://www.loop.universaleverything.com/animations/1295.gif")
+ temp_message = await ctx.send(embed=embed)
+ future = await self.loop.run_in_executor(None, self._train, conversation)
+
+ try:
+ await temp_message.delete()
+ except:
+ pass
+
+ if future:
+ await ctx.send("Training successful!")
+ else:
+ await ctx.send("Error occurred :(")
+
+ async def on_message(self, message):
+ """
+ Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py
+ for on_message recognition of @bot
+ """
+ author = message.author
+ channel = message.channel
+
+
+ if message.author.id != self.bot.user.id:
+ to_strip = "@" + author.guild.me.display_name + " "
+ text = message.clean_content
+ if not text.startswith(to_strip):
+ return
+ text = text.replace(to_strip, "", 1)
+ async with channel.typing():
+ future = await self.loop.run_in_executor(None, self.chatbot.get_response, text)
+
+ if future:
+ await channel.send(str(future))
+ else:
+ await channel.send(':thinking:')
diff --git a/chatter/chatterbot/__init__.py b/chatter/chatterbot/__init__.py
new file mode 100644
index 0000000..7a127ee
--- /dev/null
+++ b/chatter/chatterbot/__init__.py
@@ -0,0 +1,13 @@
+"""
+ChatterBot is a machine learning, conversational dialog engine.
+"""
+from .chatterbot import ChatBot
+
+__version__ = '0.8.5'
+__author__ = 'Gunther Cox'
+__email__ = 'gunthercx@gmail.com'
+__url__ = 'https://github.com/gunthercox/ChatterBot'
+
+__all__ = (
+ 'ChatBot',
+)
diff --git a/chatter/chatterbot/__main__.py b/chatter/chatterbot/__main__.py
new file mode 100644
index 0000000..0322854
--- /dev/null
+++ b/chatter/chatterbot/__main__.py
@@ -0,0 +1,22 @@
+import sys
+
+if __name__ == '__main__':
+ import importlib
+
+ if '--version' in sys.argv:
+ chatterbot = importlib.import_module('chatterbot')
+ print(chatterbot.__version__)
+
+ if 'list_nltk_data' in sys.argv:
+ import os
+ import nltk.data
+
+ data_directories = []
+
+ # Find each data directory in the NLTK path that has content
+ for path in nltk.data.path:
+ if os.path.exists(path):
+ if os.listdir(path):
+ data_directories.append(path)
+
+ print(os.linesep.join(data_directories))
diff --git a/chatter/chatterbot/adapters.py b/chatter/chatterbot/adapters.py
new file mode 100644
index 0000000..83ce94c
--- /dev/null
+++ b/chatter/chatterbot/adapters.py
@@ -0,0 +1,47 @@
+import logging
+
+
+class Adapter(object):
+ """
+ A superclass for all adapter classes.
+
+ :param logger: A python logger.
+ """
+
+ def __init__(self, **kwargs):
+ self.logger = kwargs.get('logger', logging.getLogger(__name__))
+ self.chatbot = kwargs.get('chatbot')
+
+ def set_chatbot(self, chatbot):
+ """
+ Gives the adapter access to an instance of the ChatBot class.
+
+ :param chatbot: A chat bot instance.
+ :type chatbot: ChatBot
+ """
+ self.chatbot = chatbot
+
+ class AdapterMethodNotImplementedError(NotImplementedError):
+ """
+ An exception to be raised when an adapter method has not been implemented.
+ Typically this indicates that the developer is expected to implement the
+ method in a subclass.
+ """
+
+ def __init__(self, message=None):
+ """
+ Set the message for the esception.
+ """
+ if not message:
+ message = 'This method must be overridden in a subclass method.'
+ self.message = message
+
+ def __str__(self):
+ return self.message
+
+ class InvalidAdapterTypeException(Exception):
+ """
+ An exception to be raised when an adapter
+ of an unexpected class type is received.
+ """
+ pass
diff --git a/chatter/chatterbot/chatterbot.py b/chatter/chatterbot/chatterbot.py
new file mode 100644
index 0000000..c7a92cb
--- /dev/null
+++ b/chatter/chatterbot/chatterbot.py
@@ -0,0 +1,175 @@
+from __future__ import unicode_literals
+
+import logging
+
+from . import utils
+from .input import InputAdapter
+from .output import OutputAdapter
+from .storage import StorageAdapter
+
+
+class ChatBot(object):
+ """
+ A conversational dialog chat bot.
+ """
+
+ def __init__(self, name, **kwargs):
+ from .logic import MultiLogicAdapter
+
+ self.name = name
+ kwargs['name'] = name
+ kwargs['chatbot'] = self
+
+ self.default_session = None
+
+ storage_adapter = kwargs.get('storage_adapter', 'chatter.chatterbot.storage.SQLStorageAdapter')
+
+ logic_adapters = kwargs.get('logic_adapters', [
+ 'chatter.chatterbot.logic.BestMatch'
+ ])
+
+ input_adapter = kwargs.get('input_adapter', 'chatter.chatterbot.input.VariableInputTypeAdapter')
+
+ output_adapter = kwargs.get('output_adapter', 'chatter.chatterbot.output.OutputAdapter')
+
+ # Check that each adapter is a valid subclass of it's respective parent
+ utils.validate_adapter_class(storage_adapter, StorageAdapter)
+ utils.validate_adapter_class(input_adapter, InputAdapter)
+ utils.validate_adapter_class(output_adapter, OutputAdapter)
+
+ self.logic = MultiLogicAdapter(**kwargs)
+ self.storage = utils.initialize_class(storage_adapter, **kwargs)
+ self.input = utils.initialize_class(input_adapter, **kwargs)
+ self.output = utils.initialize_class(output_adapter, **kwargs)
+
+ filters = kwargs.get('filters', tuple())
+ self.filters = tuple([utils.import_module(F)() for F in filters])
+
+ # Add required system logic adapter
+ self.logic.system_adapters.append(
+ utils.initialize_class('chatter.chatterbot.logic.NoKnowledgeAdapter', **kwargs)
+ )
+
+ for adapter in logic_adapters:
+ self.logic.add_adapter(adapter, **kwargs)
+
+ # Add the chatbot instance to each adapter to share information such as
+ # the name, the current conversation, or other adapters
+ self.logic.set_chatbot(self)
+ self.input.set_chatbot(self)
+ self.output.set_chatbot(self)
+
+ preprocessors = kwargs.get(
+ 'preprocessors', [
+ 'chatter.chatterbot.preprocessors.clean_whitespace'
+ ]
+ )
+
+ self.preprocessors = []
+
+ for preprocessor in preprocessors:
+ self.preprocessors.append(utils.import_module(preprocessor))
+
+ # Use specified trainer or fall back to the default
+ trainer = kwargs.get('trainer', 'chatter.chatterbot.trainers.Trainer')
+ TrainerClass = utils.import_module(trainer)
+ self.trainer = TrainerClass(self.storage, **kwargs)
+ self.training_data = kwargs.get('training_data')
+
+ self.default_conversation_id = None
+
+ self.logger = kwargs.get('logger', logging.getLogger(__name__))
+
+ # Allow the bot to save input it receives so that it can learn
+ self.read_only = kwargs.get('read_only', False)
+
+ if kwargs.get('initialize', True):
+ self.initialize()
+
+ def initialize(self):
+ """
+ Do any work that needs to be done before the responses can be returned.
+ """
+ self.logic.initialize()
+
+ def get_response(self, input_item, conversation_id=None):
+ """
+ Return the bot's response based on the input.
+
+ :param input_item: An input value.
+ :param conversation_id: The id of a conversation.
+ :returns: A response to the input.
+ :rtype: Statement
+ """
+ if not conversation_id:
+ if not self.default_conversation_id:
+ self.default_conversation_id = self.storage.create_conversation()
+ conversation_id = self.default_conversation_id
+
+ input_statement = self.input.process_input_statement(input_item)
+
+ # Preprocess the input statement
+ for preprocessor in self.preprocessors:
+ input_statement = preprocessor(self, input_statement)
+
+ statement, response = self.generate_response(input_statement, conversation_id)
+
+ # Learn that the user's input was a valid response to the chat bot's previous output
+ previous_statement = self.storage.get_latest_response(conversation_id)
+
+ if not self.read_only:
+ self.learn_response(statement, previous_statement)
+ self.storage.add_to_conversation(conversation_id, statement, response)
+
+ # Process the response output with the output adapter
+ return self.output.process_response(response, conversation_id)
+
+ def generate_response(self, input_statement, conversation_id):
+ """
+ Return a response based on a given input statement.
+ """
+ self.storage.generate_base_query(self, conversation_id)
+
+ # Select a response to the input statement
+ response = self.logic.process(input_statement)
+
+ return input_statement, response
+
+ def learn_response(self, statement, previous_statement):
+ """
+ Learn that the statement provided is a valid response.
+ """
+ from .conversation import Response
+
+ if previous_statement:
+ statement.add_response(
+ Response(previous_statement.text)
+ )
+ self.logger.info('Adding "{}" as a response to "{}"'.format(
+ statement.text,
+ previous_statement.text
+ ))
+
+ # Save the statement after selecting a response
+ self.storage.update(statement)
+
+ def set_trainer(self, training_class, **kwargs):
+ """
+ Set the module used to train the chatbot.
+
+ :param training_class: The training class to use for the chat bot.
+ :type training_class: `Trainer`
+
+ :param \**kwargs: Any parameters that should be passed to the training class.
+ """
+ if 'chatbot' not in kwargs:
+ kwargs['chatbot'] = self
+
+ self.trainer = training_class(self.storage, **kwargs)
+
+ @property
+ def train(self):
+ """
+ Proxy method to the chat bot's trainer class.
+ """
+ return self.trainer.train
diff --git a/chatter/chatterbot/comparisons.py b/chatter/chatterbot/comparisons.py
new file mode 100644
index 0000000..59efa95
--- /dev/null
+++ b/chatter/chatterbot/comparisons.py
@@ -0,0 +1,325 @@
+# -*- coding: utf-8 -*-
+
+
+"""
+This module contains various text-comparison algorithms
+designed to compare one statement to another.
+"""
+
+# Use python-Levenshtein if available
+try:
+ from Levenshtein.StringMatcher import StringMatcher as SequenceMatcher
+except ImportError:
+ from difflib import SequenceMatcher
+
+
+class Comparator:
+
+ def __call__(self, statement_a, statement_b):
+ return self.compare(statement_a, statement_b)
+
+ def compare(self, statement_a, statement_b):
+ return 0
+
+ def get_initialization_functions(self):
+ """
+ Return all initialization methods for the comparison algorithm.
+ Initialization methods must start with 'initialize_' and
+ take no parameters.
+ """
+ initialization_methods = [
+ (
+ method,
+ getattr(self, method),
+ ) for method in dir(self) if method.startswith('initialize_')
+ ]
+
+ return {
+ key: value for (key, value) in initialization_methods
+ }
+
+
+class LevenshteinDistance(Comparator):
+ """
+ Compare two statements based on the Levenshtein distance
+ of each statement's text.
+
+ For example, there is a 65% similarity between the statements
+ "where is the post office?" and "looking for the post office"
+ based on the Levenshtein distance algorithm.
+ """
+
+ def compare(self, statement, other_statement):
+ """
+ Compare the two input statements.
+
+ :return: The percent of similarity between the text of the statements.
+ :rtype: float
+ """
+
+ # Return 0 if either statement has a falsy text value
+ if not statement.text or not other_statement.text:
+ return 0
+
+ # Get the lowercase version of both strings
+
+ statement_text = str(statement.text.lower())
+ other_statement_text = str(other_statement.text.lower())
+
+ similarity = SequenceMatcher(
+ None,
+ statement_text,
+ other_statement_text
+ )
+
+ # Calculate a decimal percent of the similarity
+ percent = round(similarity.ratio(), 2)
+
+ return percent
+
+
+class SynsetDistance(Comparator):
+ """
+ Calculate the similarity of two statements.
+ This is based on the total maximum synset similarity between each word in each sentence.
+
+ This algorithm uses the `wordnet`_ functionality of `NLTK`_ to determine the similarity
+ of two statements based on the path similarity between each token of each statement.
+ This is essentially an evaluation of the closeness of synonyms.
+ """
+
+ def initialize_nltk_wordnet(self):
+ """
+ Download required NLTK corpora if they have not already been downloaded.
+ """
+ from .utils import nltk_download_corpus
+
+ nltk_download_corpus('corpora/wordnet')
+
+ def initialize_nltk_punkt(self):
+ """
+ Download required NLTK corpora if they have not already been downloaded.
+ """
+ from .utils import nltk_download_corpus
+
+ nltk_download_corpus('tokenizers/punkt')
+
+ def initialize_nltk_stopwords(self):
+ """
+ Download required NLTK corpora if they have not already been downloaded.
+ """
+ from .utils import nltk_download_corpus
+
+ nltk_download_corpus('corpora/stopwords')
+
+ def compare(self, statement, other_statement):
+ """
+ Compare the two input statements.
+
+ :return: The percent of similarity between the closest synset distance.
+ :rtype: float
+
+ .. _wordnet: http://www.nltk.org/howto/wordnet.html
+ .. _NLTK: http://www.nltk.org/
+ """
+ from nltk.corpus import wordnet
+ from nltk import word_tokenize
+ from chatter.chatterbot import utils
+ import itertools
+
+ tokens1 = word_tokenize(statement.text.lower())
+ tokens2 = word_tokenize(other_statement.text.lower())
+
+ # Remove all stop words from the list of word tokens
+ tokens1 = utils.remove_stopwords(tokens1, language='english')
+ tokens2 = utils.remove_stopwords(tokens2, language='english')
+
+ # The maximum possible similarity is an exact match
+ # Because path_similarity returns a value between 0 and 1,
+ # max_possible_similarity is the number of words in the longer
+ # of the two input statements.
+ max_possible_similarity = max(
+ len(statement.text.split()),
+ len(other_statement.text.split())
+ )
+
+ max_similarity = 0.0
+
+ # Get the highest matching value for each possible combination of words
+ for combination in itertools.product(*[tokens1, tokens2]):
+
+ synset1 = wordnet.synsets(combination[0])
+ synset2 = wordnet.synsets(combination[1])
+
+ if synset1 and synset2:
+
+ # Get the highest similarity for each combination of synsets
+ for synset in itertools.product(*[synset1, synset2]):
+ similarity = synset[0].path_similarity(synset[1])
+
+ if similarity and (similarity > max_similarity):
+ max_similarity = similarity
+
+ if max_possible_similarity == 0:
+ return 0
+
+ return max_similarity / max_possible_similarity
+
+
+class SentimentComparison(Comparator):
+ """
+ Calculate the similarity of two statements based on the closeness of
+ the sentiment value calculated for each statement.
+ """
+
+ def initialize_nltk_vader_lexicon(self):
+ """
+ Download the NLTK vader lexicon for sentiment analysis
+ that is required for this algorithm to run.
+ """
+ from .utils import nltk_download_corpus
+
+ nltk_download_corpus('sentiment/vader_lexicon')
+
+ def compare(self, statement, other_statement):
+ """
+ Return the similarity of two statements based on
+ their calculated sentiment values.
+
+ :return: The percent of similarity between the sentiment value.
+ :rtype: float
+ """
+ from nltk.sentiment.vader import SentimentIntensityAnalyzer
+
+ sentiment_analyzer = SentimentIntensityAnalyzer()
+ statement_polarity = sentiment_analyzer.polarity_scores(statement.text.lower())
+ statement2_polarity = sentiment_analyzer.polarity_scores(other_statement.text.lower())
+
+ statement_greatest_polarity = 'neu'
+ statement_greatest_score = -1
+ for polarity in sorted(statement_polarity):
+ if statement_polarity[polarity] > statement_greatest_score:
+ statement_greatest_polarity = polarity
+ statement_greatest_score = statement_polarity[polarity]
+
+ statement2_greatest_polarity = 'neu'
+ statement2_greatest_score = -1
+ for polarity in sorted(statement2_polarity):
+ if statement2_polarity[polarity] > statement2_greatest_score:
+ statement2_greatest_polarity = polarity
+ statement2_greatest_score = statement2_polarity[polarity]
+
+ # Check if the polarity if of a different type
+ if statement_greatest_polarity != statement2_greatest_polarity:
+ return 0
+
+ values = [statement_greatest_score, statement2_greatest_score]
+ difference = max(values) - min(values)
+
+ return 1.0 - difference
+
+
+class JaccardSimilarity(Comparator):
+ """
+ Calculates the similarity of two statements based on the Jaccard index.
+
+ The Jaccard index is composed of a numerator and denominator.
+ In the numerator, we count the number of items that are shared between the sets.
+ In the denominator, we count the total number of items across both sets.
+ Let's say we define sentences to be equivalent if 50% or more of their tokens are equivalent.
+ Here are two sample sentences:
+
+ The young cat is hungry.
+ The cat is very hungry.
+
+ When we parse these sentences to remove stopwords, we end up with the following two sets:
+
+ {young, cat, hungry}
+ {cat, very, hungry}
+
+ In our example above, our intersection is {cat, hungry}, which has count of two.
+ The union of the sets is {young, cat, very, hungry}, which has a count of four.
+ Therefore, our `Jaccard similarity index`_ is two divided by four, or 50%.
+ Given our similarity threshold above, we would consider this to be a match.
+
+ .. _`Jaccard similarity index`: https://en.wikipedia.org/wiki/Jaccard_index
+ """
+
+ SIMILARITY_THRESHOLD = 0.5
+
+ def initialize_nltk_wordnet(self):
+ """
+ Download the NLTK wordnet corpora that is required for this algorithm
+ to run only if the corpora has not already been downloaded.
+ """
+ from .utils import nltk_download_corpus
+
+ nltk_download_corpus('corpora/wordnet')
+
+ def compare(self, statement, other_statement):
+ """
+ Return the calculated similarity of two
+ statements based on the Jaccard index.
+ """
+ from nltk.corpus import wordnet
+ import nltk
+ import string
+
+ a = statement.text.lower()
+ b = other_statement.text.lower()
+
+ # Get default English stopwords and extend with punctuation
+ stopwords = nltk.corpus.stopwords.words('english')
+ stopwords.extend(string.punctuation)
+ stopwords.append('')
+ lemmatizer = nltk.stem.wordnet.WordNetLemmatizer()
+
+ def get_wordnet_pos(pos_tag):
+ if pos_tag[1].startswith('J'):
+ return (pos_tag[0], wordnet.ADJ)
+ elif pos_tag[1].startswith('V'):
+ return (pos_tag[0], wordnet.VERB)
+ elif pos_tag[1].startswith('N'):
+ return (pos_tag[0], wordnet.NOUN)
+ elif pos_tag[1].startswith('R'):
+ return (pos_tag[0], wordnet.ADV)
+ else:
+ return (pos_tag[0], wordnet.NOUN)
+
+ ratio = 0
+ pos_a = map(get_wordnet_pos, nltk.pos_tag(nltk.tokenize.word_tokenize(a)))
+ pos_b = map(get_wordnet_pos, nltk.pos_tag(nltk.tokenize.word_tokenize(b)))
+ lemma_a = [
+ lemmatizer.lemmatize(
+ token.strip(string.punctuation),
+ pos
+ ) for token, pos in pos_a if pos == wordnet.NOUN and token.strip(
+ string.punctuation
+ ) not in stopwords
+ ]
+ lemma_b = [
+ lemmatizer.lemmatize(
+ token.strip(string.punctuation),
+ pos
+ ) for token, pos in pos_b if pos == wordnet.NOUN and token.strip(
+ string.punctuation
+ ) not in stopwords
+ ]
+
+ # Calculate Jaccard similarity
+ try:
+ numerator = len(set(lemma_a).intersection(lemma_b))
+ denominator = float(len(set(lemma_a).union(lemma_b)))
+ ratio = numerator / denominator
+ except Exception as e:
+ print('Error', e)
+ return ratio >= self.SIMILARITY_THRESHOLD
+
+
+# ---------------------------------------- #
+
+
+levenshtein_distance = LevenshteinDistance()
+synset_distance = SynsetDistance()
+sentiment_comparison = SentimentComparison()
+jaccard_similarity = JaccardSimilarity()
diff --git a/chatter/chatterbot/constants.py b/chatter/chatterbot/constants.py
new file mode 100644
index 0000000..3a5ae7d
--- /dev/null
+++ b/chatter/chatterbot/constants.py
@@ -0,0 +1,15 @@
+"""
+ChatterBot constants
+"""
+
+'''
+The maximum length of characters that the text of a statement can contain.
+This should be enforced on a per-model basis by the data model for each
+storage adapter.
+'''
+STATEMENT_TEXT_MAX_LENGTH = 400
+
+# The maximum length of characters that the name of a tag can contain
+TAG_NAME_MAX_LENGTH = 50
+
+DEFAULT_DJANGO_APP_NAME = 'django_chatterbot'
diff --git a/chatter/chatterbot/conversation.py b/chatter/chatterbot/conversation.py
new file mode 100644
index 0000000..1926420
--- /dev/null
+++ b/chatter/chatterbot/conversation.py
@@ -0,0 +1,222 @@
+class StatementMixin(object):
+ """
+ This class has shared methods used to
+ normalize different statement models.
+ """
+ tags = []
+
+ def get_tags(self):
+ """
+ Return the list of tags for this statement.
+ """
+ return self.tags
+
+ def add_tags(self, tags):
+ """
+ Add a list of strings to the statement as tags.
+ """
+ for tag in tags:
+ self.tags.append(tag)
+
+
+class Statement(StatementMixin):
+ """
+ A statement represents a single spoken entity, sentence or
+ phrase that someone can say.
+ """
+
+ def __init__(self, text, **kwargs):
+
+ # Try not to allow non-string types to be passed to statements
+ try:
+ text = str(text)
+ except UnicodeEncodeError:
+ pass
+
+ self.text = text
+ self.tags = kwargs.pop('tags', [])
+ self.in_response_to = kwargs.pop('in_response_to', [])
+
+ self.extra_data = kwargs.pop('extra_data', {})
+
+ # This is the confidence with which the chat bot believes
+ # this is an accurate response. This value is set when the
+ # statement is returned by the chat bot.
+ self.confidence = 0
+
+ self.storage = None
+
+ def __str__(self):
+ return self.text
+
+ def __repr__(self):
+ return '' % (self.text)
+
+ def __hash__(self):
+ return hash(self.text)
+
+ def __eq__(self, other):
+ if not other:
+ return False
+
+ if isinstance(other, Statement):
+ return self.text == other.text
+
+ return self.text == other
+
+ def save(self):
+ """
+ Save the statement in the database.
+ """
+ self.storage.update(self)
+
+ def add_extra_data(self, key, value):
+ """
+ This method allows additional data to be stored on the statement object.
+
+ Typically this data is something that pertains just to this statement.
+ For example, a value stored here might be the tagged parts of speech for
+ each word in the statement text.
+
+ - key = 'pos_tags'
+ - value = [('Now', 'RB'), ('for', 'IN'), ('something', 'NN'), ('different', 'JJ')]
+
+ :param key: The key to use in the dictionary of extra data.
+ :type key: str
+
+ :param value: The value to set for the specified key.
+ """
+ self.extra_data[key] = value
+
+ def add_response(self, response):
+ """
+ Add the response to the list of statements that this statement is in response to.
+ If the response is already in the list, increment the occurrence count of that response.
+
+ :param response: The response to add.
+ :type response: `Response`
+ """
+ if not isinstance(response, Response):
+ raise Statement.InvalidTypeException(
+ 'A {} was received when a {} instance was expected'.format(
+ type(response),
+ type(Response(''))
+ )
+ )
+
+ updated = False
+ for index in range(0, len(self.in_response_to)):
+ if response.text == self.in_response_to[index].text:
+ self.in_response_to[index].occurrence += 1
+ updated = True
+
+ if not updated:
+ self.in_response_to.append(response)
+
+ def remove_response(self, response_text):
+ """
+ Removes a response from the statement's response list based
+ on the value of the response text.
+
+ :param response_text: The text of the response to be removed.
+ :type response_text: str
+ """
+ for response in self.in_response_to:
+ if response_text == response.text:
+ self.in_response_to.remove(response)
+ return True
+ return False
+
+ def get_response_count(self, statement):
+ """
+ Find the number of times that the statement has been used
+ as a response to the current statement.
+
+ :param statement: The statement object to get the count for.
+ :type statement: `Statement`
+
+ :returns: Return the number of times the statement has been used as a response.
+ :rtype: int
+ """
+ for response in self.in_response_to:
+ if statement.text == response.text:
+ return response.occurrence
+
+ return 0
+
+ def serialize(self):
+ """
+ :returns: A dictionary representation of the statement object.
+ :rtype: dict
+ """
+ data = {}
+
+ data['text'] = self.text
+ data['in_response_to'] = []
+ data['extra_data'] = self.extra_data
+
+ for response in self.in_response_to:
+ data['in_response_to'].append(response.serialize())
+
+ return data
+
+ @property
+ def response_statement_cache(self):
+ """
+ This property is to allow ChatterBot Statement objects to
+ be swappable with Django Statement models.
+ """
+ return self.in_response_to
+
+ class InvalidTypeException(Exception):
+
+ def __init__(self, value='Received an unexpected value type.'):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+
+class Response(object):
+ """
+ A response represents an entity which response to a statement.
+ """
+
+ def __init__(self, text, **kwargs):
+ from datetime import datetime
+ from dateutil import parser as date_parser
+
+ self.text = text
+ self.created_at = kwargs.get('created_at', datetime.now())
+ self.occurrence = kwargs.get('occurrence', 1)
+
+ if not isinstance(self.created_at, datetime):
+ self.created_at = date_parser.parse(self.created_at)
+
+ def __str__(self):
+ return self.text
+
+ def __repr__(self):
+ return '' % (self.text)
+
+ def __hash__(self):
+ return hash(self.text)
+
+ def __eq__(self, other):
+ if not other:
+ return False
+
+ if isinstance(other, Response):
+ return self.text == other.text
+
+ return self.text == other
+
+ def serialize(self):
+ data = {}
+
+ data['text'] = self.text
+ data['created_at'] = self.created_at.isoformat()
+
+ data['occurrence'] = self.occurrence
+
+ return data
diff --git a/chatter/chatterbot/corpus.py b/chatter/chatterbot/corpus.py
new file mode 100644
index 0000000..4bf0e4b
--- /dev/null
+++ b/chatter/chatterbot/corpus.py
@@ -0,0 +1,10 @@
+"""
+Seamlessly import the external chatterbot corpus module.
+View the corpus on GitHub at https://github.com/gunthercox/chatterbot-corpus
+"""
+
+from chatterbot_corpus import Corpus
+
+__all__ = (
+ 'Corpus',
+)
diff --git a/chatter/chatterbot/ext/__init__.py b/chatter/chatterbot/ext/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/chatter/chatterbot/ext/sqlalchemy_app/__init__.py b/chatter/chatterbot/ext/sqlalchemy_app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/chatter/chatterbot/ext/sqlalchemy_app/models.py b/chatter/chatterbot/ext/sqlalchemy_app/models.py
new file mode 100644
index 0000000..6a7dc00
--- /dev/null
+++ b/chatter/chatterbot/ext/sqlalchemy_app/models.py
@@ -0,0 +1,131 @@
+from sqlalchemy import Table, Column, Integer, DateTime, ForeignKey, PickleType
+from sqlalchemy.ext.declarative import declared_attr, declarative_base
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+
+from chatter.chatterbot.constants import TAG_NAME_MAX_LENGTH, STATEMENT_TEXT_MAX_LENGTH
+from chatter.chatterbot.conversation import StatementMixin
+from chatter.chatterbot.ext.sqlalchemy_app.types import UnicodeString
+
+
+class ModelBase(object):
+ """
+ An augmented base class for SqlAlchemy models.
+ """
+
+ @declared_attr
+ def __tablename__(cls):
+ """
+ Return the lowercase class name as the name of the table.
+ """
+ return cls.__name__.lower()
+
+ id = Column(
+ Integer,
+ primary_key=True,
+ autoincrement=True
+ )
+
+
+Base = declarative_base(cls=ModelBase)
+
+tag_association_table = Table(
+ 'tag_association',
+ Base.metadata,
+ Column('tag_id', Integer, ForeignKey('tag.id')),
+ Column('statement_id', Integer, ForeignKey('statement.id'))
+)
+
+
+class Tag(Base):
+ """
+ A tag that describes a statement.
+ """
+
+ name = Column(UnicodeString(TAG_NAME_MAX_LENGTH))
+
+
+class Statement(Base, StatementMixin):
+ """
+ A Statement represents a sentence or phrase.
+ """
+
+ text = Column(UnicodeString(STATEMENT_TEXT_MAX_LENGTH), unique=True)
+
+ tags = relationship(
+ 'Tag',
+ secondary=lambda: tag_association_table,
+ backref='statements'
+ )
+
+ extra_data = Column(PickleType)
+
+ in_response_to = relationship(
+ 'Response',
+ back_populates='statement_table'
+ )
+
+ def get_tags(self):
+ """
+ Return a list of tags for this statement.
+ """
+ return [tag.name for tag in self.tags]
+
+ def get_statement(self):
+ from chatter.chatterbot.conversation import Statement as StatementObject
+ from chatter.chatterbot.conversation import Response as ResponseObject
+
+ statement = StatementObject(
+ self.text,
+ tags=[tag.name for tag in self.tags],
+ extra_data=self.extra_data
+ )
+ for response in self.in_response_to:
+ statement.add_response(
+ ResponseObject(text=response.text, occurrence=response.occurrence)
+ )
+ return statement
+
+
+class Response(Base):
+ """
+ Response, contains responses related to a given statement.
+ """
+
+ text = Column(UnicodeString(STATEMENT_TEXT_MAX_LENGTH))
+
+ created_at = Column(
+ DateTime(timezone=True),
+ server_default=func.now()
+ )
+
+ occurrence = Column(Integer, default=1)
+
+ statement_text = Column(UnicodeString(STATEMENT_TEXT_MAX_LENGTH), ForeignKey('statement.text'))
+
+ statement_table = relationship(
+ 'Statement',
+ back_populates='in_response_to',
+ cascade='all',
+ uselist=False
+ )
+
+
+conversation_association_table = Table(
+ 'conversation_association',
+ Base.metadata,
+ Column('conversation_id', Integer, ForeignKey('conversation.id')),
+ Column('statement_id', Integer, ForeignKey('statement.id'))
+)
+
+
+class Conversation(Base):
+ """
+ A conversation.
+ """
+
+ statements = relationship(
+ 'Statement',
+ secondary=lambda: conversation_association_table,
+ backref='conversations'
+ )
diff --git a/chatter/chatterbot/ext/sqlalchemy_app/types.py b/chatter/chatterbot/ext/sqlalchemy_app/types.py
new file mode 100644
index 0000000..ee9b123
--- /dev/null
+++ b/chatter/chatterbot/ext/sqlalchemy_app/types.py
@@ -0,0 +1,16 @@
+from sqlalchemy.types import TypeDecorator, Unicode
+
+
+class UnicodeString(TypeDecorator):
+ """
+ Type for unicode strings.
+ """
+
+ impl = Unicode
+
+ def process_bind_param(self, value, dialect):
+ """
+ Coerce Python bytestrings to unicode before
+ saving them to the database.
+ """
+ return value
diff --git a/chatter/chatterbot/filters.py b/chatter/chatterbot/filters.py
new file mode 100644
index 0000000..9a07a09
--- /dev/null
+++ b/chatter/chatterbot/filters.py
@@ -0,0 +1,47 @@
+"""
+Filters set the base query that gets passed to the storage adapter.
+"""
+
+
+class Filter(object):
+ """
+ A base filter object from which all other
+ filters should be subclassed.
+ """
+
+ def filter_selection(self, chatterbot, conversation_id):
+ """
+ Because this is the base filter class, this method just
+ returns the storage adapter's base query. Other filters
+ are expected to override this method.
+ """
+ return chatterbot.storage.base_query
+
+
+class RepetitiveResponseFilter(Filter):
+ """
+ A filter that eliminates possibly repetitive responses to prevent
+ a chat bot from repeating statements that it has recently said.
+ """
+
+ def filter_selection(self, chatterbot, conversation_id):
+
+ text_of_recent_responses = []
+
+ # TODO: Add a larger quantity of response history
+ latest_response = chatterbot.storage.get_latest_response(conversation_id)
+ if latest_response:
+ text_of_recent_responses.append(latest_response.text)
+
+ # Return the query with no changes if there are no statements to exclude
+ if not text_of_recent_responses:
+ return super(RepetitiveResponseFilter, self).filter_selection(
+ chatterbot,
+ conversation_id
+ )
+
+ query = chatterbot.storage.base_query.statement_text_not_in(
+ text_of_recent_responses
+ )
+
+ return query
diff --git a/chatter/chatterbot/input/__init__.py b/chatter/chatterbot/input/__init__.py
new file mode 100644
index 0000000..53c53f9
--- /dev/null
+++ b/chatter/chatterbot/input/__init__.py
@@ -0,0 +1,17 @@
+from .input_adapter import InputAdapter
+from .gitter import Gitter
+from .hipchat import HipChat
+from .mailgun import Mailgun
+from .microsoft import Microsoft
+from .terminal import TerminalAdapter
+from .variable_input_type_adapter import VariableInputTypeAdapter
+
+__all__ = (
+ 'InputAdapter',
+ 'Microsoft',
+ 'Gitter',
+ 'HipChat',
+ 'Mailgun',
+ 'TerminalAdapter',
+ 'VariableInputTypeAdapter',
+)
diff --git a/chatter/chatterbot/input/gitter.py b/chatter/chatterbot/input/gitter.py
new file mode 100644
index 0000000..9018e37
--- /dev/null
+++ b/chatter/chatterbot/input/gitter.py
@@ -0,0 +1,178 @@
+from __future__ import unicode_literals
+
+from time import sleep
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.input import InputAdapter
+
+
+class Gitter(InputAdapter):
+ """
+ An input adapter that allows a ChatterBot instance to get
+ input statements from a Gitter room.
+ """
+
+ def __init__(self, **kwargs):
+ super(Gitter, self).__init__(**kwargs)
+
+ self.gitter_host = kwargs.get('gitter_host', 'https://api.gitter.im/v1/')
+ self.gitter_room = kwargs.get('gitter_room')
+ self.gitter_api_token = kwargs.get('gitter_api_token')
+ self.only_respond_to_mentions = kwargs.get('gitter_only_respond_to_mentions', True)
+ self.sleep_time = kwargs.get('gitter_sleep_time', 4)
+
+ authorization_header = 'Bearer {}'.format(self.gitter_api_token)
+
+ self.headers = {
+ 'Authorization': authorization_header,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ }
+
+ # Join the Gitter room
+ room_data = self.join_room(self.gitter_room)
+ self.room_id = room_data.get('id')
+
+ user_data = self.get_user_data()
+ self.user_id = user_data[0].get('id')
+ self.username = user_data[0].get('username')
+
+ def _validate_status_code(self, response):
+ code = response.status_code
+ if code not in [200, 201]:
+ raise self.HTTPStatusException('{} status code recieved'.format(code))
+
+ def join_room(self, room_name):
+ """
+ Join the specified Gitter room.
+ """
+ import requests
+
+ endpoint = '{}rooms'.format(self.gitter_host)
+ response = requests.post(
+ endpoint,
+ headers=self.headers,
+ json={'uri': room_name}
+ )
+ self.logger.info('{} joining room {}'.format(
+ response.status_code, endpoint
+ ))
+ self._validate_status_code(response)
+ return response.json()
+
+ def get_user_data(self):
+ import requests
+
+ endpoint = '{}user'.format(self.gitter_host)
+ response = requests.get(
+ endpoint,
+ headers=self.headers
+ )
+ self.logger.info('{} retrieving user data {}'.format(
+ response.status_code, endpoint
+ ))
+ self._validate_status_code(response)
+ return response.json()
+
+ def mark_messages_as_read(self, message_ids):
+ """
+ Mark the specified message ids as read.
+ """
+ import requests
+
+ endpoint = '{}user/{}/rooms/{}/unreadItems'.format(
+ self.gitter_host, self.user_id, self.room_id
+ )
+ response = requests.post(
+ endpoint,
+ headers=self.headers,
+ json={'chat': message_ids}
+ )
+ self.logger.info('{} marking messages as read {}'.format(
+ response.status_code, endpoint
+ ))
+ self._validate_status_code(response)
+ return response.json()
+
+ def get_most_recent_message(self):
+ """
+ Get the most recent message from the Gitter room.
+ """
+ import requests
+
+ endpoint = '{}rooms/{}/chatMessages?limit=1'.format(self.gitter_host, self.room_id)
+ response = requests.get(
+ endpoint,
+ headers=self.headers
+ )
+ self.logger.info('{} getting most recent message'.format(
+ response.status_code
+ ))
+ self._validate_status_code(response)
+ data = response.json()
+ if data:
+ return data[0]
+ return None
+
+ def _contains_mention(self, mentions):
+ for mention in mentions:
+ if self.username == mention.get('screenName'):
+ return True
+ return False
+
+ def should_respond(self, data):
+ """
+ Takes the API response data from a single message.
+ Returns true if the chat bot should respond.
+ """
+ if data:
+ unread = data.get('unread', False)
+
+ if self.only_respond_to_mentions:
+ if unread and self._contains_mention(data['mentions']):
+ return True
+ else:
+ return False
+ elif unread:
+ return True
+
+ return False
+
+ def remove_mentions(self, text):
+ """
+ Return a string that has no leading mentions.
+ """
+ import re
+ text_without_mentions = re.sub(r'@\S+', '', text)
+
+ # Remove consecutive spaces
+ text_without_mentions = re.sub(' +', ' ', text_without_mentions.strip())
+
+ return text_without_mentions
+
+ def process_input(self, statement):
+ new_message = False
+
+ while not new_message:
+ data = self.get_most_recent_message()
+ if self.should_respond(data):
+ self.mark_messages_as_read([data['id']])
+ new_message = True
+ sleep(self.sleep_time)
+
+ text = self.remove_mentions(data['text'])
+ statement = Statement(text)
+
+ return statement
+
+ class HTTPStatusException(Exception):
+ """
+ Exception raised when unexpected non-success HTTP
+ status codes are returned in a response.
+ """
+
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
diff --git a/chatter/chatterbot/input/hipchat.py b/chatter/chatterbot/input/hipchat.py
new file mode 100644
index 0000000..b5da731
--- /dev/null
+++ b/chatter/chatterbot/input/hipchat.py
@@ -0,0 +1,115 @@
+from __future__ import unicode_literals
+
+from time import sleep
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.input import InputAdapter
+
+
+class HipChat(InputAdapter):
+ """
+ An input adapter that allows a ChatterBot instance to get
+ input statements from a HipChat room.
+ """
+
+ def __init__(self, **kwargs):
+ super(HipChat, self).__init__(**kwargs)
+
+ self.hipchat_host = kwargs.get('hipchat_host')
+ self.hipchat_access_token = kwargs.get('hipchat_access_token')
+ self.hipchat_room = kwargs.get('hipchat_room')
+ self.session_id = str(self.chatbot.default_session.uuid)
+
+ import requests
+ self.session = requests.Session()
+ self.session.verify = kwargs.get('ssl_verify', True)
+
+ authorization_header = 'Bearer {}'.format(self.hipchat_access_token)
+
+ self.headers = {
+ 'Authorization': authorization_header,
+ 'Content-Type': 'application/json'
+ }
+
+ # This is a list of the messages that have been responded to
+ self.recent_message_ids = self.get_initial_ids()
+
+ def get_initial_ids(self):
+ """
+ Returns a list of the most recent message ids.
+ """
+ data = self.view_recent_room_history(
+ self.hipchat_room,
+ max_results=75
+ )
+
+ results = set()
+
+ for item in data['items']:
+ results.add(item['id'])
+
+ return results
+
+ def view_recent_room_history(self, room_id_or_name, max_results=1):
+ """
+ https://www.hipchat.com/docs/apiv2/method/view_recent_room_history
+ """
+
+ recent_histroy_url = '{}/v2/room/{}/history?max-results={}'.format(
+ self.hipchat_host,
+ room_id_or_name,
+ max_results
+ )
+
+ response = self.session.get(
+ recent_histroy_url,
+ headers=self.headers
+ )
+
+ return response.json()
+
+ def get_most_recent_message(self, room_id_or_name):
+ """
+ Return the most recent message from the HipChat room.
+ """
+ data = self.view_recent_room_history(room_id_or_name)
+
+ items = data['items']
+
+ if not items:
+ return None
+ return items[-1]
+
+ def process_input(self, statement):
+ """
+ Process input from the HipChat room.
+ """
+ new_message = False
+
+ response_statement = self.chatbot.storage.get_latest_response(
+ self.session_id
+ )
+
+ if response_statement:
+ last_message_id = response_statement.extra_data.get(
+ 'hipchat_message_id', None
+ )
+ if last_message_id:
+ self.recent_message_ids.add(last_message_id)
+
+ while not new_message:
+ data = self.get_most_recent_message(self.hipchat_room)
+
+ if data and data['id'] not in self.recent_message_ids:
+ self.recent_message_ids.add(data['id'])
+ new_message = True
+ else:
+ pass
+ sleep(3.5)
+
+ text = data['message']
+
+ statement = Statement(text)
+ statement.add_extra_data('hipchat_message_id', data['id'])
+
+ return statement
diff --git a/chatter/chatterbot/input/input_adapter.py b/chatter/chatterbot/input/input_adapter.py
new file mode 100644
index 0000000..49c63db
--- /dev/null
+++ b/chatter/chatterbot/input/input_adapter.py
@@ -0,0 +1,34 @@
+from __future__ import unicode_literals
+
+from chatter.chatterbot.adapters import Adapter
+
+
+class InputAdapter(Adapter):
+ """
+ This is an abstract class that represents the
+ interface that all input adapters should implement.
+ """
+
+ def process_input(self, *args, **kwargs):
+ """
+ Returns a statement object based on the input source.
+ """
+ raise self.AdapterMethodNotImplementedError()
+
+ def process_input_statement(self, *args, **kwargs):
+ """
+ Return an existing statement object (if one exists).
+ """
+ input_statement = self.process_input(*args, **kwargs)
+
+ self.logger.info('Received input statement: {}'.format(input_statement.text))
+
+ existing_statement = self.chatbot.storage.find(input_statement.text)
+
+ if existing_statement:
+ self.logger.info('"{}" is a known statement'.format(input_statement.text))
+ input_statement = existing_statement
+ else:
+ self.logger.info('"{}" is not a known statement'.format(input_statement.text))
+
+ return input_statement
diff --git a/chatter/chatterbot/input/mailgun.py b/chatter/chatterbot/input/mailgun.py
new file mode 100644
index 0000000..6fb78a8
--- /dev/null
+++ b/chatter/chatterbot/input/mailgun.py
@@ -0,0 +1,63 @@
+from __future__ import unicode_literals
+
+import datetime
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.input import InputAdapter
+
+
+class Mailgun(InputAdapter):
+ """
+ Get input from Mailgun.
+ """
+
+ def __init__(self, **kwargs):
+ super(Mailgun, self).__init__(**kwargs)
+
+ # Use the bot's name for the name of the sender
+ self.name = kwargs.get('name')
+ self.from_address = kwargs.get('mailgun_from_address')
+ self.api_key = kwargs.get('mailgun_api_key')
+ self.endpoint = kwargs.get('mailgun_api_endpoint')
+
+ def get_email_stored_events(self):
+ import requests
+
+ yesterday = datetime.datetime.now() - datetime.timedelta(1)
+ return requests.get(
+ '{}/events'.format(self.endpoint),
+ auth=('api', self.api_key),
+ params={
+ 'begin': yesterday.isoformat(),
+ 'ascending': 'yes',
+ 'limit': 1
+ }
+ )
+
+ def get_stored_email_urls(self):
+ response = self.get_email_stored_events()
+ data = response.json()
+
+ for item in data.get('items', []):
+ if 'storage' in item:
+ if 'url' in item['storage']:
+ yield item['storage']['url']
+
+ def get_message(self, url):
+ import requests
+
+ return requests.get(
+ url,
+ auth=('api', self.api_key)
+ )
+
+ def process_input(self, statement):
+ urls = self.get_stored_email_urls()
+ url = list(urls)[0]
+
+ response = self.get_message(url)
+ message = response.json()
+
+ text = message.get('stripped-text')
+
+ return Statement(text)
diff --git a/chatter/chatterbot/input/microsoft.py b/chatter/chatterbot/input/microsoft.py
new file mode 100644
index 0000000..3a255bf
--- /dev/null
+++ b/chatter/chatterbot/input/microsoft.py
@@ -0,0 +1,117 @@
+from __future__ import unicode_literals
+
+from time import sleep
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.input import InputAdapter
+
+
+class Microsoft(InputAdapter):
+ """
+ An input adapter that allows a ChatterBot instance to get
+ input statements from a Microsoft Bot using *Directline client protocol*.
+ https://docs.botframework.com/en-us/restapi/directline/#navtitle
+ """
+
+ def __init__(self, **kwargs):
+ super(Microsoft, self).__init__(**kwargs)
+ import requests
+ from requests.packages.urllib3.exceptions import InsecureRequestWarning
+ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+
+ self.directline_host = kwargs.get('directline_host', 'https://directline.botframework.com')
+
+ # NOTE: Direct Line client credentials are different from your bot's
+ # credentials
+ self.direct_line_token_or_secret = kwargs. \
+ get('direct_line_token_or_secret')
+
+ authorization_header = 'BotConnector {}'. \
+ format(self.direct_line_token_or_secret)
+
+ self.headers = {
+ 'Authorization': authorization_header,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'charset': 'utf-8'
+ }
+
+ conversation_data = self.start_conversation()
+ self.conversation_id = conversation_data.get('conversationId')
+ self.conversation_token = conversation_data.get('token')
+
+ def _validate_status_code(self, response):
+ code = response.status_code
+ if not code == 200:
+ raise self.HTTPStatusException('{} status code recieved'.
+ format(code))
+
+ def start_conversation(self):
+ import requests
+
+ endpoint = '{host}/api/conversations'.format(host=self.directline_host)
+ response = requests.post(
+ endpoint,
+ headers=self.headers,
+ verify=False
+ )
+ self.logger.info('{} starting conversation {}'.format(
+ response.status_code, endpoint
+ ))
+ self._validate_status_code(response)
+ return response.json()
+
+ def get_most_recent_message(self):
+ import requests
+
+ endpoint = '{host}/api/conversations/{id}/messages' \
+ .format(host=self.directline_host,
+ id=self.conversation_id)
+
+ response = requests.get(
+ endpoint,
+ headers=self.headers,
+ verify=False
+ )
+
+ self.logger.info('{} retrieving most recent messages {}'.format(
+ response.status_code, endpoint
+ ))
+
+ self._validate_status_code(response)
+
+ data = response.json()
+
+ if data['messages']:
+ last_msg = int(data['watermark'])
+ return data['messages'][last_msg - 1]
+ return None
+
+ def process_input(self, statement):
+ new_message = False
+ data = None
+ while not new_message:
+ data = self.get_most_recent_message()
+ if data and data['id']:
+ new_message = True
+ else:
+ pass
+ sleep(3.5)
+
+ text = data['text']
+ statement = Statement(text)
+ self.logger.info('processing user statement {}'.format(statement))
+
+ return statement
+
+ class HTTPStatusException(Exception):
+ """
+ Exception raised when unexpected non-success HTTP
+ status codes are returned in a response.
+ """
+
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
diff --git a/chatter/chatterbot/input/terminal.py b/chatter/chatterbot/input/terminal.py
new file mode 100644
index 0000000..20cb3c2
--- /dev/null
+++ b/chatter/chatterbot/input/terminal.py
@@ -0,0 +1,19 @@
+from __future__ import unicode_literals
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.input import InputAdapter
+from chatter.chatterbot.utils import input_function
+
+
+class TerminalAdapter(InputAdapter):
+ """
+ A simple adapter that allows ChatterBot to
+ communicate through the terminal.
+ """
+
+ def process_input(self, *args, **kwargs):
+ """
+ Read the user's input from the terminal.
+ """
+ user_input = input_function()
+ return Statement(user_input)
diff --git a/chatter/chatterbot/input/variable_input_type_adapter.py b/chatter/chatterbot/input/variable_input_type_adapter.py
new file mode 100644
index 0000000..d2d598c
--- /dev/null
+++ b/chatter/chatterbot/input/variable_input_type_adapter.py
@@ -0,0 +1,61 @@
+from __future__ import unicode_literals
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.input import InputAdapter
+
+
+class VariableInputTypeAdapter(InputAdapter):
+ JSON = 'json'
+ TEXT = 'text'
+ OBJECT = 'object'
+ VALID_FORMATS = (JSON, TEXT, OBJECT,)
+
+ def detect_type(self, statement):
+
+ string_types = str
+
+ if hasattr(statement, 'text'):
+ return self.OBJECT
+ if isinstance(statement, string_types):
+ return self.TEXT
+ if isinstance(statement, dict):
+ return self.JSON
+
+ input_type = type(statement)
+
+ raise self.UnrecognizedInputFormatException(
+ 'The type {} is not recognized as a valid input type.'.format(
+ input_type
+ )
+ )
+
+ def process_input(self, statement):
+ input_type = self.detect_type(statement)
+
+ # Return the statement object without modification
+ if input_type == self.OBJECT:
+ return statement
+
+ # Convert the input string into a statement object
+ if input_type == self.TEXT:
+ return Statement(statement)
+
+ # Convert input dictionary into a statement object
+ if input_type == self.JSON:
+ input_json = dict(statement)
+ text = input_json['text']
+ del input_json['text']
+
+ return Statement(text, **input_json)
+
+ class UnrecognizedInputFormatException(Exception):
+ """
+ Exception raised when an input format is specified that is
+ not in the VariableInputTypeAdapter.VALID_FORMATS variable.
+ """
+
+ def __init__(self, value='The input format was not recognized.'):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
diff --git a/chatter/chatterbot/logic/__init__.py b/chatter/chatterbot/logic/__init__.py
new file mode 100644
index 0000000..1930556
--- /dev/null
+++ b/chatter/chatterbot/logic/__init__.py
@@ -0,0 +1,19 @@
+from .best_match import BestMatch
+from .logic_adapter import LogicAdapter
+from .low_confidence import LowConfidenceAdapter
+from .mathematical_evaluation import MathematicalEvaluation
+from .multi_adapter import MultiLogicAdapter
+from .no_knowledge_adapter import NoKnowledgeAdapter
+from .specific_response import SpecificResponseAdapter
+from .time_adapter import TimeLogicAdapter
+
+__all__ = (
+ 'LogicAdapter',
+ 'BestMatch',
+ 'LowConfidenceAdapter',
+ 'MathematicalEvaluation',
+ 'MultiLogicAdapter',
+ 'NoKnowledgeAdapter',
+ 'SpecificResponseAdapter',
+ 'TimeLogicAdapter',
+)
diff --git a/chatter/chatterbot/logic/best_match.py b/chatter/chatterbot/logic/best_match.py
new file mode 100644
index 0000000..5c48121
--- /dev/null
+++ b/chatter/chatterbot/logic/best_match.py
@@ -0,0 +1,85 @@
+from __future__ import unicode_literals
+
+from .logic_adapter import LogicAdapter
+
+
+class BestMatch(LogicAdapter):
+ """
+ A logic adapter that returns a response based on known responses to
+ the closest matches to the input statement.
+ """
+
+ def get(self, input_statement):
+ """
+ Takes a statement string and a list of statement strings.
+ Returns the closest matching statement from the list.
+ """
+ statement_list = self.chatbot.storage.get_response_statements()
+
+ if not statement_list:
+ if self.chatbot.storage.count():
+ # Use a randomly picked statement
+ self.logger.info(
+ 'No statements have known responses. ' +
+ 'Choosing a random response to return.'
+ )
+ random_response = self.chatbot.storage.get_random()
+ random_response.confidence = 0
+ return random_response
+ else:
+ raise self.EmptyDatasetException()
+
+ closest_match = input_statement
+ closest_match.confidence = 0
+
+ # Find the closest matching known statement
+ for statement in statement_list:
+ confidence = self.compare_statements(input_statement, statement)
+
+ if confidence > closest_match.confidence:
+ statement.confidence = confidence
+ closest_match = statement
+
+ return closest_match
+
+ def can_process(self, statement):
+ """
+ Check that the chatbot's storage adapter is available to the logic
+ adapter and there is at least one statement in the database.
+ """
+ return self.chatbot.storage.count()
+
+ def process(self, input_statement):
+
+ # Select the closest match to the input statement
+ closest_match = self.get(input_statement)
+ self.logger.info('Using "{}" as a close match to "{}"'.format(
+ input_statement.text, closest_match.text
+ ))
+
+ # Get all statements that are in response to the closest match
+ response_list = self.chatbot.storage.filter(
+ in_response_to__contains=closest_match.text
+ )
+
+ if response_list:
+ self.logger.info(
+ 'Selecting response from {} optimal responses.'.format(
+ len(response_list)
+ )
+ )
+ response = self.select_response(input_statement, response_list)
+ response.confidence = closest_match.confidence
+ self.logger.info('Response selected. Using "{}"'.format(response.text))
+ else:
+ response = self.chatbot.storage.get_random()
+ self.logger.info(
+ 'No response to "{}" found. Selecting a random response.'.format(
+ closest_match.text
+ )
+ )
+
+ # Set confidence to zero because a random response is selected
+ response.confidence = 0
+
+ return response
diff --git a/chatter/chatterbot/logic/logic_adapter.py b/chatter/chatterbot/logic/logic_adapter.py
new file mode 100644
index 0000000..1239cca
--- /dev/null
+++ b/chatter/chatterbot/logic/logic_adapter.py
@@ -0,0 +1,101 @@
+from __future__ import unicode_literals
+
+from chatter.chatterbot.adapters import Adapter
+from chatter.chatterbot.utils import import_module
+
+
+class LogicAdapter(Adapter):
+ """
+ This is an abstract class that represents the interface
+ that all logic adapters should implement.
+
+ :param statement_comparison_function: The dot-notated import path to a statement comparison function.
+ Defaults to ``levenshtein_distance``.
+
+ :param response_selection_method: The a response selection method.
+ Defaults to ``get_first_response``.
+ """
+
+ def __init__(self, **kwargs):
+ super(LogicAdapter, self).__init__(**kwargs)
+ from chatter.chatterbot.comparisons import levenshtein_distance
+ from chatter.chatterbot.response_selection import get_first_response
+
+ # Import string module parameters
+ if 'statement_comparison_function' in kwargs:
+ import_path = kwargs.get('statement_comparison_function')
+ if isinstance(import_path, str):
+ kwargs['statement_comparison_function'] = import_module(import_path)
+
+ if 'response_selection_method' in kwargs:
+ import_path = kwargs.get('response_selection_method')
+ if isinstance(import_path, str):
+ kwargs['response_selection_method'] = import_module(import_path)
+
+ # By default, compare statements using Levenshtein distance
+ self.compare_statements = kwargs.get(
+ 'statement_comparison_function',
+ levenshtein_distance
+ )
+
+ # By default, select the first available response
+ self.select_response = kwargs.get(
+ 'response_selection_method',
+ get_first_response
+ )
+
+ def get_initialization_functions(self):
+ """
+ Return a dictionary of functions to be run once when the chat bot is instantiated.
+ """
+ return self.compare_statements.get_initialization_functions()
+
+ def initialize(self):
+ for function in self.get_initialization_functions().values():
+ function()
+
+ def can_process(self, statement):
+ """
+ A preliminary check that is called to determine if a
+ logic adapter can process a given statement. By default,
+ this method returns true but it can be overridden in
+ child classes as needed.
+
+ :rtype: bool
+ """
+ return True
+
+ def process(self, statement):
+ """
+ Override this method and implement your logic for selecting a response to an input statement.
+
+ A confidence value and the selected response statement should be returned.
+ The confidence value represents a rating of how accurate the logic adapter
+ expects the selected response to be. Confidence scores are used to select
+ the best response from multiple logic adapters.
+
+ The confidence value should be a number between 0 and 1 where 0 is the
+ lowest confidence level and 1 is the highest.
+
+ :param statement: An input statement to be processed by the logic adapter.
+ :type statement: Statement
+
+ :rtype: Statement
+ """
+ raise self.AdapterMethodNotImplementedError()
+
+ @property
+ def class_name(self):
+ """
+ Return the name of the current logic adapter class.
+ This is typically used for logging and debugging.
+ """
+ return str(self.__class__.__name__)
+
+ class EmptyDatasetException(Exception):
+
+ def __init__(self, value='An empty set was received when at least one statement was expected.'):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
diff --git a/chatter/chatterbot/logic/low_confidence.py b/chatter/chatterbot/logic/low_confidence.py
new file mode 100644
index 0000000..585cf20
--- /dev/null
+++ b/chatter/chatterbot/logic/low_confidence.py
@@ -0,0 +1,59 @@
+from __future__ import unicode_literals
+
+from chatter.chatterbot.conversation import Statement
+from .best_match import BestMatch
+
+
+class LowConfidenceAdapter(BestMatch):
+ """
+ Returns a default response with a high confidence
+ when a high confidence response is not known.
+
+ :kwargs:
+ * *threshold* (``float``) --
+ The low confidence value that triggers this adapter.
+ Defaults to 0.65.
+ * *default_response* (``str``) or (``iterable``)--
+ The response returned by this logic adaper.
+ * *response_selection_method* (``str``) or (``callable``)
+ The a response selection method.
+ Defaults to ``get_first_response``.
+ """
+
+ def __init__(self, **kwargs):
+ super(LowConfidenceAdapter, self).__init__(**kwargs)
+
+ self.confidence_threshold = kwargs.get('threshold', 0.65)
+
+ default_responses = kwargs.get(
+ 'default_response', "I'm sorry, I do not understand."
+ )
+
+ # Convert a single string into a list
+ if isinstance(default_responses, str):
+ default_responses = [
+ default_responses
+ ]
+
+ self.default_responses = [
+ Statement(text=default) for default in default_responses
+ ]
+
+ def process(self, input_statement):
+ """
+ Return a default response with a high confidence if
+ a high confidence response is not known.
+ """
+ # Select the closest match to the input statement
+ closest_match = self.get(input_statement)
+
+ # Choose a response from the list of options
+ response = self.select_response(input_statement, self.default_responses)
+
+ # Confidence should be high only if it is less than the threshold
+ if closest_match.confidence < self.confidence_threshold:
+ response.confidence = 1
+ else:
+ response.confidence = 0
+
+ return response
diff --git a/chatter/chatterbot/logic/mathematical_evaluation.py b/chatter/chatterbot/logic/mathematical_evaluation.py
new file mode 100644
index 0000000..af27548
--- /dev/null
+++ b/chatter/chatterbot/logic/mathematical_evaluation.py
@@ -0,0 +1,68 @@
+from __future__ import unicode_literals
+
+from chatter.chatterbot.conversation import Statement
+from chatter.chatterbot.logic import LogicAdapter
+
+
+class MathematicalEvaluation(LogicAdapter):
+ """
+ The MathematicalEvaluation logic adapter parses input to determine
+ whether the user is asking a question that requires math to be done.
+ If so, the equation is extracted from the input and returned with
+ the evaluated result.
+
+ For example:
+ User: 'What is three plus five?'
+ Bot: 'Three plus five equals eight'
+
+ :kwargs:
+ * *language* (``str``) --
+ The language is set to 'ENG' for English by default.
+ """
+
+ def __init__(self, **kwargs):
+ super(MathematicalEvaluation, self).__init__(**kwargs)
+
+ self.language = kwargs.get('language', 'ENG')
+ self.cache = {}
+
+ def can_process(self, statement):
+ """
+ Determines whether it is appropriate for this
+ adapter to respond to the user input.
+ """
+ response = self.process(statement)
+ self.cache[statement.text] = response
+ return response.confidence == 1
+
+ def process(self, statement):
+ """
+ Takes a statement string.
+ Returns the equation from the statement with the mathematical terms solved.
+ """
+ from mathparse import mathparse
+
+ input_text = statement.text
+
+ # Use the result cached by the process method if it exists
+ if input_text in self.cache:
+ cached_result = self.cache[input_text]
+ self.cache = {}
+ return cached_result
+
+ # Getting the mathematical terms within the input statement
+ expression = mathparse.extract_expression(input_text, language=self.language)
+
+ response = Statement(text=expression)
+
+ try:
+ response.text += ' = ' + str(
+ mathparse.parse(expression, language=self.language)
+ )
+
+ # The confidence is 1 if the expression could be evaluated
+ response.confidence = 1
+ except mathparse.PostfixTokenEvaluationException:
+ response.confidence = 0
+
+ return response
diff --git a/chatter/chatterbot/logic/multi_adapter.py b/chatter/chatterbot/logic/multi_adapter.py
new file mode 100644
index 0000000..6cfe30f
--- /dev/null
+++ b/chatter/chatterbot/logic/multi_adapter.py
@@ -0,0 +1,155 @@
+from __future__ import unicode_literals
+
+from collections import Counter
+
+from chatter.chatterbot import utils
+from .logic_adapter import LogicAdapter
+
+
+class MultiLogicAdapter(LogicAdapter):
+ """
+ MultiLogicAdapter allows ChatterBot to use multiple logic
+ adapters. It has methods that allow ChatterBot to add an
+ adapter, set the chat bot, and process an input statement
+ to get a response.
+ """
+
+ def __init__(self, **kwargs):
+ super(MultiLogicAdapter, self).__init__(**kwargs)
+
+ # Logic adapters added by the chat bot
+ self.adapters = []
+
+ # Required logic adapters that must always be present
+ self.system_adapters = []
+
+ def get_initialization_functions(self):
+ """
+ Get the initialization functions for each logic adapter.
+ """
+ functions_dict = {}
+
+ # Iterate over each adapter and get its initialization functions
+ for logic_adapter in self.get_adapters():
+ functions = logic_adapter.get_initialization_functions()
+ functions_dict.update(functions)
+
+ return functions_dict
+
+ def process(self, statement):
+ """
+ Returns the output of a selection of logic adapters
+ for a given input statement.
+
+ :param statement: The input statement to be processed.
+ """
+ results = []
+ result = None
+ max_confidence = -1
+
+ for adapter in self.get_adapters():
+ if adapter.can_process(statement):
+
+ output = adapter.process(statement)
+ results.append((output.confidence, output,))
+
+ self.logger.info(
+ '{} selected "{}" as a response with a confidence of {}'.format(
+ adapter.class_name, output.text, output.confidence
+ )
+ )
+
+ if output.confidence > max_confidence:
+ result = output
+ max_confidence = output.confidence
+ else:
+ self.logger.info(
+ 'Not processing the statement using {}'.format(adapter.class_name)
+ )
+
+ # If multiple adapters agree on the same statement,
+ # then that statement is more likely to be the correct response
+ if len(results) >= 3:
+ statements = [s[1] for s in results]
+ count = Counter(statements)
+ most_common = count.most_common()
+ if most_common[0][1] > 1:
+ result = most_common[0][0]
+ max_confidence = self.get_greatest_confidence(result, results)
+
+ result.confidence = max_confidence
+ return result
+
+ def get_greatest_confidence(self, statement, options):
+ """
+ Returns the greatest confidence value for a statement that occurs
+ multiple times in the set of options.
+
+ :param statement: A statement object.
+ :param options: A tuple in the format of (confidence, statement).
+ """
+ values = []
+ for option in options:
+ if option[1] == statement:
+ values.append(option[0])
+
+ return max(values)
+
+ def get_adapters(self):
+ """
+ Return a list of all logic adapters being used, including system logic adapters.
+ """
+ adapters = []
+ adapters.extend(self.adapters)
+ adapters.extend(self.system_adapters)
+ return adapters
+
+ def add_adapter(self, adapter, **kwargs):
+ """
+ Appends a logic adapter to the list of logic adapters being used.
+
+ :param adapter: The logic adapter to be added.
+ :type adapter: `LogicAdapter`
+ """
+ utils.validate_adapter_class(adapter, LogicAdapter)
+ adapter = utils.initialize_class(adapter, **kwargs)
+ self.adapters.append(adapter)
+
+ def insert_logic_adapter(self, logic_adapter, insert_index, **kwargs):
+ """
+ Adds a logic adapter at a specified index.
+
+ :param logic_adapter: The string path to the logic adapter to add.
+ :type logic_adapter: str
+
+ :param insert_index: The index to insert the logic adapter into the list at.
+ :type insert_index: int
+ """
+ utils.validate_adapter_class(logic_adapter, LogicAdapter)
+
+ NewAdapter = utils.import_module(logic_adapter)
+ adapter = NewAdapter(**kwargs)
+
+ self.adapters.insert(insert_index, adapter)
+
+ def remove_logic_adapter(self, adapter_name):
+ """
+ Removes a logic adapter from the chat bot.
+
+ :param adapter_name: The class name of the adapter to remove.
+ :type adapter_name: str
+ """
+ for index, adapter in enumerate(self.adapters):
+ if adapter_name == type(adapter).__name__:
+ del self.adapters[index]
+ return True
+ return False
+
+ def set_chatbot(self, chatbot):
+ """
+ Set the chatbot for each of the contained logic adapters.
+ """
+ super(MultiLogicAdapter, self).set_chatbot(chatbot)
+
+ for adapter in self.get_adapters():
+ adapter.set_chatbot(chatbot)
diff --git a/chatter/chatterbot/logic/no_knowledge_adapter.py b/chatter/chatterbot/logic/no_knowledge_adapter.py
new file mode 100644
index 0000000..55208b4
--- /dev/null
+++ b/chatter/chatterbot/logic/no_knowledge_adapter.py
@@ -0,0 +1,27 @@
+from __future__ import unicode_literals
+
+from .logic_adapter import LogicAdapter
+
+
+class NoKnowledgeAdapter(LogicAdapter):
+ """
+ This is a system adapter that is automatically added
+ to the list of logic adapters during initialization.
+ This adapter is placed at the beginning of the list
+ to be given the highest priority.
+ """
+
+ def process(self, statement):
+ """
+ If there are no known responses in the database,
+ then a confidence of 1 should be returned with
+ the input statement.
+ Otherwise, a confidence of 0 should be returned.
+ """
+
+ if self.chatbot.storage.count():
+ statement.confidence = 0
+ else:
+ statement.confidence = 1
+
+ return statement
diff --git a/chatter/chatterbot/logic/specific_response.py b/chatter/chatterbot/logic/specific_response.py
new file mode 100644
index 0000000..101dd3b
--- /dev/null
+++ b/chatter/chatterbot/logic/specific_response.py
@@ -0,0 +1,39 @@
+from __future__ import unicode_literals
+
+from .logic_adapter import LogicAdapter
+
+
+class SpecificResponseAdapter(LogicAdapter):
+ """
+ Return a specific response to a specific input.
+
+ :kwargs:
+ * *input_text* (``str``) --
+ The input text that triggers this logic adapter.
+ * *output_text* (``str``) --
+ The output text returned by this logic adapter.
+ """
+
+ def __init__(self, **kwargs):
+ super(SpecificResponseAdapter, self).__init__(**kwargs)
+ from chatter.chatterbot.conversation import Statement
+
+ self.input_text = kwargs.get('input_text')
+
+ output_text = kwargs.get('output_text')
+ self.response_statement = Statement(output_text)
+
+ def can_process(self, statement):
+ if statement == self.input_text:
+ return True
+
+ return False
+
+ def process(self, statement):
+
+ if statement == self.input_text:
+ self.response_statement.confidence = 1
+ else:
+ self.response_statement.confidence = 0
+
+ return self.response_statement
diff --git a/chatter/chatterbot/logic/time_adapter.py b/chatter/chatterbot/logic/time_adapter.py
new file mode 100644
index 0000000..72902e2
--- /dev/null
+++ b/chatter/chatterbot/logic/time_adapter.py
@@ -0,0 +1,93 @@
+from __future__ import unicode_literals
+
+from datetime import datetime
+
+from .logic_adapter import LogicAdapter
+
+
+class TimeLogicAdapter(LogicAdapter):
+ """
+ The TimeLogicAdapter returns the current time.
+
+ :kwargs:
+ * *positive* (``list``) --
+ The time-related questions used to identify time questions.
+ Defaults to a list of English sentences.
+ * *negative* (``list``) --
+ The non-time-related questions used to identify time questions.
+ Defaults to a list of English sentences.
+ """
+
+ def __init__(self, **kwargs):
+ super(TimeLogicAdapter, self).__init__(**kwargs)
+ from nltk import NaiveBayesClassifier
+
+ self.positive = kwargs.get('positive', [
+ 'what time is it',
+ 'hey what time is it',
+ 'do you have the time',
+ 'do you know the time',
+ 'do you know what time it is',
+ 'what is the time'
+ ])
+
+ self.negative = kwargs.get('negative', [
+ 'it is time to go to sleep',
+ 'what is your favorite color',
+ 'i had a great time',
+ 'thyme is my favorite herb',
+ 'do you have time to look at my essay',
+ 'how do you have the time to do all this'
+ 'what is it'
+ ])
+
+ labeled_data = (
+ [(name, 0) for name in self.negative] +
+ [(name, 1) for name in self.positive]
+ )
+
+ train_set = [
+ (self.time_question_features(text), n) for (text, n) in labeled_data
+ ]
+
+ self.classifier = NaiveBayesClassifier.train(train_set)
+
+ def time_question_features(self, text):
+ """
+ Provide an analysis of significant features in the string.
+ """
+ features = {}
+
+ # A list of all words from the known sentences
+ all_words = " ".join(self.positive + self.negative).split()
+
+ # A list of the first word in each of the known sentence
+ all_first_words = []
+ for sentence in self.positive + self.negative:
+ all_first_words.append(
+ sentence.split(' ', 1)[0]
+ )
+
+ for word in text.split():
+ features['first_word({})'.format(word)] = (word in all_first_words)
+
+ for word in text.split():
+ features['contains({})'.format(word)] = (word in all_words)
+
+ for letter in 'abcdefghijklmnopqrstuvwxyz':
+ features['count({})'.format(letter)] = text.lower().count(letter)
+ features['has({})'.format(letter)] = (letter in text.lower())
+
+ return features
+
+ def process(self, statement):
+ from chatter.chatterbot.conversation import Statement
+
+ now = datetime.now()
+
+ time_features = self.time_question_features(statement.text.lower())
+ confidence = self.classifier.classify(time_features)
+ response = Statement('The current time is ' + now.strftime('%I:%M %p'))
+
+ response.confidence = confidence
+ return response
diff --git a/chatter/chatterbot/output/__init__.py b/chatter/chatterbot/output/__init__.py
new file mode 100644
index 0000000..80abe4f
--- /dev/null
+++ b/chatter/chatterbot/output/__init__.py
@@ -0,0 +1,15 @@
+from .gitter import Gitter
+from .hipchat import HipChat
+from .mailgun import Mailgun
+from .microsoft import Microsoft
+from .output_adapter import OutputAdapter
+from .terminal import TerminalAdapter
+
+__all__ = (
+ 'OutputAdapter',
+ 'Microsoft',
+ 'TerminalAdapter',
+ 'Mailgun',
+ 'Gitter',
+ 'HipChat',
+)
diff --git a/chatter/chatterbot/output/gitter.py b/chatter/chatterbot/output/gitter.py
new file mode 100644
index 0000000..ba01fa8
--- /dev/null
+++ b/chatter/chatterbot/output/gitter.py
@@ -0,0 +1,86 @@
+from __future__ import unicode_literals
+
+from .output_adapter import OutputAdapter
+
+
+class Gitter(OutputAdapter):
+ """
+ An output adapter that allows a ChatterBot instance to send
+ responses to a Gitter room.
+ """
+
+ def __init__(self, **kwargs):
+ super(Gitter, self).__init__(**kwargs)
+
+ self.gitter_host = kwargs.get('gitter_host', 'https://api.gitter.im/v1/')
+ self.gitter_room = kwargs.get('gitter_room')
+ self.gitter_api_token = kwargs.get('gitter_api_token')
+
+ authorization_header = 'Bearer {}'.format(self.gitter_api_token)
+
+ self.headers = {
+ 'Authorization': authorization_header,
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Accept': 'application/json'
+ }
+
+ # Join the Gitter room
+ room_data = self.join_room(self.gitter_room)
+ self.room_id = room_data.get('id')
+
+ def _validate_status_code(self, response):
+ code = response.status_code
+ if code not in [200, 201]:
+ raise self.HTTPStatusException('{} status code recieved'.format(code))
+
+ def join_room(self, room_name):
+ """
+ Join the specified Gitter room.
+ """
+ import requests
+
+ endpoint = '{}rooms'.format(self.gitter_host)
+ response = requests.post(
+ endpoint,
+ headers=self.headers,
+ json={'uri': room_name}
+ )
+ self.logger.info('{} status joining room {}'.format(
+ response.status_code, endpoint
+ ))
+ self._validate_status_code(response)
+ return response.json()
+
+ def send_message(self, text):
+ """
+ Send a message to a Gitter room.
+ """
+ import requests
+
+ endpoint = '{}rooms/{}/chatMessages'.format(self.gitter_host, self.room_id)
+ response = requests.post(
+ endpoint,
+ headers=self.headers,
+ json={'text': text}
+ )
+ self.logger.info('{} sending message to {}'.format(
+ response.status_code, endpoint
+ ))
+ self._validate_status_code(response)
+ return response.json()
+
+ def process_response(self, statement, session_id=None):
+ self.send_message(statement.text)
+ return statement
+
+ class HTTPStatusException(Exception):
+ """
+ Exception raised when unexpected non-success HTTP
+ status codes are returned in a response.
+ """
+
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
diff --git a/chatter/chatterbot/output/hipchat.py b/chatter/chatterbot/output/hipchat.py
new file mode 100644
index 0000000..2546092
--- /dev/null
+++ b/chatter/chatterbot/output/hipchat.py
@@ -0,0 +1,69 @@
+from __future__ import unicode_literals
+
+import json
+
+from .output_adapter import OutputAdapter
+
+
+class HipChat(OutputAdapter):
+ """
+ An output adapter that allows a ChatterBot instance to send
+ responses to a HipChat room.
+ """
+
+ def __init__(self, **kwargs):
+ super(HipChat, self).__init__(**kwargs)
+
+ self.hipchat_host = kwargs.get("hipchat_host")
+ self.hipchat_access_token = kwargs.get("hipchat_access_token")
+ self.hipchat_room = kwargs.get("hipchat_room")
+
+ authorization_header = "Bearer {}".format(self.hipchat_access_token)
+
+ self.headers = {
+ 'Authorization': authorization_header,
+ 'Content-Type': 'application/json'
+ }
+
+ import requests
+ self.session = requests.Session()
+ self.session.verify = kwargs.get('ssl_verify', True)
+
+ def send_message(self, room_id_or_name, message):
+ """
+ Send a message to a HipChat room.
+ https://www.hipchat.com/docs/apiv2/method/send_message
+ """
+ message_url = "{}/v2/room/{}/message".format(
+ self.hipchat_host,
+ room_id_or_name
+ )
+
+ response = self.session.post(
+ message_url,
+ headers=self.headers,
+ data=json.dumps({
+ 'message': message
+ })
+ )
+
+ return response.json()
+
+ def reply_to_message(self):
+ """
+ The HipChat api supports responding to a given message.
+ This may be a good feature to implement in the future to
+ help with multi-user conversations.
+ https://www.hipchat.com/docs/apiv2/method/reply_to_message
+ """
+ raise self.AdapterMethodNotImplementedError()
+
+ def process_response(self, statement, session_id=None):
+ data = self.send_message(self.hipchat_room, statement.text)
+
+ # Update the output statement with the message id
+ self.chatbot.storage.update(
+ statement.add_extra_data('hipchat_message_id', data['id'])
+ )
+
+ return statement
diff --git a/chatter/chatterbot/output/mailgun.py b/chatter/chatterbot/output/mailgun.py
new file mode 100644
index 0000000..71a9a7a
--- /dev/null
+++ b/chatter/chatterbot/output/mailgun.py
@@ -0,0 +1,50 @@
+from __future__ import unicode_literals
+
+from .output_adapter import OutputAdapter
+
+
+class Mailgun(OutputAdapter):
+
+ def __init__(self, **kwargs):
+ super(Mailgun, self).__init__(**kwargs)
+
+ # Use the bot's name for the name of the sender
+ self.name = kwargs.get('name')
+ self.from_address = kwargs.get('mailgun_from_address')
+ self.api_key = kwargs.get('mailgun_api_key')
+ self.endpoint = kwargs.get('mailgun_api_endpoint')
+ self.recipients = kwargs.get('mailgun_recipients')
+
+ def send_message(self, subject, text, from_address, recipients):
+ """
+ * subject: Subject of the email.
+ * text: Text body of the email.
+ * from_email: The email address that the message will be sent from.
+ * recipients: A list of recipient email addresses.
+ """
+ import requests
+
+ return requests.post(
+ self.endpoint,
+ auth=('api', self.api_key),
+ data={
+ 'from': '%s <%s>' % (self.name, from_address),
+ 'to': recipients,
+ 'subject': subject,
+ 'text': text
+ })
+
+ def process_response(self, statement, session_id=None):
+ """
+ Send the response statement as an email.
+ """
+ subject = 'Message from %s' % (self.name)
+
+ self.send_message(
+ subject,
+ statement.text,
+ self.from_address,
+ self.recipients
+ )
+
+ return statement
diff --git a/chatter/chatterbot/output/microsoft.py b/chatter/chatterbot/output/microsoft.py
new file mode 100644
index 0000000..816fc97
--- /dev/null
+++ b/chatter/chatterbot/output/microsoft.py
@@ -0,0 +1,111 @@
+from __future__ import unicode_literals
+
+import json
+
+from .output_adapter import OutputAdapter
+
+
+class Microsoft(OutputAdapter):
+ """
+ An output adapter that allows a ChatterBot instance to send
+ responses to a Microsoft bot using *Direct Line client protocol*.
+ """
+
+ def __init__(self, **kwargs):
+ super(Microsoft, self).__init__(**kwargs)
+
+ self.directline_host = kwargs.get(
+ 'directline_host',
+ 'https://directline.botframework.com'
+ )
+ self.direct_line_token_or_secret = kwargs.get(
+ 'direct_line_token_or_secret'
+ )
+ self.conversation_id = kwargs.get('conversation_id')
+
+ authorization_header = 'BotConnector {}'.format(
+ self.direct_line_token_or_secret
+ )
+
+ self.headers = {
+ 'Authorization': authorization_header,
+ 'Content-Type': 'application/json'
+ }
+
+ def _validate_status_code(self, response):
+ status_code = response.status_code
+ if status_code not in [200, 204]:
+ raise self.HTTPStatusException('{} status code recieved'.format(status_code))
+
+ def get_most_recent_message(self):
+ """
+ Return the most recently sent message.
+ """
+ import requests
+ endpoint = '{host}/api/conversations/{id}/messages'.format(
+ host=self.directline_host,
+ id=self.conversation_id
+ )
+
+ response = requests.get(
+ endpoint,
+ headers=self.headers,
+ verify=False
+ )
+
+ self.logger.info('{} retrieving most recent messages {}'.format(
+ response.status_code, endpoint
+ ))
+
+ self._validate_status_code(response)
+
+ data = response.json()
+
+ if data['messages']:
+ last_msg = int(data['watermark'])
+ return data['messages'][last_msg - 1]
+ return None
+
+ def send_message(self, conversation_id, message):
+ """
+ Send a message to a HipChat room.
+ https://www.hipchat.com/docs/apiv2/method/send_message
+ """
+ import requests
+
+ message_url = "{host}/api/conversations/{conversationId}/messages".format(
+ host=self.directline_host,
+ conversationId=conversation_id
+ )
+
+ response = requests.post(
+ message_url,
+ headers=self.headers,
+ data=json.dumps({
+ 'message': message
+ })
+ )
+
+ self.logger.info('{} sending message {}'.format(
+ response.status_code, message_url
+ ))
+ self._validate_status_code(response)
+ # Microsoft return 204 on operation succeeded and no content was returned.
+ return self.get_most_recent_message()
+
+ def process_response(self, statement, session_id=None):
+ data = self.send_message(self.conversation_id, statement.text)
+ self.logger.info('processing user response {}'.format(data))
+ return statement
+
+ class HTTPStatusException(Exception):
+ """
+ Exception raised when unexpected non-success HTTP
+ status codes are returned in a response.
+ """
+
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
diff --git a/chatter/chatterbot/output/output_adapter.py b/chatter/chatterbot/output/output_adapter.py
new file mode 100644
index 0000000..5d13dd7
--- /dev/null
+++ b/chatter/chatterbot/output/output_adapter.py
@@ -0,0 +1,20 @@
+from chatter.chatterbot.adapters import Adapter
+
+
+class OutputAdapter(Adapter):
+ """
+ A generic class that can be overridden by a subclass to provide extended
+ functionality, such as delivering a response to an API endpoint.
+ """
+
+ def process_response(self, statement, session_id=None):
+ """
+ Override this method in a subclass to implement customized functionality.
+
+ :param statement: The statement that the chat bot has produced in response to some input.
+
+ :param session_id: The unique id of the current chat session.
+
+ :returns: The response statement.
+ """
+ return statement
diff --git a/chatter/chatterbot/output/terminal.py b/chatter/chatterbot/output/terminal.py
new file mode 100644
index 0000000..8ab63e1
--- /dev/null
+++ b/chatter/chatterbot/output/terminal.py
@@ -0,0 +1,17 @@
+from __future__ import unicode_literals
+
+from .output_adapter import OutputAdapter
+
+
+class TerminalAdapter(OutputAdapter):
+ """
+ A simple adapter that allows ChatterBot to
+ communicate through the terminal.
+ """
+
+ def process_response(self, statement, session_id=None):
+ """
+ Print the response to the user's input.
+ """
+ print(statement.text)
+ return statement.text
diff --git a/chatter/chatterbot/parsing.py b/chatter/chatterbot/parsing.py
new file mode 100644
index 0000000..5aafa75
--- /dev/null
+++ b/chatter/chatterbot/parsing.py
@@ -0,0 +1,752 @@
+# -*- coding: utf-8 -*-
+import calendar
+import re
+from datetime import timedelta, datetime
+
+# Variations of dates that the parser can capture
+year_variations = ['year', 'years', 'yrs']
+day_variations = ['days', 'day']
+minute_variations = ['minute', 'minutes', 'mins']
+hour_variations = ['hrs', 'hours', 'hour']
+week_variations = ['weeks', 'week', 'wks']
+month_variations = ['month', 'months']
+
+# Variables used for RegEx Matching
+day_names = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday'
+month_names_long = (
+ 'january|february|march|april|may|june|july|august|september|october|november|december'
+)
+month_names = month_names_long + '|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec'
+day_nearest_names = 'today|yesterday|tomorrow|tonight|tonite'
+numbers = (
+ '(^a(?=\s)|one|two|three|four|five|six|seven|eight|nine|ten|'
+ 'eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|'
+ 'eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|'
+ 'eighty|ninety|hundred|thousand)'
+)
+re_dmy = '(' + '|'.join(day_variations + minute_variations + year_variations + week_variations + month_variations) + ')'
+re_duration = '(before|after|earlier|later|ago|from\snow)'
+re_year = '(19|20)\d{2}|^(19|20)\d{2}'
+re_timeframe = 'this|coming|next|following|previous|last|end\sof\sthe'
+re_ordinal = 'st|nd|rd|th|first|second|third|fourth|fourth|' + re_timeframe
+re_time = r'(?P\d{1,2})(\:(?P\d{1,2})|(?Pam|pm))'
+re_separator = 'of|at|on'
+
+# A list tuple of regular expressions / parser fn to match
+# Start with the widest match and narrow it down because the order of the match in this list matters
+regex = [
+ (
+ re.compile(
+ r'''
+ (
+ ((?P%s)[,\s]\s*)? #Matches Monday, 12 Jan 2012, 12 Jan 2012 etc
+ (?P\d{1,2}) # Matches a digit
+ (%s)?
+ [-\s] # One or more space
+ (?P%s) # Matches any month name
+ [-\s] # Space
+ (?P%s) # Year
+ ((\s|,\s|\s(%s))?\s*(%s))?
+ )
+ ''' % (day_names, re_ordinal, month_names, re_year, re_separator, re_time),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: datetime(
+ int(m.group('year') if m.group('year') else base_date.year),
+ HASHMONTHS[m.group('month').strip().lower()],
+ int(m.group('day') if m.group('day') else 1),
+ ) + timedelta(**convert_time_to_hour_minute(
+ m.group('hour'),
+ m.group('minute'),
+ m.group('convention')
+ ))
+ ),
+ (
+ re.compile(
+ r'''
+ (
+ ((?P%s)[,\s][-\s]*)? #Matches Monday, Jan 12 2012, Jan 12 2012 etc
+ (?P%s) # Matches any month name
+ [-\s] # Space
+ ((?P\d{1,2})) # Matches a digit
+ (%s)?
+ ([-\s](?P%s))? # Year
+ ((\s|,\s|\s(%s))?\s*(%s))?
+ )
+ ''' % (day_names, month_names, re_ordinal, re_year, re_separator, re_time),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: datetime(
+ int(m.group('year') if m.group('year') else base_date.year),
+ HASHMONTHS[m.group('month').strip().lower()],
+ int(m.group('day') if m.group('day') else 1)
+ ) + timedelta(**convert_time_to_hour_minute(
+ m.group('hour'),
+ m.group('minute'),
+ m.group('convention')
+ ))
+ ),
+ (
+ re.compile(
+ r'''
+ (
+ (?P%s) # Matches any month name
+ [-\s] # One or more space
+ (?P\d{1,2}) # Matches a digit
+ (%s)?
+ [-\s]\s*?
+ (?P%s) # Year
+ ((\s|,\s|\s(%s))?\s*(%s))?
+ )
+ ''' % (month_names, re_ordinal, re_year, re_separator, re_time),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: datetime(
+ int(m.group('year') if m.group('year') else base_date.year),
+ HASHMONTHS[m.group('month').strip().lower()],
+ int(m.group('day') if m.group('day') else 1),
+ ) + timedelta(**convert_time_to_hour_minute(
+ m.group('hour'),
+ m.group('minute'),
+ m.group('convention')
+ ))
+ ),
+ (
+ re.compile(
+ r'''
+ (
+ ((?P\d+|(%s[-\s]?)+)\s)? # Matches any number or string 25 or twenty five
+ (?P%s)s?\s # Matches days, months, years, weeks, minutes
+ (?P%s) # before, after, earlier, later, ago, from now
+ (\s*(?P(%s)))?
+ ((\s|,\s|\s(%s))?\s*(%s))?
+ )
+ ''' % (numbers, re_dmy, re_duration, day_nearest_names, re_separator, re_time),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: date_from_duration(
+ base_date,
+ m.group('number'),
+ m.group('unit').lower(),
+ m.group('duration').lower(),
+ m.group('base_time')
+ ) + timedelta(**convert_time_to_hour_minute(
+ m.group('hour'),
+ m.group('minute'),
+ m.group('convention')
+ ))
+ ),
+ (
+ re.compile(
+ r'''
+ (
+ (?P%s) # First quarter of 2014
+ \s+
+ quarter\sof
+ \s+
+ (?P%s)
+ )
+ ''' % (re_ordinal, re_year),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: date_from_quarter(
+ base_date,
+ HASHORDINALS[m.group('ordinal').lower()],
+ int(m.group('year') if m.group('year') else base_date.year)
+ )
+ ),
+ (
+ re.compile(
+ r'''
+ (
+ (?P\d+)
+ (?P%s) # 1st January 2012
+ ((\s|,\s|\s(%s))?\s*)?
+ (?P%s)
+ ([,\s]\s*(?P%s))?
+ )
+ ''' % (re_ordinal, re_separator, month_names, re_year),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: datetime(
+ int(m.group('year') if m.group('year') else base_date.year),
+ int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1),
+ int(m.group('ordinal_value') if m.group('ordinal_value') else 1),
+ )
+ ),
+ (
+ re.compile(
+ r'''
+ (
+ (?P%s)
+ \s+
+ (?P\d+)
+ (?P%s) # January 1st 2012
+ ([,\s]\s*(?P%s))?
+ )
+ ''' % (month_names, re_ordinal, re_year),
+ (re.VERBOSE | re.IGNORECASE)
+ ),
+ lambda m, base_date: datetime(
+ int(m.group('year') if m.group('year') else base_date.year),
+ int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1),
+ int(m.group('ordinal_value') if m.group('ordinal_value') else 1),
+ )
+ ),
+ (
+ re.compile(
+ r'''
+ (?P