List of commits:
Subject Hash Author Date (UTC)
Changed db interface and users/groups models. d0fdf8873a107c44d6acbef5cbffc869461d9aab Detche 2016-11-03 15:22:10
Changed UserId from element to attribute. 5407592cba9e7488519e069927a8ffb9ed32fdf6 Detche 2016-11-02 15:02:30
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 d0fdf8873a107c44d6acbef5cbffc869461d9aab - Changed db interface and users/groups models.
Author: Detche
Author date (UTC): 2016-11-03 15:22
Committer name: Detche
Committer date (UTC): 2016-11-03 15:22
Parent(s): 5407592cba9e7488519e069927a8ffb9ed32fdf6
Signer:
Signing key:
Signing status: N
Tree: 6bdbecafac250744757d0ad59c67e56afb947f80
File Lines added Lines deleted
botly/bot.py 8 5
botly/db.py 80 11
botly/groups.py 35 0
botly/knowledge.py 4 115
botly/reaction.py 3 3
botly/settings.py 10 11
botly/task.py 10 4
botly/trigger.py 3 1
botly/users.py 50 0
examplebot/reactions/HELP.py.example 1 1
examplebot/reactions/sayhi.py 3 1
File botly/bot.py changed (mode: 100644) (index 8e6311b..b0047a2)
... ... import asyncio
5 5
6 6 import discord import discord
7 7
8 from botly.db import Db
8 9 from botly.settings import Settings from botly.settings import Settings
9 10 from botly.knowledge import Knowledge from botly.knowledge import Knowledge
10 11 from botly.comm import Comm from botly.comm import Comm
 
... ... class Bot:
19 20 def __init__(self, rootModule, dbPath): def __init__(self, rootModule, dbPath):
20 21 """Initialization loads database, reactions and tasks from drive.""" """Initialization loads database, reactions and tasks from drive."""
21 22 self.me = False self.me = False
22
23
24 # Load database and models
25 Db.load(dbPath)
23 26 self.settings = Settings() self.settings = Settings()
24 self.settings.load(dbPath)
25 27 self.knowledge = Knowledge() self.knowledge = Knowledge()
26 28
27 29 # Create discord client object for communication and event reception # Create discord client object for communication and event reception
 
... ... class Bot:
41 43 print("Connecting to Discord...") print("Connecting to Discord...")
42 44 self.comm.run() self.comm.run()
43 45 print("\nBotly exited.") print("\nBotly exited.")
44
46
47 def print(self, message, msgtype='INFO'):
48 print('[' + msgtype + ']' + message)
49
45 50 async def leave(self): async def leave(self):
46 51 """Coroutine that disconnects the robot from Discord.""" """Coroutine that disconnects the robot from Discord."""
47 52 await self.comm.logout() await self.comm.logout()
 
