List of commits:
Subject Hash Author Date (UTC)
Added tasks system. 13bbf9857a0a61d5e2b0fd9483f5efd235cbd4f2 Detche 2016-11-02 14:58:20
Modified Knowledge process. Fixed indent 28e52095a297cbb0e6b9b6d799bb80ebe52828c1 Detche 2016-10-23 12:11:30
Updated README 8e298151286ec3cb4df29633aaca90fc871f2e80 Detche 2016-10-23 10:10:45
Update README.rst 7965df28cbef557fd3819919b4d44f0733dcecf0 Guillaume 2016-10-23 00:06:05
Added README page 8339ac5b13803b026dbfc5edd77fa50a24a70ecb Detche 2016-10-22 23:47:05
Added comments to sources. ce6491ef1337ea34d33b7e4ede0eb9f634e7ab1a Detche 2016-10-22 23:32:26
Added examples. Modified reaction class to add triggers. 545a35d1c5bb36e1b4e4d818b0e6a8f6642a222f Detche 2016-10-22 20:56:54
Initial commit 2cd79df6224290a994b1453f0327740816b0f632 Detche 2016-10-21 16:21:02
Commit 13bbf9857a0a61d5e2b0fd9483f5efd235cbd4f2 - Added tasks system.
Author: Detche
Author date (UTC): 2016-11-02 14:58
Committer name: Detche
Committer date (UTC): 2016-11-02 14:58
Parent(s): 28e52095a297cbb0e6b9b6d799bb80ebe52828c1
Signing key:
Tree: 3a7a402583cf7d63dad1c970accbb4b8656dc15f
File Lines added Lines deleted
botly/bot.py 18 6
botly/comm.py 11 11
botly/knowledge.py 1 1
botly/reaction.py 10 10
botly/task.py 69 0
example.py 2 2
examplebot/botdb.xml 1 0
examplebot/reactions/HELP.py.example 4 5
examplebot/reactions/sayhi.py 1 1
examplebot/tasks/EMPTY.py.example 16 0
File botly/bot.py renamed from botly/botly.py (similarity 78%) (mode: 100644) (index 0f835d9..8e6311b)
... ... import discord
8 8 from botly.settings import Settings from botly.settings import Settings
9 9 from botly.knowledge import Knowledge from botly.knowledge import Knowledge
10 10 from botly.comm import Comm from botly.comm import Comm
11 from botly.task import load_tasks
11 12 from botly.reaction import load_reactions from botly.reaction import load_reactions
12 13 from botly.behaviour import Behaviour from botly.behaviour import Behaviour
13 14
14 15
15 class Botly:
16 class Bot:
16 17 """Main class that represents the bot. Will load various dependencies.""" """Main class that represents the bot. Will load various dependencies."""
17 18
18 19 def __init__(self, rootModule, dbPath): def __init__(self, rootModule, dbPath):
19 20 """Initialization loads database, reactions and tasks from drive.""" """Initialization loads database, reactions and tasks from drive."""
20 21 self.me = False self.me = False
21 self.comm = False
22 22
23 23 self.settings = Settings() self.settings = Settings()
24 24 self.settings.load(dbPath) self.settings.load(dbPath)
25 25 self.knowledge = Knowledge() self.knowledge = Knowledge()
26 26
27 # Create discord client object for communication and event reception
28 self.comm = Comm(self)
29
30 # Load the bot background tasks
31 for task in load_tasks(self, rootModule + '.tasks'):
32 self.comm.loop.create_task(task.run())
33
34 # Load the bot behaviour (reactions to events)
27 35 self.behaviour = Behaviour(load_reactions(self, self.behaviour = Behaviour(load_reactions(self,
28 36 rootModule + '.reactions')) rootModule + '.reactions'))
29 37
30 38 def live(self): def live(self):
31 39 """Runs the bot until it is exited. """ """Runs the bot until it is exited. """
32 self.comm = Comm(self)
33 40 # Listen to Discord's events (blocking call) # Listen to Discord's events (blocking call)
34 41 print("Connecting to Discord...") print("Connecting to Discord...")
35 42 self.comm.run() self.comm.run()
36 43 print("\nBotly exited.") print("\nBotly exited.")
37 44
45 async def leave(self):
46 """Coroutine that disconnects the robot from Discord."""
47 await self.comm.logout()
48
49 async def say(self, channel, message):
50 """Coroutine that send a message to discord."""
51 await self.comm.send_message(channel, message)
52
38 53 def set_whoami(self, user): def set_whoami(self, user):
39 54 """This will be called once the bot is connected to Discord.""" """This will be called once the bot is connected to Discord."""
40 55 self.me = user self.me = user
 
