List of commits:
Subject Hash Author Date (UTC)
Added scheduler to task core. Modified comm for new event. ed51d5d18684dabdb17675294727ad5e8b8af454 Detche 2016-11-13 19:44:05
Started adding possibility to save in database. cf28a7731335cd1ffd2ebd03cc2e890c25e1c64b Detche 2016-11-13 19:43:21
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 ed51d5d18684dabdb17675294727ad5e8b8af454 - Added scheduler to task core. Modified comm for new event.
Author: Detche
Author date (UTC): 2016-11-13 19:44
Committer name: Detche
Committer date (UTC): 2016-11-13 19:44
Parent(s): cf28a7731335cd1ffd2ebd03cc2e890c25e1c64b
Signing key:
Tree: 21f4c99949eb62fb78ae601651c91e41d09817fb
File Lines added Lines deleted
botly/bot.py 10 4
botly/comm.py 14 1
botly/reaction.py 5 2
botly/task.py 158 21
botly/trigger.py 3 1
File botly/bot.py changed (mode: 100644) (index b0047a2..32af3d8)
... ... class Bot:
19 19
20 20 def __init__(self, rootModule, dbPath): def __init__(self, rootModule, dbPath):
21 21 """Initialization loads database, reactions and tasks from drive.""" """Initialization loads database, reactions and tasks from drive."""
22 self.me = False
23
22 self.me = None
23 self.server = None
24
24 25 # Load database and models # Load database and models
25 26 Db.load(dbPath) Db.load(dbPath)
26 27 self.settings = Settings() self.settings = Settings()
 
... ... class Bot:
31 32
32 33 # Load the bot background tasks # Load the bot background tasks
33 34 for task in load_tasks(self, rootModule + '.tasks'): for task in load_tasks(self, rootModule + '.tasks'):
34 self.comm.loop.create_task(task.run())
35 self.comm.loop.create_task(task._run())
35 36
36 37 # Load the bot behaviour (reactions to events) # Load the bot behaviour (reactions to events)
37 38 self.behaviour = Behaviour(load_reactions(self, self.behaviour = Behaviour(load_reactions(self,
38 39 rootModule + '.reactions')) rootModule + '.reactions'))
39 40
41 # Set the default channel that was saved in bot settings
42 self.mainChannel = discord.Object(
43 id=self.settings.get_string('DefaultChannelId'))
44
40 45 def live(self): def live(self):
41 46 """Runs the bot until it is exited. """ """Runs the bot until it is exited. """
42 47 # Listen to Discord's events (blocking call) # Listen to Discord's events (blocking call)
 
... ... class Bot:
55 60 """Coroutine that send a message to discord.""" """Coroutine that send a message to discord."""
56 61 await self.comm.send_message(channel, message) await self.comm.send_message(channel, message)
57 62
58 def set_whoami(self, user):
63 def set_bot_info(self, user, server):
59 64 """This will be called once the bot is connected to Discord.""" """This will be called once the bot is connected to Discord."""
60 65 self.me = user self.me = user
66 self.server = server
61 67
62 68 async def react_to(self, eventName, **eventInfo): async def react_to(self, eventName, **eventInfo):
63 69 """Coroutine that will be called upon events. Checks for triggers.""" """Coroutine that will be called upon events. Checks for triggers."""
File botly/comm.py changed (mode: 100644) (index 66f2085..85d021b)
... ... class Comm(discord.Client):
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.bot.set_whoami(self.user)
19 server = None
20 for s in self.servers:
21 server = s
22 self.bot.set_bot_info(self.user, server)
20 23 await self.bot.react_to('on_ready') await self.bot.react_to('on_ready')
21 24
22 25 async def on_typing(self, channel, user, when): async def on_typing(self, channel, user, when):
 
