List of commits:
Subject Hash Author Date (UTC)
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 545a35d1c5bb36e1b4e4d818b0e6a8f6642a222f - Added examples. Modified reaction class to add triggers.
Author: Detche
Author date (UTC): 2016-10-22 20:56
Committer name: Detche
Committer date (UTC): 2016-10-22 20:56
Parent(s): 2cd79df6224290a994b1453f0327740816b0f632
Signer:
Signing key:
Signing status: N
Tree: 403408f8190fcee44890b4a382c553c4ad5e53f8
File Lines added Lines deleted
botly/botly.py 12 26
botly/comm.py 15 5
botly/knowledge.py 10 3
botly/reaction.py 47 25
botly/reactions/sayhi.py 0 19
botly/trigger.py 103 0
example.py 1 2
examplebot/botdb.xml 33 0
examplebot/reactions/EMPTY.py.example 15 0
examplebot/reactions/HELP.py.example 84 0
examplebot/reactions/sayhi.py 23 0
File botly/botly.py changed (mode: 100644) (index 960e5ad..bc6e6cf)
... ... from .behaviour import Behaviour
11 11
12 12 class Botly: class Botly:
13 13
14 def __init__(self):
15 self.dbLoaded = False
16 self.knowledge = False
17 self.settings = False
14 def __init__(self, rootModule, dbPath):
18 15 self.me = False self.me = False
19 16 self.comm = False self.comm = False
20 17
21 self.behaviour = Behaviour(load_reactions(self))
18 self.settings = Settings()
19 self.settings.load(dbPath)
20 self.knowledge = Knowledge()
22 21
23 def live(self):
24 assert self.dbLoaded, 'Database not loaded.'
22 self.behaviour = Behaviour(load_reactions(self, \
23 rootModule + '.reactions'))
25 24
25 def live(self):
26 26 self.comm = Comm(self) self.comm = Comm(self)
27 27 # Listen to Discord's events (blocking call) # Listen to Discord's events (blocking call)
28 28 self.comm.run() self.comm.run()
 
... ... class Botly:
35 35 """ """
36 36 self.me = user self.me = user
37 37
38 async def react_to(self, eventName, **kwargs):
38 async def react_to(self, eventName, **eventInfo):
39 39 reactions = self.behaviour.get_reactions(eventName) reactions = self.behaviour.get_reactions(eventName)
40 40 if reactions: if reactions:
41 41 for reaction in reactions: for reaction in reactions:
42 if reaction.is_triggered(**kwargs):
42 if reaction.get_trigger().is_triggered(self, **eventInfo):
43 43 print('('+eventName+') -> ' + reaction.get_module_name()) print('('+eventName+') -> ' + reaction.get_module_name())
44 reaction.prepare_react(**kwargs)
44 reaction.prepare_react(**eventInfo)
45 45 await reaction.react() await reaction.react()
46
47 def set_knowledge(self, knowledge):
48 self.knowledge = knowledge
49
50 def load_db(self, path):
51 self.settings = Settings()
52 self.settings.load(path)
53 self.knowledge = Knowledge()
54 self.dbLoaded = True
55
56 def test(self):
57 pass
58 46
59 def settings(self):
60 return self.settings
47 async def leave(self):
48 await self.comm.logout()
61 49
62 def knowledge(self):
63 return self.knowledge
File botly/comm.py changed (mode: 100644) (index bfe3843..9e61871)
... ... class Comm(discord.Client):
10 10 self.updatedBot = False self.updatedBot = False
11 11 super(Comm, self).__init__(max_messages=botly.settings.get_int( \ super(Comm, self).__init__(max_messages=botly.settings.get_int( \
12 12 'CacheMaxMessages')) 'CacheMaxMessages'))
13
13
14 14 async def on_ready(self): async def on_ready(self):
15 15 print('Bot ready') print('Bot ready')
16 16 if not self.updatedBot: if not self.updatedBot:
 