... ... class Botly:
50 65 reaction.prepare_react(**eventInfo) reaction.prepare_react(**eventInfo)
51 66 await reaction.react() await reaction.react()
52 67
53 async def leave(self):
54 """Coroutine that disconnects the robot from Discord."""
55 await self.comm.logout()
56 68
57 69
File botly/comm.py changed (mode: 100644) (index f2576da..66f2085)
... ... import discord
9 9 class Comm(discord.Client): class Comm(discord.Client):
10 10 """Inherits from discord.client. Used for communication with server.""" """Inherits from discord.client. Used for communication with server."""
11 11
12 def __init__(self, botly):
13 self.botly = botly
14 super(Comm, self).__init__(max_messages=botly.settings.get_int(
12 def __init__(self, bot):
13 self.bot = bot
14 super(Comm, self).__init__(max_messages=bot.settings.get_int(
15 15 'CacheMaxMessages')) 'CacheMaxMessages'))
16 16
17 17 async def on_ready(self): async def on_ready(self):
18 18 print('Bot ready. Press Ctrl+C to shutdown.') print('Bot ready. Press Ctrl+C to shutdown.')
19 self.botly.set_whoami(self.user)
20 await self.botly.react_to('on_ready')
19 self.bot.set_whoami(self.user)
20 await self.bot.react_to('on_ready')
21 21
22 22 async def on_typing(self, channel, user, when): async def on_typing(self, channel, user, when):
23 23 if user != self.user: if user != self.user:
24 await self.botly.react_to('on_typing', channel=channel, user=user,
24 await self.bot.react_to('on_typing', channel=channel, user=user,
25 25 when=when) when=when)
26 26
27 27 async def on_message_edit(self, before, after): async def on_message_edit(self, before, after):
28 28 if before.author != self.user: if before.author != self.user:
29 await self.botly.react_to('on_message_edit', message=before,
29 await self.bot.react_to('on_message_edit', message=before,
30 30 after=after) after=after)
31 31
32 32 async def on_message_delete(self, message): async def on_message_delete(self, message):
33 33 if message.author != self.user: if message.author != self.user:
34 await self.botly.react_to('on_message_delete', message=message)
34 await self.bot.react_to('on_message_delete', message=message)
35 35
36 36 async def on_message(self, message): async def on_message(self, message):
37 37 if message.author != self.user: if message.author != self.user:
38 await self.botly.react_to('on_message', message=message)
38 await self.bot.react_to('on_message', message=message)
39 39
40 40 async def on_member_update(self, before, after): async def on_member_update(self, before, after):
41 await self.botly.react_to('on_member_update', before=before,
41 await self.bot.react_to('on_member_update', before=before,
42 42 after=after) after=after)
43 43
44 44 def run(self): def run(self):
45 super(Comm, self).run(self.botly.settings.get_string('DiscordToken'))
45 super(Comm, self).run(self.bot.settings.get_string('DiscordToken'))
46 46
47 47
File botly/knowledge.py changed (mode: 100644) (index 78325c2..4d59510)
... ... class Knowledge(Db):
87 87 def _load_user(self, userId): def _load_user(self, userId):
88 88 assert self.is_loaded(), 'Xml document is not loaded' assert self.is_loaded(), 'Xml document is not loaded'
89 89 userxml = self.get_item('/BotDb/Knowledge/Users/User', 'Id', userId) userxml = self.get_item('/BotDb/Knowledge/Users/User', 'Id', userId)
90 if userxml:
90 if userxml is not None:
91 91 user = {} user = {}
92 92 user['Id'] = userxml.get('Id') # Id is a XML attribute user['Id'] = userxml.get('Id') # Id is a XML attribute
93 93 for valueName in Knowledge.userValues: for valueName in Knowledge.userValues:
File botly/reaction.py changed (mode: 100644) (index 0520ccf..cdd185c)
... ... class ReactionBase:
27 27 def __init__(self, eventName): def __init__(self, eventName):
28 28 """When subclassed, pass the event name to the mother class.""" """When subclassed, pass the event name to the mother class."""
29 29 self.eventName = eventName self.eventName = eventName
30 self.botly = False
30 self.bot = False
31 31 self.knowledge = False self.knowledge = False
32 32 trigger = Trigger(eventName) trigger = Trigger(eventName)
33 33 # Call the function that should have been subclassed # Call the function that should have been subclassed
 
