Merge branch 'staging' into feat-profiles
This commit is contained in:
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,12 +1,12 @@
|
|||||||
[submodule "bot/gui"]
|
[submodule "bot/gui"]
|
||||||
path = src/gui
|
path = src/gui
|
||||||
url = https://github.com/StudyLions/StudyLion-Plugin-GUI.git
|
url = git@github.com:Intery/CafeHelper-GUI.git
|
||||||
[submodule "skins"]
|
[submodule "skins"]
|
||||||
path = skins
|
path = skins
|
||||||
url = https://github.com/Intery/pillow-skins.git
|
url = git@github.com:Intery/CafeHelper-Skins.git
|
||||||
[submodule "src/modules/voicefix"]
|
[submodule "src/modules/voicefix"]
|
||||||
path = src/modules/voicefix
|
path = src/modules/voicefix
|
||||||
url = https://github.com/Intery/StudyLion-voicefix.git
|
url = git@github.com:Intery/StudyLion-voicefix.git
|
||||||
[submodule "src/modules/streamalerts"]
|
[submodule "src/modules/streamalerts"]
|
||||||
path = src/modules/streamalerts
|
path = src/modules/streamalerts
|
||||||
url = https://github.com/Intery/StudyLion-streamalerts.git
|
url = https://github.com/Intery/StudyLion-streamalerts.git
|
||||||
|
|||||||
@@ -1454,6 +1454,7 @@ CREATE TABLE shoutouts(
|
|||||||
CREATE TABLE counters(
|
CREATE TABLE counters(
|
||||||
counterid SERIAL PRIMARY KEY,
|
counterid SERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
category TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX counters_name ON counters (name);
|
CREATE UNIQUE INDEX counters_name ON counters (name);
|
||||||
@@ -1464,6 +1465,7 @@ CREATE TABLE counter_log(
|
|||||||
userid INTEGER NOT NULL,
|
userid INTEGER NOT NULL,
|
||||||
value INTEGER NOT NULL,
|
value INTEGER NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
details TEXT,
|
||||||
context_str TEXT
|
context_str TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX counter_log_counterid ON counter_log (counterid);
|
CREATE INDEX counter_log_counterid ON counter_log (counterid);
|
||||||
@@ -1484,6 +1486,16 @@ CREATE UNIQUE INDEX channel_tags_channelid_name ON channel_tags (channelid, name
|
|||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
|
-- Voice Roles {{{
|
||||||
|
CREATE TABLE voice_roles(
|
||||||
|
voice_role_id SERIAL PRIMARY KEY,
|
||||||
|
channelid BIGINT NOT NULL,
|
||||||
|
roleid BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX voice_role_channels on voice_roles (channelid);
|
||||||
|
|
||||||
|
-- }}}
|
||||||
|
|
||||||
-- User and Community Profiles {{{
|
-- User and Community Profiles {{{
|
||||||
|
|
||||||
|
|||||||
2
skins
2
skins
Submodule skins updated: d3d6a28bc9...686857321e
2
src/gui
2
src/gui
Submodule src/gui updated: c1bcb05c25...40bc140355
@@ -1,9 +1,12 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import twitchio
|
||||||
from twitchio.ext import commands
|
from twitchio.ext import commands
|
||||||
from twitchio.ext import pubsub
|
from twitchio.ext import pubsub
|
||||||
|
from twitchio.ext.commands.core import itertools
|
||||||
|
|
||||||
from data import Database
|
from data import Database
|
||||||
|
|
||||||
@@ -23,5 +26,51 @@ class CrocBot(commands.Bot):
|
|||||||
self.data = data
|
self.data = data
|
||||||
self.pubsub = pubsub.PubSubPool(self)
|
self.pubsub = pubsub.PubSubPool(self)
|
||||||
|
|
||||||
|
self._member_cache = defaultdict(dict)
|
||||||
|
|
||||||
async def event_ready(self):
|
async def event_ready(self):
|
||||||
logger.info(f"Logged in as {self.nick}. User id is {self.user_id}")
|
logger.info(f"Logged in as {self.nick}. User id is {self.user_id}")
|
||||||
|
|
||||||
|
async def event_join(self, channel: twitchio.Channel, user: twitchio.User):
|
||||||
|
self._member_cache[channel.name][user.name] = user
|
||||||
|
|
||||||
|
async def event_message(self, message: twitchio.Message):
|
||||||
|
if message.channel and message.author:
|
||||||
|
self._member_cache[message.channel.name][message.author.name] = message.author
|
||||||
|
await self.handle_commands(message)
|
||||||
|
|
||||||
|
async def seek_user(self, userstr: str, matching=True, fuzzy=True):
|
||||||
|
if userstr.startswith('@'):
|
||||||
|
matching = False
|
||||||
|
userstr = userstr.strip('@ ')
|
||||||
|
|
||||||
|
result = None
|
||||||
|
if matching and len(userstr) >= 3:
|
||||||
|
lowered = userstr.lower()
|
||||||
|
full_matches = []
|
||||||
|
for user in itertools.chain(*(cmems.values() for cmems in self._member_cache.values())):
|
||||||
|
matchstr = user.name.lower()
|
||||||
|
print(matchstr)
|
||||||
|
if matchstr.startswith(lowered):
|
||||||
|
result = user
|
||||||
|
break
|
||||||
|
if lowered in matchstr:
|
||||||
|
full_matches.append(user)
|
||||||
|
if result is None and full_matches:
|
||||||
|
result = full_matches[0]
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
lookup = userstr
|
||||||
|
elif result.id is None:
|
||||||
|
lookup = result.name
|
||||||
|
else:
|
||||||
|
lookup = None
|
||||||
|
|
||||||
|
if lookup:
|
||||||
|
found = await self.fetch_users(names=[lookup])
|
||||||
|
if found:
|
||||||
|
result = found[0]
|
||||||
|
|
||||||
|
# No matches found
|
||||||
|
return result
|
||||||
|
|||||||
@@ -47,6 +47,27 @@ class LionCog(Cog):
|
|||||||
|
|
||||||
return await super()._inject(bot, *args, *kwargs)
|
return await super()._inject(bot, *args, *kwargs)
|
||||||
|
|
||||||
|
def add_twitch_command(self, bot: Bot, command: Command):
|
||||||
|
"""
|
||||||
|
Dynamically register a command with the given bot.
|
||||||
|
|
||||||
|
The command will be deregistered on cog unload.
|
||||||
|
"""
|
||||||
|
# Remove any conflicting commands
|
||||||
|
if cmd := bot.get_command(command.name):
|
||||||
|
bot.remove_command(cmd.name)
|
||||||
|
self._twitch_cmds_.pop(command.name, None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._twitch_cmds_[command.name] = command
|
||||||
|
command._instance = self
|
||||||
|
command.cog = self
|
||||||
|
bot.add_command(command)
|
||||||
|
except Exception:
|
||||||
|
# Ensure the command doesn't die in the internal command cache
|
||||||
|
self._twitch_cmds_.pop(command.name, None)
|
||||||
|
raise
|
||||||
|
|
||||||
def _load_twitch_methods(self, bot: Bot):
|
def _load_twitch_methods(self, bot: Bot):
|
||||||
for name, command in self._twitch_cmds_.items():
|
for name, command in self._twitch_cmds_.items():
|
||||||
command._instance = self
|
command._instance = self
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ active_discord = [
|
|||||||
'.nowdoing',
|
'.nowdoing',
|
||||||
'.shoutouts',
|
'.shoutouts',
|
||||||
'.tagstrings',
|
'.tagstrings',
|
||||||
|
'.voiceroles',
|
||||||
]
|
]
|
||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
|
|||||||
@@ -25,6 +25,75 @@ class PERIOD(Enum):
|
|||||||
YEAR = ('this year', 'y', 'year', 'yearly')
|
YEAR = ('this year', 'y', 'year', 'yearly')
|
||||||
|
|
||||||
|
|
||||||
|
def counter_cmd_factory(
|
||||||
|
counter: str,
|
||||||
|
response: str,
|
||||||
|
default_period: Optional[PERIOD] = PERIOD.STREAM,
|
||||||
|
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)
|
||||||
|
|
||||||
|
args = (args or '').strip(" ")
|
||||||
|
splits = args.split(maxsplit=1)
|
||||||
|
splits = [split.strip() for split in splits if split]
|
||||||
|
|
||||||
|
details = None
|
||||||
|
amount = 1
|
||||||
|
|
||||||
|
if splits:
|
||||||
|
if splits[0].isdigit() or (splits[0].startswith('-') and splits[0][1:].isdigit()):
|
||||||
|
amount = int(splits[0])
|
||||||
|
splits = splits[1:]
|
||||||
|
if splits:
|
||||||
|
details = ' '.join(splits)
|
||||||
|
|
||||||
|
await cog.add_to_counter(
|
||||||
|
counter, userid, amount,
|
||||||
|
context=context,
|
||||||
|
details=details
|
||||||
|
)
|
||||||
|
lb = await cog.leaderboard(counter, start_time=start_time)
|
||||||
|
user_total = lb.get(userid, 0)
|
||||||
|
total = sum(lb.values())
|
||||||
|
await ctx.reply(
|
||||||
|
response.format(
|
||||||
|
total=total,
|
||||||
|
period=period,
|
||||||
|
period_name=period.value[0],
|
||||||
|
detailsorname=details or counter,
|
||||||
|
user_total=user_total,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 undo_cmd(cog, ctx: commands.Context):
|
||||||
|
userid = int(ctx.author.id)
|
||||||
|
channelid = int((await ctx.channel.user()).id)
|
||||||
|
_counter = await cog.fetch_counter(counter)
|
||||||
|
query = cog.data.CounterEntry.fetch_where(
|
||||||
|
counterid=_counter.counterid,
|
||||||
|
userid=userid,
|
||||||
|
)
|
||||||
|
query.order_by('created_at', direction=ORDER.DESC)
|
||||||
|
query.limit(1)
|
||||||
|
results = await query
|
||||||
|
if not results:
|
||||||
|
await ctx.reply("Nothing to delete!")
|
||||||
|
else:
|
||||||
|
row = results[0]
|
||||||
|
await row.delete()
|
||||||
|
await ctx.reply("Undo successful!")
|
||||||
|
|
||||||
|
return (counter_cmd, lb_cmd, undo_cmd)
|
||||||
|
|
||||||
|
|
||||||
class CounterCog(LionCog):
|
class CounterCog(LionCog):
|
||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -38,6 +107,7 @@ class CounterCog(LionCog):
|
|||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
self._load_twitch_methods(self.crocbot)
|
self._load_twitch_methods(self.crocbot)
|
||||||
|
await self.load_counter_commands()
|
||||||
|
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
await self.load_counters()
|
await self.load_counters()
|
||||||
@@ -46,6 +116,29 @@ class CounterCog(LionCog):
|
|||||||
async def cog_unload(self):
|
async def cog_unload(self):
|
||||||
self._unload_twitch_methods(self.crocbot)
|
self._unload_twitch_methods(self.crocbot)
|
||||||
|
|
||||||
|
async def load_counter_commands(self):
|
||||||
|
rows = await self.data.CounterCommand.fetch_where()
|
||||||
|
for row in rows:
|
||||||
|
counter = await self.data.Counter.fetch(row.counterid)
|
||||||
|
counter_cb, lb_cb, undo_cb = counter_cmd_factory(
|
||||||
|
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)
|
||||||
|
|
||||||
|
for cmd in cmds:
|
||||||
|
self.add_twitch_command(self.crocbot, cmd)
|
||||||
|
|
||||||
|
logger.info(f"(Re)Loaded {len(rows)} counter commands!")
|
||||||
|
|
||||||
async def cog_check(self, ctx):
|
async def cog_check(self, ctx):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -80,13 +173,19 @@ class CounterCog(LionCog):
|
|||||||
if row:
|
if row:
|
||||||
await self.data.CounterEntry.table.delete_where(counterid=row.counterid)
|
await self.data.CounterEntry.table.delete_where(counterid=row.counterid)
|
||||||
|
|
||||||
async def add_to_counter(self, counter: str, userid: int, value: int, context: Optional[str]=None):
|
async def add_to_counter(
|
||||||
|
self,
|
||||||
|
counter: str, userid: int, value: int,
|
||||||
|
context: Optional[str]=None,
|
||||||
|
details: Optional[str]=None,
|
||||||
|
):
|
||||||
row = await self.fetch_counter(counter)
|
row = await self.fetch_counter(counter)
|
||||||
return await self.data.CounterEntry.create(
|
return await self.data.CounterEntry.create(
|
||||||
counterid=row.counterid,
|
counterid=row.counterid,
|
||||||
userid=userid,
|
userid=userid,
|
||||||
value=value,
|
value=value,
|
||||||
context_str=context
|
context_str=context,
|
||||||
|
details=details
|
||||||
)
|
)
|
||||||
|
|
||||||
async def leaderboard(self, counter: str, start_time=None):
|
async def leaderboard(self, counter: str, start_time=None):
|
||||||
@@ -155,8 +254,43 @@ class CounterCog(LionCog):
|
|||||||
elif subcmd == 'clear':
|
elif subcmd == 'clear':
|
||||||
await self.reset_counter(name)
|
await self.reset_counter(name)
|
||||||
await ctx.reply(f"'{name}' counter reset.")
|
await ctx.reply(f"'{name}' counter reset.")
|
||||||
|
elif subcmd == 'alias':
|
||||||
|
splits = args.split(maxsplit=3) if args else []
|
||||||
|
counter = await self.fetch_counter(name)
|
||||||
|
rows = await self.data.CounterCommand.fetch_where(counterid=counter.counterid)
|
||||||
|
existing = rows[0] if rows else None
|
||||||
|
if existing and not args:
|
||||||
|
# Show current alias
|
||||||
|
await ctx.reply(
|
||||||
|
f"Counter '{name}' aliases: '!{existing.name}' to add to counter; "
|
||||||
|
f"'!{existing.lbname}' to view counter leaderboard; "
|
||||||
|
f"'!{existing.undoname}' to undo (your) last addition."
|
||||||
|
)
|
||||||
|
elif len(splits) < 4:
|
||||||
|
# Show usage
|
||||||
|
await ctx.reply(
|
||||||
|
"USAGE: !counter <name> alias <cmdname> <lbname> <undoname> <response> -- "
|
||||||
|
"Response accepts keywords {total}, {period}, {period_name}, {detailsorname}, {user_total}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.reply(f"Unrecognised subcommand {subcmd}. Supported subcommands: 'show', 'add', 'lb', 'clear'.")
|
# Create new alias
|
||||||
|
cmdname, lbname, undoname, response = splits
|
||||||
|
# Remove any existing alias
|
||||||
|
await self.data.CounterCommand.table.delete_where(name=cmdname)
|
||||||
|
|
||||||
|
alias = await self.data.CounterCommand.create(
|
||||||
|
name=cmdname,
|
||||||
|
counterid=counter.counterid,
|
||||||
|
lbname=lbname, undoname=undoname, response=response
|
||||||
|
)
|
||||||
|
await self.load_counter_commands()
|
||||||
|
await ctx.reply(
|
||||||
|
f"Alias created for counter '{name}': '!{alias.name}' to add to counter; "
|
||||||
|
f"'!{alias.lbname}' to view counter leaderboard; "
|
||||||
|
f"'!{alias.undoname}' to undo (your) last addition."
|
||||||
|
)
|
||||||
|
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, userid: int, periodstr: str, default=PERIOD.STREAM):
|
||||||
if periodstr:
|
if periodstr:
|
||||||
@@ -211,82 +345,3 @@ class CounterCog(LionCog):
|
|||||||
return f"{counter} {period.value[-1]} leaderboard --- {lbstr}"
|
return f"{counter} {period.value[-1]} leaderboard --- {lbstr}"
|
||||||
else:
|
else:
|
||||||
return f"{counter} {period.value[-1]} leaderboard is empty!"
|
return f"{counter} {period.value[-1]} leaderboard is empty!"
|
||||||
|
|
||||||
# Misc actual counter commands
|
|
||||||
# TODO: Factor this out to a different module...
|
|
||||||
@commands.command()
|
|
||||||
async def tea(self, ctx: commands.Context, *, args: Optional[str]=None):
|
|
||||||
userid = int(ctx.author.id)
|
|
||||||
channelid = int((await ctx.channel.user()).id)
|
|
||||||
period, start_time = await self.parse_period(channelid, '')
|
|
||||||
counter = 'tea'
|
|
||||||
|
|
||||||
await self.add_to_counter(
|
|
||||||
counter,
|
|
||||||
userid,
|
|
||||||
1,
|
|
||||||
context='cmd: tea'
|
|
||||||
)
|
|
||||||
lb = await self.leaderboard(counter, start_time=start_time)
|
|
||||||
user_total = lb.get(userid, 0)
|
|
||||||
total = sum(lb.values())
|
|
||||||
await ctx.reply(f"Enjoy your tea! We have had {total} cups of tea {period.value[0]}.")
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def tealb(self, ctx: commands.Context, *, args: str = ''):
|
|
||||||
user = await ctx.channel.user()
|
|
||||||
await ctx.reply(await self.formatted_lb('tea', args, int(user.id)))
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def coffee(self, ctx: commands.Context, *, args: Optional[str]=None):
|
|
||||||
userid = int(ctx.author.id)
|
|
||||||
channelid = int((await ctx.channel.user()).id)
|
|
||||||
period, start_time = await self.parse_period(channelid, '')
|
|
||||||
counter = 'coffee'
|
|
||||||
|
|
||||||
await self.add_to_counter(
|
|
||||||
counter,
|
|
||||||
userid,
|
|
||||||
1,
|
|
||||||
context='cmd: coffee'
|
|
||||||
)
|
|
||||||
lb = await self.leaderboard(counter, start_time=start_time)
|
|
||||||
user_total = lb.get(userid, 0)
|
|
||||||
total = sum(lb.values())
|
|
||||||
await ctx.reply(f"Enjoy your coffee! We have had {total} cups of coffee {period.value[0]}.")
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def coffeelb(self, ctx: commands.Context, *, args: str = ''):
|
|
||||||
user = await ctx.channel.user()
|
|
||||||
await ctx.reply(await self.formatted_lb('coffee', args, int(user.id)))
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def water(self, ctx: commands.Context, *, args: Optional[str]=None):
|
|
||||||
userid = int(ctx.author.id)
|
|
||||||
channelid = int((await ctx.channel.user()).id)
|
|
||||||
period, start_time = await self.parse_period(channelid, '')
|
|
||||||
counter = 'water'
|
|
||||||
|
|
||||||
await self.add_to_counter(
|
|
||||||
counter,
|
|
||||||
userid,
|
|
||||||
1,
|
|
||||||
context='cmd: water'
|
|
||||||
)
|
|
||||||
lb = await self.leaderboard(counter, start_time=start_time)
|
|
||||||
user_total = lb.get(userid, 0)
|
|
||||||
total = sum(lb.values())
|
|
||||||
await ctx.reply(f"Good job hydrating! We have had {total} cups of water {period.value[0]}.")
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def waterlb(self, ctx: commands.Context, *, args: str = ''):
|
|
||||||
user = await ctx.channel.user()
|
|
||||||
await ctx.reply(await self.formatted_lb('water', args, int(user.id)))
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def stuff(self, ctx: commands.Context, *, args: str = ''):
|
|
||||||
await ctx.reply(f"Stuff {args}")
|
|
||||||
|
|
||||||
@cmds.hybrid_command('water')
|
|
||||||
async def d_water_cmd(self, ctx):
|
|
||||||
await ctx.reply(repr(ctx))
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ class CounterData(Registry):
|
|||||||
CREATE TABLE counters(
|
CREATE TABLE counters(
|
||||||
counterid SERIAL PRIMARY KEY,
|
counterid SERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
category TEXT
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX counters_name ON counters (name);
|
CREATE UNIQUE INDEX counters_name ON counters (name);
|
||||||
"""
|
"""
|
||||||
@@ -19,6 +20,7 @@ class CounterData(Registry):
|
|||||||
|
|
||||||
counterid = Integer(primary=True)
|
counterid = Integer(primary=True)
|
||||||
name = String()
|
name = String()
|
||||||
|
category = String()
|
||||||
created_at = Timestamp()
|
created_at = Timestamp()
|
||||||
|
|
||||||
class CounterEntry(RowModel):
|
class CounterEntry(RowModel):
|
||||||
@@ -31,7 +33,8 @@ class CounterData(Registry):
|
|||||||
userid INTEGER NOT NULL,
|
userid INTEGER NOT NULL,
|
||||||
value INTEGER NOT NULL,
|
value INTEGER NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
context_str TEXT
|
context_str TEXT,
|
||||||
|
details TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX counter_log_counterid ON counter_log (counterid);
|
CREATE INDEX counter_log_counterid ON counter_log (counterid);
|
||||||
"""
|
"""
|
||||||
@@ -44,5 +47,28 @@ class CounterData(Registry):
|
|||||||
value = Integer()
|
value = Integer()
|
||||||
created_at = Timestamp()
|
created_at = Timestamp()
|
||||||
context_str = String()
|
context_str = String()
|
||||||
|
details = String()
|
||||||
|
|
||||||
|
class CounterCommand(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE counter_commands(
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
counterid INTEGER NOT NULL REFERENCES counters (counterid) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
lbname TEXT,
|
||||||
|
undoname TEXT,
|
||||||
|
response TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
# NOTE: This table will be replaced by aliases soon anyway
|
||||||
|
# So no need to worry about integrity or future-proofing
|
||||||
|
_tablename_ = 'counter_commands'
|
||||||
|
_cache_ = {}
|
||||||
|
|
||||||
|
name = String(primary=True)
|
||||||
|
counterid = Integer()
|
||||||
|
lbname = String()
|
||||||
|
undoname = String()
|
||||||
|
response = String()
|
||||||
|
|
||||||
|
|||||||
9
src/modules/counters/migration.sql
Normal file
9
src/modules/counters/migration.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE counters ADD COLUMN category TEXT;
|
||||||
|
ALTER TABLE counter_log ADD COLUMN details TEXT;
|
||||||
|
CREATE TABLE counter_commands(
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
counterid INTEGER NOT NULL REFERENCES counters (counterid) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
lbname TEXT,
|
||||||
|
undoname TEXT,
|
||||||
|
response TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -47,17 +47,33 @@ class TimerChannel(Channel):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
|
|
||||||
|
self.channelid = 1261999440160624734
|
||||||
|
self.goal = 12
|
||||||
|
|
||||||
async def on_connection(self, websocket, event):
|
async def on_connection(self, websocket, event):
|
||||||
await super().on_connection(websocket, event)
|
await super().on_connection(websocket, event)
|
||||||
timer = self.cog.get_channel_timer(1261999440160624734)
|
|
||||||
if timer is not None:
|
|
||||||
await self.send_set(
|
await self.send_set(
|
||||||
timer.data.last_started,
|
**await self.get_args_for(self.channelid),
|
||||||
timer.data.focus_length,
|
goal=self.goal,
|
||||||
timer.data.break_length,
|
|
||||||
websocket=websocket,
|
websocket=websocket,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def send_updates(self):
|
||||||
|
await self.send_set(
|
||||||
|
**await self.get_args_for(self.channelid),
|
||||||
|
goal=self.goal,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_args_for(self, channelid):
|
||||||
|
timer = self.cog.get_channel_timer(channelid)
|
||||||
|
if timer is None:
|
||||||
|
raise ValueError(f"Timer {channelid} doesn't exist.")
|
||||||
|
return {
|
||||||
|
'start_at': timer.data.last_started,
|
||||||
|
'focus_length': timer.data.focus_length,
|
||||||
|
'break_length': timer.data.break_length,
|
||||||
|
}
|
||||||
|
|
||||||
async def send_set(self, start_at, focus_length, break_length, goal=12, websocket=None):
|
async def send_set(self, start_at, focus_length, break_length, goal=12, websocket=None):
|
||||||
await self.send_event({
|
await self.send_event({
|
||||||
'type': "DO",
|
'type': "DO",
|
||||||
@@ -304,8 +320,6 @@ class TimerCog(LionCog):
|
|||||||
return
|
return
|
||||||
if member.bot:
|
if member.bot:
|
||||||
return
|
return
|
||||||
if 1148167212901859328 not in [role.id for role in member.roles]:
|
|
||||||
return
|
|
||||||
|
|
||||||
# If a member is leaving or joining a running timer, trigger a status update
|
# If a member is leaving or joining a running timer, trigger a status update
|
||||||
if before.channel != after.channel:
|
if before.channel != after.channel:
|
||||||
@@ -315,6 +329,7 @@ class TimerCog(LionCog):
|
|||||||
tasks = []
|
tasks = []
|
||||||
if leaving is not None:
|
if leaving is not None:
|
||||||
tasks.append(asyncio.create_task(leaving.update_status_card()))
|
tasks.append(asyncio.create_task(leaving.update_status_card()))
|
||||||
|
leaving.last_seen.pop(member.id, None)
|
||||||
if joining is not None:
|
if joining is not None:
|
||||||
joining.last_seen[member.id] = utc_now()
|
joining.last_seen[member.id] = utc_now()
|
||||||
if not joining.running and joining.auto_restart:
|
if not joining.running and joining.auto_restart:
|
||||||
@@ -1059,8 +1074,18 @@ class TimerCog(LionCog):
|
|||||||
@low_management_ward
|
@low_management_ward
|
||||||
async def streamtimer_update_cmd(self, ctx: LionContext,
|
async def streamtimer_update_cmd(self, ctx: LionContext,
|
||||||
new_start: Optional[str] = None,
|
new_start: Optional[str] = None,
|
||||||
new_goal: int = 12):
|
new_goal: Optional[int] = None,
|
||||||
timer = self.get_channel_timer(1261999440160624734)
|
new_channel: Optional[discord.VoiceChannel] = None,
|
||||||
|
):
|
||||||
|
if new_channel is not None:
|
||||||
|
channelid = self.channel.channelid = new_channel.id
|
||||||
|
else:
|
||||||
|
channelid = self.channel.channelid
|
||||||
|
|
||||||
|
if new_goal is not None:
|
||||||
|
self.channel.goal = new_goal
|
||||||
|
|
||||||
|
timer = self.get_channel_timer(channelid)
|
||||||
if timer is None:
|
if timer is None:
|
||||||
return
|
return
|
||||||
if new_start:
|
if new_start:
|
||||||
@@ -1068,10 +1093,5 @@ class TimerCog(LionCog):
|
|||||||
start_at = await self.bot.get_cog('Reminders').parse_time_static(new_start, timezone)
|
start_at = await self.bot.get_cog('Reminders').parse_time_static(new_start, timezone)
|
||||||
await timer.data.update(last_started=start_at)
|
await timer.data.update(last_started=start_at)
|
||||||
|
|
||||||
await self.channel.send_set(
|
await self.channel.send_updates()
|
||||||
timer.data.last_started,
|
|
||||||
timer.data.focus_length,
|
|
||||||
timer.data.break_length,
|
|
||||||
goal=new_goal,
|
|
||||||
)
|
|
||||||
await ctx.reply("Stream Timer Updated")
|
await ctx.reply("Stream Timer Updated")
|
||||||
|
|||||||
@@ -17,9 +17,19 @@ class ShoutoutCog(LionCog):
|
|||||||
and drop a follow! \
|
and drop a follow! \
|
||||||
They {areorwere} streaming {game} at {channel}
|
They {areorwere} streaming {game} at {channel}
|
||||||
"""
|
"""
|
||||||
|
COWO_SHOUTOUT = """
|
||||||
|
We think that {name} is a great coworker and you should check them out for more productive vibes! \
|
||||||
|
They {areorwere} streaming {game} at {channel}
|
||||||
|
"""
|
||||||
|
ART_SHOUTOUT = """
|
||||||
|
We think that {name} is an awesome artist and you should check them out for cool art and cosy vibes! \
|
||||||
|
They {areorwere} streaming {game} at {channel}
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.crocbot = bot.crocbot
|
self.crocbot: CrocBot = bot.crocbot
|
||||||
|
|
||||||
self.data = bot.db.load_registry(ShoutoutData())
|
self.data = bot.db.load_registry(ShoutoutData())
|
||||||
|
|
||||||
self.loaded = asyncio.Event()
|
self.loaded = asyncio.Event()
|
||||||
@@ -59,19 +69,28 @@ class ShoutoutCog(LionCog):
|
|||||||
return replace_multiple(text, mapping)
|
return replace_multiple(text, mapping)
|
||||||
|
|
||||||
@commands.command(aliases=['so'])
|
@commands.command(aliases=['so'])
|
||||||
async def shoutout(self, ctx: commands.Context, user: twitchio.User):
|
async def shoutout(self, ctx: commands.Context, target: str, typ: Optional[str]=None):
|
||||||
# Make sure caller is mod/broadcaster
|
# Make sure caller is mod/broadcaster
|
||||||
# Lookup custom shoutout for this user
|
# Lookup custom shoutout for this user
|
||||||
# If it exists use it, otherwise use default shoutout
|
# If it exists use it, otherwise use default shoutout
|
||||||
if (ctx.author.is_mod or ctx.author.is_broadcaster):
|
if (ctx.author.is_mod or ctx.author.is_broadcaster):
|
||||||
|
user = await self.crocbot.seek_user(target)
|
||||||
|
if user is None:
|
||||||
|
await ctx.reply(f"Couldn't resolve '{target}' to a valid user.")
|
||||||
|
else:
|
||||||
data = await self.data.CustomShoutout.fetch(int(user.id))
|
data = await self.data.CustomShoutout.fetch(int(user.id))
|
||||||
if data:
|
if data:
|
||||||
shoutout = data.content
|
shoutout = data.content
|
||||||
|
elif typ == 'cowo':
|
||||||
|
shoutout = self.COWO_SHOUTOUT
|
||||||
|
elif typ == 'art':
|
||||||
|
shoutout = self.ART_SHOUTOUT
|
||||||
else:
|
else:
|
||||||
shoutout = self.DEFAULT_SHOUTOUT
|
shoutout = self.DEFAULT_SHOUTOUT
|
||||||
formatted = await self.format_shoutout(shoutout, user)
|
formatted = await self.format_shoutout(shoutout, user)
|
||||||
await ctx.reply(formatted)
|
await ctx.reply(formatted)
|
||||||
# TODO: How to /shoutout with lib?
|
# TODO: How to /shoutout with lib?
|
||||||
|
# TODO Shoutout queue
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def editshoutout(self, ctx: commands.Context, user: twitchio.User, *, text: str):
|
async def editshoutout(self, ctx: commands.Context, user: twitchio.User, *, text: str):
|
||||||
|
|||||||
Submodule src/modules/voicefix updated: 5146e46515...cca1c94bd5
7
src/modules/voiceroles/__init__.py
Normal file
7
src/modules/voiceroles/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
from .cog import VoiceRoleCog
|
||||||
|
await bot.add_cog(VoiceRoleCog(bot))
|
||||||
166
src/modules/voiceroles/cog.py
Normal file
166
src/modules/voiceroles/cog.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
from cachetools import FIFOCache
|
||||||
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.abc import GuildChannel
|
||||||
|
from discord.ext import commands as cmds
|
||||||
|
from discord import app_commands as appcmds
|
||||||
|
|
||||||
|
from meta import LionBot, LionCog, LionContext
|
||||||
|
from meta.logger import log_wrap
|
||||||
|
from meta.errors import ResponseTimedOut, SafeCancellation, UserInputError
|
||||||
|
from utils.ui import Confirm
|
||||||
|
|
||||||
|
from . import logger
|
||||||
|
from .data import VoiceRoleData
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceRoleCog(LionCog):
|
||||||
|
def __init__(self, bot: LionBot):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = bot.db.load_registry(VoiceRoleData())
|
||||||
|
|
||||||
|
self._event_locks: WeakValueDictionary[tuple[int, int], asyncio.Lock] = WeakValueDictionary()
|
||||||
|
|
||||||
|
async def cog_load(self):
|
||||||
|
await self.data.init()
|
||||||
|
|
||||||
|
@LionCog.listener('on_voice_state_update')
|
||||||
|
@log_wrap(action='Voice Role Update')
|
||||||
|
async def voicerole_update(self, member: discord.Member,
|
||||||
|
before: discord.VoiceState, after: discord.VoiceState):
|
||||||
|
if member.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
after_channel = after.channel
|
||||||
|
before_channel = before.channel
|
||||||
|
if after_channel == before_channel:
|
||||||
|
return
|
||||||
|
|
||||||
|
task_key = (member.guild.id, member.id)
|
||||||
|
async with self.event_lock(task_key):
|
||||||
|
# Get the roles of the channel they left to remove
|
||||||
|
# Get the roles of the channel they are joining to add
|
||||||
|
# Use a set difference to remove the roles to be added from the ones to remove
|
||||||
|
if before_channel is not None:
|
||||||
|
leaving_roles = await self.get_roles_for(before_channel.id)
|
||||||
|
else:
|
||||||
|
leaving_roles = []
|
||||||
|
|
||||||
|
if after_channel is not None:
|
||||||
|
gaining_roles = await self.get_roles_for(after_channel.id)
|
||||||
|
else:
|
||||||
|
gaining_roles = []
|
||||||
|
|
||||||
|
to_remove = []
|
||||||
|
for role in leaving_roles:
|
||||||
|
if role in member.roles and role not in gaining_roles and role.is_assignable():
|
||||||
|
to_remove.append(role)
|
||||||
|
|
||||||
|
to_add = []
|
||||||
|
for role in gaining_roles:
|
||||||
|
if role not in member.roles and role.is_assignable():
|
||||||
|
to_add.append(role)
|
||||||
|
|
||||||
|
if to_remove:
|
||||||
|
await member.remove_roles(*to_remove, reason="Removing voice channel associated roles.")
|
||||||
|
if to_add:
|
||||||
|
await member.add_roles(*to_add, reason="Adding voice channel associated roles.")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Voice roles removed {len(to_remove)} roles "
|
||||||
|
f"and added {len(to_add)} roles to <uid: {member.id}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_roles_for(self, channelid: int) -> list[discord.Role]:
|
||||||
|
"""
|
||||||
|
Get the voice roles associated to the given channel, as a list.
|
||||||
|
|
||||||
|
Returns an empty list if there are no associated voice roles.
|
||||||
|
"""
|
||||||
|
rows = await self.data.VoiceRole.fetch_where(channelid=channelid)
|
||||||
|
channel = self.bot.get_channel(channelid)
|
||||||
|
if not channel:
|
||||||
|
raise ValueError("Provided voice role target channel is not in cache.")
|
||||||
|
|
||||||
|
target_roles = []
|
||||||
|
for row in rows:
|
||||||
|
role = channel.guild.get_role(row.roleid)
|
||||||
|
if role is not None:
|
||||||
|
target_roles.append(role)
|
||||||
|
|
||||||
|
return target_roles
|
||||||
|
|
||||||
|
def event_lock(self, key) -> asyncio.Lock:
|
||||||
|
"""
|
||||||
|
Get an asyncio.Lock for the given key.
|
||||||
|
|
||||||
|
Guarantees sequential event handling.
|
||||||
|
"""
|
||||||
|
lock = self._event_locks.get(key, None)
|
||||||
|
if lock is None:
|
||||||
|
lock = self._event_locks[key] = asyncio.Lock()
|
||||||
|
logger.debug(f"Getting video event lock {key} (locked: {lock.locked()})")
|
||||||
|
return lock
|
||||||
|
|
||||||
|
|
||||||
|
# -------- Commands --------
|
||||||
|
@cmds.hybrid_group(
|
||||||
|
name='voiceroles',
|
||||||
|
description="Base command group for voice channel -> role associationes."
|
||||||
|
)
|
||||||
|
@appcmds.default_permissions(manage_channels=True)
|
||||||
|
async def voicerole_group(self, ctx: LionContext):
|
||||||
|
...
|
||||||
|
|
||||||
|
@voicerole_group.command(
|
||||||
|
name="link",
|
||||||
|
description="Link a given voice channel with a given role."
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
channel="The voice channel to link.",
|
||||||
|
role="The associated role to give to members joining the voice channel."
|
||||||
|
)
|
||||||
|
async def voicerole_link(self, ctx: LionContext,
|
||||||
|
channel: discord.VoiceChannel,
|
||||||
|
role: discord.Role):
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
if not channel.permissions_for(ctx.author).manage_channels:
|
||||||
|
await ctx.error_reply(f"You don't have the manage channels permission in {channel.mention}")
|
||||||
|
return
|
||||||
|
if not ctx.author.guild_permissions.manage_roles or not (role < ctx.author.top_role):
|
||||||
|
await ctx.error_reply(f"You don't have the permission to manage this role!")
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.data.VoiceRole.table.insert(channelid=channel.id, roleid=role.id)
|
||||||
|
await ctx.reply("Voice role associated!")
|
||||||
|
|
||||||
|
@voicerole_group.command(
|
||||||
|
name="unlink",
|
||||||
|
description="Unlink a given voice channel from a given role."
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
channel="The voice channel to unlink.",
|
||||||
|
role="The role to remove from this voice channel."
|
||||||
|
)
|
||||||
|
async def voicerole_unlink(self, ctx: LionContext,
|
||||||
|
channel: discord.VoiceChannel,
|
||||||
|
role: discord.Role):
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
if not channel.permissions_for(ctx.author).manage_channels:
|
||||||
|
await ctx.error_reply(f"You don't have the manage channels permission in {channel.mention}")
|
||||||
|
return
|
||||||
|
if not ctx.author.guild_permissions.manage_roles or not (role < ctx.author.top_role):
|
||||||
|
await ctx.error_reply(f"You don't have the permission to manage this role!")
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.data.VoiceRole.table.delete_where(channelid=channel.id, roleid=role.id)
|
||||||
|
await ctx.reply("Voice role disassociated!")
|
||||||
|
|
||||||
|
# TODO: Display and visual editing of roles.
|
||||||
|
|
||||||
27
src/modules/voiceroles/data.py
Normal file
27
src/modules/voiceroles/data.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from data import Registry, RowModel
|
||||||
|
from data.columns import Integer, Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceRoleData(Registry):
|
||||||
|
class VoiceRole(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE voice_roles(
|
||||||
|
voice_role_id SERIAL PRIMARY KEY,
|
||||||
|
channelid BIGINT NOT NULL,
|
||||||
|
roleid BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX voice_role_channels on voice_roles (channelid);
|
||||||
|
"""
|
||||||
|
# TODO: Worth associating a guildid to this as well? Denormalises though
|
||||||
|
# Makes more theoretical sense to associated configurable channels to the guilds in a join table.
|
||||||
|
_tablename_ = 'voice_roles'
|
||||||
|
_cache_ = {}
|
||||||
|
|
||||||
|
voice_role_id = Integer(primary=True)
|
||||||
|
channelid = Integer()
|
||||||
|
roleid = Integer()
|
||||||
|
|
||||||
|
created_at = Timestamp()
|
||||||
@@ -5,7 +5,7 @@ import datetime as dt
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as cmds
|
from discord.ext import commands as cmds
|
||||||
from discord import app_commands as appcmds
|
from discord import AllowedMentions, app_commands as appcmds
|
||||||
|
|
||||||
from data import Condition
|
from data import Condition
|
||||||
from meta import LionBot, LionCog, LionContext
|
from meta import LionBot, LionCog, LionContext
|
||||||
@@ -668,7 +668,7 @@ class VoiceTrackerCog(LionCog):
|
|||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
tag=_p(
|
tag=_p(
|
||||||
'cmd:now|param:tag|desc',
|
'cmd:now|param:tag|desc',
|
||||||
"Describe what you are working on in 10 characters or less!"
|
"Describe what you are working!"
|
||||||
),
|
),
|
||||||
user=_p(
|
user=_p(
|
||||||
'cmd:now|param:user|desc',
|
'cmd:now|param:user|desc',
|
||||||
@@ -681,17 +681,15 @@ class VoiceTrackerCog(LionCog):
|
|||||||
)
|
)
|
||||||
@appcmds.guild_only
|
@appcmds.guild_only
|
||||||
async def now_cmd(self, ctx: LionContext,
|
async def now_cmd(self, ctx: LionContext,
|
||||||
tag: Optional[appcmds.Range[str, 0, 10]] = None,
|
tag: Optional[str] = None,
|
||||||
|
*,
|
||||||
user: Optional[discord.Member] = None,
|
user: Optional[discord.Member] = None,
|
||||||
clear: Optional[bool] = None
|
clear: Optional[bool] = None
|
||||||
):
|
):
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return
|
return
|
||||||
if not ctx.interaction:
|
|
||||||
return
|
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
|
||||||
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
|
||||||
is_moderator = await moderator_ctxward(ctx)
|
is_moderator = await moderator_ctxward(ctx)
|
||||||
target = user if user is not None else ctx.author
|
target = user if user is not None else ctx.author
|
||||||
session = self.get_session(ctx.guild.id, target.id, create=False)
|
session = self.get_session(ctx.guild.id, target.id, create=False)
|
||||||
@@ -715,7 +713,7 @@ class VoiceTrackerCog(LionCog):
|
|||||||
"{mention} has no running session!"
|
"{mention} has no running session!"
|
||||||
)).format(mention=target.mention)
|
)).format(mention=target.mention)
|
||||||
)
|
)
|
||||||
await ctx.interaction.edit_original_response(embed=error)
|
await ctx.reply(embed=error)
|
||||||
return
|
return
|
||||||
|
|
||||||
if clear:
|
if clear:
|
||||||
@@ -723,87 +721,27 @@ class VoiceTrackerCog(LionCog):
|
|||||||
if target == ctx.author:
|
if target == ctx.author:
|
||||||
# Clear the author's tag
|
# Clear the author's tag
|
||||||
await session.set_tag(None)
|
await session.set_tag(None)
|
||||||
ack = discord.Embed(
|
ack = "Cleared your current task!"
|
||||||
colour=discord.Colour.brand_green(),
|
|
||||||
title=t(_p(
|
|
||||||
'cmd:now|target:self|mode:clear|success|title',
|
|
||||||
"Session Tag Cleared"
|
|
||||||
)),
|
|
||||||
description=t(_p(
|
|
||||||
'cmd:now|target:self|mode:clear|success|desc',
|
|
||||||
"Successfully unset your session tag."
|
|
||||||
))
|
|
||||||
)
|
|
||||||
elif not is_moderator:
|
elif not is_moderator:
|
||||||
# Trying to clear someone else's tag without being a moderator
|
# Trying to clear someone else's tag without being a moderator
|
||||||
ack = discord.Embed(
|
ack = "You need to be a moderator to set or clear someone else's task!"
|
||||||
colour=discord.Colour.brand_red(),
|
|
||||||
title=t(_p(
|
|
||||||
'cmd:now|target:other|mode:clear|error:perms|title',
|
|
||||||
"You can't do that!"
|
|
||||||
)),
|
|
||||||
description=t(_p(
|
|
||||||
'cmd:now|target:other|mode:clear|error:perms|desc',
|
|
||||||
"You need to be a moderator to set or clear someone else's session tag."
|
|
||||||
))
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Clearing someone else's tag as a moderator
|
# Clearing someone else's tag as a moderator
|
||||||
await session.set_tag(None)
|
await session.set_tag(None)
|
||||||
ack = discord.Embed(
|
ack = f"Cleared {target}'s current task!"
|
||||||
colour=discord.Colour.brand_green(),
|
|
||||||
title=t(_p(
|
|
||||||
'cmd:now|target:other|mode:clear|success|title',
|
|
||||||
"Session Tag Cleared!"
|
|
||||||
)),
|
|
||||||
description=t(_p(
|
|
||||||
'cmd:now|target:other|mode:clear|success|desc',
|
|
||||||
"Cleared {target}'s session tag."
|
|
||||||
)).format(target=target.mention)
|
|
||||||
)
|
|
||||||
elif tag:
|
elif tag:
|
||||||
# Tag setting mode
|
# Tag setting mode
|
||||||
if target == ctx.author:
|
if target == ctx.author:
|
||||||
# Set the author's tag
|
# Set the author's tag
|
||||||
await session.set_tag(tag)
|
await session.set_tag(tag)
|
||||||
ack = discord.Embed(
|
ack = f"Set your current task to `{tag}`, good luck! <:goodluck:1266447460146876497>"
|
||||||
colour=discord.Colour.brand_green(),
|
|
||||||
title=t(_p(
|
|
||||||
'cmd:now|target:self|mode:set|success|title',
|
|
||||||
"Session Tag Set!"
|
|
||||||
)),
|
|
||||||
description=t(_p(
|
|
||||||
'cmd:now|target:self|mode:set|success|desc',
|
|
||||||
"You are now working on `{new_tag}`. Good luck!"
|
|
||||||
)).format(new_tag=tag)
|
|
||||||
)
|
|
||||||
elif not is_moderator:
|
elif not is_moderator:
|
||||||
# Trying the set someone else's tag without being a moderator
|
# Trying the set someone else's tag without being a moderator
|
||||||
ack = discord.Embed(
|
ack = "You need to be a moderator to set or clear someone else's task!"
|
||||||
colour=discord.Colour.brand_red(),
|
|
||||||
title=t(_p(
|
|
||||||
'cmd:now|target:other|mode:set|error:perms|title',
|
|
||||||
"You can't do that!"
|
|
||||||
)),
|
|
||||||
description=t(_p(
|
|
||||||
'cmd:now|target:other|mode:set|error:perms|desc',
|
|
||||||
"You need to be a moderator to set or clear someone else's session tag!"
|
|
||||||
))
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Setting someone else's tag as a moderator
|
# Setting someone else's tag as a moderator
|
||||||
await session.set_tag(tag)
|
await session.set_tag(tag)
|
||||||
ack = discord.Embed(
|
ack = f"Set {target}'s current task to `{tag}`"
|
||||||
colour=discord.Colour.brand_green(),
|
|
||||||
title=t(_p(
|
|
||||||
'cmd:now|target:other|mode:set|success|title',
|
|
||||||
"Session Tag Set!"
|
|
||||||
)),
|
|
||||||
description=t(_p(
|
|
||||||
'cmd:now|target:other|mode:set|success|desc',
|
|
||||||
"Set {target}'s session tag to `{new_tag}`."
|
|
||||||
)).format(target=target.mention, new_tag=tag)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Display tag and voice time
|
# Display tag and voice time
|
||||||
if target == ctx.author:
|
if target == ctx.author:
|
||||||
@@ -815,14 +753,14 @@ class VoiceTrackerCog(LionCog):
|
|||||||
else:
|
else:
|
||||||
desc = t(_p(
|
desc = t(_p(
|
||||||
'cmd:now|target:self|mode:show_without_tag|desc',
|
'cmd:now|target:self|mode:show_without_tag|desc',
|
||||||
"You have been working in {channel} since {time}!\n\n"
|
"You have been working in {channel} since {time}! "
|
||||||
"Use `/now <tag>` to set what you are working on."
|
"Use `/now <tag>` to set what you are working on."
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
if session.tag:
|
if session.tag:
|
||||||
desc = t(_p(
|
desc = t(_p(
|
||||||
'cmd:now|target:other|mode:show_with_tag|desc',
|
'cmd:now|target:other|mode:show_with_tag|desc',
|
||||||
"{target} is current working in {channel}!\n"
|
"{target} is current working in {channel}! "
|
||||||
"They have been working on **{tag}** since {time}."
|
"They have been working on **{tag}** since {time}."
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
@@ -830,18 +768,13 @@ class VoiceTrackerCog(LionCog):
|
|||||||
'cmd:now|target:other|mode:show_without_tag|desc',
|
'cmd:now|target:other|mode:show_without_tag|desc',
|
||||||
"{target} has been working in {channel} since {time}!"
|
"{target} has been working in {channel} since {time}!"
|
||||||
))
|
))
|
||||||
desc = desc.format(
|
ack = desc.format(
|
||||||
tag=session.tag,
|
tag=session.tag,
|
||||||
channel=f"<#{session.state.channelid}>",
|
channel=f"<#{session.state.channelid}>",
|
||||||
time=discord.utils.format_dt(session.start_time, 't'),
|
time=discord.utils.format_dt(session.start_time, 'R'),
|
||||||
target=target.mention,
|
target=target.mention,
|
||||||
)
|
)
|
||||||
ack = discord.Embed(
|
await ctx.reply(ack, allowed_mentions=AllowedMentions.none())
|
||||||
colour=discord.Colour.orange(),
|
|
||||||
description=desc,
|
|
||||||
timestamp=utc_now()
|
|
||||||
)
|
|
||||||
await ctx.interaction.edit_original_response(embed=ack)
|
|
||||||
|
|
||||||
# ----- Configuration Commands -----
|
# ----- Configuration Commands -----
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
|
|||||||
7
tests/__init__.py
Normal file
7
tests/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# !/bin/python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.getcwd()))
|
||||||
|
sys.path.insert(0, os.path.join(os.getcwd(), "src"))
|
||||||
0
tests/gui/__init__.py
Normal file
0
tests/gui/__init__.py
Normal file
@@ -1,11 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from src.cards import WeeklyGoalCard
|
from gui.cards import WeeklyGoalCard
|
||||||
|
|
||||||
|
|
||||||
async def get_card():
|
async def get_card():
|
||||||
card = await WeeklyGoalCard.generate_sample()
|
card = await WeeklyGoalCard.generate_sample()
|
||||||
with open('samples/weekly-sample.png', 'wb') as image_file:
|
with open('output/weekly-sample.png', 'wb') as image_file:
|
||||||
image_file.write(card.fp.read())
|
image_file.write(card.fp.read())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
15
tests/gui/cards/pomo_sample.py
Normal file
15
tests/gui/cards/pomo_sample.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import asyncio
|
||||||
|
import datetime as dt
|
||||||
|
from gui.cards import BreakTimerCard, FocusTimerCard
|
||||||
|
|
||||||
|
|
||||||
|
async def get_card():
|
||||||
|
card = await BreakTimerCard.generate_sample()
|
||||||
|
with open('output/break_timer_sample.png', 'wb') as image_file:
|
||||||
|
image_file.write(card.fp.read())
|
||||||
|
card = await FocusTimerCard.generate_sample()
|
||||||
|
with open('output/focus_timer_sample.png', 'wb') as image_file:
|
||||||
|
image_file.write(card.fp.read())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(get_card())
|
||||||
Reference in New Issue
Block a user