... ... class Bot:
60 65 if reactions: if reactions:
61 66 for reaction in reactions: for reaction in reactions:
62 67 if reaction.trigger.is_triggered(self, **eventInfo): if reaction.trigger.is_triggered(self, **eventInfo):
63 print("Reaction '" + reaction.moduleName
64 + "' triggered by event '" + eventName + "'.")
65 68 reaction.prepare_react(**eventInfo) reaction.prepare_react(**eventInfo)
66 69 await reaction.react() await reaction.react()
67 70
File botly/db.py changed (mode: 100644) (index ede5918..5789619)
... ... class Db:
8 8
9 9 dbxml = False dbxml = False
10 10
11 def load(self, filePath):
12 Db.dbxml = etree.parse(filePath)
11 def __init__(self):
12 assert False, "No instance of this class should be created."
13
14 @classmethod
15 def load(cls, filePath):
16 """Loads the database XML file at given path."""
17 cls.dbxml = etree.parse(filePath)
18
19 def get_node(self, path):
20 """Returns the node at the given absolute path"""
21 return Db.dbxml.xpath(path)[0]
13 22
14 23 def is_loaded(self): def is_loaded(self):
24 """Returns whether or not the database was loaded."""
15 25 return Db.dbxml != False return Db.dbxml != False
16 26
17 def get_all_items(self, elementName):
18 return Db.dbxml.xpath('{0}'.format(elementName))
27 def query_value(self, valueName, parentPath = None):
28 """Loads a single value from the given parent."""
29 self._pre_check()
30
31 # Define parent: Either the root elem or given parent path
32 parent = None
33 if parentPath is None:
34 parent = self.root
35 else:
36 parent = self.root.xpath(parentXpath)[0]
37
38 if parent is not None:
39 print(parent)
40 valueElem = parent.find(valueName)
41 if valueElem is not None and valueElem.text is not None:
42 return valueElem.text
43 return None
44
45 def query_object(self, parentXpath, objectId):
46 """Loads a single object identified by its Id. None if not found."""
47 self._pre_check()
48
49 parent = self.root.xpath(parentXpath)[0]
50 if parent is not None:
51 for child1 in parent.iterchildren():
52 if child1.get('Id') != objectId:
53 continue
54 return self._load_object_values(child1)
55 return None
56
57 def query_object_list(self, parentXpath):
58 """Loads a list of objects, represented by a dict with its values"""
59 self._pre_check()
19 60
20 def get_item(self, elementName, attributeName, value):
21 item = Db.dbxml.xpath('{0}[@{1}="{2}"]'.format(elementName,
22 attributeName, value))
23 return item[0] if item else None
61 parent = self.root.xpath(parentXpath)[0]
62 if parent is not None:
63 objects = []
64 for child1 in parent.iterchildren():
65 objects.append(self._load_object_values(child1))
66 return objects
67 return None
24 68
25 def get_value(self, element, childName):
26 child = element.xpath(childName + '/text()')
27 return child[0] if child else None
69 def _load_object_values(self, xmlobj):
70 """Loads the given xml node in a dictionary, considered as an object.
28 71
72 Objects are suspected to have an Id specified as an attribute on their
73 root xml element. It will be loaded in the dictionary as 'Id'.
74
75 Elements containing no children are considered as simple values.
76 They are loaded in the dict using their tagname and their value.
77
78 Elements that contains children are considered a list of value. A list
79 is created in the dict under that element's tag name containing the
80 value of its children. Those children's tag name is ignored."""
81
82 obj = {}
83 obj['Id'] = xmlobj.get('Id')
84 for child in xmlobj.iterchildren():
85 if len(child) == 0:
86 # We have no children, this node is value
87 obj[child.tag] = child.text
88 else:
89 # We have children. We consider this node as a list
90 obj[child.tag] = []
91 for child2 in child.iterchildren():
92 if child2.text is not None:
93 obj[child.tag].append(child2.text)
94 return obj
29 95
96 def _pre_check(self):
97 assert self.is_loaded(), "The DB is not loaded."
98 assert self.root is not None, "Root node not defined."
30 99
File botly/groups.py added (mode: 100644) (index 0000000..3969824)
1 #!/usr/bin/env python
2 """Contains Groups class which represents the groups database"""
3
4 from botly.db import Db
5
6
7 class Groups(Db):
8
9 def __init__(self):
10 assert self.is_loaded(), "Db is not loaded upon Users object creation."
11 self.groupCache = []
12 self.root = self.get_node('/BotDb/Knowledge')
13
14 def get(self, groupid):
15 """Returns the group with given ID or None if it does not exist."""
16 group = self._get_cached_group(groupid)
17 if group is None:
18 group = self.query_object('Groups', groupid)
19 return group
20
21 def get_all(self):
22 """Returns all groups from the database."""
23 groups = self.query_object_list('Groups')
24 self.groupCache.clear()
25 self.groupCache = groups
26 return groups
27
28 def _get_cached_group(self, groupid):
29 """Returns the cached group if it exists, None otherwise."""
30 for group in self.groupCache:
31 if group['Id'] == groupid:
32 return group
33 return None
34
35
File botly/knowledge.py changed (mode: 100644) (index 4d59510..4127b2d)
2 2 """Class Knowledge represents what bot knows, such as users and their info.""" """Class Knowledge represents what bot knows, such as users and their info."""
3 3
4 4 from botly.db import Db from botly.db import Db
5 from botly.users import Users
6 from botly.groups import Groups
5 7
6 8
7 9 class Knowledge(Db): class Knowledge(Db):
8 10 """Represents what bot knows. Use accessors to access data.""" """Represents what bot knows. Use accessors to access data."""
9 11
10 userValues=['Alias', 'BotAffinity', 'IsMaster']
11
12 12 def __init__(self): def __init__(self):
13 self.users = []
14
15 # Shortcut accessors for built in values:
16
17 def get_user_value(self, userId, valueName, default=False):
18 """Returns a precise value for given user. Try to load from XML."""
19 user = self.get_user(userId)
20 if not user:
21 return default
22 if valueName not in user:
23 # Try to load this value as it may not have been loaded yet
24 self._load_user_value(userId, valueName)
25 return user[valueName]
26
27 def get_user_affinity(self, userId):
28 """Returns the affinity of the user designated by given id."""
29 user = self.get_user(userId)
30 if not user or user['BotAffinity'] is None:
31 # We have an affinity of 0 (neutral) for unknown users
32 return 0
33 return int(user['BotAffinity'])
34
35 def get_user_alias(self, userId):
36 """Returns the alias of the user designated by given id."""
37 user = self.get_user(userId)
38 if not user or user['Alias'] is None:
39 return ''
40 return user['Alias']
41
42 def is_user_master(self, userId):
43 """Returns whether or not user is a master (can command bot)."""
44 user = self.get_user(userId)
45 if not user or user['IsMaster'] is None:
46 return False
47 return user['IsMaster'] == '1'
48
49 # User methods:
50
51 def add_user_values(self, *valueNames):
52 for name in valueNames:
53 Knowledge.userValues.append(name)
54 # Empty the table, as we now have new values for each users
55 self.users.clear()
56
57 def get_users(self):
58 """Returns the whole users table. Loads it from XML if not done yet."""
59 if not self.users:
60 self._load_all_users()
61 return self.users
62
63 def get_user(self, userId):
64 """Return all botly values for given user id."""
65 user = self._has_loaded_user(userId, getuser=True)
66 if not user:
67 user = self._load_user(userId)
68 if not user:
69 return False
70 return user
71
72 def user_exists(self, userId):
73 """Returns whether or not we have this user in our knowledge db."""
74 return True if self.get_user(userId) else False
75
76 # Internal methods:
77
78 def _load_user_value(self, userId, valueName):
79 assert self.is_loaded(), 'Xml document is not loaded'
80 userxml = self.get_item('/BotDb/Knowledge/Users/User', 'Id', userId)
81 if userxml is not None:
82 value = self.get_value(userxml, valueName)
83 user = self._has_loaded_user(userId, getuser=True)
84 user[valueName] = value # Value could still be None
85 return None
86
87 def _load_user(self, userId):
88 assert self.is_loaded(), 'Xml document is not loaded'
89 userxml = self.get_item('/BotDb/Knowledge/Users/User', 'Id', userId)
90 if userxml is not None:
91 user = {}
92 user['Id'] = userxml.get('Id') # Id is a XML attribute
93 for valueName in Knowledge.userValues:
94 user[valueName] = self.get_value(userxml, valueName)
95 # Remove already loaded user. Does nothing if it doesn't exist yet
96 self._remove_loaded_user(userId)
97 self.users.append(user)
98 return user
99 return False
13 self.users = Users()
14 self.groups = Groups()
100 15
101 def _load_all_users(self):
102 assert self.is_loaded(), 'Xml document is not loaded'
103 # Clear the user table, in case we are reloading users
104 self.users.clear()
105 for userxml in self.get_all_items('/BotDb/Knowledge/Users/User'):
106 user = {}
107 user['Id'] = userxml.get('Id')
108 # Retrieve user values
109 for valueName in Knowledge.userValues:
110 user[valueName] = self.get_value(userxml, valueName)
111 self.users.append(user)
112
113 def _remove_loaded_user(self, userId):
114 for user in self.users:
115 if user['Id'] == userId:
116 self.users.remove(user)
117 break
118 return False
119
120 def _has_loaded_user(self, userId, getuser=False):
121 for user in self.users:
122 if user['Id'] == userId:
123 return user if getuser else True
124 return False
125
126
File botly/reaction.py changed (mode: 100644) (index cdd185c..99c136c)
... ... class ReactionBase:
49 49
50 50 def print(self, message): def print(self, message):
51 51 """Helper for console printing. Will prepend current Reaction name.""" """Helper for console printing. Will prepend current Reaction name."""
52 print('[{0}] {1}'.format(self.moduleName, message))
52 self.bot.print('[Reaction][{0}] {1}'.format(self.moduleName, message))
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."""
 
... ... class ReactionBase:
66 66 """Coroutine that send message to given channel.""" """Coroutine that send message to given channel."""
67 67 await self.bot.say(channel, message) await self.bot.say(channel, message)
68 68
69 def set_bot_instance(self, bot):
69 def set_instance_info(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 71 self.bot = bot self.bot = bot
72 72 self.knowledge = bot.knowledge self.knowledge = bot.knowledge
 
... ... def load_reactions(bot, 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_bot_instance(bot)
125 reaction.set_instance_info(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/settings.py changed (mode: 100644) (index bad6ea3..84d336b)
... ... from botly.db import Db
7 7 class Settings(Db): class Settings(Db):
8 8
9 9 def __init__(self): def __init__(self):
10 self.settings = {}
10 assert self.is_loaded(), "Db is not loaded before loading settings."
11 self.root = self.get_node('/BotDb/Settings')
12 self.settingsCache = {}
11 13
12 def get_string(self, key, default='None'):
14 def get_string(self, key, default=''):
13 15 """Returns the string value if it exists.""" """Returns the string value if it exists."""
14 if not key in self.settings:
15 self.settings[key] = self._load_setting(key, default)
16 return self.settings[key]
16 if key not in self.settingsCache:
17 self.settingsCache[key] = self._load_setting(key, default)
18 return self.settingsCache[key]
17 19
18 20 def get_int(self, key, default='0'): def get_int(self, key, default='0'):
19 21 """Returns the int value if it exists.""" """Returns the int value if it exists."""
 
... ... class Settings(Db):
23 25 return int(v) return int(v)
24 26
25 27 def _load_setting(self, key, default): def _load_setting(self, key, default):
26 assert self.is_loaded(), "Database is not loaded."
27 setting = Db.dbxml.xpath('/BotDb/Settings/' + key)
28 if setting:
29 return setting[0].text
30 else:
31 return default
28 setting = self.query_value(key)
29 return setting if setting is not None else default
30
File botly/task.py changed (mode: 100644) (index cbc32e0..6cf8f59)
... ... import discord
12 12 class TaskBase: class TaskBase:
13 13 """Base class for bot's background tasks""" """Base class for bot's background tasks"""
14 14
15 def __init__(self):
16 self.endTask = False
17
18 15 def set_instance_info(self, bot): def set_instance_info(self, bot):
16 self.endTask = False
19 17 self.bot = bot self.bot = bot
20 18 self.knowledge = bot.knowledge self.knowledge = bot.knowledge
21 19 self.settings = bot.settings self.settings = bot.settings
 
... ... class TaskBase:
24 22 self.mainChannel = discord.Object( self.mainChannel = discord.Object(
25 23 id=bot.settings.get_string('DefaultChannelId')) id=bot.settings.get_string('DefaultChannelId'))
26 24
25 def print(self, message):
26 self.bot.print('[Tasks][' + self.moduleName + ']' + ' ' + message)
27
27 28 async def run(self): async def run(self):
28 29 """Entry point of task. Do not overwrite.""" """Entry point of task. Do not overwrite."""
29 30
30 # Wait until the bot is ready before running
31 # Wait until the bot is ready before running the task
31 32 await self.comm.wait_until_ready() await self.comm.wait_until_ready()
32 33
33 34 while not self.comm.is_closed or not self.endTask: while not self.comm.is_closed or not self.endTask:
34 35 await asyncio.sleep(self.get_next_timeout()) await asyncio.sleep(self.get_next_timeout())
36 if self.comm is None or self.comm.is_closed or self.endTask:
37 break
35 38 await self.do() await self.do()
36 39
37 40 def get_next_timeout(self): def get_next_timeout(self):
 
... ... class TaskBase:
42 45 """Proceed with the task's action. To be overwritten when inherited""" """Proceed with the task's action. To be overwritten when inherited"""
43 46 pass pass
44 47
48 def set_module_name(self, name):
49 self.moduleName = name
45 50
46 51 def load_tasks(bot, tasksParent): def load_tasks(bot, tasksParent):
47 52 """Function that loads the tasks from given paren module directory.""" """Function that loads the tasks from given paren module directory."""
 
... ... def load_tasks(bot, tasksParent):
63 68 tasks.append(task) tasks.append(task)
64 69 # Inject instance info in task object # Inject instance info in task object
65 70 task.set_instance_info(bot) task.set_instance_info(bot)
71 task.set_module_name(module)
66 72 print('Loaded ' + str(len(tasks)) + ' tasks from drive.') print('Loaded ' + str(len(tasks)) + ' tasks from drive.')
67 73 return tasks return tasks
68 74
File botly/trigger.py changed (mode: 100644) (index 6a06731..29e6975)
... ... class Trigger:
54 54 self.advConditions.append(callback) self.advConditions.append(callback)
55 55
56 56 def set_trigger_chance(self, percent): def set_trigger_chance(self, percent):
57 """Adds a chance for the trigger to activate. 100% is default value."""
58
57 59 assert isinstance(value, int), 'Int expected for trigger chance.' assert isinstance(value, int), 'Int expected for trigger chance.'
58 60 percent = percent if percent >= 1 else 1 percent = percent if percent >= 1 else 1
59 61 percent = percent if percent <= 100 else 100 percent = percent if percent <= 100 else 100
 
... ... class Trigger:
65 67 self.requireMention = value self.requireMention = value
66 68
67 69 def is_triggered(self, botly, **eventInfo): def is_triggered(self, botly, **eventInfo):
68 """This should only be called from Botly class.
70 """This should only be called from Bot class.
69 71
70 72 Checks whether or not the trigger object activates based on the Checks whether or not the trigger object activates based on the
71 73 given information. This is meant to be called from the Botly class given information. This is meant to be called from the Botly class
File botly/users.py added (mode: 100644) (index 0000000..c125b44)
1 #!/usr/bin/env python
2 """Contains Users class which represents the users database"""
3
4 from botly.db import Db
5
6
7 class Users(Db):
8
9 def __init__(self):
10 assert self.is_loaded(), "Db is not loaded upon Users object creation."
11 self.userCache = []
12 self.root = self.get_node('/BotDb/Knowledge')
13
14 def is_master(self, userid):
15 """Returns whether or not this user can control the bot."""
16 user = self.get(userid)
17 if user is not None:
18 if user['IsMaster'] == '1':
19 return True
20 return False
21
22 def affinity(self, userid):
23 """Returns the bot affinity to the user."""
24 user = self.get(userid)
25 if user is not None:
26 return int(user['BotAffinity'])
27 return 0
28
29 def get(self, userid):
30 """Returns the user with given ID or None if it does not exist."""
31 user = self._get_cached_user(userid)
32 if user is None:
33 user = self.query_object('Users', userid)
34 return user
35
36 def get_all(self):
37 """Returns all users from the database."""
38 users = self.query_object_list('Users')
39 self.userCache.clear()
40 self.userCache = users
41 return users
42
43 def _get_cached_user(self, userid):
44 """Returns the cached user if it exists, None otherwise."""
45 for user in self.userCache:
46 if user['Id'] == userid:
47 return user
48 return None
49
50
File examplebot/reactions/HELP.py.example changed (mode: 100644) (index d6797ba..0811430)
... ... class Reaction(ReactionBase):
39 39 # False. # False.
40 40 # #
41 41 # def myCallback(**eventInfo): # def myCallback(**eventInfo):
42 # if (eventInfo['message'].content == 'prout'
42 # if (eventInfo['message'].content == 'coucou'
43 43 # and eventInfo['message'].author.name == 'Blaise'): # and eventInfo['message'].author.name == 'Blaise'):
44 44 # return True # return True
45 45 # return False # return False
File examplebot/reactions/sayhi.py changed (mode: 100644) (index 4e6b788..de79e9c)
... ... class Reaction(ReactionBase):
17 17 trigger.add_condition('message', '(?i).*((hello)|(hi)|(hey)).*') trigger.add_condition('message', '(?i).*((hello)|(hi)|(hey)).*')
18 18
19 19 async def react(self): async def react(self):
20 affinity = self.knowledge.get_user_affinity(self.author.id)
20 affinity = self.knowledge.users.get(self.author.id)['BotAffinity']
21 if affinity is None:
22 affinity = 0
21 23
22 24 if affinity < 0: if affinity < 0:
23 25 await self.reply(self.author.mention + " I don't like you.") await self.reply(self.author.mention + " I don't like you.")
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