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