... ... class Comm(discord.Client):
24 27 await self.bot.react_to('on_typing', channel=channel, user=user, await self.bot.react_to('on_typing', channel=channel, user=user,
25 28 when=when) when=when)
26 29
30 async def on_reaction_add(self, reaction, user):
31 if user != self.user:
32 await self.bot.react_to('on_reaction_add', reaction=reaction,
33 user=user)
34
35 async def on_reaction_remove(self, reaction, user):
36 if user != self.user:
37 await self.bot.react_to('on_reaction_remove', reaction=reaction,
38 user=user)
39
27 40 async def on_message_edit(self, before, after): async def on_message_edit(self, before, after):
28 41 if before.author != self.user: if before.author != self.user:
29 42 await self.bot.react_to('on_message_edit', message=before, await self.bot.react_to('on_message_edit', message=before,
File botly/reaction.py changed (mode: 100644) (index 99c136c..e27b1bf)
... ... class ReactionBase:
41 41 It should prepare the given trigger by defining its conditions. It should prepare the given trigger by defining its conditions.
42 42 It does not require to return anything It does not require to return anything
43 43 """ """
44 pass
44 raise NotImplementedError
45 45
46 46 async def react(self): async def react(self):
47 47 """Method to be subclassed. Called when conditions were met.""" """Method to be subclassed. Called when conditions were met."""
48 pass
48 raise NotImplementedError
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."""
 
... ... class ReactionBase:
99 99 elif 'on_member_update' == self.eventName: elif 'on_member_update' == self.eventName:
100 100 self.before = eventInfo['before'] self.before = eventInfo['before']
101 101 self.after = eventInfo['after'] self.after = eventInfo['after']
102 elif 'reaction' in self.eventName:
103 self.reaction = eventInfo['reaction']
104 self.user = eventInfo['user']
102 105
103 106
104 107 def load_reactions(bot, reactionsParent): def load_reactions(bot, reactionsParent):
File botly/task.py changed (mode: 100644) (index 6cf8f59..0e0cfa8)
... ... import asyncio
5 5 from importlib import import_module from importlib import import_module
6 6 from os import listdir from os import listdir
7 7 from os.path import isfile, join from os.path import isfile, join
8 import datetime
8 9
9 10 import discord import discord
10 11
12 FREQUENCY_NONE = 0
13 FREQUENCY_INTERVAL = 1
14 FREQUENCY_SCHEDULE = 2
15
16 GRANULARITY_SECOND = 1
17 GRANULARITY_MINUTE = 60
18 GRANULARITY_HOUR = 3600
19 GRANULARITY_DAY = 86400
20
21 DAYS_ALL = 127
22 DAYS_WEEKDAYS = 31
23 DAYS_WEEKEND = 96
24
25 DAY_MONDAY = 1
26 DAY_TUESDAY = 2
27 DAY_WEDNESDAY = 4
28 DAY_THURSDAY= 8
29 DAY_FRIDAY = 16
30 DAY_SATURDAY = 32
31 DAY_SUNDAY = 64
11 32
12 33 class TaskBase: class TaskBase:
13 34 """Base class for bot's background tasks""" """Base class for bot's background tasks"""
14 35
15 def set_instance_info(self, bot):
36 def init(self):
37 """Initialize the task. This should be subclassed to define timeout.
38
39 Timeouts can be defined either using a schedule or fixed interval.
40 Available methods are:
41 self.set_interval(interval, granularity=GRANULARITY_SECOND)
42 self.set_schedule(days=DAYS_ALL, time=datetime.time(0,0))
43
44 Only one of these should be called."""
45 raise NotImplementedError
46
47 async def do(self):
48 """Proceed with the task's action. To be overwritten when inherited"""
49 raise NotImplementedError
50
51 def set_interval(self, interval, granularity=GRANULARITY_SECOND):
52 """Defines an interval in seconds at which the task will be ran.
53
54 This cannot be used along with the [AUTRE FONCTION] method."""
55
56 self.interval = interval * granularity
57 self.frequency = FREQUENCY_INTERVAL
58
59 def set_schedule(self, days=DAYS_ALL, time=datetime.time(0, 0)):
60 """Schedule the timeout to given days at the given time.
61
62 Use bitwise operators to combine days. Time should be declared under
63 the format HH:MM."""
64
65 self.frequency = FREQUENCY_SCHEDULE
66 self.scheduleTime = time
67 self.scheduleDays = days
68
69 def print(self, message):
70 self.bot.print('[Tasks][' + self.moduleName + ']' + ' ' + message)
71
72 async def announce(self, message):
73 await self.bot.say(self.bot.mainChannel, message)
74
75 def _set_instance_info(self, bot):
76 """Cache instance metadata"""
77 self.frequency = FREQUENCY_NONE
78
16 79 self.endTask = False self.endTask = False
17 80 self.bot = bot self.bot = bot
18 81 self.knowledge = bot.knowledge self.knowledge = bot.knowledge
19 82 self.settings = bot.settings self.settings = bot.settings
20 83 self.comm = bot.comm self.comm = bot.comm
21 84
22 self.mainChannel = discord.Object(
23 id=bot.settings.get_string('DefaultChannelId'))
85 self.maxDaysLookup=self.settings.get_int('MaximumTaskDaysLookup', '15')
86 self.minimumTimeout = bot.settings.get_int('MinimumTaskTimeout', '10')
24 87
25 def print(self, message):
26 self.bot.print('[Tasks][' + self.moduleName + ']' + ' ' + message)
88 # Call the init method that should have been redefined in subclass
89 self.init()
90
91 def _set_module_name(self, name):
92 """So name of this task based on the filename will be cached."""
93 self.moduleName = name
27 94
28 async def run(self):
29 """Entry point of task. Do not overwrite."""
95 async def _run(self):
96 """Will start the task scheduler. Do not overwrite."""
30 97
31 98 # Wait until the bot is ready before running the task # Wait until the bot is ready before running the task
32 99 await self.comm.wait_until_ready() await self.comm.wait_until_ready()
33 100
34 101 while not self.comm.is_closed or not self.endTask: while not self.comm.is_closed or not self.endTask:
35 await asyncio.sleep(self.get_next_timeout())
102 timeout = self._get_next_timeout()
103 if timeout is None or timeout == 0:
104 break
105 # Make sure we don't go faster than we are allowed to
106 if timeout < self.minimumTimeout:
107 timeout = self.minimumTimeout
108 # We'll wait till the task is allowed to run
109 await asyncio.sleep(timeout)
110 # Was the communication after waiting for the trigger?
36 111 if self.comm is None or self.comm.is_closed or self.endTask: if self.comm is None or self.comm.is_closed or self.endTask:
37 112 break break
113 # Execute the task
38 114 await self.do() await self.do()
39
40 def get_next_timeout(self):
41 """Should return the time, in sec, to wait before each do() call."""
42 return 10
43
44 def do(self):
45 """Proceed with the task's action. To be overwritten when inherited"""
46 pass
47
48 def set_module_name(self, name):
49 self.moduleName = name
115 self.print('Task ended')
116
117 def _is_day_scheduled(self, day):
118 """Returns whether or not given weekday is scheduled for this task."""
119 if self.frequency != FREQUENCY_SCHEDULE:
120 return False
121 d = self.scheduleDays
122 if day == 0 and d & DAY_MONDAY:
123 return True
124 elif day == 1 and d & DAY_TUESDAY:
125 return True
126 elif day == 2 and d & DAY_WEDNESDAY:
127 return True
128 elif day == 3 and d & DAY_THURSDAY:
129 return True
130 elif day == 4 and d & DAY_FRIDAY:
131 return True
132 elif day == 5 and d & DAY_SATURDAY:
133 return True
134 elif day == 6 and d & DAY_SUNDAY:
135 return True
136 else:
137 return False
138
139 def _get_next_timeout(self):
140 """Will return the time, in sec, to wait before each do() call."""
141 msg = 'Task ' + self.moduleName + ' will run in {0} seconds.'
142 if self.frequency == FREQUENCY_INTERVAL:
143 return self.interval
144 elif self.frequency == FREQUENCY_SCHEDULE:
145 # Initial search will be today at hour+minute specified by the
146 # task (subclass)
147 search = datetime.datetime.now()
148 search = search.replace(hour=self.scheduleTime.hour,
149 minute=self.scheduleTime.minute,
150 second=0)
151
152 # Cache current time so we don't call it on every loop iteration
153 # Place it a bit forward in the future, so we are sure we
154 # cannot trigger an event twice or more times. Indeed, after a task
155 # runs, we shedule its next execution.
156 # Lets make sure it cannot happen again if its first execution
157 # happens real fast.
158 # TODO: Can this really happen?
159 now = datetime.datetime.now()
160 now += datetime.timedelta(minutes=1)
161
162 # Find the next possible datetime
163 daysHop = 0
164 while not (self._is_day_scheduled(search.weekday())
165 and search > now):
166 # Make sure we are not lock into looking for a day that will
167 # never be found.
168 daysHop += 1
169 if (daysHop > self.maxDaysLookup):
170 self.print('Maximum days lookup of '
171 + str(self.maxDaysLookup)
172 + ' reached. Task will be ended.')
173 return None
174 # Add a day to the search
175 search += datetime.timedelta(days=1)
176
177 # Calculate, round and return the seconds till the next scheduled
178 # execution of the task
179 waitDelta = search - datetime.datetime.now()
180 seconds = round(waitDelta.total_seconds())
181 self.print('Task will run in ' + str(seconds) + ' seconds.')
182 return seconds
183
184 else:
185 self.print('No frequency set for task. It will be ended.')
186 return None
50 187
51 188 def load_tasks(bot, tasksParent): def load_tasks(bot, tasksParent):
52 189 """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):
67 204 task = _taskModule.Task() task = _taskModule.Task()
68 205 tasks.append(task) tasks.append(task)
69 206 # Inject instance info in task object # Inject instance info in task object
70 task.set_instance_info(bot)
71 task.set_module_name(module)
207 task._set_instance_info(bot)
208 task._set_module_name(module)
72 209 print('Loaded ' + str(len(tasks)) + ' tasks from drive.') print('Loaded ' + str(len(tasks)) + ' tasks from drive.')
73 210 return tasks return tasks
74 211
File botly/trigger.py changed (mode: 100644) (index 29e6975..049d3b0)
... ... class Trigger:
37 37 elif 'author' == variable: elif 'author' == variable:
38 38 assert eventName != 'on_ready', \ assert eventName != 'on_ready', \
39 39 'author not supported for on_ready event.' 'author not supported for on_ready event.'
40
40
41 # TODO: add possibility to react on reactions
42
41 43 condition = [] condition = []
42 44 condition.append(variable) condition.append(variable)
43 45 condition.append(pattern) condition.append(pattern)
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