Add module.

This commit is contained in:
2021-09-12 11:36:22 +03:00
parent d6b3adc27e
commit d2357f0e53
5 changed files with 969 additions and 0 deletions

View File

@@ -0,0 +1,703 @@
import re
import datetime
import discord
import asyncio
from cmdClient.lib import SafeCancellation
from meta import client
from core import Lion
from settings import GuildSettings
from utils.lib import parse_ranges
from . import data
# from .module import module
class Tasklist:
"""
Class representing an interactive updating tasklist.
"""
max_task_length = 100
active = {} # Map (userid, channelid) -> Tasklist
messages = {} # Map messageid -> Tasklist
checkmark = ""
block_size = 15
next_emoji = ""
prev_emoji = ""
question_emoji = ""
cancel_emoji = ""
refresh_emoji = "🔄"
paged_reaction_order = (
prev_emoji, cancel_emoji, question_emoji, refresh_emoji, next_emoji
)
non_paged_reaction_order = (
question_emoji, cancel_emoji, refresh_emoji
)
reaction_hint = "*Press {} for info, {} to exit and {} to refresh.*".format(
question_emoji,
cancel_emoji,
refresh_emoji
)
_re_flags = re.DOTALL | re.IGNORECASE | re.VERBOSE
add_regex = re.compile(
r"^(?: (?:add) | \+) \s+ (.+)",
_re_flags
)
delete_regex = re.compile(
r"^(?: d(?:el(?:ete)?)? | (?: r(?:(?:emove)|m)?) | -) \s* ([0-9, -]+)$",
_re_flags
)
edit_regex = re.compile(
r"^e(?:dit)? \s+ (\d+ \s+ .+)",
_re_flags
)
check_regex = re.compile(
r"^(?: c(?:heck)? | (?: done) | (?: complete))\s* ([0-9, -]+)$",
_re_flags
)
uncheck_regex = re.compile(
r"^(?: u(?:ncheck)? | (?: undone) | (?: uncomplete)) \s* ([0-9, -]+)$",
_re_flags
)
toggle_regex = re.compile(
r"^([0-9, -]+)$",
_re_flags
)
cancel_regex = re.compile(
r"^(cancel)|(exit)|(quit)$",
_re_flags
)
interactive_help = """
Send the following to modify your tasks while the todolist is visible. \
`<taskids>` may be given as comma separated numbers and ranges.
`<taskids>` Toggle the status (checked/unchecked) of the provided tasks.
`add/+ <task>` Add a new TODO `task`. Each line is added as a separate task.
`d/rm/- <taskids>` Remove the specified tasks.
`c/check <taskids>` Check (mark complete) the specified tasks.
`u/uncheck <taskids>` Uncheck (mark incomplete) the specified tasks.
`cancel` Cancel the interactive tasklist mode.
**Examples**
`add Read chapter 1` Adds a new task `Read chapter 1`.
`e 1 Notes chapter 1` Edit task `1` to `Notes chapter 1`.
`d 1, 5-7, 9` Deletes tasks `1, 5, 6, 7, 9`.
`1, 2-5, 9` Toggle the completion status of tasks `1, 2, 3, 4, 5, 9`.
You may also edit your tasklist at any time with `{prefix}todo` (see `{prefix}help todo`).
Note that tasks expire after 24 hours.
""".format(prefix=client.prefix)
def __init__(self, member, channel, activate=True):
self.member = member # Discord Member owning the tasklist
self.channel = channel # Discord Channel for display and input
self.message = None # Discord Message currently displaying the tasklist
self.tasklist = [] # Displayed list of Row(tasklist)
self.pages = [] # Pages to display
self.current_page = None # Current displayed page. None means set automatically
self.show_help = False # Whether to show a help section in the pages
self.has_paging = None # Whether we have added paging reactions
self._refreshed_at = None # Timestamp of the last tasklist refresh
self._deactivation_task = None # Task for scheduled tasklist deactivation
self.interaction_lock = asyncio.Lock() # Lock to ensure interactions execute sequentially
self._deactivated = False # Flag for checking deactivation
if activate:
# Populate the tasklist
self._refresh()
# Add the tasklist to active tasklists
self.active[(member.id, channel.id)] = self
@classmethod
def fetch_or_create(cls, member, channel):
tasklist = cls.active.get((member.id, channel.id), None)
return tasklist if tasklist is not None else cls(member, channel)
def _refresh(self):
"""
Update the in-memory tasklist from data and regenerate the pages
"""
self.tasklist = data.tasklist.fetch_rows_where(
guildid=self.member.guild.id,
userid=self.member.id,
_extra=("AND last_updated_at > timezone('utc', NOW()) - INTERVAL '24h' "
"ORDER BY created_at ASC, taskid ASC")
)
self._refreshed_at = datetime.datetime.utcnow()
def _format_tasklist(self):
"""
Generates a sequence of pages from the tasklist
"""
# Format tasks
task_strings = [
"{num:>{numlen}}. [{mark}] {content}".format(
num=i,
numlen=((self.block_size * (i // self.block_size + 1) - 1) // 10) + 1,
mark=self.checkmark if task.complete else ' ',
content=task.content
)
for i, task in enumerate(self.tasklist)
]
# Split up tasklist into formatted blocks
task_pages = [task_strings[i:i+self.block_size] for i in range(0, len(task_strings), self.block_size)]
task_blocks = [
"```md\n{}```".format('\n'.join(page)) for page in task_pages
]
# Formatting strings and data
page_count = len(task_blocks) or 1
task_count = len(task_strings)
complete_count = len([task for task in self.tasklist if task.complete])
if task_count > 0:
title = "TODO list ({}/{} complete)".format(
complete_count,
task_count,
# ((complete_count * 100) // task_count),
)
if complete_count == task_count:
hint = "You have completed all your tasks! Well done!"
else:
hint = ""
else:
title = "TODO list"
hint = "Type `add <task>` to start adding tasks! E.g. `add Revise Maths Paper 1`."
task_blocks = [""] # Empty page so we can post
# Create formtted page embeds, adding help if required
pages = []
for i, block in enumerate(task_blocks):
embed = discord.Embed(
title=title,
description="{}\n{}\n{}".format(hint, block, self.reaction_hint),
timestamp=self._refreshed_at
).set_author(name=self.member.display_name, icon_url=self.member.avatar_url)
if page_count > 1:
embed.set_footer(text="Page {}/{}".format(i+1, page_count))
if self.show_help:
embed.add_field(
name="Cheatsheet",
value=self.interactive_help
)
pages.append(embed)
self.pages = pages
return pages
def _adjust_current_page(self):
"""
Update the current page number to point to a valid page.
"""
# Calculate or adjust the current page number
if self.current_page is None:
# First page with incomplete task, or the first page
first_incomplete = next((i for i, task in enumerate(self.tasklist) if not task.complete), 0)
self.current_page = first_incomplete // self.block_size
elif self.current_page >= len(self.pages):
self.current_page = len(self.pages) - 1
elif self.current_page < 0:
self.current_page %= len(self.pages)
async def _post(self):
"""
Post the interactive widget, add reactions, and update the message cache
"""
pages = self.pages
# Post the page
message = await self.channel.send(embed=pages[self.current_page])
# Add the reactions
self.has_paging = len(pages) > 1
for emoji in (self.paged_reaction_order if self.has_paging else self.non_paged_reaction_order):
await message.add_reaction(emoji)
# Register
if self.message:
self.messages.pop(self.message.id, None)
self.message = message
self.messages[message.id] = self
async def update(self, repost=None):
"""
Update the displayed tasklist.
If required, delete and repost the tasklist.
"""
if self._deactivated:
return
# Update data and make page list
self._refresh()
self._format_tasklist()
self._adjust_current_page()
if self.message and not repost:
# Read the channel history, see if we need to repost
height = 0
async for message in self.channel.history(limit=20):
if message.id == self.message.id:
break
height += len(message.content.splitlines())
if message.embeds or message.attachments or height > 20:
repost = True
break
if message.id < self.message.id:
# Our message was deleted?
repost = True
break
else:
repost = True
if not repost:
try:
await self.message.edit(embed=self.pages[self.current_page])
# Add or remove paging reactions as required
should_have_paging = len(self.pages) > 1
if self.has_paging != should_have_paging:
try:
await self.message.clear_reactions()
except discord.HTTPException:
pass
if should_have_paging:
reaction_order = self.paged_reaction_order
else:
reaction_order = self.non_paged_reaction_order
for emoji in reaction_order:
await self.message.add_reaction(emoji)
self.has_paging = should_have_paging
except discord.NotFound:
self.messages.pop(self.message.id, None)
self.message = None
repost = True
if not self.message or repost:
if self.message:
# Delete previous message
try:
await self.message.delete()
except discord.HTTPException:
pass
await self._post()
asyncio.create_task(self._schedule_deactivation())
async def deactivate(self, delete=False):
"""
Delete from active tasklists and message cache, and remove the reactions.
If `delete` is given, deletes any output message
"""
self._deactivated = True
if self._deactivation_task and not self._deactivation_task.cancelled():
self._deactivation_task.cancel()
self.active.pop((self.member.id, self.channel.id), None)
if self.message:
self.messages.pop(self.message.id, None)
try:
if delete:
await self.message.delete()
else:
await self.message.clear_reactions()
except discord.HTTPException:
pass
async def _reward_complete(self, *checked_rows):
# Fetch guild task reward settings
guild_settings = GuildSettings(self.member.guild.id)
task_reward = guild_settings.task_reward.value
task_reward_limit = guild_settings.task_reward_limit.value
# Select only tasks that haven't been rewarded before
unrewarded = [task for task in checked_rows if not task['rewarded']]
if unrewarded:
# Select tasks to reward up to the limit of rewards
recent_rewards = data.tasklist_rewards.queries.count_recent_for(self.member.guild.id, self.member.id)
max_to_reward = max((task_reward_limit - recent_rewards, 0))
reward_tasks = unrewarded[:max_to_reward]
rewarding_count = len(reward_tasks)
# reached_max = (recent_rewards + rewarding_count) >= task_reward_limit
reward_coins = task_reward * len(reward_tasks)
if reward_coins:
# Rewarding process, now that we know what we need to reward
# Add coins
user = Lion.fetch(self.member.guild.id, self.member.id)
user.addCoins(reward_coins)
# Mark tasks as rewarded
taskids = [task['taskid'] for task in reward_tasks]
data.tasklist.update_where(
{'rewarded': True},
taskid=taskids,
)
# Track reward
data.tasklist_rewards.insert(
guildid=self.member.guild.id,
userid=self.member.id,
reward_count=rewarding_count
)
# Log reward
client.log(
"Giving '{}' LionCoins to '{}' (uid:{}) for completing TODO tasks.".format(
reward_coins,
self.member,
self.member.id
)
)
# TODO: Message in channel? Might be too spammy?
pass
def _add_tasks(self, *tasks):
"""
Add provided tasks to the task list
"""
insert = [
(self.member.guild.id, self.member.id, task)
for task in tasks
]
return data.tasklist.insert_many(
*insert,
insert_keys=('guildid', 'userid', 'content')
)
def _delete_tasks(self, *indexes):
"""
Delete tasks from the task list
"""
taskids = [self.tasklist[i].taskid for i in indexes]
return data.tasklist.delete_where(
taskid=taskids
)
def _edit_task(self, index, new_content):
"""
Update the provided task with the new content
"""
taskid = self.tasklist[index].taskid
return data.tasklist.update_where(
{
'content': new_content,
'last_updated_at': datetime.datetime.utcnow()
},
taskid=taskid,
)
def _check_tasks(self, *indexes):
"""
Mark provided tasks as complete
"""
taskids = [self.tasklist[i].taskid for i in indexes]
return data.tasklist.update_where(
{
'complete': True,
'last_updated_at': datetime.datetime.utcnow()
},
taskid=taskids,
complete=False,
)
def _uncheck_tasks(self, *indexes):
"""
Mark provided tasks as incomplete
"""
taskids = [self.tasklist[i].taskid for i in indexes]
return data.tasklist.update_where(
{
'complete': False,
'last_updated_at': datetime.datetime.utcnow()
},
taskid=taskids,
complete=True,
)
def _index_range_parser(self, userstr):
"""
Parse user provided task indicies.
"""
try:
indexes = parse_ranges(userstr)
except SafeCancellation:
raise SafeCancellation(
"Couldn't parse the provided task numbers! "
"Please list the task numbers or ranges separated by a comma, e.g. `1, 3, 5-7, 11`."
) from None
return [index for index in indexes if index < len(self.tasklist)]
async def parse_add(self, userstr):
"""
Process arguments to an `add` request
"""
tasks = [line for line in userstr.splitlines() if line]
if not tasks:
# TODO: Maybe have interactive input here
return
# Fetch accurate count of current tasks
count = data.tasklist.select_one_where(
select_columns=("COUNT(*)",),
guildid=self.member.guild.id,
userid=self.member.id
)[0]
# Fetch maximum allowed count
max_task_count = GuildSettings(self.member.guild.id).task_limit.value
# Check if we are exceeding the count
if count + len(tasks) > max_task_count:
raise SafeCancellation("Too many tasks! You can have a maximum of `{}` todo items!".format(max_task_count))
# Check if any task is too long
if any(len(task) > self.max_task_length for task in tasks):
raise SafeCancellation("Please keep your tasks under `{}` characters long.".format(self.max_task_length))
# Finally, add the tasks
self._add_tasks(*tasks)
# Set the current page to the last one
self.current_page = -1
async def parse_delete(self, userstr):
"""
Process arguments to a `delete` request
"""
# Parse provided ranges
indexes = self._index_range_parser(userstr)
if indexes:
self._delete_tasks(*indexes)
async def parse_toggle(self, userstr):
"""
Process arguments to a `toggle` request
"""
# Parse provided ranges
indexes = self._index_range_parser(userstr)
to_check = [index for index in indexes if not self.tasklist[index].complete]
to_uncheck = [index for index in indexes if self.tasklist[index].complete]
if to_uncheck:
self._uncheck_tasks(*to_uncheck)
if to_check:
checked = self._check_tasks(*to_check)
await self._reward_complete(*checked)
async def parse_check(self, userstr):
"""
Process arguments to a `check` request
"""
# Parse provided ranges
indexes = self._index_range_parser(userstr)
if indexes:
checked = self._check_tasks(*indexes)
await self._reward_complete(*checked)
async def parse_uncheck(self, userstr):
"""
Process arguments to an `uncheck` request
"""
# Parse provided ranges
indexes = self._index_range_parser(userstr)
if indexes:
self._uncheck_tasks(*indexes)
async def parse_edit(self, userstr):
"""
Process arguments to an `edit` request
"""
splits = userstr.split(maxsplit=1)
if len(splits) < 2 or not splits[0].isdigit():
raise SafeCancellation("Please provide the task number and the new content, "
"e.g. `edit 1 Biology homework`.")
index = int(splits[0])
new_content = splits[1]
if index >= len(self.tasklist):
raise SafeCancellation(
"You do not have a task number `{}` to edit!".format(index)
)
if len(new_content) > self.max_task_length:
raise SafeCancellation("Please keep your tasks under `{}` characters long.".format(self.max_task_length))
self._edit_task(index, new_content)
self.current_page = index // self.block_size
async def handle_reaction(self, reaction, user, added):
"""
Reaction handler for reactions on our message.
"""
str_emoji = str(reaction.emoji)
if added and str_emoji in self.paged_reaction_order:
# Attempt to remove reaction
try:
await self.message.remove_reaction(reaction.emoji, user)
except discord.HTTPException:
pass
old_message_id = self.message.id
async with self.interaction_lock:
# Return if the message changed while we were waiting
if self.message.id != old_message_id:
return
if str_emoji == self.next_emoji and user.id == self.member.id:
self.current_page += 1
self.current_page %= len(self.pages)
if self.show_help:
self.show_help = False
self._format_tasklist()
await self.message.edit(embed=self.pages[self.current_page])
elif str_emoji == self.prev_emoji and user.id == self.member.id:
self.current_page -= 1
self.current_page %= len(self.pages)
if self.show_help:
self.show_help = False
self._format_tasklist()
await self.message.edit(embed=self.pages[self.current_page])
elif str_emoji == self.cancel_emoji and user.id == self.member.id:
await self.deactivate(delete=True)
elif str_emoji == self.question_emoji and user.id == self.member.id:
self.show_help = not self.show_help
self._format_tasklist()
await self.message.edit(embed=self.pages[self.current_page])
elif str_emoji == self.refresh_emoji and user.id == self.member.id:
await self.update()
async def handle_message(self, message, content=None):
"""
Message handler for messages from out member, in the correct channel.
"""
content = content or message.content
funcmap = {
self.add_regex: self.parse_add,
self.delete_regex: self.parse_delete,
self.check_regex: self.parse_check,
self.uncheck_regex: self.parse_uncheck,
self.toggle_regex: self.parse_toggle,
self.edit_regex: self.parse_edit,
self.cancel_regex: self.deactivate,
}
async with self.interaction_lock:
for reg, func in funcmap.items():
matches = re.search(reg, content)
if matches:
try:
await func(matches.group(1))
await self.update()
except SafeCancellation as e:
embed = discord.Embed(
description=e.msg,
colour=discord.Colour.red()
)
await message.reply(embed=embed)
else:
try:
await message.delete()
except discord.HTTPException:
pass
break
async def _schedule_deactivation(self):
"""
Automatically deactivate the tasklist after some time has passed, and many messages have been sent.
"""
delay = 5 * 10
# Remove previous scheduled task
if self._deactivation_task and not self._deactivation_task.cancelled():
self._deactivation_task.cancel()
# Schedule a new task
try:
self._deactivation_task = asyncio.create_task(asyncio.sleep(delay))
await self._deactivation_task
except asyncio.CancelledError:
return
# If we don't have a message, nothing to do
if not self.message:
return
# If we were updated in that time, go back to sleep
if datetime.datetime.utcnow().timestamp() - self._refreshed_at.timestamp() < delay:
asyncio.create_task(self._schedule_deactivation)
return
# Check if lots of content has been sent since
height = 0
async for message in self.channel.history(limit=20):
if message.id == self.message.id:
break
height += len(message.content.splitlines())
if message.embeds or message.attachments:
height += 10
if height >= 100:
break
if message.id < self.message.id:
# Our message was deleted?
return
else:
height = 100
if height >= 100:
await self.deactivate()
else:
asyncio.create_task(self._schedule_deactivation())
@client.add_after_event("message")
async def tasklist_message_handler(client, message):
key = (message.author.id, message.channel.id)
if key in Tasklist.active:
await Tasklist.active[key].handle_message(message)
@client.add_after_event("reaction_add")
async def tasklist_reaction_add_handler(client, reaction, user):
if user != client.user and reaction.message.id in Tasklist.messages:
await Tasklist.messages[reaction.message.id].handle_reaction(reaction, user, True)
# @module.launch_task
# Commented because we don't actually need to expire these
async def tasklist_expiry_watchdog(client):
removed = data.tasklist.queries.expire_old_tasks()
if removed:
client.log(
"Remove {} stale todo tasks.".format(len(removed)),
context="TASKLIST_EXPIRY",
post=True
)

111
bot/modules/todo/admin.py Normal file
View File

@@ -0,0 +1,111 @@
from settings import GuildSettings, GuildSetting
import settings
from wards import guild_admin
from .data import tasklist_channels
@GuildSettings.attach_setting
class task_limit(settings.Integer, GuildSetting):
category = "TODO List"
attr_name = "task_limit"
_data_column = "max_tasks"
display_name = "task_limit"
desc = "Maximum number of tasks each user may have."
_default = 30
long_desc = (
"Maximum number of tasks each user may have in the todo system."
)
_accepts = "An integer number of tasks."
@property
def success_response(self):
return "The task limit is now `{}`.".format(self.formatted)
@GuildSettings.attach_setting
class task_reward(settings.Integer, GuildSetting):
category = "TODO List"
attr_name = "task_reward"
_data_column = "task_reward"
display_name = "task_reward"
desc = "Number of LionCoins given for each completed TODO task."
_default = 250
long_desc = (
"LionCoin reward given for completing each task on the TODO list."
)
_accepts = "An integer number of coins."
@property
def success_response(self):
return "Task completion will now reward `{}` LionCoins.".format(self.formatted)
@GuildSettings.attach_setting
class task_reward_limit(settings.Integer, GuildSetting):
category = "TODO List"
attr_name = "task_reward_limit"
_data_column = "task_reward_limit"
display_name = "task_reward_limit"
desc = "Maximum number of task rewards given in each 24h period."
_default = 10
long_desc = (
"Maximum number of times in each 24h period that TODO task completion can reward LionCoins."
)
_accepts = "An integer number of times."
@property
def success_response(self):
return "LionCoins will only be reward `{}` timers per 24h".format(self.formatted)
@GuildSettings.attach_setting
class tasklist_channels_setting(settings.ChannelList, settings.ListData, settings.Setting):
category = "TODO List"
attr_name = 'tasklist_channels'
_table_interface = tasklist_channels
_id_column = 'guildid'
_data_column = 'channelid'
_setting = settings.TextChannel
write_ward = guild_admin
display_name = "todo_channels"
desc = "Channels where members may use the todo list."
_force_unique = True
long_desc = (
"Members will only be allowed to use the `todo` command in these channels."
)
# Flat cache, no need to expire objects
_cache = {}
@property
def success_response(self):
if self.value:
return "The todo channels have been updated:\n{}".format(self.formatted)
else:
return "The `todo` command may now be used anywhere."
@property
def formatted(self):
if not self.data:
return "All channels!"
else:
return super().formatted

View File

@@ -0,0 +1,115 @@
import asyncio
import discord
from cmdClient.checks import in_guild
from .module import module
from .Tasklist import Tasklist
@module.cmd(
name="todo",
desc="Display and edit your personal TODO list.",
group="Productivity",
flags=('add==', 'delete==', 'check==', 'uncheck==', 'edit==')
)
@in_guild()
async def cmd_todo(ctx, flags):
"""
Usage``:
{prefix}todo
{prefix}todo <tasks>
{prefix}todo add <tasks>
{prefix}todo delete <taskids>
{prefix}todo check <taskids>
{prefix}todo uncheck <taskids>
{prefix}todo edit <taskid> <new task>
Description:
Open your personal interactive TODO list with `{prefix}todo`, \
and start adding tasks by sending `add your_task_here`. \
Press ❔ to see more ways to use the interactive list.
You can also use the commands above to modify your TODOs (see the examples below).
You may add several tasks at once by writing them on different lines \
(type Shift-Enter to make a new line on the desktop client).
Examples::
{prefix}todo: Open your TODO list.
{prefix}todo My New task: Add `My New task`.
{prefix}todo delete 1, 3-5: Delete tasks `1, 3, 4, 5`.
{prefix}todo check 1, 2: Mark tasks `1` and `2` as done.
{prefix}todo edit 1 My new task: Edit task `1`.
"""
tasklist_channels = ctx.guild_settings.tasklist_channels.value
if tasklist_channels and ctx.ch not in tasklist_channels:
visible = [channel for channel in tasklist_channels if channel.permissions_for(ctx.author).read_messages]
if not visible:
prompt = "The `todo` command may not be used here!"
elif len(visible) == 1:
prompt = (
"The `todo` command may not be used here! "
"Please go to {}."
).format(visible[0].mention)
else:
prompt = (
"The `todo` command may not be used here! "
"Please go to one of the following.\n{}"
).format(' '.join(vis.mention for vis in visible))
out_msg = await ctx.msg.reply(
embed=discord.Embed(
description=prompt,
colour=discord.Colour.red()
)
)
await asyncio.sleep(60)
try:
await out_msg.delete()
await ctx.msg.delete()
except discord.HTTPException:
pass
return
# TODO: Custom module, with pre-command hooks
tasklist = Tasklist.fetch_or_create(ctx.author, ctx.ch)
keys = {
'add': (('add', ), tasklist.parse_add),
'check': (('check', 'done', 'complete'), tasklist.parse_check),
'uncheck': (('uncheck', 'uncomplete'), tasklist.parse_uncheck),
'edit': (('edit',), tasklist.parse_edit),
'delete': (('delete',), tasklist.parse_delete)
}
# Handle subcommands
cmd = None
args = ctx.args
splits = args.split(maxsplit=1)
if len(splits) > 1:
maybe_cmd = splits[0].lower()
for key, (aliases, _) in keys.items():
if maybe_cmd in aliases:
cmd = key
break
# Default to adding if no command given
if cmd:
args = splits[1].strip()
elif args:
cmd = 'add'
async with tasklist.interaction_lock:
# Run required parsers
for key, (_, func) in keys.items():
if flags[key] or cmd == key:
await func((flags[key] or args).strip())
if not (any(flags.values()) or args):
# Force a repost if no flags were provided
await tasklist.update(repost=True)
else:
# Delete if the tasklist already had a message
if tasklist.message:
try:
await ctx.msg.delete()
except discord.HTTPException:
pass
await tasklist.update()

37
bot/modules/todo/data.py Normal file
View File

@@ -0,0 +1,37 @@
from data import RowTable, Table
tasklist = RowTable(
'tasklist',
('taskid', 'guildid', 'userid', 'content', 'complete', 'rewarded', 'created_at', 'last_updated_at'),
'taskid'
)
@tasklist.save_query
def expire_old_tasks():
with tasklist.conn:
with tasklist.conn.cursor() as curs:
curs.execute(
"DELETE FROM tasklist WHERE "
"last_updated_at < timezone('utc', NOW()) - INTERVAL '7d' "
"RETURNING *"
)
return curs.fetchall()
tasklist_channels = Table('tasklist_channels')
tasklist_rewards = Table('tasklist_reward_history')
@tasklist_rewards.save_query
def count_recent_for(guildid, userid, interval='24h'):
with tasklist_rewards.conn:
with tasklist_rewards.conn.cursor() as curs:
curs.execute(
"SELECT SUM(reward_count) FROM tasklist_reward_history "
"WHERE "
"guildid = {} AND userid = {}"
"AND reward_time > timezone('utc', NOW()) - INTERVAL '{}'".format(guildid, userid, interval)
)
return curs.fetchone()[0] or 0

View File

@@ -0,0 +1,3 @@
from LionModule import LionModule
module = LionModule("Todo")