... ... class Comm(discord.Client):
19 19 await self.botly.react_to('on_ready') await self.botly.react_to('on_ready')
20 20
21 21 async def on_typing(self, channel, user, when): async def on_typing(self, channel, user, when):
22 await self.botly.react_to('on_typing', channel=channel, user=user, when=when)
22 if user != self.user:
23 await self.botly.react_to('on_typing', channel=channel, user=user,
24 when=when)
23 25
24 26 async def on_message_edit(self, before, after): async def on_message_edit(self, before, after):
25 await self.botly.react_to('on_message_edit', message=before, after=after)
27 if before.author != self.user:
28 await self.botly.react_to('on_message_edit', message=before, \
29 after=after)
26 30
27 31 async def on_message_delete(self, message): async def on_message_delete(self, message):
28 await self.botly.react_to('on_message_delete', message=message)
32 if message.author != self.user:
33 await self.botly.react_to('on_message_delete', message=message)
29 34
30 35 async def on_message(self, message): async def on_message(self, message):
31 await self.botly.react_to('on_message', message=message)
36 if message.author != self.user:
37 await self.botly.react_to('on_message', message=message)
38
39 async def on_member_update(self, before, after):
40 await self.botly.react_to('on_member_update', before=before, \
41 after=after)
32 42
33 43 def run(self): def run(self):
34 44 super(Comm, self).run(self.botly.settings.get_string('DiscordToken')) super(Comm, self).run(self.botly.settings.get_string('DiscordToken'))
File botly/knowledge.py changed (mode: 100644) (index 376b489..bce2dea)
... ... class Knowledge(Db):
13 13 user['Id'] = self.get_value(userxml, 'Id') user['Id'] = self.get_value(userxml, 'Id')
14 14 user['Alias'] = self.get_value(userxml, 'Alias') user['Alias'] = self.get_value(userxml, 'Alias')
15 15 user['BotAffinity'] = self.get_value(userxml, 'BotAffinity') user['BotAffinity'] = self.get_value(userxml, 'BotAffinity')
16 user['EpsiName'] = self.get_value(userxml, 'EpsiName')
16 user['IsMaster'] = self.get_value(userxml, 'IsMaster')
17 17 self.users.append(user) self.users.append(user)
18 18
19 19 def get_users(self): def get_users(self):
 
... ... class Knowledge(Db):
27 27 return user return user
28 28 return False return False
29 29
30 def get_user_bot_affinity(self, id):
30 def get_user_affinity(self, id):
31 31 user = self.get_user(id) user = self.get_user(id)
32 32 if user == False: if user == False:
33 return False
33 # We have an affinity of 0 (neutral) for unknown users
34 return 0
34 35 return int(user['BotAffinity']) return int(user['BotAffinity'])
35 36
36 37 def get_user_alias(self, id): def get_user_alias(self, id):
 
... ... class Knowledge(Db):
39 40 return False return False
40 41 return user['Alias'] return user['Alias']
41 42
43 def is_user_master(self, id):
44 user = self.get_user(id)
45 if user == False:
46 return False
47 return user['IsMaster'] == '1'
48
File botly/reaction.py changed (mode: 100644) (index 0cc6e7b..098ae11)
... ... from importlib import import_module
3 3 from os import listdir from os import listdir
4 4 from os.path import isfile, join from os.path import isfile, join
5 5
6 from .trigger import Trigger
7
6 8 class ReactionBase: class ReactionBase:
7 9 def __init__(self, eventName): def __init__(self, eventName):
8 10 """ """
9 11 Subclass this and pass the event that this Subclass this and pass the event that this
10 reaction should react to as the constructor
11 parameter.
12 reaction should react and the trigger object to
13 to the mother class constructor.
12 14 """ """
13 15 self.eventName = eventName self.eventName = eventName
14 16 self.botly = False self.botly = False
17 self.knowledge = False
18 trigger = Trigger(eventName)
19 # Call the function that should have been subclassed
20 self.prepare_trigger(trigger)
21 self.trigger = trigger
15 22
23 def prepare_trigger(self, trigger):
24 """
25 This method should be redefined in the subclass.
26 It should prepare the given trigger by defining its
27 conditions.
28 """
29 pass
30
16 31 def set_botly_instance(self, botly): def set_botly_instance(self, botly):
17 32 self.botly = botly self.botly = botly
18 self.db = botly.knowledge
33 self.knowledge = botly.knowledge
19 34
20 35 def set_module_name(self, name): def set_module_name(self, name):
21 36 self.moduleName = name self.moduleName = name
37
38 def get_trigger(self):
39 return self.trigger
22 40
23 41 def get_module_name(self): def get_module_name(self):
24 42 return self.moduleName return self.moduleName
 
