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/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 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 |
|
|