Skip to content

Commit 704ce16

Browse files
authored
Merge pull request #68 from davidhozic/develop
Scheduled message example
2 parents d4086ca + 5978400 commit 704ce16

File tree

7 files changed

+632
-5
lines changed

7 files changed

+632
-5
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
~ Configuration ~
3+
4+
@Info:
5+
This file contains everything you need
6+
to configure to send scheduled messages.
7+
"""
8+
9+
# USER ACCOUNT TOKEN FOR THE BOT
10+
TOKEN = ""
11+
12+
13+
# TIME SETTINGS
14+
MIN_TO_SEC = 60
15+
HOUR_TO_SEC = 60 * MIN_TO_SEC
16+
DAY_TO_SEC = 24 * HOUR_TO_SEC
17+
18+
# TIMING
19+
CHECK_PERIOD = 20 # Period in second at which to check the files
20+
DELETE_PERIOD = 14 * DAY_TO_SEC # Period after which sent messages will be deleted
21+
22+
# FOLDERS
23+
UPLOAD_FOLDER = "./OBV/UPLOAD_FILES_HERE" # Folder to which to upload json scheduled messages to
24+
UPLOAD_ATTEMPT_FOLDER = "./OBV/LOGS_OF_ATTEMPTS" # Folder to which to send logs
25+
UPLOAD_TEMP_FILE_FOLDER = "./TMP" # Folder for creating temporary files
26+
27+
# AUTHORIZED GUILDS and CHANNELS
28+
## You need to define the guilds and channels to which you want to send here, otherwise the scheduled message will be ignored.
29+
WHITELISTED_GUILDS = [
30+
{"SERVER-ID" : 12345, "CHANNEL-IDs" : [6789, 10111213]}
31+
]
32+
33+
# Channel ID's protected from having messages deleted
34+
DO_NOT_DELETE_CH_IDS = [
35+
12345,
36+
6789,
37+
10120
38+
]
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
"""
2+
~ The advertiser server ~
3+
@Info:
4+
This file is the server script for sending scheduled messages.
5+
You don't need to change anything here, all the configuration can be done in
6+
conf.py file.
7+
"""
8+
9+
10+
from contextlib import suppress
11+
from framework import discord
12+
import framework as fw
13+
import asyncio
14+
import conf
15+
import os
16+
import re
17+
import datetime
18+
import json
19+
import base64
20+
21+
22+
23+
m_awaiting_send = [] # (Ready) Messages buffer waiting to be sent
24+
25+
class UNIMESSAGE:
26+
def __init__(self, text=None, embed=None, files=None, send_date=None, channel=None):
27+
self.text = text
28+
self.embed = embed
29+
self.files = files
30+
31+
self.send_date = send_date
32+
self.channel = channel
33+
34+
class UNIEMBEDEDFIELD:
35+
all_fields = []
36+
def __init__(self, pr_name, pr_text):
37+
self.name = pr_name
38+
self.text = pr_text
39+
40+
class UNIFILE:
41+
class FileSizeException(Exception):
42+
pass
43+
44+
all_files = []
45+
def __init__(self, filename, fdata=None):
46+
self.fname = re.sub(r".*(\/|\\)", "", filename)
47+
self.fdata = fdata
48+
if fdata is None:
49+
stats = os.stat(filename)
50+
if stats.st_size > 8000000:
51+
raise UNIFILE.FileSizeException("Velikost datoteke je lahko najvec 8MB!")
52+
with open(filename, "rb") as l_openedfile:
53+
self.fdata = l_openedfile.read()
54+
55+
def serialize(obj):
56+
"""
57+
~ serialize ~
58+
@Param: UNIMESSAGE object
59+
@Info:
60+
This function converts a UNIMESSAGE objects into json form, so it can be transfered to
61+
an eg. FTP server that then decodes it.
62+
This is only used by the message generator.
63+
"""
64+
ret = None
65+
with suppress(Exception):
66+
ret = {
67+
"text" : obj.text,
68+
"channel" : obj.channel,
69+
"files" : [
70+
{"fname" : file.fname, "fdata" : base64.b64encode(file.fdata).decode("ascii")} for file in obj.files
71+
] if obj.files is not None else None,
72+
"embed" : {
73+
"title" : obj.embed.title if discord.Embed.Empty != obj.embed.title and not isinstance(obj.embed.title, discord.embeds.EmbedProxy) else discord.embeds.EmptyEmbed,
74+
"author" : {"name": obj.embed.author.name, "icon_url": obj.embed.author.icon_url},
75+
"thumbnail" : obj.embed.thumbnail.url,
76+
"image" : obj.embed.image.url,
77+
"fields" : [
78+
{"inline" : field.inline, "name": field.name, "value": field.value} for field in obj.embed.fields
79+
]
80+
} if obj.embed is not None else None,
81+
"send_date" : {
82+
"seconds" : obj.send_date.timestamp(),
83+
"tz" : obj.send_date.tzinfo
84+
}
85+
}
86+
return json.dumps(ret)
87+
88+
def deserialize(data):
89+
"""
90+
~ deserialize ~
91+
@Param: json data
92+
@Info:
93+
This function is used to convert a json file representing a scheduled message
94+
into a UNIMESSAGE object which is a scheduled message object.
95+
"""
96+
ret = None
97+
with suppress(Exception):
98+
ret = UNIMESSAGE()
99+
d = json.loads(data)
100+
ret.text = d["text"]
101+
ret.channel = d["channel"]
102+
ret.send_date = datetime.datetime.fromtimestamp(d["send_date"]["seconds"], d["send_date"]["tz"])
103+
if d["files"] is not None:
104+
ret.files = []
105+
for file in d["files"]:
106+
ret.files.append(UNIFILE(file["fname"], base64.b64decode(file["fdata"])))
107+
if d["embed"] is not None:
108+
ret.embed = discord.Embed()
109+
ret.embed.title = d["embed"]["title"]
110+
ret.embed.set_author(name=d["embed"]["author"]["name"], icon_url=d["embed"]["author"]["icon_url"])
111+
ret.embed.set_thumbnail(url=d["embed"]["thumbnail"])
112+
ret.embed.set_image(url=d["embed"]["image"])
113+
ret.embed._fields = d["embed"]["fields"]
114+
return ret
115+
116+
117+
async def parser():
118+
"""
119+
~ async parser ~
120+
@Param: void
121+
@Info:
122+
This is the task responsible for parsing files inside the OBV/UPLOAD_FILES_HERE folder
123+
, which contains json files that represent a scheduled message.
124+
"""
125+
while True:
126+
for path, dirname, files in os.walk(conf.UPLOAD_FOLDER):
127+
for filename in files:
128+
filepath = os.path.join(path, filename)
129+
filestats = os.stat(filepath)
130+
if filename.endswith(".json") and filestats.st_size < 100 * 10**6: # Ends with bin and smaller than 100 MB
131+
message_contex = None
132+
#os.system("sudo chmod 777 OBV/UPLOAD_FILES_HERE/ -R") # For debugging
133+
with suppress(Exception):
134+
with open(filepath,"r") as op_file:
135+
message_contex = deserialize(op_file.read())
136+
137+
# Successfully parsed
138+
if message_contex is not None:
139+
if message_contex.send_date <= datetime.datetime.now():
140+
m_awaiting_send.append ( message_contex )
141+
142+
if message_contex is None or message_contex.send_date <= datetime.datetime.now():
143+
with suppress(FileNotFoundError):
144+
os.remove(filepath)
145+
146+
else:
147+
with suppress(FileNotFoundError):
148+
os.remove(filepath)
149+
150+
151+
await asyncio.sleep(conf.CHECK_PERIOD)
152+
153+
async def message_deleter():
154+
"""
155+
~ async message_deleter ~
156+
@Param: void
157+
@Info:
158+
This is a task that deletes messages sent by this bot
159+
if they are older than the configured period in conf.py (DELETE_PERIOD).
160+
The resolution is 1 hour.
161+
"""
162+
client = fw.get_client()
163+
channels = []
164+
for guild in conf.WHITELISTED_GUILDS:
165+
guild["CHANNEL-IDs"] = [x for x in [client.get_channel(x) for x in guild["CHANNEL-IDs"] if x not in conf.DO_NOT_DELETE_CH_IDS] if x is not None]
166+
channels.extend(guild["CHANNEL-IDs"] )
167+
168+
while True:
169+
for channel in channels:
170+
try:
171+
async for message in channel.history(limit=20):
172+
if message.author.id == client.user.id and (datetime.datetime.now(tz=message.created_at.tzinfo) - message.created_at).total_seconds() >= conf.DELETE_PERIOD:
173+
for tries in range(3):
174+
try:
175+
await message.delete()
176+
await asyncio.sleep(2)
177+
break
178+
except discord.HTTPException as ex:
179+
if ex.status == 429:
180+
await asyncio.sleep(int(ex.headers["retry-after"]))
181+
except Exception:
182+
break
183+
except discord.HTTPException as ex:
184+
pass
185+
186+
await asyncio.sleep(1*fw.C_HOUR_TO_SECOND)
187+
188+
189+
@fw.data_function
190+
def get_data(ch_id):
191+
"""
192+
~ get_data ~
193+
@Param:
194+
- channel id:
195+
This function is called multiple times for each channel defined in the conf.py
196+
and is used to check which channel the data is to be sent, the function
197+
checks the buffer if any messages in the buffer are meant for this channel
198+
and then returns the data if any or the None object signaling noting is to be sent.
199+
@Info:
200+
This is the data retriever function used by the framework to get the data
201+
that is to be sent into a certain channel.
202+
"""
203+
context = None
204+
for unimsg in m_awaiting_send:
205+
if type(unimsg) is UNIMESSAGE and unimsg.channel == ch_id: # The channel id must match the ch_id as this is the channel that requested data
206+
# Delete any of the temporary files that were possibly created by the previos calls
207+
for filepath in os.listdir(conf.UPLOAD_TEMP_FILE_FOLDER):
208+
filepath = os.path.join(conf.UPLOAD_TEMP_FILE_FOLDER, filepath)
209+
os.remove(filepath)
210+
211+
context = unimsg
212+
m_awaiting_send.remove(unimsg)
213+
214+
l_ret = []
215+
if context.files is not None:
216+
# Create temporary files as the API wrapper library doesn't support direct raw data to be passed
217+
for file_context in context.files:
218+
tmp_file_path = os.path.join(conf.UPLOAD_TEMP_FILE_FOLDER,file_context.fname)
219+
with open(tmp_file_path, "wb") as tmp_file:
220+
tmp_file.write(file_context.fdata)
221+
l_ret.append( fw.FILE(tmp_file_path) )
222+
if context.text is not None:
223+
l_ret.append(context.text)
224+
if context.embed is not None:
225+
l_tr_datum = datetime.datetime.now()
226+
l_fw_embed = fw.EMBED.from_discord_embed(context.embed).set_footer(text="{:02d}.{:02d}.{:04d}\t{:02d}:{:02d}".format(l_tr_datum.day,
227+
l_tr_datum.month,
228+
l_tr_datum.year,
229+
l_tr_datum.hour,
230+
l_tr_datum.minute))
231+
232+
l_ret.append( l_fw_embed )
233+
return l_ret
234+
235+
return None
236+
237+
238+
def main():
239+
"""
240+
~ main ~
241+
@Param: void
242+
@Info:
243+
This is the callback function that is called after the framework is run,
244+
it starts the json file parser where the json files contain the scheduled message
245+
and the message deleter task deletes messages that were sent with the bot and are older
246+
than the configured period.
247+
"""
248+
tasks = [
249+
asyncio.create_task(parser()), # File parser
250+
asyncio.create_task(message_deleter()) # Discord message deleter
251+
]
252+
asyncio.gather(*tasks)
253+
254+
255+
# Create the server list
256+
servers = [
257+
fw.GUILD (
258+
guild_id=guild["SERVER-ID"],
259+
messages_to_send= [
260+
fw.TextMESSAGE(None, 5, get_data(ch_id), [ch_id], "send", True) for ch_id in guild["CHANNEL-IDs"]
261+
],
262+
generate_log=True
263+
) for guild in conf.WHITELISTED_GUILDS
264+
]
265+
266+
267+
if __name__ == "__main__":
268+
fw.run(
269+
conf.TOKEN,
270+
servers,
271+
user_callback=main,
272+
server_log_output=conf.UPLOAD_ATTEMPT_FOLDER,
273+
debug=True)

0 commit comments

Comments
 (0)