... ... class ReactionBase:
26 44 def get_event_name(self): def get_event_name(self):
27 45 return self.eventName return self.eventName
28 46
29 def is_triggered(self, **eventInfo):
30 """
31 Method to be subclassed.
32 Should return true if we should react to the given
33 eventInfo. Should return false instead.
34 """
35 pass
36
37 47 def prepare_react(self, **eventInfo): def prepare_react(self, **eventInfo):
38 48 self.message = False self.message = False
39 49 self.messageAfter = False self.messageAfter = False
50 self.author = False
40 51 self.channel = False self.channel = False
41 52 self.user = False self.user = False
42 53 self.when = False self.when = False
54 self.before = False
55 self.after = False
43 56
44 57 if 'message' in self.eventName: if 'message' in self.eventName:
45 if 'message' in eventInfo:
46 self.message = eventInfo['message']
58 self.message = eventInfo['message']
59 self.author = eventInfo['message'].author
60 self.channel = eventInfo['message'].channel
47 61 if 'on_message_edit' == self.eventName: if 'on_message_edit' == self.eventName:
48 62 self.messageAfter = eventInfo['after'] self.messageAfter = eventInfo['after']
49 if 'on_typing' == self.eventName:
50 if 'channel' in eventInfo:
51 self.channel = eventInfo['channel']
52 if 'user' in eventInfo:
53 self.user = eventInfo['user']
54 if 'when' in eventInfo:
55 self.when = eventInfo['when']
56
63 elif 'on_typing' == self.eventName:
64 self.channel = eventInfo['channel']
65 self.user = eventInfo['user']
66 self.when = eventInfo['when']
67 elif 'on_member_update' == self.eventName:
68 self.before = eventInfo['before']
69 self.after = eventInfo['after']
70
57 71 def is_mentioned(self): def is_mentioned(self):
58 72 assert self.botly, 'Botly instance not passed to this object' assert self.botly, 'Botly instance not passed to this object'
59 73 if self.message: if self.message:
60 74 return self.botly.me.mentioned_in(self.message) return self.botly.me.mentioned_in(self.message)
75
76 async def reply(self, message):
77 if self.channel:
78 self.botly.comm.send_message(self.channel, message)
79
80 async def reply_to_chan(self, channel, message):
81 self.botly.comm.send_message(channel, message)
61 82
62 83 async def react(self): async def react(self):
63 84 """ """
 
