9 Commits

7 changed files with 423 additions and 86 deletions

View File

@@ -5,12 +5,16 @@ from datetime import timedelta
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
import twitchio
from twitchio.ext import commands
from data.queries import ORDER
from meta import LionCog, LionBot, CrocBot
from meta import LionCog, LionBot, CrocBot, LionContext
from modules.profiles.community import Community
from modules.profiles.profile import UserProfile
from utils.lib import utc_now
from . import logger
from .data import CounterData
@@ -25,6 +29,11 @@ class PERIOD(Enum):
YEAR = ('this year', 'y', 'year', 'yearly')
class ORIGIN(Enum):
DISCORD = 'discord'
TWITCH = 'twitch'
def counter_cmd_factory(
counter: str,
response: str,
@@ -32,10 +41,16 @@ def counter_cmd_factory(
context: Optional[str] = None
):
context = context or f"cmd: {counter}"
async def counter_cmd(cog, ctx: commands.Context, *, args: Optional[str] = None):
userid = int(ctx.author.id)
channelid = int((await ctx.channel.user()).id)
period, start_time = await cog.parse_period(channelid, '', default=default_period)
async def counter_cmd(
cog,
ctx: commands.Context | LionContext,
origin: ORIGIN,
author: UserProfile,
community: Community,
args: Optional[str]
):
userid = author.profileid
period, start_time = await cog.parse_period(community, '', default=default_period)
args = (args or '').strip(" 󠀀 ")
splits = args.split(maxsplit=1)
@@ -69,13 +84,25 @@ def counter_cmd_factory(
)
)
async def lb_cmd(cog, ctx: commands.Context, *, args: str = ''):
user = await ctx.channel.user()
await ctx.reply(await cog.formatted_lb(counter, args, int(user.id)))
async def lb_cmd(
cog,
ctx: commands.Context | LionContext,
origin: ORIGIN,
author: UserProfile,
community: Community,
args: Optional[str]
):
await ctx.reply(await cog.formatted_lb(counter, args, community, origin))
async def undo_cmd(cog, ctx: commands.Context):
userid = int(ctx.author.id)
channelid = int((await ctx.channel.user()).id)
async def undo_cmd(
cog,
ctx: commands.Context | LionContext,
origin: ORIGIN,
author: UserProfile,
community: Community,
args: Optional[str]
):
userid = author.profileid
_counter = await cog.fetch_counter(counter)
query = cog.data.CounterEntry.fetch_where(
counterid=_counter.counterid,
@@ -113,6 +140,9 @@ class CounterCog(LionCog):
await self.load_counters()
self.loaded.set()
profiles = self.bot.get_cog('ProfileCog')
profiles.add_profile_migrator(self.migrate_profiles, name='counters')
async def cog_unload(self):
self._unload_twitch_methods(self.crocbot)
@@ -124,18 +154,49 @@ class CounterCog(LionCog):
counter.name,
row.response
)
cmds = []
main_cmd = commands.command(name=row.name)(counter_cb)
cmds.append(main_cmd)
if row.lbname:
lb_cmd = commands.command(name=row.lbname)(lb_cb)
cmds.append(lb_cmd)
if row.undoname:
undo_cmd = commands.command(name=row.undoname)(undo_cb)
cmds.append(undo_cmd)
twitch_cmds = []
disc_cmds = []
twitch_cmds.append(
commands.command(
name=row.name
)(self.twitch_callback(counter_cb))
)
disc_cmds.append(
cmds.hybrid_command(
name=row.name
)(self.discord_callback(counter_cb))
)
for cmd in cmds:
if row.lbname:
twitch_cmds.append(
commands.command(
name=row.lbname
)(self.twitch_callback(lb_cb))
)
disc_cmds.append(
cmds.hybrid_command(
name=row.lbname
)(self.discord_callback(lb_cb))
)
if row.undoname:
twitch_cmds.append(
commands.command(
name=row.undoname
)(self.twitch_callback(undo_cb))
)
disc_cmds.append(
cmds.hybrid_command(
name=row.undoname
)(self.discord_callback(undo_cb))
)
for cmd in twitch_cmds:
self.add_twitch_command(self.crocbot, cmd)
for cmd in disc_cmds:
# cmd.cog = self
self.bot.remove_command(cmd.name)
self.bot.add_command(cmd)
print(f"Adding command: {cmd}")
logger.info(f"(Re)Loaded {len(rows)} counter commands!")
@@ -152,6 +213,87 @@ class CounterCog(LionCog):
f"Loaded {len(self.counters)} counters."
)
async def migrate_profiles(self, source_profile: UserProfile, target_profile: UserProfile):
"""
Move source profile entries to target profile entries
"""
results = ["(Counters)"]
rows = await self.data.CounterEntry.table.update_where(userid=source_profile.profileid).set(userid=target_profile.profileid)
if rows:
results.append(
f"Migrated {len(rows)} counter entries from source profile."
)
else:
results.append(
"No counter entries to migrate in source profile."
)
return ' '.join(results)
async def user_profile_migration(self):
"""
Manual single-use migration method from the old userid format to the new profileid format.
"""
async with self.bot.db.connection() as conn:
self.bot.db.conn = conn
async with conn.transaction():
entries = await self.data.CounterEntry.fetch_where()
for entry in entries:
if entry.userid > 1000:
# Assume userid is a twitch userid
profile = await UserProfile.fetch_from_twitchid(self.bot, entry.userid)
if not profile:
# Need to create
users = await self.crocbot.fetch_users(ids=[entry.userid])
if not users:
continue
user = users[0]
profile = await UserProfile.create_from_twitch(self.bot, user)
await entry.update(userid=profile.profileid)
logger.info("Completed single-shot user profile migration")
# General API
def twitch_callback(self, callback):
"""
Generate a Twitch command callback from the given general callback.
General callback must be of the form
callback(cog, ctx: GeneralContext, origin: ORIGIN, author: Profile, comm: Community, args: Optional[str])
Return will be a command callback of the form
callback(cog, ctx: Context, *, args: Optional[str] = None)
"""
async def command_callback(cog: CounterCog, ctx: commands.Context, *, args: Optional[str] = None):
profiles = cog.bot.get_cog('ProfileCog')
# Compute author profile
author = await profiles.fetch_profile_twitch(ctx.author)
# Compute community profile
community = await profiles.fetch_community_twitch(await ctx.channel.user())
return await callback(cog, ctx, ORIGIN.TWITCH, author, community, args)
return command_callback
def discord_callback(self, callback):
"""
Generate a Discord command callback from the given general callback.
General callback must be of the form
callback(cog, ctx: GeneralContext, origin: ORIGIN, author: Profile, comm: Community, args: Optional[str])
Return will be a command callback of the form
callback(cog, ctx: LionContext, *, args: Optional[str] = None)
"""
cog = self
async def command_callback(ctx: LionContext, *, args: Optional[str] = None):
profiles = cog.bot.get_cog('ProfileCog')
# Compute author profile
author = await profiles.fetch_profile_discord(ctx.author)
# Compute community profile
community = await profiles.fetch_community_discord(ctx.guild)
return await callback(cog, ctx, ORIGIN.DISCORD, author, community, args)
return command_callback
# Counters API
async def fetch_counter(self, counter: str) -> CounterData.Counter:
@@ -218,6 +360,14 @@ class CounterCog(LionCog):
results = await query
return results[0]['counter_total'] if results else 0
# Manage commands
@commands.command()
async def countermigration(self, ctx: commands.Context, *, args: Optional[str]=None):
if not (ctx.author.is_mod or ctx.author.is_broadcaster):
return
await self.user_profile_migration()
await ctx.reply("Counter userid->profileid migration done.")
# Counters commands
@commands.command()
async def counter(self, ctx: commands.Context, name: str, subcmd: Optional[str], *, args: Optional[str]=None):
@@ -225,6 +375,10 @@ class CounterCog(LionCog):
return
name = name.lower()
profiles = self.bot.get_cog('ProfileCog')
author = await profiles.fetch_profile_twitch(ctx.author)
userid = author.profileid
community = await profiles.fetch_community_twitch(await ctx.channel.user())
if subcmd is None or subcmd == 'show':
# Show
@@ -241,15 +395,14 @@ class CounterCog(LionCog):
return
await self.add_to_counter(
name,
int(ctx.author.id),
userid,
value,
context='cmd: counter add'
)
total = await self.totals(name)
await ctx.reply(f"'{name}' counter is now: {total}")
elif subcmd == 'lb':
user = await ctx.channel.user()
lbstr = await self.formatted_lb(name, args or '', int(user.id))
lbstr = await self.formatted_lb(name, args or '', community)
await ctx.reply(lbstr)
elif subcmd == 'clear':
await self.reset_counter(name)
@@ -292,7 +445,7 @@ class CounterCog(LionCog):
else:
await ctx.reply(f"Unrecognised subcommand {subcmd}. Supported subcommands: 'show', 'add', 'lb', 'clear', 'alias'.")
async def parse_period(self, userid: int, periodstr: str, default=PERIOD.STREAM):
async def parse_period(self, community: Community, periodstr: str, default=PERIOD.STREAM):
if periodstr:
period = next((period for period in PERIOD if periodstr.lower() in period.value), None)
if period is None:
@@ -306,9 +459,13 @@ class CounterCog(LionCog):
if period is PERIOD.ALL:
start_time = None
elif period is PERIOD.STREAM:
streams = await self.crocbot.fetch_streams(user_ids=[userid])
if streams:
stream = streams[0]
twitches = await community.twitch_channels()
stream = None
if twitches:
twitch = twitches[0]
streams = await self.crocbot.fetch_streams(user_ids=[int(twitch.channelid)])
stream = streams[0] if streams else None
if stream:
start_time = stream.started_at
else:
period = PERIOD.ALL
@@ -327,21 +484,33 @@ class CounterCog(LionCog):
return (period, start_time)
async def formatted_lb(self, counter: str, periodstr: str, channelid: int):
async def formatted_lb(
self,
counter: str,
periodstr: str,
community: Community,
origin: ORIGIN = ORIGIN.TWITCH
):
period, start_time = await self.parse_period(channelid, periodstr)
period, start_time = await self.parse_period(community, periodstr)
lb = await self.leaderboard(counter, start_time=start_time)
if lb:
userids = list(lb.keys())
users = await self.crocbot.fetch_users(ids=userids)
name_map = {user.id: user.display_name for user in users}
name_map = {}
for userid in lb.keys():
profile = await UserProfile.fetch(self.bot, userid)
name = await profile.get_name()
name_map[userid] = name
parts = []
for userid, total in lb.items():
items = list(lb.items())
prefix = 'top 10 ' if len(items) > 10 else ''
items = items[:10]
for userid, total in items:
name = name_map.get(userid, str(userid))
part = f"{name}: {total}"
parts.append(part)
lbstr = '; '.join(parts)
return f"{counter} {period.value[-1]} leaderboard --- {lbstr}"
return f"{counter} {period.value[-1]} {prefix}leaderboard --- {lbstr}"
else:
return f"{counter} {period.value[-1]} leaderboard is empty!"