... ... class ReactionBase:
53 53
54 54 def is_mentioned(self): def is_mentioned(self):
55 55 """Returns whether or not our bot was mentioned in the message.""" """Returns whether or not our bot was mentioned in the message."""
56 assert self.botly, 'Botly instance not passed to this object'
56 assert self.bot, 'Botly instance not passed to this object'
57 57 if self.message: if self.message:
58 return self.botly.me.mentioned_in(self.message)
58 return self.bot.me.mentioned_in(self.message)
59 59
60 60 async def reply(self, message): async def reply(self, message):
61 61 """Coroutine that replies in current chan with given message.""" """Coroutine that replies in current chan with given message."""
62 62 if self.channel: if self.channel:
63 await self.botly.comm.send_message(self.channel, message)
63 await self.bot.say(self.channel, message)
64 64
65 65 async def send_to_chan(self, channel, message): async def send_to_chan(self, channel, message):
66 66 """Coroutine that send message to given channel.""" """Coroutine that send message to given channel."""
67 await self.botly.comm.send_message(channel, message)
67 await self.bot.say(channel, message)
68 68
69 def set_botly_instance(self, botly):
69 def set_bot_instance(self, bot):
70 70 """Saves botly instance. Only meant to becalled upon module import.""" """Saves botly instance. Only meant to becalled upon module import."""
71 self.botly = botly
72 self.knowledge = botly.knowledge
71 self.bot = bot
72 self.knowledge = bot.knowledge
73 73
74 74 def set_module_name(self, name): def set_module_name(self, name):
75 75 """Saves the name of this Reaction. Called upon module import.""" """Saves the name of this Reaction. Called upon module import."""
 
... ... class ReactionBase:
101 101 self.after = eventInfo['after'] self.after = eventInfo['after']
102 102
103 103
104 def load_reactions(botly, reactionsParent):
104 def load_reactions(bot, reactionsParent):
105 105 """Function that loads reactions from given parent module directory.""" """Function that loads reactions from given parent module directory."""
106 106 # Convert module path to drive path. # Convert module path to drive path.
107 107 dpath = './' + reactionsParent.replace('.', '/') dpath = './' + reactionsParent.replace('.', '/')
 