... ... class ReactionBase:
66 87 """ """
67 88 pass pass
68 89
69 def load_reactions(botly):
70 files = [f for f in listdir('./botly/reactions/') \
71 if isfile(join('./botly/reactions/',f)) \
90 def load_reactions(botly, reactionsParent):
91 dpath = './' + reactionsParent.replace('.', '/')
92 files = [f for f in listdir(dpath) \
93 if isfile(join(dpath,f)) \
72 94 and f.endswith('.py')] and f.endswith('.py')]
73 95 reactions = [] reactions = []
74 96 for file in files: for file in files:
75 97 module = file[:-3] module = file[:-3]
76 _reaction = import_module('botly.reactions.' + module)
98 _reaction = import_module(reactionsParent + '.' + module)
77 99 reaction = _reaction.Reaction() reaction = _reaction.Reaction()
78 100 assert len(reaction.get_event_name()), \ assert len(reaction.get_event_name()), \
79 101 'Loaded a reaction linked to no event.' 'Loaded a reaction linked to no event.'
File botly/reactions/sayhi.py deleted (index 898459a..0000000)
1 from botly.reaction import ReactionBase
2 import discord
3
4 class Reaction(ReactionBase):
5
6 def __init__(self):
7 super(Reaction, self).__init__('on_message')
8
9 def is_triggered(self, **eventInfo):
10 if 'message' not in eventInfo:
11 return False
12 message = eventInfo['message']
13 if not self.botly.me.mentioned_in(message):
14 return False
15 return True
16
17 async def react(self):
18 await self.botly.comm.send_message(self.message.channel, \
19 self.message.author.mention + ' OK')
File botly/trigger.py added (mode: 100644) (index 0000000..871224c)
1 import discord
2 import re
3 import random
4
5 from .knowledge import Knowledge
6
7 class Trigger:
8 def __init__(self, eventName):
9 self.eventName = eventName
10 self.conditions = []
11 self.advConditions = []
12 self.requireMention = False
13 self.triggerChance = 100
14
15 def _pattern_valid(self, pattern):
16 try:
17 re.compile(pattern)
18 return True
19 except re.error:
20 return False
21
22 def add_condition(self, variable, pattern):
23 """
24 Add a condition where given variable should match given
25 regular expression pattern.
26 """
27 assert self._pattern_valid(pattern), 'Given pattern is invalid.'
28
29 if 'message' == variable:
30 assert 'message' in self.eventName, \
31 'message variable not expected for this event'
32 elif 'author' == variable:
33 assert eventName != 'on_ready', \
34 'author not supported for on_ready event'
35
36 condition = []
37 condition.append(variable)
38 condition.append(pattern)
39 self.conditions.append(condition)
40
41 def add_adv_condition(self, callback):
42 """
43 Added a function as the condition. It should return
44 True if the condition is respected or False if not.
45 It will be called with the full event info table.
46 """
47 assert callable(callback), 'Given argument must be a callback function'
48 self.advConditions.append(callback)
49
50 def set_trigger_chance(self, percent):
51 assert isinstance(value, int), 'Int expected.'
52 percent = percent if percent >= 1 else 1
53 percent = percent if percent <= 100 else 100
54 self.triggerChance = percent
55
56 def require_mention(self, value):
57 assert isinstance(value, bool), 'Bool expected.'
58 self.requireMention = value
59
60 def _can_we_trigger(self):
61 if self.triggerChance == 100:
62 return True
63 return random.randrange(1, 101) < self.triggerChance
64
65 def _is_condition_true(self, condition, **eventInfo):
66 v = condition[0]
67 p = condition[1]
68
69 if v == 'author':
70 if 'message' in self.eventName:
71 if re.match(p, eventInfo['message'].author.id):
72 return True
73 if 'on_typing' == self.eventName:
74 if re.match(p, eventInfo['user'].id):
75 return True
76 elif v == 'message':
77 if 'message' in self.eventName:
78 if re.match(p, eventInfo['message'].content):
79 return True
80
81 def is_triggered(self, botly, **eventInfo):
82 # Run random. Do we have a chance to trigger?
83 if not self._can_we_trigger():
84 return False
85
86 # Checks if bot is mentioned if it is required:
87 if 'message' in self.eventName and self.requireMention:
88 if not botly.me.mentioned_in(eventInfo['message']):
89 return False
90
91 # Check for conditions:
92 for condition in self.conditions:
93 if not self._is_condition_true(condition, **eventInfo):
94 return False
95
96 # Check advanced conditions:
97 for condition in self.advConditions:
98 if not condition(**eventInfo):
99 return False
100
101 # We trigger yo.
102 return True
103
File example.py renamed from main.py (similarity 64%) (mode: 100644) (index 3ad615a..dba7f91)
1 1 from botly.botly import Botly from botly.botly import Botly
2 2
3 3 def main(): def main():
4 bot = Botly()
5 bot.load_db('botdb.xml')
4 bot = Botly('examplebot', 'examplebot/botdb.xml')
6 5 bot.live() bot.live()
7 6
8 7 if __name__ == "__main__": if __name__ == "__main__":
File examplebot/botdb.xml added (mode: 100644) (index 0000000..43c2657)
1 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 <BotDb>
3 <Settings>
4 <DiscordToken>YOUR_DISCORD_TOKEN</DiscordToken>
5 <CacheMaxMessages>100</CacheMaxMessages>
6 </Settings>
7 <Knowledge>
8 <Users>
9 <User>
10 <Id>222658568346796032</Id>
11 <Alias>Detche</Alias>
12 <BotAffinity>0</BotAffinity>
13 <IsMaster>1</IsMaster>
14 </User>
15 </Users>
16 <Groups>
17 <Group>
18 <Id>1</Id>
19 <Referrer>222658568346796032</Referrer>
20 <Members>
21 <Member>222658568346796032</Member>
22 </Members>
23 </Group>
24 </Groups>
25 </Knowledge>
26 <Memory>
27 <Reactions>
28 <Shared/>
29 <UserSpecific/>
30 </Reactions>
31 <Tasks/>
32 </Memory>
33 </BotDb>
File examplebot/reactions/EMPTY.py.example added (mode: 100644) (index 0000000..d6ce171)
1 from botly.reaction import ReactionBase
2 from botly.trigger import Trigger
3 import discord
4
5 class Reaction(ReactionBase):
6
7 def __init__(self):
8 super(Reaction, self).__init__('EVENT_NAME')
9
10 def prepare_trigger(self, trigger):
11 # Prepare trigger here.
12
13 async def react(self):
14 # React here.
15
File examplebot/reactions/HELP.py.example added (mode: 100644) (index 0000000..ac94ff8)
1 from botly.reaction import ReactionBase
2 from botly.trigger import Trigger
3 import discord
4
5 class Reaction(ReactionBase):
6
7 def __init__(self):
8 ###
9 # Define the event name this reaction should react on by passing it
10 # to the mother class constructor
11 super(Reaction, self).__init__('EVENT_NAME')
12
13 def prepare_trigger(self, trigger):
14 ###
15 # Should we require mention? (Default: false)
16 #
17 # trigger.require_mention(False)
18 #
19 ###
20 # Should the trigger have an additionnal random chance to activate?
21 # 100% (default) -> Will always trigger if conditions are met
22 # 50% -> Has half the chances to trigger, even if every condtion is met
23 # ...
24 #
25 # trigger.set_trigger_chance(100)
26 #
27 ###
28 # Add a basic condition.
29 # Available variable names: 'message', 'author'
30 # Pattern is using the Python RE syntax
31 #
32 # trigger.add_condition(variableName, contentPattern)
33 #
34 ###
35 # Add a complex condition, made of a callback function, that takes
36 # a eventInfo table as an argument. It should return either True or
37 # False.
38 #
39 # def myCallback(**eventInfo):
40 # if eventInfo['message'].content == 'prout' \
41 # and eventInfo['message'].author.name == 'Blaise':
42 # return True
43 # return False
44 #
45 # trigger.add_adv_condition(myCallback)
46 #
47 ###
48 # A trigger can have as many condition as wished, altough abusing
49 # this will lead to delay for the bot's responses.
50
51 async def react(self):
52 ###
53 # We triggered. Now react accordingly.
54 # Remember to use 'await' for Discord API calls
55 # Do not use blocking function calls in this method.
56 #
57 ###
58 # Available accesses:
59 # self.botly Access to the bot object instance
60 # self.botly.comm Discord communication object instance
61 # self.knowledge Access to the bot knowledge database
62 #
63 ###
64 # Available event variables, depending on the event:
65 # 'on_message*' events:
66 # self.message Discord Message object
67 # self.author Discord Member/User object shortcut
68 # self.channel Discord Channel object where this occured
69 # 'on_message_edit' event only:
70 # self.messageAfter Message post-edition
71 # 'on_typing' event:
72 # self.channel Discord channel object
73 # self.user Discord Member/User object
74 # self.when When did it happen?
75 # 'on_member_update' event:
76 # self.before Discord Member object (before update)
77 # self.after Discord Member object (after updaate)
78 #
79 ###
80 #
81 # Exemple response to a message
82 # await self.botly.comm.send_message(self.message.channel, \
83 # self.message.author.mention + ' lol')
84
File examplebot/reactions/sayhi.py added (mode: 100644) (index 0000000..e4ee71d)
1 from botly.reaction import ReactionBase
2 from botly.trigger import Trigger
3 import discord
4
5 class Reaction(ReactionBase):
6
7 def __init__(self):
8 super(Reaction, self).__init__('on_message')
9
10 def prepare_trigger(self, trigger):
11 trigger.require_mention(True)
12 trigger.add_condition('message', '(?i).*((hello)|(hi)|(hey)).*')
13
14 async def react(self):
15 affinity = self.knowledge.get_user_affinity(self.author.id)
16
17 if affinity < 0:
18 await self.reply(self.author.mention + " I don't like you."))
19 elif affinity > 0:
20 await self.reply(self.author.mention + " Hi buddy!")
21 else:
22 await self.reply(self.author.mention + ' Hello.')
23
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