feat(twitch): Single-task feature parity
This commit is contained in:
72
data.py
72
data.py
@@ -0,0 +1,72 @@
|
|||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from data import Registry, RowModel, Table
|
||||||
|
from data.columns import String, Timestamp, Integer, Bool
|
||||||
|
|
||||||
|
|
||||||
|
class Task(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
"""
|
||||||
|
_tablename_ = 'tasklist'
|
||||||
|
_cache_ = {}
|
||||||
|
|
||||||
|
taskid = Integer(primary=True)
|
||||||
|
profileid = Integer()
|
||||||
|
content = String()
|
||||||
|
created_at = Timestamp()
|
||||||
|
deleted_at = Timestamp()
|
||||||
|
duration = Integer()
|
||||||
|
started_at = Timestamp()
|
||||||
|
completed_at = Timestamp()
|
||||||
|
_timestamp = Timestamp()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskProfile(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
"""
|
||||||
|
_tablename_ = 'task_profiles'
|
||||||
|
_cache_ = {}
|
||||||
|
|
||||||
|
profileid = Integer(primary=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskInfo(RowModel):
|
||||||
|
_tablename_ = 'tasklist_info'
|
||||||
|
|
||||||
|
taskid = Integer(primary=True)
|
||||||
|
profileid = Integer()
|
||||||
|
content = String()
|
||||||
|
created_at = Timestamp()
|
||||||
|
duration = Integer()
|
||||||
|
started_at = Timestamp()
|
||||||
|
completed_at = Timestamp()
|
||||||
|
|
||||||
|
last_started = Timestamp()
|
||||||
|
nowid = Integer()
|
||||||
|
order_idx = Integer()
|
||||||
|
is_running = Bool()
|
||||||
|
is_planned = Bool()
|
||||||
|
is_complete = Bool()
|
||||||
|
|
||||||
|
tasklabel = Integer()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_duration(self):
|
||||||
|
dur = self.duration
|
||||||
|
if self.is_running and self.last_started:
|
||||||
|
dur += (dt.datetime.now(tz=dt.UTC) - self.last_started).total_seconds()
|
||||||
|
return int(dur)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TaskData(Registry):
|
||||||
|
tasklist = Task.table
|
||||||
|
task_profiles = TaskProfile.table
|
||||||
|
tasklist_info = TaskInfo.table
|
||||||
|
|
||||||
|
nowlist = Table('nowlist')
|
||||||
|
taskplan = Table('taskplan')
|
||||||
|
|||||||
2
notes.md
2
notes.md
@@ -96,6 +96,8 @@ Okay, I want all these things, but I think a lot of these are a step too far rig
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
We definitely want Tips as well, for a kind of docs, flow guidlines.
|
||||||
|
|
||||||
|
|
||||||
# Data structure
|
# Data structure
|
||||||
|
|
||||||
|
|||||||
256
tasklist.py
Normal file
256
tasklist.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
from data import Database
|
||||||
|
from utils.lib import utc_now
|
||||||
|
|
||||||
|
from .data import Task, TaskInfo, TaskProfile, TaskData
|
||||||
|
|
||||||
|
|
||||||
|
class TasklistParseCreateError(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Tasklist:
|
||||||
|
"""
|
||||||
|
Represents the tasklist for a single user.
|
||||||
|
"""
|
||||||
|
profileid: int
|
||||||
|
data: TaskData
|
||||||
|
id_tasks: dict[int, TaskInfo]
|
||||||
|
label_ids: dict[int, int]
|
||||||
|
current: Optional[int]
|
||||||
|
plan: list[int]
|
||||||
|
|
||||||
|
taskspec_re = re.compile(
|
||||||
|
r"^(?P<start>\d+)((\s*(?P<range>-)\s*)(?P<end>\d*))?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, profileid: int, tasks: list[TaskInfo], data: TaskData, update_callback=None):
|
||||||
|
self.profileid = profileid
|
||||||
|
self.data = data
|
||||||
|
self._set_tasks(tasks)
|
||||||
|
|
||||||
|
self._callback = update_callback
|
||||||
|
|
||||||
|
async def refresh(self):
|
||||||
|
tasks = await TaskInfo.fetch_where(profileid=self.profileid)
|
||||||
|
self._set_tasks(tasks)
|
||||||
|
|
||||||
|
def _set_tasks(self, tasks: list[TaskInfo]):
|
||||||
|
self.id_tasks = {task.taskid: task for task in tasks}
|
||||||
|
self.label_ids = {task.tasklabel: task.taskid for task in tasks}
|
||||||
|
self.current = next((task.taskid for task in tasks if task.is_running), None)
|
||||||
|
plan = [task for task in tasks if task.is_planned]
|
||||||
|
plan.sort(key=lambda task: task.order_idx)
|
||||||
|
self.plan = [task.taskid for task in plan]
|
||||||
|
|
||||||
|
async def on_update(self):
|
||||||
|
await self.refresh()
|
||||||
|
if self._callback is not None:
|
||||||
|
await self._callback(self)
|
||||||
|
|
||||||
|
async def create_tasks(self, *contents, **kwargs) -> list[TaskInfo]:
|
||||||
|
if not contents:
|
||||||
|
return []
|
||||||
|
|
||||||
|
rows = await self.data.tasklist.insert_many(
|
||||||
|
('profileid', 'content', *kwargs.keys()),
|
||||||
|
*((self.profileid, content, *kwargs.values()) for content in contents)
|
||||||
|
)
|
||||||
|
taskids = [row['taskid'] for row in rows]
|
||||||
|
info = await TaskInfo.fetch_where(taskid=taskids)
|
||||||
|
await self.on_update()
|
||||||
|
# self.id_tasks.update({task.taskid: task for task in info})
|
||||||
|
# self.label_ids.update({task.tasklabel: task.taskid for task in info})
|
||||||
|
return info
|
||||||
|
|
||||||
|
async def edit_task(self, taskid, new_content):
|
||||||
|
await self.data.tasklist.update_where(taskid=taskid).set(content=new_content)
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
async def delete_tasks(self, *taskids):
|
||||||
|
"""
|
||||||
|
Mark the given taskids as deleted.
|
||||||
|
"""
|
||||||
|
if taskids:
|
||||||
|
await self.data.tasklist.update_where(taskid=taskids).set(deleted_at=utc_now())
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
def get_plan(self) -> list[TaskInfo]:
|
||||||
|
return [self.id_tasks[id] for id in self.plan]
|
||||||
|
|
||||||
|
async def push_plan_head(self, *taskids: int):
|
||||||
|
plan = self.get_plan()
|
||||||
|
min_idx = min([task.order_idx for task in plan], default=0)
|
||||||
|
shift = len(taskids)
|
||||||
|
plan_data = zip(taskids, range(min_idx - shift, min_idx))
|
||||||
|
await self.data.taskplan.delete_where(taskid=taskids)
|
||||||
|
await self.data.taskplan.insert_many(
|
||||||
|
('taskid', 'order_idx'),
|
||||||
|
*plan_data
|
||||||
|
)
|
||||||
|
# Refreshing here instead of modifying cache
|
||||||
|
# Because the planned flag will be wrong on the saved taskinfo as well
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
async def push_plan_tail(self, *taskids: int):
|
||||||
|
plan = self.get_plan()
|
||||||
|
max_idx = max([task.order_idx for task in plan], default=0)
|
||||||
|
shift = len(taskids)
|
||||||
|
plan_data = zip(taskids, range(max_idx + 1, max_idx + shift + 1))
|
||||||
|
await self.data.taskplan.delete_where(taskid=taskids)
|
||||||
|
await self.data.taskplan.insert_many(
|
||||||
|
('taskid', 'order_idx'),
|
||||||
|
*plan_data
|
||||||
|
)
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
async def set_plan(self, *taskids: int):
|
||||||
|
# TODO: Should really be in a transaction
|
||||||
|
await self.data.taskplan.delete_where(profileid=self.profileid)
|
||||||
|
if taskids:
|
||||||
|
plan_data = zip(taskids, range(len(taskids)))
|
||||||
|
await self.data.taskplan.insert_many(('taskid', 'order_idx'), *plan_data)
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
def get_current(self) -> Optional[TaskInfo]:
|
||||||
|
return self.id_tasks[self.current] if self.current is not None else None
|
||||||
|
|
||||||
|
async def set_now(self, taskid: int):
|
||||||
|
# Unset current task if it exists
|
||||||
|
await self.unset_now()
|
||||||
|
task = self.id_tasks[taskid]
|
||||||
|
await self.data.nowlist.insert(taskid=taskid, last_started=None if task.is_complete else utc_now())
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
async def unset_now(self):
|
||||||
|
# Unset the current task and update the duration correctly
|
||||||
|
# Does not put the current task on the plan
|
||||||
|
current = self.get_current()
|
||||||
|
if current is not None:
|
||||||
|
now = utc_now()
|
||||||
|
assert current.is_running or (current.last_started is None)
|
||||||
|
if current.is_running:
|
||||||
|
duration = (now - current.last_started).total_seconds()
|
||||||
|
duration += current.duration
|
||||||
|
await self.data.tasklist.update_where(taskid=current.taskid).set(duration=duration)
|
||||||
|
await self.data.nowlist.delete_where(taskid=current.taskid)
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
async def complete_tasks(self, *taskids) -> list[TaskInfo]:
|
||||||
|
# Remove any tasks which are already complete
|
||||||
|
# TODO: Transaction
|
||||||
|
taskids = [id for id in taskids if not self.id_tasks[id].is_complete]
|
||||||
|
if taskids:
|
||||||
|
now = utc_now()
|
||||||
|
await self.data.tasklist.update_where(taskid=taskids).set(completed_at=now)
|
||||||
|
if self.current in taskids:
|
||||||
|
current = self.get_current()
|
||||||
|
assert current is not None
|
||||||
|
assert current.last_started is not None
|
||||||
|
|
||||||
|
duration = (utc_now() - current.last_started).total_seconds() + current.duration
|
||||||
|
await self.data.tasklist.update_where(taskid=self.current).set(duration=duration)
|
||||||
|
await self.data.nowlist.update_where(taskid=self.current).set(last_started=None)
|
||||||
|
await self.on_update()
|
||||||
|
|
||||||
|
# Return tasks which were actually completed
|
||||||
|
return [self.id_tasks[taskid] for taskid in taskids]
|
||||||
|
|
||||||
|
async def parse_taskspec(self, taskspec: str, multiple=True, create=True) -> list[TaskInfo]:
|
||||||
|
"""
|
||||||
|
Parse a user provide taskspec string.
|
||||||
|
TasklistParseError
|
||||||
|
TasklistParseNoCreateError
|
||||||
|
|
||||||
|
taskspec is comma (not escaped) or semicolon tasks. Can't mix these, and prefer semicolon.
|
||||||
|
No way of writing an actual semicolon, but that's too bad.
|
||||||
|
|
||||||
|
Tasklabels (numerical indexes) may also be specified in ranges.
|
||||||
|
|
||||||
|
NOTE: The tasklabels will include completed tasks, even if they aren't shown.
|
||||||
|
This is inevitable, but important to note.
|
||||||
|
"""
|
||||||
|
# First split the userstring
|
||||||
|
if '\n' in taskspec:
|
||||||
|
splits = taskspec.split('\n')
|
||||||
|
elif ';' in taskspec:
|
||||||
|
splits = taskspec.split(';')
|
||||||
|
else:
|
||||||
|
splits = taskspec.split(',')
|
||||||
|
splits = [split.strip(',; \n') for split in splits]
|
||||||
|
splits = [split for split in splits if split]
|
||||||
|
|
||||||
|
taskids: list[Optional[int]] = []
|
||||||
|
seen = set()
|
||||||
|
to_create: list[tuple[int, str]] = []
|
||||||
|
parsed: list[Optional[int]] = []
|
||||||
|
|
||||||
|
# Get max label for use on unterminated ends
|
||||||
|
maxlabel = max(self.label_ids.keys(), default=None)
|
||||||
|
|
||||||
|
i = 0 # Insertion index for parsed, used in to_create
|
||||||
|
for split in splits:
|
||||||
|
match = self.taskspec_re.match(split)
|
||||||
|
if match:
|
||||||
|
# Task looks like a range or numeric
|
||||||
|
start = int(match['start'])
|
||||||
|
ranged = match['range']
|
||||||
|
end = int(match['end'] or maxlabel or -1)
|
||||||
|
if ranged:
|
||||||
|
labels = list(range(start, end + 1))
|
||||||
|
else:
|
||||||
|
labels = [start]
|
||||||
|
for label in labels:
|
||||||
|
if label in self.label_ids:
|
||||||
|
taskid = self.label_ids[label]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown task label {label}")
|
||||||
|
if taskid not in seen:
|
||||||
|
i += 1
|
||||||
|
taskids.append(taskid)
|
||||||
|
seen.add(taskid)
|
||||||
|
else:
|
||||||
|
# Presume it is a task we need to create
|
||||||
|
to_create.append((i, split))
|
||||||
|
taskids.append(None)
|
||||||
|
i += 1
|
||||||
|
if not create:
|
||||||
|
raise TasklistParseCreateError()
|
||||||
|
|
||||||
|
for i, content in to_create:
|
||||||
|
task, = await self.create_tasks(content)
|
||||||
|
taskids[i] = task.taskid
|
||||||
|
|
||||||
|
assert all(taskid is not None for taskid in taskids)
|
||||||
|
|
||||||
|
return [self.id_tasks[taskid] for taskid in taskids]
|
||||||
|
|
||||||
|
|
||||||
|
class TaskRegistry:
|
||||||
|
def __init__(self, data: TaskData, update_callback=None):
|
||||||
|
# TODO: Also need to pass some way of fetching the UserProfile
|
||||||
|
# Possibly community as well, in general
|
||||||
|
# i.e. pass in the ProfileRegistry
|
||||||
|
self.data = data
|
||||||
|
self._callback = update_callback
|
||||||
|
|
||||||
|
async def setup(self):
|
||||||
|
await self.data.init()
|
||||||
|
|
||||||
|
async def get_task_profile(self, profileid: int) -> TaskProfile:
|
||||||
|
# TODO: If we add anything to the TaskProfile, we probably want to make this smarter
|
||||||
|
return await TaskProfile.fetch_or_create(profileid)
|
||||||
|
|
||||||
|
async def get_current_task(self, profileid: int) -> Optional[TaskInfo]:
|
||||||
|
info = await TaskInfo.fetch_where(profileid=profileid, is_running=True)
|
||||||
|
return info[0] if info else None
|
||||||
|
|
||||||
|
async def get_tasklist(self, profileid: int):
|
||||||
|
tasks = await TaskInfo.fetch_where(profileid=profileid)
|
||||||
|
tasklist = Tasklist(profileid, tasks, self.data, update_callback=self._callback)
|
||||||
|
return tasklist
|
||||||
|
|
||||||
|
async def get_nowlist(self) -> list[TaskInfo]:
|
||||||
|
return await TaskInfo.fetch_where(is_running=True)
|
||||||
274
twitch/component.py
Normal file
274
twitch/component.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import twitchio
|
||||||
|
from twitchio.ext import commands as cmds
|
||||||
|
|
||||||
|
from meta import Bot
|
||||||
|
from utils.lib import strfdelta, utc_now
|
||||||
|
|
||||||
|
from ..data import TaskInfo, TaskProfile, Task, TaskData
|
||||||
|
from ..tasklist import TaskRegistry
|
||||||
|
from .. import logger
|
||||||
|
|
||||||
|
|
||||||
|
# TaskView and TasklistView?
|
||||||
|
# TaskView
|
||||||
|
|
||||||
|
|
||||||
|
class TaskComponent(cmds.Component):
|
||||||
|
"""
|
||||||
|
Command Interface
|
||||||
|
-----------------
|
||||||
|
!now 1, task, 2-5
|
||||||
|
!edit
|
||||||
|
!clear
|
||||||
|
Deletes the currently set 'now' task.
|
||||||
|
!done
|
||||||
|
|
||||||
|
!plan
|
||||||
|
!plan a, b, c
|
||||||
|
!unplan
|
||||||
|
!clearplan
|
||||||
|
|
||||||
|
!notnow
|
||||||
|
!resume
|
||||||
|
!soon
|
||||||
|
|
||||||
|
!tasklist
|
||||||
|
!add
|
||||||
|
!delete
|
||||||
|
|
||||||
|
!now a, b, c
|
||||||
|
!now x
|
||||||
|
!next
|
||||||
|
|
||||||
|
TODO: Probably a TaskView class for consistently formatting the task contents?
|
||||||
|
Maybe with a DiscordTaskView and TwitchTaskView
|
||||||
|
|
||||||
|
TODO: Moderator command clearfor? Not sure how moderators should manage the tasklist.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = bot.dbconn.load_registry(TaskData())
|
||||||
|
self.tasker = TaskRegistry(self.data)
|
||||||
|
|
||||||
|
async def component_load(self):
|
||||||
|
await self.data.init()
|
||||||
|
await self.tasker.setup()
|
||||||
|
# TODO: Add the IPC task update callback
|
||||||
|
|
||||||
|
async def get_profile_for(self, user):
|
||||||
|
...
|
||||||
|
|
||||||
|
@cmds.command(name='now', aliases=['task', 'check', 'add'])
|
||||||
|
async def cmd_now(self, ctx: cmds.Context, *, args: Optional[str]):
|
||||||
|
"""
|
||||||
|
Examples:
|
||||||
|
{prefix}now
|
||||||
|
{prefix}now current task
|
||||||
|
{prefix}now current task; next task; task after that
|
||||||
|
"""
|
||||||
|
# TODO: Breaking change: check and add should change behaviour.
|
||||||
|
# Check shows the status of the given task (does not allow creation)
|
||||||
|
# Add adds the task(s) to the end of the plan.
|
||||||
|
# TODOTODO: Support newline breaking on multi-tasks!! Although prefer semicolons
|
||||||
|
|
||||||
|
profileid = (await self.get_profile_for(ctx.author)).profileid
|
||||||
|
|
||||||
|
# Get tasklist for this profile
|
||||||
|
tasklist = await self.tasker.get_tasklist(profileid)
|
||||||
|
current = tasklist.get_current()
|
||||||
|
|
||||||
|
# If we are given a new task, delete the current now, set the new now
|
||||||
|
if args:
|
||||||
|
if current:
|
||||||
|
await tasklist.delete_tasks(current.taskid)
|
||||||
|
task, = await tasklist.create_tasks(args)
|
||||||
|
await tasklist.set_now(task.taskid)
|
||||||
|
# TODO: Maybe I should send the update now instead, after three separate changes..
|
||||||
|
await ctx.reply("Updated your current task, good luck!")
|
||||||
|
elif current:
|
||||||
|
if current.is_complete:
|
||||||
|
done_ago = strfdelta(utc_now() - current.completed_at)
|
||||||
|
await ctx.reply(f"You finished '{current.content}' {done_ago} ago!")
|
||||||
|
else:
|
||||||
|
started_ago = strfdelta(timedelta(seconds=current.total_duration))
|
||||||
|
await ctx.reply(f"You have been working on '{current.content}' for {started_ago}!")
|
||||||
|
else:
|
||||||
|
await ctx.reply(
|
||||||
|
"You don't have a task on the tasklist! "
|
||||||
|
"Show what you are currently working on with, e.g., !now Reading notes"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmds.command(name='edit')
|
||||||
|
async def cmd_edit(self, ctx: cmds.Context, *, args: Optional[str]):
|
||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
{prefix}edit [taskid] <new content>
|
||||||
|
Examples:
|
||||||
|
{prefix}edit new current task
|
||||||
|
{prefix}edit 2 New task content for task 2
|
||||||
|
"""
|
||||||
|
# TODO: Breaking changes for multi-tasks
|
||||||
|
# edit will support a numberic argument
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
# TODO: Better USAGE fetching
|
||||||
|
await ctx.reply(f"Usage: {ctx.prefix}edit <new content>. E.g. {ctx.prefix}edit Now Reading")
|
||||||
|
return
|
||||||
|
|
||||||
|
profile = await self.get_profile_for(ctx.author)
|
||||||
|
tasklist = await self.tasker.get_tasklist(profile.profileid)
|
||||||
|
current = tasklist.get_current()
|
||||||
|
if current:
|
||||||
|
await tasklist.edit_task(current.taskid, args)
|
||||||
|
await ctx.reply("Updated your current task!")
|
||||||
|
else:
|
||||||
|
await ctx.reply("No current task to edit! ")
|
||||||
|
|
||||||
|
@cmds.command(name='delete', aliases=['clear', 'remove'])
|
||||||
|
async def cmd_delete(self, ctx: cmds.Context, *, taskspec: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Description:
|
||||||
|
Deletes the specified tasks or the current task.
|
||||||
|
Examples:
|
||||||
|
!delete
|
||||||
|
!delete 1-
|
||||||
|
"""
|
||||||
|
# TODO: Breaking changes for multi-tasks
|
||||||
|
# delete will support an argument
|
||||||
|
profile = await self.get_profile_for(ctx.author)
|
||||||
|
tasklist = await self.tasker.get_tasklist(profile.profileid)
|
||||||
|
current = tasklist.get_current()
|
||||||
|
|
||||||
|
if current:
|
||||||
|
await tasklist.delete_tasks(current.taskid)
|
||||||
|
await ctx.reply("Deleted your current task from the tasklist!")
|
||||||
|
else:
|
||||||
|
await ctx.reply("You don't have a current task set at the moment!")
|
||||||
|
|
||||||
|
@cmds.command(name='done')
|
||||||
|
async def cmd_done(self, ctx: cmds.Context, *, taskspec: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Description:
|
||||||
|
Marks the specified tasks as complete, or the current task.
|
||||||
|
Examples:
|
||||||
|
!done
|
||||||
|
!done 1-, 3-5, 6
|
||||||
|
"""
|
||||||
|
# TODO: We can actually create the task here if it's not done.
|
||||||
|
|
||||||
|
profile = await self.get_profile_for(ctx.author)
|
||||||
|
tasklist = await self.tasker.get_tasklist(profile.profileid)
|
||||||
|
current = tasklist.get_current()
|
||||||
|
|
||||||
|
if taskspec:
|
||||||
|
tasks = await tasklist.parse_taskspec(taskspec)
|
||||||
|
else:
|
||||||
|
tasks = [current] if current else []
|
||||||
|
|
||||||
|
if tasks:
|
||||||
|
# Complete the tasks
|
||||||
|
completed = await tasklist.complete_tasks(task.taskid for task in tasks)
|
||||||
|
|
||||||
|
# Response depends on how many tasks were complete
|
||||||
|
# Don't show if duration is less than 30 seconds
|
||||||
|
if not completed:
|
||||||
|
# No tasks were actually completed
|
||||||
|
if len(tasks) > 1:
|
||||||
|
await ctx.reply("You already finished these tasks!")
|
||||||
|
else:
|
||||||
|
task = tasks[0]
|
||||||
|
await ctx.reply(f"You already finished '{task.content}'")
|
||||||
|
else:
|
||||||
|
# Note that duration for completed tasks will always be correct
|
||||||
|
duration = int(sum(task.duration for task in completed))
|
||||||
|
durstr = strfdelta(timedelta(seconds=duration))
|
||||||
|
if len(completed) == 1:
|
||||||
|
task = completed[0]
|
||||||
|
taskstr = f"Good work finishing '{task.content}'"
|
||||||
|
if duration > 60:
|
||||||
|
taskstr += f" You worked on it for {durstr}"
|
||||||
|
else:
|
||||||
|
taskstr = f"{len(completed)} more tasks completed, great work!"
|
||||||
|
if duration > 60:
|
||||||
|
taskstr += f" You worked on them for {durstr}"
|
||||||
|
await ctx.reply(taskstr)
|
||||||
|
elif taskspec:
|
||||||
|
await ctx.reply(f"'{taskspec}' didn't match any tasks!")
|
||||||
|
else:
|
||||||
|
await ctx.reply(
|
||||||
|
"You don't have a task on the tasklist! "
|
||||||
|
f"Show what you are currently working on with, e.g., {ctx.prefix}now Reading Notes"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmds.command(name='next')
|
||||||
|
async def cmd_next(self, ctx: cmds.Context, *, taskspec: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Description:
|
||||||
|
Marks the current task as complete, if it exists,
|
||||||
|
and moves to the next task on your plan.
|
||||||
|
|
||||||
|
If an argument is given,
|
||||||
|
acts as first a !done and then !now with the same argument
|
||||||
|
Examples:
|
||||||
|
!next
|
||||||
|
"""
|
||||||
|
if not taskspec:
|
||||||
|
await ctx.reply(
|
||||||
|
f"Usage:{ctx.prefix}next <next task> "
|
||||||
|
f"TIP: {ctx.prefix}next completes your current task and sets the given task as your new current task."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Very similar to !now, but with a !done first, and different arguments
|
||||||
|
profile = await self.get_profile_for(ctx.author)
|
||||||
|
tasklist = await self.tasker.get_tasklist(profile.profileid)
|
||||||
|
current = tasklist.get_current()
|
||||||
|
|
||||||
|
# Complete the current task if it exists
|
||||||
|
if current and not current.is_complete:
|
||||||
|
current, = await tasklist.complete_tasks(current.taskid)
|
||||||
|
|
||||||
|
new_current, = await tasklist.create_tasks(taskspec)
|
||||||
|
await tasklist.set_now(new_current.taskid)
|
||||||
|
|
||||||
|
if current:
|
||||||
|
started_ago = strfdelta(timedelta(seconds=current.total_duration))
|
||||||
|
await ctx.reply(
|
||||||
|
"Completed your current task and started your next one! Good luck! "
|
||||||
|
f"You worked on '{current.content}' for {started_ago}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.reply(
|
||||||
|
"Started your next task, good luck!"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmds.command(name='plan')
|
||||||
|
async def cmd_plan(self, ctx: cmds.Context):
|
||||||
|
"""
|
||||||
|
Description:
|
||||||
|
View or set a plan of tasks to do from your tasklist.
|
||||||
|
The plan lets you work with just your immediate next tasks,
|
||||||
|
and writing !next with no arguments will switch you to your next task in the plan.
|
||||||
|
|
||||||
|
If given arguments, this command adds the specified tasks
|
||||||
|
to the end of the plan.
|
||||||
|
|
||||||
|
See also !clearplan, !replan, !soon, !later/add
|
||||||
|
Examples:
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
await ctx.reply("Planning is not implemented yet, coming soon!")
|
||||||
|
...
|
||||||
|
|
||||||
|
@cmds.command(name='unplan', aliases=('clearplan',))
|
||||||
|
async def cmd_unplan(self, ctx: cmds.Context):
|
||||||
|
"""
|
||||||
|
Description:
|
||||||
|
Clears your plan, or moves the
|
||||||
|
"""
|
||||||
|
await ctx.reply("Planning is not implemented yet, coming soon!")
|
||||||
|
...
|
||||||
|
|
||||||
Reference in New Issue
Block a user