Merge branch 'staging' of cgithub:StudyLions/StudyLion into staging
This commit is contained in:
140
.gitignore
vendored
Normal file
140
.gitignore
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
config/**
|
||||
@@ -13,7 +13,7 @@ class LionCommand(Command):
|
||||
"""
|
||||
Subclass to allow easy attachment of custom hooks and structure to commands.
|
||||
"""
|
||||
...
|
||||
allow_before_ready = False
|
||||
|
||||
|
||||
class LionModule(Module):
|
||||
@@ -72,25 +72,38 @@ class LionModule(Module):
|
||||
"""
|
||||
Lion pre-command hook.
|
||||
"""
|
||||
if not self.ready and not ctx.cmd.allow_before_ready:
|
||||
try:
|
||||
await ctx.embed_reply(
|
||||
"I am currently restarting! Please try again in a couple of minutes."
|
||||
)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
raise SafeCancellation(details="Module '{}' is not ready.".format(self.name))
|
||||
|
||||
# Check that the channel and guild still exists
|
||||
if not ctx.client.get_guild(ctx.guild.id) or not ctx.guild.get_channel(ctx.ch.id):
|
||||
raise SafeCancellation(details='Command channel is no longer reachable.')
|
||||
|
||||
# Check global user blacklist
|
||||
if ctx.author.id in ctx.client.objects['blacklisted_users']:
|
||||
raise SafeCancellation
|
||||
raise SafeCancellation(details='User is blacklisted.')
|
||||
|
||||
if ctx.guild:
|
||||
# Check global guild blacklist
|
||||
if ctx.guild.id in ctx.client.objects['blacklisted_guilds']:
|
||||
raise SafeCancellation
|
||||
raise SafeCancellation(details='Guild is blacklisted.')
|
||||
|
||||
# Check guild's own member blacklist
|
||||
if ctx.author.id in ctx.client.objects['ignored_members'][ctx.guild.id]:
|
||||
raise SafeCancellation
|
||||
raise SafeCancellation(details='User is ignored in this guild.')
|
||||
|
||||
# Check channel permissions are sane
|
||||
if not ctx.ch.permissions_for(ctx.guild.me).send_messages:
|
||||
raise SafeCancellation
|
||||
raise SafeCancellation(details='I cannot send messages in this channel.')
|
||||
if not ctx.ch.permissions_for(ctx.guild.me).embed_links:
|
||||
await ctx.reply("I need permission to send embeds in this channel before I can run any commands!")
|
||||
raise SafeCancellation
|
||||
raise SafeCancellation(details='I cannot send embeds in this channel.')
|
||||
|
||||
# Start typing
|
||||
await ctx.ch.trigger_typing()
|
||||
|
||||
Submodule bot/cmdClient updated: 75410acc12...6eb4269034
@@ -53,13 +53,19 @@ def add_pending(pending):
|
||||
|
||||
guild_config = RowTable(
|
||||
'guild_config',
|
||||
('guildid', 'admin_role', 'mod_role', 'event_log_channel', 'alert_channel',
|
||||
('guildid', 'admin_role', 'mod_role', 'event_log_channel', 'mod_log_channel', 'alert_channel',
|
||||
'studyban_role',
|
||||
'min_workout_length', 'workout_reward',
|
||||
'max_tasks', 'task_reward', 'task_reward_limit',
|
||||
'study_hourly_reward', 'study_hourly_live_bonus',
|
||||
'study_ban_role', 'max_study_bans'),
|
||||
'renting_price', 'renting_category', 'renting_cap', 'renting_role', 'renting_sync_perms',
|
||||
'accountability_category', 'accountability_lobby', 'accountability_bonus',
|
||||
'accountability_reward', 'accountability_price',
|
||||
'video_studyban', 'video_grace_period',
|
||||
'greeting_channel', 'greeting_message', 'returning_message',
|
||||
'starting_funds', 'persist_roles'),
|
||||
'guildid',
|
||||
cache=TTLCache(1000, ttl=60*5)
|
||||
cache=TTLCache(2500, ttl=60*5)
|
||||
)
|
||||
|
||||
unranked_roles = Table('unranked_roles')
|
||||
@@ -72,6 +78,7 @@ lions = RowTable(
|
||||
('guildid', 'userid',
|
||||
'tracked_time', 'coins',
|
||||
'workout_count', 'last_workout_start',
|
||||
'revision_mute_count',
|
||||
'last_study_badgeid',
|
||||
'video_warned',
|
||||
'_timestamp'
|
||||
|
||||
@@ -69,7 +69,8 @@ class TimeSlot:
|
||||
|
||||
_everyone_overwrite = discord.PermissionOverwrite(
|
||||
view_channel=False,
|
||||
connect=False
|
||||
connect=False,
|
||||
speak=False
|
||||
)
|
||||
|
||||
happy_lion = "https://media.discordapp.net/stickers/898266283559227422.png"
|
||||
@@ -218,6 +219,9 @@ class TimeSlot:
|
||||
"""
|
||||
Load data and update applicable caches.
|
||||
"""
|
||||
if not self.guild:
|
||||
return self
|
||||
|
||||
# Load setting data
|
||||
self.category = GuildSettings(self.guild.id).accountability_category.value
|
||||
self.lobby = GuildSettings(self.guild.id).accountability_lobby.value
|
||||
@@ -389,13 +393,14 @@ class TimeSlot:
|
||||
pass
|
||||
|
||||
# Reward members appropriately
|
||||
guild_settings = GuildSettings(self.guild.id)
|
||||
reward = guild_settings.accountability_reward.value
|
||||
if all(mem.has_attended for mem in self.members.values()):
|
||||
reward += guild_settings.accountability_bonus.value
|
||||
if self.guild:
|
||||
guild_settings = GuildSettings(self.guild.id)
|
||||
reward = guild_settings.accountability_reward.value
|
||||
if all(mem.has_attended for mem in self.members.values()):
|
||||
reward += guild_settings.accountability_bonus.value
|
||||
|
||||
for memid in self.members:
|
||||
Lion.fetch(self.guild.id, memid).addCoins(reward)
|
||||
for memid in self.members:
|
||||
Lion.fetch(self.guild.id, memid).addCoins(reward)
|
||||
|
||||
async def cancel(self):
|
||||
"""
|
||||
|
||||
@@ -374,14 +374,15 @@ async def _accountability_system_resume():
|
||||
None, mow.slotid, mow.userid)
|
||||
for mow in slot_members[row.slotid] if mow.last_joined_at
|
||||
)
|
||||
slot = TimeSlot(client.get_guild(row.guildid), row.start_at, data=row).load(
|
||||
memberids=[mow.userid for mow in slot_members[row.slotid]]
|
||||
)
|
||||
if client.get_guild(row.guildid):
|
||||
slot = TimeSlot(client.get_guild(row.guildid), row.start_at, data=row).load(
|
||||
memberids=[mow.userid for mow in slot_members[row.slotid]]
|
||||
)
|
||||
try:
|
||||
await slot.close()
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
row.closed_at = now
|
||||
try:
|
||||
await slot.close()
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
# Load the in-progress room data
|
||||
if current_room_data:
|
||||
@@ -451,7 +452,7 @@ async def launch_accountability_system(client):
|
||||
guilds = tables.guild_config.fetch_rows_where(
|
||||
accountability_category=NOTNULL
|
||||
)
|
||||
[AccountabilityGuild(guild.guildid) for guild in guilds]
|
||||
[AccountabilityGuild(guild.guildid) for guild in guilds if client.get_guild(guild.guildid)]
|
||||
await _accountability_system_resume()
|
||||
asyncio.create_task(_accountability_loop())
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ from . import guild_config
|
||||
from . import statreset
|
||||
from . import new_members
|
||||
from . import reaction_roles
|
||||
from . import economy
|
||||
|
||||
3
bot/modules/guild_admin/economy/__init__.py
Normal file
3
bot/modules/guild_admin/economy/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from ..module import module
|
||||
|
||||
from . import set_coins
|
||||
104
bot/modules/guild_admin/economy/set_coins.py
Normal file
104
bot/modules/guild_admin/economy/set_coins.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import discord
|
||||
import datetime
|
||||
from wards import guild_admin
|
||||
|
||||
from settings import GuildSettings
|
||||
from core import Lion
|
||||
|
||||
from ..module import module
|
||||
|
||||
POSTGRES_INT_MAX = 2147483647
|
||||
|
||||
@module.cmd(
|
||||
"set_coins",
|
||||
group="Guild Admin",
|
||||
desc="Set coins on a member."
|
||||
)
|
||||
@guild_admin()
|
||||
async def cmd_set(ctx):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}set_coins <user mention> <amount>
|
||||
Description:
|
||||
Sets the given number of coins on the mentioned user.
|
||||
If a number greater than 0 is mentioned, will add coins.
|
||||
If a number less than 0 is mentioned, will remove coins.
|
||||
Note: LionCoins on a member cannot be negative.
|
||||
Example:
|
||||
{prefix}set_coins {ctx.author.mention} 100
|
||||
{prefix}set_coins {ctx.author.mention} -100
|
||||
"""
|
||||
# Extract target and amount
|
||||
# Handle a slightly more flexible input than stated
|
||||
splits = ctx.args.split()
|
||||
digits = [isNumber(split) for split in splits[:2]]
|
||||
mentions = ctx.msg.mentions
|
||||
if len(splits) < 2 or not any(digits) or not (all(digits) or mentions):
|
||||
return await _send_usage(ctx)
|
||||
|
||||
if all(digits):
|
||||
# Both are digits, hopefully one is a member id, and one is an amount.
|
||||
target, amount = ctx.guild.get_member(int(splits[0])), int(splits[1])
|
||||
if not target:
|
||||
amount, target = int(splits[0]), ctx.guild.get_member(int(splits[1]))
|
||||
if not target:
|
||||
return await _send_usage(ctx)
|
||||
elif digits[0]:
|
||||
amount, target = int(splits[0]), mentions[0]
|
||||
elif digits[1]:
|
||||
target, amount = mentions[0], int(splits[1])
|
||||
|
||||
# Fetch the associated lion
|
||||
target_lion = Lion.fetch(ctx.guild.id, target.id)
|
||||
|
||||
# Check sanity conditions
|
||||
if target == ctx.client.user:
|
||||
return await ctx.embed_reply("Thanks, but Ari looks after all my needs!")
|
||||
if target.bot:
|
||||
return await ctx.embed_reply("We are still waiting for {} to open an account.".format(target.mention))
|
||||
|
||||
# Finally, send the amount and the ack message
|
||||
# Postgres `coins` column is `integer`, sanity check postgres int limits - which are smalled than python int range
|
||||
target_coins_to_set = target_lion.coins + amount
|
||||
if target_coins_to_set >= 0 and target_coins_to_set <= POSTGRES_INT_MAX:
|
||||
target_lion.addCoins(amount)
|
||||
elif target_coins_to_set < 0:
|
||||
target_coins_to_set = -target_lion.coins # Coins cannot go -ve, cap to 0
|
||||
target_lion.addCoins(target_coins_to_set)
|
||||
target_coins_to_set = 0
|
||||
else:
|
||||
return await ctx.embed_reply("Member coins cannot be more than {}".format(POSTGRES_INT_MAX))
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Funds Set",
|
||||
description="You have set LionCoins on {} to **{}**!".format(target.mention,target_coins_to_set),
|
||||
colour=discord.Colour.orange(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
).set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url)
|
||||
|
||||
await ctx.reply(embed=embed, reference=ctx.msg)
|
||||
GuildSettings(ctx.guild.id).event_log.log(
|
||||
"{} set {}'s LionCoins to`{}`.".format(
|
||||
ctx.author.mention,
|
||||
target.mention,
|
||||
target_coins_to_set
|
||||
),
|
||||
title="Funds Set"
|
||||
)
|
||||
|
||||
def isNumber(var):
|
||||
try:
|
||||
return isinstance(int(var), int)
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _send_usage(ctx):
|
||||
return await ctx.error_reply(
|
||||
"**Usage:** `{prefix}set_coins <mention> <amount>`\n"
|
||||
"**Example:**\n"
|
||||
" {prefix}set_coins {ctx.author.mention} 100\n"
|
||||
" {prefix}set_coins {ctx.author.mention} -100".format(
|
||||
prefix=ctx.best_prefix,
|
||||
ctx=ctx
|
||||
)
|
||||
)
|
||||
@@ -485,7 +485,7 @@ async def cmd_reactionroles(ctx, flags):
|
||||
await ctx.error_reply(
|
||||
"The provided channel no longer exists!"
|
||||
)
|
||||
elif channel.type != discord.ChannelType.text:
|
||||
elif not isinstance(channel, discord.TextChannel):
|
||||
await ctx.error_reply(
|
||||
"The provided channel is not a text channel!"
|
||||
)
|
||||
@@ -821,8 +821,8 @@ async def cmd_reactionroles(ctx, flags):
|
||||
setting = await setting_class.parse(target.messageid, ctx, flags[flag])
|
||||
except UserInputError as e:
|
||||
return await ctx.error_reply(
|
||||
title="Couldn't save settings!",
|
||||
description="{} {}\nNo settings were modified.".format(cross, e.msg)
|
||||
"{} {}\nNo settings were modified.".format(cross, e.msg),
|
||||
title="Couldn't save settings!"
|
||||
)
|
||||
else:
|
||||
update_lines.append(
|
||||
@@ -861,8 +861,8 @@ async def cmd_reactionroles(ctx, flags):
|
||||
setting = await setting_class.parse(reaction.reactionid, ctx, flags[flag])
|
||||
except UserInputError as e:
|
||||
return await ctx.error_reply(
|
||||
"{} {}\nNo reaction roles were modified.".format(cross, e.msg),
|
||||
title="Couldn't save reaction role settings!",
|
||||
description="{} {}\nNo reaction roles were modified.".format(cross, e.msg)
|
||||
)
|
||||
else:
|
||||
update_lines.append(
|
||||
|
||||
@@ -199,6 +199,7 @@ class price(setting_types.Integer, ReactionSetting):
|
||||
)
|
||||
|
||||
accepts = "An integer number of coins. Use `0` to make the role free, or `None` to use the message default."
|
||||
_max = 2 ** 20
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
|
||||
@@ -272,7 +272,7 @@ class ReactionRoleMessage:
|
||||
# Fetch the number of applicable roles the user has
|
||||
roleids = set(reaction.data.roleid for reaction in self.reactions)
|
||||
member_roleids = set(role.id for role in member.roles)
|
||||
if len(roleids.intersection(member_roleids)) > maximum:
|
||||
if len(roleids.intersection(member_roleids)) >= maximum:
|
||||
# Notify the user
|
||||
embed = discord.Embed(
|
||||
title="Maximum group roles reached!",
|
||||
|
||||
@@ -12,7 +12,8 @@ from .module import module
|
||||
@module.cmd(
|
||||
"stats",
|
||||
group="Statistics",
|
||||
desc="View a summary of your study statistics!"
|
||||
desc="View a summary of your study statistics!",
|
||||
allow_before_ready=True
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_stats(ctx):
|
||||
|
||||
@@ -26,21 +26,22 @@ async def embed_reply(ctx, desc, colour=discord.Colour.orange(), **kwargs):
|
||||
|
||||
|
||||
@Context.util
|
||||
async def error_reply(ctx, error_str, **kwargs):
|
||||
async def error_reply(ctx, error_str, send_args={}, **kwargs):
|
||||
"""
|
||||
Notify the user of a user level error.
|
||||
Typically, this will occur in a red embed, posted in the command channel.
|
||||
"""
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.red(),
|
||||
description=error_str
|
||||
description=error_str,
|
||||
**kwargs
|
||||
)
|
||||
message = None
|
||||
try:
|
||||
message = await ctx.ch.send(
|
||||
embed=embed,
|
||||
reference=ctx.msg.to_reference(fail_if_not_exists=False),
|
||||
**kwargs
|
||||
**send_args
|
||||
)
|
||||
ctx.sent_messages.append(message)
|
||||
return message
|
||||
|
||||
@@ -182,7 +182,11 @@ async def find_channel(ctx, userstr, interactive=False, collection=None, chan_ty
|
||||
# Create the collection to search from args or guild channels
|
||||
collection = collection if collection else ctx.guild.channels
|
||||
if chan_type is not None:
|
||||
collection = [chan for chan in collection if chan.type == chan_type]
|
||||
if chan_type == discord.ChannelType.text:
|
||||
# Hack to support news channels as text channels
|
||||
collection = [chan for chan in collection if isinstance(chan, discord.TextChannel)]
|
||||
else:
|
||||
collection = [chan for chan in collection if chan.type == chan_type]
|
||||
|
||||
# If the user input was a number or possible channel mention, extract it
|
||||
chanid = userstr.strip('<#@&!>')
|
||||
@@ -413,7 +417,7 @@ async def find_message(ctx, msgid, chlist=None, ignore=[]):
|
||||
|
||||
|
||||
async def _search_in_channel(channel: discord.TextChannel, msgid: int):
|
||||
if channel.type != discord.ChannelType.text:
|
||||
if not isinstance(channel, discord.TextChannel):
|
||||
return
|
||||
try:
|
||||
message = await channel.fetch_message(msgid)
|
||||
|
||||
Reference in New Issue
Block a user