... ... def load_reactions(botly, reactionsParent):
122 122 assert len(reaction.eventName), \ assert len(reaction.eventName), \
123 123 'Loaded a reaction linked to no event.' 'Loaded a reaction linked to no event.'
124 124 # Inject instance info in reaction object # Inject instance info in reaction object
125 reaction.set_botly_instance(botly)
125 reaction.set_bot_instance(bot)
126 126 reaction.set_module_name(module) reaction.set_module_name(module)
127 127 print('Loaded ' + str(len(reactions)) + ' reactions from drive.') print('Loaded ' + str(len(reactions)) + ' reactions from drive.')
128 128 return reactions return reactions
File botly/task.py added (mode: 100644) (index 0000000..cbc32e0)
1 #!/usr/bin/env python
2 """Contains the TaskBase to be inherited for Tasks creation"""
3
4 import asyncio
5 from importlib import import_module
6 from os import listdir
7 from os.path import isfile, join
8
9 import discord
10
11
12 class TaskBase:
13 """Base class for bot's background tasks"""
14
15 def __init__(self):
16 self.endTask = False
17
18 def set_instance_info(self, bot):
19 self.bot = bot
20 self.knowledge = bot.knowledge
21 self.settings = bot.settings
22 self.comm = bot.comm
23
24 self.mainChannel = discord.Object(
25 id=bot.settings.get_string('DefaultChannelId'))
26
27 async def run(self):
28 """Entry point of task. Do not overwrite."""
29
30 # Wait until the bot is ready before running
31 await self.comm.wait_until_ready()
32
33 while not self.comm.is_closed or not self.endTask:
34 await asyncio.sleep(self.get_next_timeout())
35 await self.do()
36
37 def get_next_timeout(self):
38 """Should return the time, in sec, to wait before each do() call."""
39 return 10
40
41 def do(self):
42 """Proceed with the task's action. To be overwritten when inherited"""
43 pass
44
45
46 def load_tasks(bot, tasksParent):
47 """Function that loads the tasks from given paren module directory."""
48 # Get fs path to module directory
49 dpath = './' + tasksParent.replace('.', '/')
50
51 # Retreive every python script in the tasks module directory
52 files = [f for f in listdir(dpath)
53 if isfile(join(dpath, f))
54 and f.endswith('.py')]
55
56 # Build the list of tasks
57 tasks = []
58 for file in files:
59 # Module name should not end with '.py', so remove that part
60 module = file[:-3]
61 _taskModule = import_module(tasksParent + '.' + module)
62 task = _taskModule.Task()
63 tasks.append(task)
64 # Inject instance info in task object
65 task.set_instance_info(bot)
66 print('Loaded ' + str(len(tasks)) + ' tasks from drive.')
67 return tasks
68
69
File example.py changed (mode: 100644) (index 96e4ba8..3f148ab)
1 1 #!/usr/bin/env python #!/usr/bin/env python
2 2
3 from botly.botly import Botly
3 from botly.bot import Bot
4 4
5 5
6 6 def main(): def main():
7 bot = Botly('examplebot', 'examplebot/botdb.xml')
7 bot = Bot('examplebot', 'examplebot/botdb.xml')
8 8 bot.live() bot.live()
9 9
10 10
File examplebot/botdb.xml changed (mode: 100644) (index 43c2657..a472f78)
3 3 <Settings> <Settings>
4 4 <DiscordToken>YOUR_DISCORD_TOKEN</DiscordToken> <DiscordToken>YOUR_DISCORD_TOKEN</DiscordToken>
5 5 <CacheMaxMessages>100</CacheMaxMessages> <CacheMaxMessages>100</CacheMaxMessages>
6 <DefaultChannelId>DEFAULT_CHANNEL_ID</DefaultChannelId>
6 7 </Settings> </Settings>
7 8 <Knowledge> <Knowledge>
8 9 <Users> <Users>
File examplebot/reactions/HELP.py.example changed (mode: 100644) (index 1cb51e7..d6797ba)
... ... class Reaction(ReactionBase):
58 58 # #
59 59 ### ###
60 60 # Available accesses: # Available accesses:
61 # self.botly Access to the bot object instance
62 # self.botly.comm Discord communication object instance
61 # self.bot Access to the bot object instance
62 # self.bot.comm Discord communication object instance
63 63 # self.knowledge Access to the bot knowledge database # self.knowledge Access to the bot knowledge database
64 64 # #
65 65 ### ###
 
... ... class Reaction(ReactionBase):
69 69 # self.author Discord Member/User object shortcut # self.author Discord Member/User object shortcut
70 70 # self.channel Discord Channel object where this occured # self.channel Discord Channel object where this occured
71 71 # 'on_message_edit' event only: # 'on_message_edit' event only:
72 # self.messageAfter Message post-edition
72 # self.messageAfter Message post-edition
73 73 # 'on_typing' event: # 'on_typing' event:
74 74 # self.channel Discord channel object # self.channel Discord channel object
75 75 # self.user Discord Member/User object # self.user Discord Member/User object
 
... ... class Reaction(ReactionBase):
81 81 ### ###
82 82 # #
83 83 # Exemple response to a message # Exemple response to a message
84 # await self.botly.comm.send_message(self.message.channel, \
85 # self.message.author.mention + ' lol')
84 # await self.reply(self.message.author.mention + ' lol')
86 85
File examplebot/reactions/sayhi.py changed (mode: 100644) (index 78d8338..4e6b788)
... ... class Reaction(ReactionBase):
20 20 affinity = self.knowledge.get_user_affinity(self.author.id) affinity = self.knowledge.get_user_affinity(self.author.id)
21 21
22 22 if affinity < 0: if affinity < 0:
23 await self.reply(self.author.mention + " I don't like you."))
23 await self.reply(self.author.mention + " I don't like you.")
24 24 elif affinity > 0: elif affinity > 0:
25 25 await self.reply(self.author.mention + " Hi buddy!") await self.reply(self.author.mention + " Hi buddy!")
26 26 else: else:
File examplebot/tasks/EMPTY.py.example added (mode: 100644) (index 0000000..fa384ee)
1 import discord
2
3 from botly.task import TaskBase
4
5
6 class Task(TaskBase):
7
8 def __init__(self):
9 pass
10
11 def get_next_timeout(self):
12 # Return the timeout for next do() call
13 return 10
14
15 def do(self):
16 # Do task action here
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/detche/Botly

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/detche/Botly

Clone this repository using git:
git clone git://git.rocketgit.com/user/detche/Botly

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main