View File

@@ -4,17 +4,21 @@ import json
import os
from typing import Optional
from attr import dataclass
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
import twitchio
from twitchio.ext import commands
from meta import CrocBot, LionCog
from meta.LionBot import LionBot
from meta import CrocBot, LionCog, LionContext, LionBot
from meta.sockets import Channel, register_channel
from utils.lib import strfdelta, utc_now
from . import logger
from .data import NowListData
from modules.profiles.profile import UserProfile
class NowDoingChannel(Channel):
name = 'NowList'
@@ -25,19 +29,7 @@ class NowDoingChannel(Channel):
async def on_connection(self, websocket, event):
await super().on_connection(websocket, event)
for task in self.cog.tasks.values():
await self.send_set(*self.task_args(task), websocket=websocket)
async def send_test_set(self):
tasks = [
(0, 'Tester0', "Testing Tasklist", True),
(1, 'Tester1', "Getting Confused", False),
(2, "Tester2", "Generating Bugs", True),
(3, "Tester3", "Fixing Bugs", False),
(4, "Tester4", "Pushing the red button", False),
]
for task in tasks:
await self.send_set(*task)
await self.reload_tasklist(websocket=websocket)
def task_args(self, task: NowListData.Task):
return (
@@ -48,6 +40,14 @@ class NowDoingChannel(Channel):
task.done_at.isoformat() if task.done_at else None,
)
async def reload_tasklist(self, websocket=None):
"""
Clear tasklist and re-send current tasks.
"""
await self.send_clear(websocket=websocket)
for task in self.cog.tasks.values():
await self.send_set(*self.task_args(task), websocket=websocket)
async def send_set(self, userid, name, task, start_at, end_at, websocket=None):
await self.send_event({
'type': "DO",
@@ -61,28 +61,28 @@ class NowDoingChannel(Channel):
}
}, websocket=websocket)
async def send_del(self, userid):
async def send_del(self, userid, websocket=None):
await self.send_event({
'type': "DO",
'method': "delTask",
'args': {
'userid': userid,
}
})
}, websocket=websocket)
async def send_clear(self):
async def send_clear(self, websocket=None):
await self.send_event({
'type': "DO",
'method': "clearTasks",
'args': {
}
})
}, websocket=websocket)
class NowDoingCog(LionCog):
def __init__(self, bot: LionBot):
self.bot = bot
self.crocbot = bot.crocbot
self.crocbot: CrocBot = bot.crocbot
self.data = bot.db.load_registry(NowListData())
self.channel = NowDoingChannel(self)
register_channel(self.channel.name, self.channel)
@@ -94,17 +94,82 @@ class NowDoingCog(LionCog):
async def cog_load(self):
await self.data.init()
await self.load_tasks()
self.bot.get_cog('ProfileCog').add_profile_migrator(self.migrate_profiles, name='task-migrator')
self._load_twitch_methods(self.crocbot)
self.loaded.set()
async def cog_unload(self):
self.loaded.clear()
self.tasks.clear()
if profiles := self.bot.get_cog('ProfileCog'):
profiles.del_profile_migrator('task-migrator')
self._unload_twitch_methods(self.crocbot)
async def migrate_profiles(self, source_profile: UserProfile, target_profile: UserProfile):
"""
Move current source task to target profile if there's room for it, otherwise annihilate
"""
await self.load_tasks()
source_task = self.tasks.pop(source_profile.profileid, None)
results = ["(Tasklist)"]
if source_task:
target_task = self.tasks.get(target_profile.profileid, None)
if target_task and (target_task.is_done or target_task.started_at < source_task.started_at):
# If target is done, remove it so we can overwrite
results.append("Removed older task from target profile.")
await target_task.delete()
target_task = None
if not target_task:
# Update source task with new profile id
await source_task.update(userid=target_profile.profileid)
target_task = source_task
await self.channel.send_set(*self.channel.task_args(target_task))
results.append("Migrated 1 currently running task from source profile.")
else:
# If there is a target task we can't overwrite, just delete the source task
await source_task.delete()
results.append("Ignoring and removing older task from source profile.")
self.tasks.pop(source_profile.profileid, None)
await self.channel.send_del(source_profile.profileid)
else:
results.append("No running task in source profile, nothing to migrate!")
await self.load_tasks()
return ' '.join(results)
async def user_profile_migration(self):
"""
Manual single-use migration method from the old userid format to the new profileid format.
"""
await self.load_tasks()
for userid, task in self.tasks.items():
userid = int(userid)
if userid > 1000:
# Assume it is a twitch userid
profile = await UserProfile.fetch_from_twitchid(self.bot, userid)
if not profile:
# Create a new profile with this twitch user
users = await self.crocbot.fetch_users(ids=[userid])
if not users:
continue
user = users[0]
profile = await UserProfile.create_from_twitch(self.bot, user)
if not await self.data.Task.fetch(profile.profileid):
await task.update(userid=profile.profileid)
else:
await task.delete()
await self.load_tasks()
await self.channel.reload_tasklist()
async def cog_check(self, ctx):
if not self.loaded.is_set():
await ctx.reply("Tasklists are still loading! Please wait a moment~")
@@ -123,25 +188,27 @@ class NowDoingCog(LionCog):
# await self.channel.send_test_set()
# await ctx.send(f"Hello {ctx.author.name}! This command does something, we aren't sure what yet.")
# await ctx.send(str(list(self.tasks.items())[0]))
await self.user_profile_migration()
await ctx.send(str(ctx.author.id))
await ctx.reply("Userid -> profile migration done.")
else:
await ctx.send(f"Hello {ctx.author.name}! I don't think you have permission to test that.")
@commands.command(aliases=['task', 'check'])
async def now(self, ctx: commands.Context, *, args: Optional[str] = None):
userid = int(ctx.author.id)
async def now(self, ctx: commands.Context | LionContext, profile: UserProfile, args: Optional[str] = None, edit=False):
args = args.strip() if args else None
userid = profile.profileid
if args:
existing = self.tasks.get(userid, None)
await self.data.Task.table.delete_where(userid=userid)
task = await self.data.Task.create(
userid=userid,
name=ctx.author.display_name,
name=await profile.get_name(),
task=args,
started_at=utc_now(),
started_at=existing.started_at if (existing and edit) else utc_now(),
)
self.tasks[task.userid] = task
await self.channel.send_set(*self.channel.task_args(task))
await ctx.send(f"Updated your current task, good luck!")
await ctx.send("Updated your current task, good luck!")
elif task := self.tasks.get(userid, None):
if task.is_done:
done_ago = strfdelta(utc_now() - task.done_at)
@@ -159,9 +226,38 @@ class NowDoingCog(LionCog):
"Show what you are currently working on with, e.g. !now Reading notes"
)
@commands.command(name='next')
async def nownext(self, ctx: commands.Context, *, args: Optional[str] = None):
userid = int(ctx.author.id)
@commands.command(
name='now',
aliases=['task', 'check']
)
async def twi_now(self, ctx: commands.Context, *, args: Optional[str] = None):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_twitch(ctx.author)
await self.now(ctx, profile, args)
@cmds.hybrid_command(
name='now',
aliases=['task', 'check']
)
async def disc_now(self, ctx: LionContext, *, args: Optional[str] = None):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_discord(ctx.author)
await self.now(ctx, profile, args)
@commands.command(
name='edit',
)
async def twi_edit(self, ctx: commands.Context, *, args: Optional[str] = None):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_twitch(ctx.author)
await self.now(ctx, profile, args, edit=True)
@cmds.hybrid_command(
name='edit',
)
async def disc_edit(self, ctx: LionContext, *, args: Optional[str] = None):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_discord(ctx.author)
await self.now(ctx, profile, args, edit=True)
async def nownext(self, ctx: commands.Context | LionContext, profile: UserProfile, args: Optional[str]):
userid = profile.profileid
task = self.tasks.get(userid, None)
if args:
if task:
@@ -176,13 +272,13 @@ class NowDoingCog(LionCog):
await self.data.Task.table.delete_where(userid=userid)
task = await self.data.Task.create(
userid=userid,
name=ctx.author.display_name,
name=await profile.get_name(),
task=args,
started_at=utc_now(),
)
self.tasks[task.userid] = task
await self.channel.send_set(*self.channel.task_args(task))
await ctx.send(f"Next task set, good luck!" + ' ' + prefix)
await ctx.send("Next task set, good luck!" + ' ' + prefix)
elif task:
if task.is_done:
done_ago = strfdelta(utc_now() - task.done_at)
@@ -200,9 +296,22 @@ class NowDoingCog(LionCog):
"Show what you are currently working on with, e.g. !now Reading notes"
)
@commands.command()
async def done(self, ctx: commands.Context):
userid = int(ctx.author.id)
@commands.command(
name='next',
)
async def twi_next(self, ctx: commands.Context, *, args: Optional[str] = None):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_twitch(ctx.author)
await self.nownext(ctx, profile, args)
@cmds.hybrid_command(
name='next',
)
async def disc_next(self, ctx: LionContext, *, args: Optional[str] = None):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_discord(ctx.author)
await self.nownext(ctx, profile, args)
async def done(self, ctx: commands.Context | LionContext, profile: UserProfile):
userid = profile.profileid
if task := self.tasks.get(userid, None):
if task.is_done:
await ctx.send(
@@ -222,9 +331,36 @@ class NowDoingCog(LionCog):
"Show what you are currently working on with, e.g. !now Reading notes"
)
@commands.command()
async def clear(self, ctx: commands.Context):
userid = int(ctx.author.id)
@commands.command(
name='done',
)
async def twi_done(self, ctx: commands.Context):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_twitch(ctx.author)
await self.done(ctx, profile)
@cmds.hybrid_command(
name='done',
)
async def disc_done(self, ctx: LionContext):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_discord(ctx.author)
await self.done(ctx, profile)
@commands.command(
name='clear',
)
async def twi_clear(self, ctx: commands.Context):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_twitch(ctx.author)
await self.clear(ctx, profile)
@cmds.hybrid_command(
name='clear',
)
async def disc_clear(self, ctx: LionContext):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_discord(ctx.author)
await self.clear(ctx, profile)
async def clear(self, ctx: commands.Context | LionContext, profile):
userid = profile.profileid
if task := self.tasks.pop(userid, None):
await task.delete()
await self.channel.send_del(userid)

View File

@@ -8,10 +8,12 @@ from gui.cards import FocusTimerCard, BreakTimerCard
if TYPE_CHECKING:
from .timer import Timer, Stage
from tracking.voice.cog import VoiceTrackerCog
from modules.nowdoing.cog import NowDoingCog
async def get_timer_card(bot: LionBot, timer: 'Timer', stage: 'Stage'):
voicecog: 'VoiceTrackerCog' = bot.get_cog('VoiceTrackerCog')
nowcog: 'NowDoingCog' = bot.get_cog('NowDoingCog')
name = timer.base_name
if stage is not None:
@@ -23,16 +25,22 @@ async def get_timer_card(bot: LionBot, timer: 'Timer', stage: 'Stage'):
card_users = []
guildid = timer.data.guildid
for member in timer.members:
if voicecog is not None:
session = voicecog.get_session(guildid, member.id)
tag = session.tag
if session.start_time:
session_duration = (utc_now() - session.start_time).total_seconds()
else:
session_duration = 0
profile = await bot.get_cog('ProfileCog').fetch_profile_discord(member)
task = nowcog.tasks.get(profile.profileid, None)
tag = ''
session_duration = 0
if task:
tag = task.task
session_duration = ((task.done_at or utc_now()) - task.started_at).total_seconds()
else:
session_duration = 0
tag = None
session = voicecog.get_session(guildid, member.id)
if session:
tag = session.tag
if session.start_time:
session_duration = (utc_now() - session.start_time).total_seconds()
else:
session_duration = 0
card_user = (
(member.id, (member.avatar or member.default_avatar).key),

View File

@@ -92,7 +92,7 @@ class ProfileCog(LionCog):
results.append(f"Migrated {len(twitch_rows)} attached twitch account(s).")
# And then mark the old profile as migrated
await source_profile.update(migrated=target_profile.profileid)
await source_profile.profile_row.update(migrated=target_profile.profileid)
results.append("Marking old profile as migrated.. finished!")
return results

View File

@@ -31,6 +31,30 @@ class UserProfile:
def __repr__(self):
return f"<UserProfile profileid={self.profileid} profile={self.profile_row}>"
async def get_name(self):
# TODO: Store a preferred name in the profile preferences
# TODO Should have a multi-fetch system
name = None
twitches = await self.twitch_accounts()
if twitches:
users = await self.bot.crocbot.fetch_users(
ids=[int(twitch.userid) for twitch in twitches]
)
if users:
user = users[0]
name = user.display_name
if not name:
discords = await self.discord_accounts()
if discords:
user = await self.bot.fetch_user(discords[0].userid)
name = user.display_name
if not name:
name = 'Unknown'
return name
async def attach_discord(self, user: discord.User | discord.Member):
"""
Attach a new discord user to this profile.

View File

@@ -654,7 +654,7 @@ class VoiceTrackerCog(LionCog):
# ----- Commands -----
@cmds.hybrid_command(
name=_p('cmd:now', "now"),
name="tag",
description=_p(
'cmd:now|desc',
"Describe what you are working on, or see what your friends are working on!"

View File

@@ -56,7 +56,7 @@ class UserAuthFlow:
result = await self._comm_task
if result.get('error', None):
# TODO Custom auth errors
# This is only documented to occure when the user denies the auth
# This is only documented to occur when the user denies the auth
raise SafeCancellation(f"Could not authenticate user! Reason: {result['error_description']}")
if result.get('state', None) != self.auth.state: