Merge branch 'v2.0' into 'dev'

V2.0 Alpha 0

Closes #116, #113, #117, #92, #93, #94, #96, #98, #99, #100, #103, #97, #101, #105, and #104

See merge request stark-industries/j.a.r.v.i.s.!48
This commit is contained in:
Zeva Rose 2022-02-04 21:26:27 +00:00
commit 6fb8b8ba53
56 changed files with 3951 additions and 2865 deletions

14
.flake8 Normal file
View file

@ -0,0 +1,14 @@
[flake8]
extend-ignore =
Q0, E501, C812, E203, W503, # These default to arguing with Black. We might configure some of them eventually
ANN1, # Ignore self and cls annotations
ANN204, ANN206, # return annotations for special methods and class methods
D105, D107, # Missing Docstrings in magic method and __init__
S311, # Standard pseudo-random generators are not suitable for security/cryptographic purposes.
D401, # First line should be in imperative mood; try rephrasing
D400, # First line should end with a period
D101, # Missing docstring in public class
# Plugins we don't currently include: flake8-return
R503, # missing explicit return at the end of function ableto return non-None value.
max-line-length=100

View file

@ -1,12 +1,15 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v4.1.0
hooks:
- id: check-toml
- id: check-yaml
args: [--unsafe]
- id: check-merge-conflict
- id: requirements-txt-fixer
- id: end-of-file-fixer
- id: debug-statements
language_version: python3.10
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
@ -16,23 +19,31 @@ repos:
- id: python-check-blanket-noqa
- repo: https://github.com/psf/black
rev: 21.7b0
rev: 22.1.0
hooks:
- id: black
args: [--line-length=120]
args: [--line-length=100, --target-version=py310]
language_version: python3.10
- repo: https://github.com/pre-commit/mirrors-isort
rev: V5.9.3
rev: V5.10.1
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/pycqa/flake8
rev: 3.9.2
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies:
- flake8-annotations~=2.0
- flake8-bandit~=2.1
- flake8-docstrings~=1.5
args: [--max-line-length=120, --ignore=ANN101 D107 ANN102 ANN206 D105 ANN204]
- flake8-bugbear
- flake8-comprehensions
- flake8-quotes
- flake8-raise
- flake8-deprecated
- flake8-print
- flake8-return
language_version: python3.10

View file

@ -1,12 +1,16 @@
<div align="center">
<img width=15% alt="J.A.R.V.I.S" src="https://git.zevaryx.com/stark-industries/j.a.r.v.i.s./-/raw/main/jarvis_small.png">
# Just Another Rather Very Intelligent System
<br />
[![python 3.8+](https://img.shields.io/badge/python-3.8+-blue)]()
[![tokei lines of code](https://tokei.rs/b1/git.zevaryx.com/stark-industries/j.a.r.v.i.s.?category=code)](https://git.zevaryx.com/stark-industries/j.a.r.v.i.s.)
[![discord chat widget](https://img.shields.io/discord/862402786116763668?style=social&logo=discord)](https://discord.gg/VtgZntXcnZ)
</div>
<br />
<img width="150" height="150" align="left" alt="J.A.R.V.I.S" src="https://i.imgur.com/7bnHam2.png">
# Just Another Very Intelligent System (J.A.R.V.I.S.)
Welcome to the J.A.R.V.I.S. Initiative! While the main goal is to create the best discord bot there can be, a great achievement would be to present him to the Robots and have him integrated into the dbrand server. Feel free to suggest anything you may think to be useful… or cool.
**Note:** Some commands have been custom made to be used in the dbrand server.
@ -34,19 +38,17 @@ If you wish to contribute to the J.A.R.V.I.S codebase or documentation, join the
Join the [Stark R&D Department Discord server](https://discord.gg/VtgZntXcnZ) to be kept up-to-date on code updates and issues.
## Requirements
- MongoDB 4.4 or higher
- Python 3.8 or higher
- MongoDB 5.0 or higher
- Python 3.10 or higher
- [tokei](https://github.com/XAMPPRocky/tokei) 12.1 or higher
On top of the above requirements, the following pip packages are also required:
- `discord-py>=1.7, <2`
- `dis-snek>=5.0.0`
- `psutil>=5.8, <6`
- `GitPython>=3.1, <4`
- `PyYaml>=5.4, <6`
- `discord-py-slash-command>=2.3.2, <3`
- `pymongo>=3.12.0, <4`
- `opencv-python>=4.5, <5`
- `ButtonPaginator>=0.0.3`
- `Pillow>=8.2.0, <9`
- `python-gitlab>=2.9.0, <3`
- `ulid-py>=1.1.0, <2`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,16 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15360" height="15360" viewBox="0 0 1080 1080">
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
<defs>
<style>
.a {
fill: #3498DB;
fill: #3498db;
}
</style>
</defs>
<title>logotests</title>
<title>jarvis</title>
<g>
<path class="a" d="M500.16,699.361c-15.864,0-31.731-.186-47.589.121-4.415.085-6.757-1.462-8.868-5.129Q358.817,546.94,273.657,399.686c-2.385-4.122-1.979-6.914.278-10.773q23.957-40.968,47.4-82.235c2.583-4.557,5.44-6.172,10.731-6.164q168.183.256,336.365,0c5.351-.009,8.141,1.751,10.688,6.244q23.394,41.288,47.384,82.233c2.6,4.409,2.259,7.322-.148,11.48q-84.848,146.517-169.341,293.24c-2.477,4.3-5.14,5.879-10.072,5.776C531.354,699.156,515.755,699.362,500.16,699.361Z"/>
<path class="a" d="M830.455,402.341c3.487-6.006,3.164-10.053-.2-15.857L679.691,124.975a5.046,5.046,0,0,1,4.38-7.558c31.658.014,62.194.087,92.727-.117,5.027-.034,6.191,3.24,8.042,6.444L938,388.752c2.542,4.305,2.461,7.329,0,11.574Q853.239,546.711,768.849,693.311c-2.624,4.561-5.4,6.337-10.714,6.291-25.437-.22-57.755-.138-85.331-.118a8.44,8.44,0,0,1-7.3-12.679Z"/>
<path class="a" d="M281.535,209.29c-6.873-.016-10.394,2.148-13.747,7.989L116.067,479.618a4.479,4.479,0,0,1-7.758,0c-15.635-27.113-31.377-54.571-47.067-81.306-2.578-4.394-.135-7.026,1.659-10.136L215.492,123.665c2.542-4.576,5.217-6.387,10.594-6.379q168.249.252,336.5.156c3.287,0,6.7-.987,9.082,3.193,15.182,26.623,31.107,54.043,46.743,81.068a5.139,5.139,0,0,1-4.389,7.71Z"/>
<path class="a" d="M705.242,800.514a6.557,6.557,0,0,0-5.583-9.992c-45.182-.007-209.265-.024-294.784.122-5.965.01-9.493-1.368-12.719-6.924C340.927,695.465,229.08,504.187,224.47,496.4a6.562,6.562,0,0,0-11.329.073c-15.415,26.744-30.437,52.884-45.654,78.911-2.168,3.708-.393,5.992,1.218,8.719Q255,730.093,341.177,876.15c2.812,4.774,5.763,6.589,11.348,6.564q148.016-.678,296.032-.843c4.973-.007,7.683-1.729,10.232-5.823Z"/>
<path class="a" d="M500.163,703.949c-16.228,0-32.461-.191-48.684.123-4.516.088-6.912-1.5-9.072-5.247q-86.839-150.8-173.959-301.448c-2.44-4.217-2.024-7.072.285-11.02q24.507-41.912,48.491-84.128c2.641-4.662,5.564-6.314,10.978-6.306q172.052.264,344.105,0c5.474-.009,8.329,1.791,10.934,6.388q23.933,42.237,48.474,84.125c2.658,4.511,2.312,7.491-.151,11.744q-86.8,149.889-173.238,299.989c-2.534,4.4-5.259,6.014-10.3,5.909C532.075,703.739,516.117,703.95,500.163,703.949Z"/>
<path class="a" d="M838.06,400.093c3.567-6.144,3.236-10.284-.206-16.221L683.826,116.344a5.162,5.162,0,0,1,4.481-7.731c32.387.014,63.625.088,94.861-.12,5.143-.035,6.334,3.315,8.227,6.592L948.08,386.192c2.6,4.4,2.518,7.5,0,11.84Q861.369,547.786,775.037,697.76c-2.685,4.665-5.529,6.483-10.962,6.436-26.022-.225-59.084-.142-87.294-.121A8.635,8.635,0,0,1,669.31,691.1Z"/>
<path class="a" d="M276.507,202.6c-7.031-.016-10.633,2.2-14.063,8.172L107.231,479.149a4.582,4.582,0,0,1-7.937,0C83.3,451.414,67.2,423.324,51.145,395.974c-2.638-4.5-.138-7.188,1.7-10.369L208.944,115c2.6-4.682,5.338-6.534,10.838-6.526q172.121.258,344.243.16c3.362,0,6.852-1.01,9.291,3.267,15.531,27.235,31.822,55.286,47.818,82.933a5.258,5.258,0,0,1-4.49,7.888Z"/>
<path class="a" d="M709.965,807.43a6.707,6.707,0,0,0-5.711-10.222c-46.221-.007-214.081-.024-301.569.125-6.1.01-9.711-1.4-13.01-7.083-52.409-90.286-166.83-285.967-171.546-293.935a6.714,6.714,0,0,0-11.59.074c-15.769,27.36-31.137,54.1-46.7,80.728-2.218,3.794-.4,6.13,1.247,8.92q88.278,149.351,176.441,298.769c2.877,4.885,5.9,6.741,11.609,6.715q151.422-.693,302.845-.862c5.088-.007,7.859-1.768,10.467-5.957Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,20 +1,13 @@
"""Main J.A.R.V.I.S. package."""
import asyncio
import logging
from pathlib import Path
from typing import Optional
from discord import Intents
from discord.ext import commands
from discord.utils import find
from discord_slash import SlashCommand
from dis_snek import Intents, Snake, listen
from mongoengine import connect
from psutil import Process
from jarvis import logo # noqa: F401
# from jarvis import logo # noqa: F401
from jarvis import tasks, utils
from jarvis.config import get_config
from jarvis.events import guild, member, message
from jarvis.events import member, message
jconfig = get_config()
@ -24,53 +17,32 @@ file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode
file_handler.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s][%(name)s] %(message)s"))
logger.addHandler(file_handler)
if asyncio.get_event_loop().is_closed():
asyncio.set_event_loop(asyncio.new_event_loop())
intents = Intents.default()
intents = Intents.DEFAULT
intents.members = True
restart_ctx = None
jarvis = commands.Bot(
command_prefix=utils.get_prefix,
intents=intents,
help_command=None,
max_messages=jconfig.max_messages,
)
jarvis = Snake(intents=intents, default_prefix="!", sync_interactions=jconfig.sync)
slash = SlashCommand(jarvis, sync_commands=False, sync_on_cog_reload=True)
jarvis_self = Process()
__version__ = "1.11.4"
__version__ = "2.0.0a0"
@jarvis.event
@listen()
async def on_ready() -> None:
"""d.py on_ready override."""
"""Lepton on_ready override."""
global restart_ctx
print(" Logged in as {0.user}".format(jarvis))
print(" Connected to {} guild(s)".format(len(jarvis.guilds)))
with jarvis_self.oneshot():
print(f" Current PID: {jarvis_self.pid}")
Path(f"jarvis.{jarvis_self.pid}.pid").touch()
if restart_ctx:
channel = None
if "guild" in restart_ctx:
guild = find(lambda x: x.id == restart_ctx["guild"], jarvis.guilds)
if guild:
channel = find(lambda x: x.id == restart_ctx["channel"], guild.channels)
elif "user" in restart_ctx:
channel = jarvis.get_user(restart_ctx["user"])
if channel:
await channel.send("Core systems restarted and back online.")
restart_ctx = None
print(" Logged in as {0.user}".format(jarvis)) # noqa: T001
print(" Connected to {} guild(s)".format(len(jarvis.guilds))) # noqa: T001
def run(ctx: dict = None) -> Optional[dict]:
@listen()
async def on_startup() -> None:
"""Lepton on_startup override."""
tasks.init()
def run() -> None:
"""Run J.A.R.V.I.S."""
global restart_ctx
if ctx:
restart_ctx = ctx
connect(
db="ctc2",
alias="ctc2",
@ -84,27 +56,21 @@ def run(ctx: dict = None) -> Optional[dict]:
**jconfig.mongo["connect"],
)
jconfig.get_db_config()
for extension in utils.get_extensions():
jarvis.load_extension(extension)
print(
print( # noqa: T001
" https://discord.com/api/oauth2/authorize?client_id="
+ "{}&permissions=8&scope=bot%20applications.commands".format(jconfig.client_id) # noqa: W503
"{}&permissions=8&scope=bot%20applications.commands".format(jconfig.client_id)
)
jarvis.max_messages = jconfig.max_messages
tasks.init()
# Add event listeners
if jconfig.events:
_ = [
guild.GuildEventHandler(jarvis),
member.MemberEventHandler(jarvis),
message.MessageEventHandler(jarvis),
]
jarvis.run(jconfig.token, bot=True, reconnect=True)
for cog in jarvis.cogs:
session = getattr(cog, "_session", None)
if session:
session.close()
if restart_ctx:
return restart_ctx
jarvis.start(jconfig.token)

View file

@ -1,16 +1,16 @@
"""J.A.R.V.I.S. Admin Cogs."""
from discord.ext.commands import Bot
from dis_snek import Snake
from jarvis.cogs.admin import ban, kick, lock, lockdown, mute, purge, roleping, warning
from jarvis.cogs.admin import ban, kick, mute, purge, roleping, warning
def setup(bot: Bot) -> None:
def setup(bot: Snake) -> None:
"""Add admin cogs to J.A.R.V.I.S."""
bot.add_cog(ban.BanCog(bot))
bot.add_cog(kick.KickCog(bot))
bot.add_cog(lock.LockCog(bot))
bot.add_cog(lockdown.LockdownCog(bot))
bot.add_cog(mute.MuteCog(bot))
bot.add_cog(purge.PurgeCog(bot))
bot.add_cog(roleping.RolepingCog(bot))
bot.add_cog(warning.WarningCog(bot))
ban.BanCog(bot)
kick.KickCog(bot)
# lock.LockCog(bot)
# lockdown.LockdownCog(bot)
mute.MuteCog(bot)
purge.PurgeCog(bot)
roleping.RolepingCog(bot)
warning.WarningCog(bot)

View file

@ -2,30 +2,33 @@
import re
from datetime import datetime, timedelta
from ButtonPaginator import Paginator
from discord import User
from discord.ext import commands
from discord.utils import find
from discord_slash import SlashContext, cog_ext
from discord_slash.model import ButtonStyle
from discord_slash.utils.manage_commands import create_choice, create_option
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Ban, Unban
from jarvis.utils import build_embed
from jarvis.utils import build_embed, find, find_all
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions
class BanCog(CacheCog):
"""J.A.R.V.I.S. BanCog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
super().__init__(bot)
async def discord_apply_ban(
self,
ctx: SlashContext,
ctx: InteractionContext,
reason: str,
user: User,
duration: int,
@ -37,7 +40,7 @@ class BanCog(CacheCog):
await ctx.guild.ban(user, reason=reason)
_ = Ban(
user=user.id,
username=user.name,
username=user.username,
discrim=user.discriminator,
reason=reason,
admin=ctx.author.id,
@ -54,20 +57,20 @@ class BanCog(CacheCog):
)
embed.set_author(
name=user.nick if user.nick else user.name,
icon_url=user.avatar_url,
name=user.display_name,
icon_url=user.avatar,
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
embed.set_thumbnail(url=user.avatar)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
async def discord_apply_unban(self, ctx: SlashContext, user: User, reason: str) -> None:
async def discord_apply_unban(self, ctx: InteractionContext, user: User, reason: str) -> None:
"""Apply a Discord unban."""
await ctx.guild.unban(user, reason=reason)
_ = Unban(
user=user.id,
username=user.name,
username=user.username,
discrim=user.discriminator,
guild=ctx.guild.id,
admin=ctx.author.id,
@ -77,77 +80,56 @@ class BanCog(CacheCog):
embed = build_embed(
title="User Unbanned",
description=f"<@{user.id}> was unbanned",
fields=[Field(name="Reason", value=reason)],
fields=[EmbedField(name="Reason", value=reason)],
)
embed.set_author(
name=user.name,
icon_url=user.avatar_url,
name=user.username,
icon_url=user.avatar,
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
embed.set_thumbnail(url=user.avatar)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
@cog_ext.cog_slash(
name="ban",
description="Ban a user",
options=[
create_option(
name="user",
description="User to ban",
option_type=6,
required=True,
),
create_option(
name="reason",
description="Ban reason",
required=True,
option_type=3,
),
create_option(
name="btype",
description="Ban type",
option_type=3,
required=False,
choices=[
create_choice(value="perm", name="Permanent"),
create_choice(value="temp", name="Temporary"),
create_choice(value="soft", name="Soft"),
],
),
create_option(
name="duration",
description="Ban duration in hours if temporary",
required=False,
option_type=4,
),
@slash_command(name="ban", description="Ban a user")
@slash_option(name="user", description="User to ban", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="reason", description="Ban reason", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="btype",
description="Ban type",
opt_type=OptionTypes.STRING,
required=True,
choices=[
SlashCommandChoice(name="Permanent", value="perm"),
SlashCommandChoice(name="Temporary", value="temp"),
SlashCommandChoice(name="Soft", value="soft"),
],
)
@admin_or_permissions(ban_members=True)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _ban(
self,
ctx: SlashContext,
ctx: InteractionContext,
reason: str,
user: User = None,
reason: str = None,
btype: str = "perm",
duration: int = 4,
) -> None:
if not user or user == ctx.author:
await ctx.send("You cannot ban yourself.", hidden=True)
await ctx.send("You cannot ban yourself.", ephemeral=True)
return
if user == self.bot.user:
await ctx.send("I'm afraid I can't let you do that", hidden=True)
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
return
if btype == "temp" and duration < 0:
await ctx.send("You cannot set a temp ban to < 0 hours.", hidden=True)
await ctx.send("You cannot set a temp ban to < 0 hours.", ephemeral=True)
return
elif btype == "temp" and duration > 744:
await ctx.send("You cannot set a temp ban to > 1 month", hidden=True)
await ctx.send("You cannot set a temp ban to > 1 month", ephemeral=True)
return
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", hidden=True)
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
if not reason:
reason = "Mr. Stark is displeased with your presence. Please leave."
await ctx.defer()
@ -160,10 +142,10 @@ class BanCog(CacheCog):
if mtype == "temp":
user_message += f"\nDuration: {duration} hours"
fields = [Field(name="Type", value=mtype)]
fields = [EmbedField(name="Type", value=mtype)]
if mtype == "temp":
fields.append(Field(name="Duration", value=f"{duration} hour(s)"))
fields.append(EmbedField(name="Duration", value=f"{duration} hour(s)"))
user_embed = build_embed(
title=f"You have been banned from {ctx.guild.name}",
@ -172,10 +154,10 @@ class BanCog(CacheCog):
)
user_embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url,
name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar,
)
user_embed.set_thumbnail(url=ctx.guild.icon_url)
user_embed.set_thumbnail(url=ctx.guild.icon.url)
try:
await user.send(embed=user_embed)
@ -184,13 +166,13 @@ class BanCog(CacheCog):
try:
await ctx.guild.ban(user, reason=reason)
except Exception as e:
await ctx.send(f"Failed to ban user:\n```\n{e}\n```", hidden=True)
await ctx.send(f"Failed to ban user:\n```\n{e}\n```", ephemeral=True)
return
send_failed = False
if mtype == "soft":
await ctx.guild.unban(user, reason="Ban was softban")
fields.append(Field(name="DM Sent?", value=str(not send_failed)))
fields.append(EmbedField(name="DM Sent?", value=str(not send_failed)))
if btype != "temp":
duration = None
active = True
@ -199,33 +181,22 @@ class BanCog(CacheCog):
await self.discord_apply_ban(ctx, reason, user, duration, active, fields, mtype)
@cog_ext.cog_slash(
name="unban",
description="Unban a user",
options=[
create_option(
name="user",
description="User to unban",
option_type=3,
required=True,
),
create_option(
name="reason",
description="Unban reason",
required=True,
option_type=3,
),
],
@slash_command(name="unban", description="Unban a user")
@slash_option(
name="user", description="User to unban", opt_type=OptionTypes.STRING, required=True
)
@admin_or_permissions(ban_members=True)
@slash_option(
name="reason", description="Unban reason", opt_type=OptionTypes.STRING, required=True
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _unban(
self,
ctx: SlashContext,
ctx: InteractionContext,
user: str,
reason: str,
) -> None:
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", hidden=True)
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
orig_user = user
@ -236,26 +207,31 @@ class BanCog(CacheCog):
bans = await ctx.guild.bans()
# Try to get ban information out of Discord
if re.match("^[0-9]{1,}$", user): # User ID
if re.match(r"^[0-9]{1,}$", user): # User ID
user = int(user)
discord_ban_info = find(lambda x: x.user.id == user, bans)
else: # User name
if re.match("#[0-9]{4}$", user): # User name has discrim
if re.match(r"#[0-9]{4}$", user): # User name has discrim
user, discrim = user.split("#")
if discrim:
discord_ban_info = find(
lambda x: x.user.name == user and x.user.discriminator == discrim,
lambda x: x.user.username == user and x.user.discriminator == discrim,
bans,
)
else:
results = [x for x in filter(lambda x: x.user.name == user, bans)]
results = find_all(lambda x: x.user.username == user, bans)
if results:
if len(results) > 1:
active_bans = []
for ban in bans:
active_bans.append("{0} ({1}): {2}".format(ban.user.name, ban.user.id, ban.reason))
active_bans.append(
"{0} ({1}): {2}".format(ban.user.username, ban.user.id, ban.reason)
)
ab_message = "\n".join(active_bans)
message = f"More than one result. Please use one of the following IDs:\n```{ab_message}\n```"
message = (
"More than one result. "
f"Please use one of the following IDs:\n```{ab_message}\n```"
)
await ctx.send(message)
return
else:
@ -278,7 +254,7 @@ class BanCog(CacheCog):
database_ban_info = Ban.objects(**search).first()
if not discord_ban_info and not database_ban_info:
await ctx.send(f"Unable to find user {orig_user}", hidden=True)
await ctx.send(f"Unable to find user {orig_user}", ephemeral=True)
elif discord_ban_info:
await self.discord_apply_unban(ctx, discord_ban_info.user, reason)
@ -297,46 +273,41 @@ class BanCog(CacheCog):
admin=ctx.author.id,
reason=reason,
).save()
await ctx.send("Unable to find user in Discord, " + "but removed entry from database.")
await ctx.send(
"Unable to find user in Discord, " + "but removed entry from database."
)
@cog_ext.cog_subcommand(
base="bans",
name="list",
description="List bans",
options=[
create_option(
name="type",
description="Ban type",
option_type=4,
required=False,
choices=[
create_choice(value=0, name="All"),
create_choice(value=1, name="Permanent"),
create_choice(value=2, name="Temporary"),
create_choice(value=3, name="Soft"),
],
),
create_option(
name="active",
description="Active bans",
option_type=4,
required=False,
choices=[
create_choice(value=1, name="Yes"),
create_choice(value=0, name="No"),
],
),
@slash_command(
name="bans", description="User bans", sub_cmd_name="list", sub_cmd_description="List bans"
)
@slash_option(
name="btype",
description="Ban type",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[
SlashCommandChoice(name="All", value=0),
SlashCommandChoice(name="Permanent", value=1),
SlashCommandChoice(name="Temporary", value=2),
SlashCommandChoice(name="Soft", value=3),
],
)
@admin_or_permissions(ban_members=True)
async def _bans_list(self, ctx: SlashContext, type: int = 0, active: int = 1) -> None:
@slash_option(
name="active",
description="Active bans",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[SlashCommandChoice(name="Yes", value=1), SlashCommandChoice(name="No", value=0)],
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _bans_list(self, ctx: InteractionContext, type: int = 0, active: int = 1) -> None:
active = bool(active)
exists = self.check_cache(ctx, type=type, active=active)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
types = [0, "perm", "temp", "soft"]
@ -351,9 +322,9 @@ class BanCog(CacheCog):
for ban in bans:
if not ban.username:
user = await self.bot.fetch_user(ban.user)
ban.username = user.name if user else "[deleted user]"
ban.username = user.username if user else "[deleted user]"
fields.append(
Field(
EmbedField(
name=f"Username: {ban.username}#{ban.discrim}",
value=(
f"Date: {ban.created_at.strftime('%d-%m-%Y')}\n"
@ -370,8 +341,8 @@ class BanCog(CacheCog):
for ban in bans:
if ban.user.id not in db_bans:
fields.append(
Field(
name=f"Username: {ban.user.name}#" + f"{ban.user.discriminator}",
EmbedField(
name=f"Username: {ban.user.username}#" + f"{ban.user.discriminator}",
value=(
f"Date: [unknown]\n"
f"User ID: {ban.user.id}\n"
@ -395,26 +366,15 @@ class BanCog(CacheCog):
description=f"No {'in' if not active else ''}active bans",
fields=[],
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed)
else:
for i in range(0, len(bans), 5):
embed = build_embed(title=title, description="", fields=fields[i : i + 5]) # noqa: E203
embed.set_thumbnail(url=ctx.guild.icon_url)
embed = build_embed(title=title, description="", fields=fields[i : i + 5])
embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed)
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=pages,
only=ctx.author,
timeout=60 * 5, # 5 minute timeout
disable_after_timeout=True,
use_extend=len(pages) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
@ -426,4 +386,4 @@ class BanCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)

View file

@ -1,53 +1,38 @@
"""J.A.R.V.I.S. KickCog."""
from discord import User
from discord.ext.commands import Bot
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from dis_snek import InteractionContext, Permissions, Scale
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Kick
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions
class KickCog(CacheCog):
class KickCog(Scale):
"""J.A.R.V.I.S. KickCog."""
def __init__(self, bot: Bot):
super().__init__(bot)
@cog_ext.cog_slash(
name="kick",
description="Kick a user",
options=[
create_option(
name="user",
description="User to kick",
option_type=6,
required=True,
),
create_option(
name="reason",
description="Kick reason",
required=False,
option_type=3,
),
],
@slash_command(name="kick", description="Kick a user")
@slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="reason", description="Kick reason", opt_type=OptionTypes.STRING, required=True
)
@admin_or_permissions(kick_members=True)
async def _kick(self, ctx: SlashContext, user: User, reason: str = None) -> None:
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _kick(self, ctx: InteractionContext, user: User, reason: str) -> None:
if not user or user == ctx.author:
await ctx.send("You cannot kick yourself.", hidden=True)
await ctx.send("You cannot kick yourself.", ephemeral=True)
return
if user == self.bot.user:
await ctx.send("I'm afraid I can't let you do that", hidden=True)
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
return
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", hidden=True)
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
if not reason:
reason = "Mr. Stark is displeased with your presence. Please leave."
guild_name = ctx.guild.name
embed = build_embed(
title=f"You have been kicked from {guild_name}",
@ -56,10 +41,10 @@ class KickCog(CacheCog):
)
embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url,
name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar.url,
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_thumbnail(url=ctx.guild.icon.url)
send_failed = False
try:
@ -68,19 +53,16 @@ class KickCog(CacheCog):
send_failed = True
await ctx.guild.kick(user, reason=reason)
fields = [Field(name="DM Sent?", value=str(not send_failed))]
fields = [EmbedField(name="DM Sent?", value=str(not send_failed))]
embed = build_embed(
title="User Kicked",
description=f"Reason: {reason}",
fields=fields,
)
embed.set_author(
name=user.nick if user.nick else user.name,
icon_url=user.avatar_url,
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
_ = Kick(

View file

@ -1,134 +1,113 @@
"""J.A.R.V.I.S. LockCog."""
from contextlib import suppress
from typing import Union
from dis_snek import Scale
from discord import Role, TextChannel, User, VoiceChannel
from discord.ext.commands import Bot
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from jarvis.db.models import Lock
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.permissions import admin_or_permissions
# TODO: Uncomment 99% of code once implementation is figured out
# from contextlib import suppress
# from typing import Union
#
# from dis_snek import InteractionContext, Scale, Snake
# from dis_snek.models.discord.enums import Permissions
# from dis_snek.models.discord.role import Role
# from dis_snek.models.discord.user import User
# from dis_snek.models.discord.channel import GuildText, GuildVoice, PermissionOverwrite
# from dis_snek.models.snek.application_commands import (
# OptionTypes,
# PermissionTypes,
# slash_command,
# slash_option,
# )
# from dis_snek.models.snek.command import check
#
# from jarvis.db.models import Lock
# from jarvis.utils.permissions import admin_or_permissions
class LockCog(CacheCog):
class LockCog(Scale):
"""J.A.R.V.I.S. LockCog."""
def __init__(self, bot: Bot):
super().__init__(bot)
async def _lock_channel(
self,
channel: Union[TextChannel, VoiceChannel],
role: Role,
admin: User,
reason: str,
allow_send: bool = False,
) -> None:
overrides = channel.overwrites_for(role)
if isinstance(channel, TextChannel):
overrides.send_messages = allow_send
elif isinstance(channel, VoiceChannel):
overrides.speak = allow_send
await channel.set_permissions(role, overwrite=overrides, reason=reason)
async def _unlock_channel(
self,
channel: Union[TextChannel, VoiceChannel],
role: Role,
admin: User,
) -> None:
overrides = channel.overwrites_for(role)
if isinstance(channel, TextChannel):
overrides.send_messages = None
elif isinstance(channel, VoiceChannel):
overrides.speak = None
await channel.set_permissions(role, overwrite=overrides)
@cog_ext.cog_slash(
name="lock",
description="Locks a channel",
options=[
create_option(
name="reason",
description="Lock Reason",
option_type=3,
required=True,
),
create_option(
name="duration",
description="Lock duration in minutes (default 10)",
option_type=4,
required=False,
),
create_option(
name="channel",
description="Channel to lock",
option_type=7,
required=False,
),
],
)
@admin_or_permissions(manage_channels=True)
async def _lock(
self,
ctx: SlashContext,
reason: str,
duration: int = 10,
channel: Union[TextChannel, VoiceChannel] = None,
) -> None:
await ctx.defer(hidden=True)
if duration <= 0:
await ctx.send("Duration must be > 0", hidden=True)
return
elif duration >= 300:
await ctx.send("Duration must be < 5 hours", hidden=True)
return
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", hidden=True)
return
if not channel:
channel = ctx.channel
for role in ctx.guild.roles:
with suppress(Exception):
await self._lock_channel(channel, role, ctx.author, reason)
_ = Lock(
channel=channel.id,
guild=ctx.guild.id,
admin=ctx.author.id,
reason=reason,
duration=duration,
).save()
await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
@cog_ext.cog_slash(
name="unlock",
description="Unlocks a channel",
options=[
create_option(
name="channel",
description="Channel to lock",
option_type=7,
required=False,
),
],
)
@admin_or_permissions(manage_channels=True)
async def _unlock(
self,
ctx: SlashContext,
channel: Union[TextChannel, VoiceChannel] = None,
) -> None:
if not channel:
channel = ctx.channel
lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first()
if not lock:
await ctx.send(f"{channel.mention} not locked.", hidden=True)
return
for role in ctx.guild.roles:
with suppress(Exception):
await self._unlock_channel(channel, role, ctx.author)
lock.active = False
lock.save()
await ctx.send(f"{channel.mention} unlocked")
# @slash_command(name="lock", description="Lock a channel")
# @slash_option(name="reason",
# description="Lock Reason",
# opt_type=3,
# required=True,)
# @slash_option(name="duration",
# description="Lock duration in minutes (default 10)",
# opt_type=4,
# required=False,)
# @slash_option(name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,)
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _lock(
# self,
# ctx: InteractionContext,
# reason: str,
# duration: int = 10,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# await ctx.defer(ephemeral=True)
# if duration <= 0:
# await ctx.send("Duration must be > 0", ephemeral=True)
# return
#
# elif duration > 60 * 12:
# await ctx.send("Duration must be <= 12 hours", ephemeral=True)
# return
#
# if len(reason) > 100:
# await ctx.send("Reason must be <= 100 characters", ephemeral=True)
# return
# if not channel:
# channel = ctx.channel
#
# # role = ctx.guild.default_role # Uncomment once implemented
# if isinstance(channel, GuildText):
# to_deny = Permissions.SEND_MESSAGES
# elif isinstance(channel, GuildVoice):
# to_deny = Permissions.CONNECT | Permissions.SPEAK
#
# overwrite = PermissionOverwrite(type=PermissionTypes.ROLE, deny=to_deny)
# # TODO: Get original permissions
# # TODO: Apply overwrite
# overwrite = overwrite
# _ = Lock(
# channel=channel.id,
# guild=ctx.guild.id,
# admin=ctx.author.id,
# reason=reason,
# duration=duration,
# ) # .save() # Uncomment once implemented
# # await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
# await ctx.send("Unfortunately, this is not yet implemented", hidden=True)
#
# @cog_ext.cog_slash(
# name="unlock",
# description="Unlocks a channel",
# choices=[
# create_option(
# name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,
# ),
# ],
# )
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _unlock(
# self,
# ctx: InteractionContext,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# if not channel:
# channel = ctx.channel
# lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first()
# if not lock:
# await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
# return
# for role in ctx.guild.roles:
# with suppress(Exception):
# await self._unlock_channel(channel, role, ctx.author)
# lock.active = False
# lock.save()
# await ctx.send(f"{channel.mention} unlocked")

View file

@ -8,7 +8,8 @@ from discord_slash.utils.manage_commands import create_option
from jarvis.db.models import Lock
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.permissions import admin_or_permissions
# from jarvis.utils.permissions import admin_or_permissions
class LockdownCog(CacheCog):
@ -21,34 +22,34 @@ class LockdownCog(CacheCog):
base="lockdown",
name="start",
description="Locks a server",
options=[
choices=[
create_option(
name="reason",
description="Lockdown Reason",
option_type=3,
opt_type=3,
required=True,
),
create_option(
name="duration",
description="Lockdown duration in minutes (default 10)",
option_type=4,
opt_type=4,
required=False,
),
],
)
@admin_or_permissions(manage_channels=True)
# @check(admin_or_permissions(manage_channels=True))
async def _lockdown_start(
self,
ctx: SlashContext,
reason: str,
duration: int = 10,
) -> None:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
if duration <= 0:
await ctx.send("Duration must be > 0", hidden=True)
await ctx.send("Duration must be > 0", ephemeral=True)
return
elif duration >= 300:
await ctx.send("Duration must be < 5 hours", hidden=True)
await ctx.send("Duration must be < 5 hours", ephemeral=True)
return
channels = ctx.guild.channels
roles = ctx.guild.roles
@ -87,7 +88,7 @@ class LockdownCog(CacheCog):
update = False
locks = Lock.objects(guild=ctx.guild.id, active=True)
if not locks:
await ctx.send("No lockdown detected.", hidden=True)
await ctx.send("No lockdown detected.", ephemeral=True)
return
await ctx.defer()
for channel in channels:

View file

@ -1,132 +1,125 @@
"""J.A.R.V.I.S. MuteCog."""
from discord import Member
from discord.ext import commands
from discord.utils import get
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from datetime import datetime
from jarvis.db.models import Mute, Setting
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Mute
from jarvis.utils import build_embed
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions
class MuteCog(commands.Cog):
class MuteCog(Scale):
"""J.A.R.V.I.S. MuteCog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
@cog_ext.cog_slash(
name="mute",
description="Mute a user",
options=[
create_option(
name="user",
description="User to mute",
option_type=6,
required=True,
),
create_option(
name="reason",
description="Reason for mute",
option_type=3,
required=True,
),
create_option(
name="duration",
description="Duration of mute in minutes, default 30",
option_type=4,
required=False,
),
@slash_command(name="mute", description="Mute a user")
@slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="reason",
description="Reason for mute",
opt_type=OptionTypes.STRING,
required=True,
)
@slash_option(
name="time",
description="Duration of mute, default 1",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="scale",
description="Time scale, default Hour(s)",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[
SlashCommandChoice(name="Minute(s)", value=1),
SlashCommandChoice(name="Hour(s)", value=60),
SlashCommandChoice(name="Day(s)", value=3600),
SlashCommandChoice(name="Week(s)", value=604800),
],
)
@admin_or_permissions(mute_members=True)
async def _mute(self, ctx: SlashContext, user: Member, reason: str, duration: int = 30) -> None:
@check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
)
)
async def _timeout(
self, ctx: InteractionContext, user: Member, reason: str, time: int = 1, scale: int = 60
) -> None:
if user == ctx.author:
await ctx.send("You cannot mute yourself.", hidden=True)
await ctx.send("You cannot mute yourself.", ephemeral=True)
return
if user == self.bot.user:
await ctx.send("I'm afraid I can't let you do that", hidden=True)
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
return
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", hidden=True)
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
mute_setting = Setting.objects(guild=ctx.guild.id, setting="mute").first()
if not mute_setting:
await ctx.send(
"Please configure a mute role with /settings mute <role> first",
hidden=True,
)
# Max 4 weeks (2419200 seconds) per API
duration = time * scale
if duration > 2419200:
await ctx.send("Mute must be less than 4 weeks (2419200 seconds)", ephemeral=True)
return
role = get(ctx.guild.roles, id=mute_setting.value)
if role in user.roles:
await ctx.send("User already muted", hidden=True)
return
await user.add_roles(role, reason=reason)
if duration < 0 or duration > 300:
duration = -1
await user.timeout(communication_disabled_until=duration, reason=reason)
_ = Mute(
user=user.id,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
duration=duration,
active=True if duration >= 0 else False,
active=True,
).save()
embed = build_embed(
title="User Muted",
description=f"{user.mention} has been muted",
fields=[Field(name="Reason", value=reason)],
fields=[EmbedField(name="Reason", value=reason)],
)
embed.set_author(
name=user.nick if user.nick else user.name,
icon_url=user.avatar_url,
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
@cog_ext.cog_slash(
name="unmute",
description="Unmute a user",
options=[
create_option(
name="user",
description="User to unmute",
option_type=6,
required=True,
)
],
@slash_command(name="unmute", description="Unmute a user")
@slash_option(
name="user", description="User to unmute", opt_type=OptionTypes.USER, required=True
)
@admin_or_permissions(mute_members=True)
async def _unmute(self, ctx: SlashContext, user: Member) -> None:
mute_setting = Setting.objects(guild=ctx.guild.id, setting="mute").first()
if not mute_setting:
await ctx.send(
"Please configure a mute role with /settings mute <role> first.",
hidden=True,
)
@check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
)
)
async def _unmute(self, ctx: InteractionContext, user: Member) -> None:
if (
not user.communication_disabled_until
or user.communication_disabled_until < datetime.now() # noqa: W503
):
await ctx.send("User is not muted", ephemeral=True)
return
role = get(ctx.guild.roles, id=mute_setting.value)
if role in user.roles:
await user.remove_roles(role, reason="Unmute")
else:
await ctx.send("User is not muted.", hidden=True)
return
_ = Mute.objects(guild=ctx.guild.id, user=user.id).update(set__active=False)
embed = build_embed(
title="User Unmuted",
description=f"{user.mention} has been unmuted",
fields=[],
)
embed.set_author(
name=user.nick if user.nick else user.name,
icon_url=user.avatar_url,
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)

View file

@ -1,35 +1,34 @@
"""J.A.R.V.I.S. PurgeCog."""
from discord import TextChannel
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Autopurge, Purge
from jarvis.utils.permissions import admin_or_permissions
class PurgeCog(commands.Cog):
class PurgeCog(Scale):
"""J.A.R.V.I.S. PurgeCog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
@cog_ext.cog_slash(
name="purge",
description="Purge messages from channel",
options=[
create_option(
name="amount",
description="Amount of messages to purge",
required=False,
option_type=4,
)
],
@slash_command(name="purge", description="Purge messages from channel")
@slash_option(
name="amount",
description="Amount of messages to purge, default 10",
opt_type=OptionTypes.INTEGER,
required=False,
)
@admin_or_permissions(manage_messages=True)
async def _purge(self, ctx: SlashContext, amount: int = 10) -> None:
@check(admin_or_permissions(Permissions.MANAGE_MESSAGES))
async def _purge(self, ctx: InteractionContext, amount: int = 10) -> None:
if amount < 1:
await ctx.send("Amount must be >= 1", hidden=True)
await ctx.send("Amount must be >= 1", ephemeral=True)
return
await ctx.defer()
channel = ctx.channel
@ -44,39 +43,37 @@ class PurgeCog(commands.Cog):
count=amount,
).save()
@cog_ext.cog_subcommand(
base="autopurge",
name="add",
description="Automatically purge messages after x seconds",
options=[
create_option(
name="channel",
description="Channel to autopurge",
option_type=7,
required=True,
),
create_option(
name="delay",
description="Seconds to keep message before purge, default 30",
option_type=4,
required=False,
),
],
@slash_command(
name="autopurge", sub_cmd_name="add", sub_cmd_description="Automatically purge messages"
)
@admin_or_permissions(manage_messages=True)
async def _autopurge_add(self, ctx: SlashContext, channel: TextChannel, delay: int = 30) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", hidden=True)
@slash_option(
name="channel",
description="Channel to autopurge",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@slash_option(
name="delay",
description="Seconds to keep message before purge, default 30",
opt_type=OptionTypes.INTEGER,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_MESSAGES))
async def _autopurge_add(
self, ctx: InteractionContext, channel: GuildText, delay: int = 30
) -> None:
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a GuildText channel", ephemeral=True)
return
if delay <= 0:
await ctx.send("Delay must be > 0", hidden=True)
await ctx.send("Delay must be > 0", ephemeral=True)
return
elif delay > 300:
await ctx.send("Delay must be < 5 minutes", hidden=True)
await ctx.send("Delay must be < 5 minutes", ephemeral=True)
return
autopurge = Autopurge.objects(guild=ctx.guild.id, channel=channel.id).first()
if autopurge:
await ctx.send("Autopurge already exists.", hidden=True)
await ctx.send("Autopurge already exists.", ephemeral=True)
return
_ = Autopurge(
guild=ctx.guild.id,
@ -86,52 +83,48 @@ class PurgeCog(commands.Cog):
).save()
await ctx.send(f"Autopurge set up on {channel.mention}, delay is {delay} seconds")
@cog_ext.cog_subcommand(
base="autopurge",
name="remove",
description="Remove an autopurge",
options=[
create_option(
name="channel",
description="Channel to remove from autopurge",
option_type=7,
required=True,
),
],
@slash_command(
name="autopurge", sub_cmd_name="remove", sub_cmd_description="Remove an autopurge"
)
@admin_or_permissions(manage_messages=True)
async def _autopurge_remove(self, ctx: SlashContext, channel: TextChannel) -> None:
@slash_option(
name="channel",
description="Channel to remove from autopurge",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_MESSAGES))
async def _autopurge_remove(self, ctx: InteractionContext, channel: GuildText) -> None:
autopurge = Autopurge.objects(guild=ctx.guild.id, channel=channel.id)
if not autopurge:
await ctx.send("Autopurge does not exist.", hidden=True)
await ctx.send("Autopurge does not exist.", ephemeral=True)
return
autopurge.delete()
await ctx.send(f"Autopurge removed from {channel.mention}.")
@cog_ext.cog_subcommand(
base="autopurge",
name="update",
description="Update autopurge on a channel",
options=[
create_option(
name="channel",
description="Channel to update",
option_type=7,
required=True,
),
create_option(
name="delay",
description="New time to save",
option_type=4,
required=True,
),
],
@slash_command(
name="autopurge",
sub_cmd_name="update",
sub_cmd_description="Update autopurge on a channel",
)
@admin_or_permissions(manage_messages=True)
async def _autopurge_update(self, ctx: SlashContext, channel: TextChannel, delay: int) -> None:
@slash_option(
name="channel",
description="Channel to update",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@slash_option(
name="delay",
description="New time to save",
opt_type=OptionTypes.INTEGER,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_MESSAGES))
async def _autopurge_update(
self, ctx: InteractionContext, channel: GuildText, delay: int
) -> None:
autopurge = Autopurge.objects(guild=ctx.guild.id, channel=channel.id)
if not autopurge:
await ctx.send("Autopurge does not exist.", hidden=True)
await ctx.send("Autopurge does not exist.", ephemeral=True)
return
autopurge.delay = delay
autopurge.save()

View file

@ -1,44 +1,39 @@
"""J.A.R.V.I.S. RolepingCog."""
from datetime import datetime, timedelta
from ButtonPaginator import Paginator
from discord import Member, Role
from discord.ext.commands import Bot
from discord_slash import SlashContext, cog_ext
from discord_slash.model import ButtonStyle
from discord_slash.utils.manage_commands import create_option
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.role import Role
from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Roleping
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions
class RolepingCog(CacheCog):
"""J.A.R.V.I.S. RolepingCog."""
def __init__(self, bot: Bot):
def __init__(self, bot: Snake):
super().__init__(bot)
@cog_ext.cog_subcommand(
base="roleping",
name="add",
description="Add a role to roleping",
options=[
create_option(
name="role",
description="Role to add to roleping",
option_type=8,
required=True,
)
],
@slash_command(
name="roleping", sub_cmd_name="add", sub_cmd_description="Add a role to roleping"
)
@admin_or_permissions(manage_guild=True)
async def _roleping_add(self, ctx: SlashContext, role: Role) -> None:
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_add(self, ctx: InteractionContext, role: Role) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=role.id).first()
if roleping:
await ctx.send(f"Role `{role.name}` already in roleping.", hidden=True)
await ctx.send(f"Role `{role.name}` already in roleping.", ephemeral=True)
return
_ = Roleping(
role=role.id,
@ -49,55 +44,45 @@ class RolepingCog(CacheCog):
).save()
await ctx.send(f"Role `{role.name}` added to roleping.")
@cog_ext.cog_subcommand(
base="roleping",
name="remove",
description="Remove a role from the roleping",
options=[
create_option(
name="role",
description="Role to remove from roleping",
option_type=8,
required=True,
)
],
@slash_command(name="roleping", sub_cmd_name="remove", sub_cmd_description="Remove a role")
@slash_option(
name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
)
@admin_or_permissions(manage_guild=True)
async def _roleping_remove(self, ctx: SlashContext, role: Role) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_remove(self, ctx: InteractionContext, role: Role) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=role.id)
if not roleping:
await ctx.send("Roleping does not exist", hidden=True)
await ctx.send("Roleping does not exist", ephemeral=True)
return
roleping.delete()
await ctx.send(f"Role `{role.name}` removed from roleping.")
@cog_ext.cog_subcommand(
base="roleping",
name="list",
description="List all blocklisted roles",
)
async def _roleping_list(self, ctx: SlashContext) -> None:
@slash_command(name="roleping", sub_cmd_name="list", description="Lick all blocklisted roles")
async def _roleping_list(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
rolepings = Roleping.objects(guild=ctx.guild.id)
if not rolepings:
await ctx.send("No rolepings configured", hidden=True)
await ctx.send("No rolepings configured", ephemeral=True)
return
embeds = []
for roleping in rolepings:
role = ctx.guild.get_role(roleping.role)
role = await ctx.guild.get_role(roleping.role)
bypass_roles = list(filter(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles))
bypass_roles = [r.mention or "||`[redacted]`||" for r in bypass_roles]
bypass_users = [ctx.guild.get_member(u).mention or "||`[redacted]`||" for u in roleping.bypass["users"]]
bypass_users = [
await ctx.guild.get_member(u).mention or "||`[redacted]`||"
for u in roleping.bypass["users"]
]
bypass_roles = bypass_roles or ["None"]
bypass_users = bypass_users or ["None"]
embed = build_embed(
@ -105,44 +90,33 @@ class RolepingCog(CacheCog):
description=role.mention,
color=str(role.color),
fields=[
Field(
EmbedField(
name="Created At",
value=roleping.created_at.strftime("%a, %b %d, %Y %I:%M %p"),
inline=False,
),
Field(name="Active", value=str(roleping.active)),
Field(
EmbedField(name="Active", value=str(roleping.active)),
EmbedField(
name="Bypass Users",
value="\n".join(bypass_users),
),
Field(
EmbedField(
name="Bypass Roles",
value="\n".join(bypass_roles),
),
],
)
admin = ctx.guild.get_member(roleping.admin)
admin = await ctx.guild.get_member(roleping.admin)
if not admin:
admin = self.bot.user
embed.set_author(name=admin.nick or admin.name, icon_url=admin.avatar_url)
embed.set_author(name=admin.display_name, icon_url=admin.display_avatar.url)
embed.set_footer(text=f"{admin.name}#{admin.discriminator} | {admin.id}")
embeds.append(embed)
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=embeds,
only=ctx.author,
timeout=60 * 5, # 5 minute timeout
disable_after_timeout=True,
use_extend=len(embeds) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator.create_from_embeds(self.bot, *embeds, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
@ -152,45 +126,37 @@ class RolepingCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)
@cog_ext.cog_subcommand(
base="roleping",
subcommand_group="bypass",
name="user",
description="Add a user as a bypass to a roleping",
base_desc="Block roles from being pinged",
sub_group_desc="Allow specific users/roles to ping rolepings",
options=[
create_option(
name="user",
description="User to add",
option_type=6,
required=True,
),
create_option(
name="rping",
description="Rolepinged role",
option_type=8,
required=True,
),
],
@slash_command(
name="roleping",
description="Block roles from being pinged",
group_name="bypass",
group_description="Allow specific users/roles to ping rolepings",
sub_cmd_name="user",
sub_cmd_description="Add a user as a bypass to a roleping",
)
@admin_or_permissions(manage_guild=True)
async def _roleping_bypass_user(self, ctx: SlashContext, user: Member, rping: Role) -> None:
@slash_option(name="user", description="User to add", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_bypass_user(
self, ctx: InteractionContext, user: Member, rping: Role
) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", hidden=True)
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
return
if user.id in roleping.bypass["users"]:
await ctx.send(f"{user.mention} already in bypass", hidden=True)
await ctx.send(f"{user.mention} already in bypass", ephemeral=True)
return
if len(roleping.bypass["users"]) == 10:
await ctx.send(
"Already have 10 users in bypass. Please consider using roles for roleping bypass",
hidden=True,
ephemeral=True,
)
return
@ -199,51 +165,40 @@ class RolepingCog(CacheCog):
if matching_role:
await ctx.send(
f"{user.mention} already has bypass via {matching_role[0].mention}",
hidden=True,
ephemeral=True,
)
return
roleping.bypass["users"].append(user.id)
roleping.save()
await ctx.send(f"{user.nick or user.name} user bypass added for `{rping.name}`")
await ctx.send(f"{user.display_name} user bypass added for `{rping.name}`")
@cog_ext.cog_subcommand(
base="roleping",
subcommand_group="bypass",
name="role",
description="Add a role as a bypass to a roleping",
base_desc="Block roles from being pinged",
sub_group_desc="Allow specific users/roles to ping rolepings",
options=[
create_option(
name="role",
description="Role to add",
option_type=8,
required=True,
),
create_option(
name="rping",
description="Rolepinged role",
option_type=8,
required=True,
),
],
@slash_command(
name="roleping",
group_name="bypass",
sub_cmd_name="role",
description="Add a role as a bypass to roleping",
)
@admin_or_permissions(manage_guild=True)
async def _roleping_bypass_role(self, ctx: SlashContext, role: Role, rping: Role) -> None:
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_bypass_role(self, ctx: InteractionContext, role: Role, rping: Role) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", hidden=True)
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
return
if role.id in roleping.bypass["roles"]:
await ctx.send(f"{role.mention} already in bypass", hidden=True)
await ctx.send(f"{role.mention} already in bypass", ephemeral=True)
return
if len(roleping.bypass["roles"]) == 10:
await ctx.send(
"Already have 10 roles in bypass. Please consider consolidating roles for roleping bypass",
hidden=True,
"Already have 10 roles in bypass. "
"Please consider consolidating roles for roleping bypass",
ephemeral=True,
)
return
@ -251,80 +206,67 @@ class RolepingCog(CacheCog):
roleping.save()
await ctx.send(f"{role.name} role bypass added for `{rping.name}`")
@cog_ext.cog_subcommand(
base="roleping",
subcommand_group="restore",
name="user",
description="Remove a role bypass",
base_desc="Block roles from being pinged",
sub_group_desc="Remove a bypass from a roleping (restoring it)",
options=[
create_option(
name="user",
description="User to add",
option_type=6,
required=True,
),
create_option(
name="rping",
description="Rolepinged role",
option_type=8,
required=True,
),
],
@slash_command(
name="roleping",
description="Block roles from being pinged",
group_name="restore",
group_description="Remove a roleping bypass",
sub_cmd_name="user",
sub_cmd_description="Remove a bypass from a roleping (restoring it)",
)
@admin_or_permissions(manage_guild=True)
async def _roleping_restore_user(self, ctx: SlashContext, user: Member, rping: Role) -> None:
@slash_option(
name="user", description="User to remove", opt_type=OptionTypes.USER, required=True
)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_restore_user(
self, ctx: InteractionContext, user: Member, rping: Role
) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", hidden=True)
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
return
if user.id not in roleping.bypass["users"]:
await ctx.send(f"{user.mention} not in bypass", hidden=True)
await ctx.send(f"{user.mention} not in bypass", ephemeral=True)
return
roleping.bypass["users"].delete(user.id)
roleping.save()
await ctx.send(f"{user.nick or user.name} user bypass removed for `{rping.name}`")
await ctx.send(f"{user.display_name} user bypass removed for `{rping.name}`")
@cog_ext.cog_subcommand(
base="roleping",
subcommand_group="restore",
name="role",
description="Remove a role bypass",
base_desc="Block roles from being pinged",
sub_group_desc="Remove a bypass from a roleping (restoring it)",
options=[
create_option(
name="role",
description="Role to add",
option_type=8,
required=True,
),
create_option(
name="rping",
description="Rolepinged role",
option_type=8,
required=True,
),
],
@slash_command(
name="roleping",
group_name="restore",
sub_cmd_name="role",
description="Remove a bypass from a roleping (restoring it)",
)
@admin_or_permissions(manage_guild=True)
async def _roleping_restore_role(self, ctx: SlashContext, role: Role, rping: Role) -> None:
@slash_option(
name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(manage_guild=True))
async def _roleping_restore_role(
self, ctx: InteractionContext, role: Role, rping: Role
) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", hidden=True)
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
return
if role.id in roleping.bypass["roles"]:
await ctx.send(f"{role.mention} already in bypass", hidden=True)
await ctx.send(f"{role.mention} already in bypass", ephemeral=True)
return
if len(roleping.bypass["roles"]) == 10:
await ctx.send(
"Already have 10 roles in bypass. Please consider consolidating roles for roleping bypass",
hidden=True,
"Already have 10 roles in bypass. "
"Please consider consolidating roles for roleping bypass",
ephemeral=True,
)
return

View file

@ -1,12 +1,16 @@
"""J.A.R.V.I.S. WarningCog."""
from datetime import datetime, timedelta
from ButtonPaginator import Paginator
from discord import User
from discord.ext.commands import Bot
from discord_slash import SlashContext, cog_ext
from discord_slash.model import ButtonStyle
from discord_slash.utils.manage_commands import create_choice, create_option
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Warning
from jarvis.utils import build_embed
@ -18,43 +22,35 @@ from jarvis.utils.permissions import admin_or_permissions
class WarningCog(CacheCog):
"""J.A.R.V.I.S. WarningCog."""
def __init__(self, bot: Bot):
def __init__(self, bot: Snake):
super().__init__(bot)
@cog_ext.cog_slash(
name="warn",
description="Warn a user",
options=[
create_option(
name="user",
description="User to warn",
option_type=6,
required=True,
),
create_option(
name="reason",
description="Reason for warning",
option_type=3,
required=True,
),
create_option(
name="duration",
description="Duration of warning in hours, default 24",
option_type=4,
required=False,
),
],
@slash_command(name="warn", description="Warn a user")
@slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="reason",
description="Reason for warning",
opt_type=OptionTypes.STRING,
required=True,
)
@admin_or_permissions(manage_guild=True)
async def _warn(self, ctx: SlashContext, user: User, reason: str, duration: int = 24) -> None:
@slash_option(
name="duration",
description="Duration of warning in hours, default 24",
opt_type=OptionTypes.INTEGER,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warn(
self, ctx: InteractionContext, user: User, reason: str, duration: int = 24
) -> None:
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", hidden=True)
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
if duration <= 0:
await ctx.send("Duration must be > 0", hidden=True)
await ctx.send("Duration must be > 0", ephemeral=True)
return
elif duration >= 120:
await ctx.send("Duration must be < 5 days", hidden=True)
await ctx.send("Duration must be < 5 days", ephemeral=True)
return
await ctx.defer()
_ = Warning(
@ -71,52 +67,41 @@ class WarningCog(CacheCog):
description=f"{user.mention} has been warned",
fields=fields,
)
embed.set_author(
name=user.nick if user.nick else user.name,
icon_url=user.avatar_url,
)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
@cog_ext.cog_slash(
name="warnings",
description="Get count of user warnings",
options=[
create_option(
name="user",
description="User to view",
option_type=6,
required=True,
),
create_option(
name="active",
description="View only active",
option_type=4,
required=False,
choices=[
create_choice(name="Yes", value=1),
create_choice(name="No", value=0),
],
),
@slash_command(name="warnings", description="Get count of user warnings")
@slash_option(name="user", description="User to view", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="active",
description="View active only",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[
SlashCommandChoice(name="Yes", value=1),
SlashCommandChoice(name="No", value=0),
],
)
@admin_or_permissions(manage_guild=True)
async def _warnings(self, ctx: SlashContext, user: User, active: bool = 1) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(self, ctx: InteractionContext, user: User, active: bool = 1) -> None:
active = bool(active)
exists = self.check_cache(ctx, user_id=user.id, active=active)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
warnings = Warning.objects(
user=user.id,
guild=ctx.guild.id,
).order_by("-created_at")
active_warns = Warning.objects(user=user.id, guild=ctx.guild.id, active=True).order_by("-created_at")
active_warns = Warning.objects(user=user.id, guild=ctx.guild.id, active=True).order_by(
"-created_at"
)
pages = []
if active:
@ -126,16 +111,16 @@ class WarningCog(CacheCog):
description=f"{warnings.count()} total | 0 currently active",
fields=[],
)
embed.set_author(name=user.name, icon_url=user.avatar_url)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_author(name=user.username, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed)
else:
fields = []
for warn in active_warns:
admin = ctx.guild.get_member(warn.admin)
admin = await ctx.guild.get_member(warn.admin)
admin_name = "||`[redacted]`||"
if admin:
admin_name = f"{admin.name}#{admin.discriminator}"
admin_name = f"{admin.username}#{admin.discriminator}"
fields.append(
Field(
name=warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC"),
@ -146,15 +131,17 @@ class WarningCog(CacheCog):
for i in range(0, len(fields), 5):
embed = build_embed(
title="Warnings",
description=f"{warnings.count()} total | {active_warns.count()} currently active",
fields=fields[i : i + 5], # noqa: E203
description=(
f"{warnings.count()} total | {active_warns.count()} currently active"
),
fields=fields[i : i + 5],
)
embed.set_author(
name=user.name + "#" + user.discriminator,
icon_url=user.avatar_url,
name=user.username + "#" + user.discriminator,
icon_url=user.display_avatar.url,
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
pages.append(embed)
else:
fields = []
@ -171,28 +158,18 @@ class WarningCog(CacheCog):
for i in range(0, len(fields), 5):
embed = build_embed(
title="Warnings",
description=f"{warnings.count()} total | {active_warns.count()} currently active",
fields=fields[i : i + 5], # noqa: E203
description=(
f"{warnings.count()} total | {active_warns.count()} currently active"
),
fields=fields[i : i + 5],
)
embed.set_author(
name=user.name + "#" + user.discriminator,
icon_url=user.avatar_url,
name=user.username + "#" + user.discriminator, icon_url=user.display_avatar.url
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed)
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=pages,
only=ctx.author,
timeout=60 * 5, # 5 minute timeout
disable_after_timeout=True,
use_extend=len(pages) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator(bot=self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
@ -204,4 +181,4 @@ class WarningCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)

View file

@ -1,46 +1,45 @@
"""J.A.R.V.I.S. Autoreact Cog."""
import re
from typing import Optional, Tuple
from discord import TextChannel
from discord.ext import commands
from discord.utils import find
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.data.unicode import emoji_list
from jarvis.db.models import Autoreact
from jarvis.utils import find
from jarvis.utils.permissions import admin_or_permissions
class AutoReactCog(commands.Cog):
class AutoReactCog(Scale):
"""J.A.R.V.I.S. Autoreact Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
self.custom_emote = re.compile(r"^<:\w+:(\d+)>$")
@cog_ext.cog_subcommand(
base="autoreact",
name="create",
description="Add an autoreact to a channel",
options=[
create_option(
name="channel",
description="Channel to monitor",
option_type=7,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
async def _autoreact_create(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a text channel", hidden=True)
return
async def create_autoreact(
self, ctx: InteractionContext, channel: GuildText
) -> Tuple[bool, Optional[str]]:
"""
Create an autoreact monitor on a channel.
Args:
ctx: Interaction context of command
channel: Channel to monitor
Returns:
Tuple of success? and error message
"""
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if exists:
await ctx.send(f"Autoreact already exists for {channel.mention}.", hidden=True)
return
return False, f"Autoreact already exists for {channel.mention}."
_ = Autoreact(
guild=ctx.guild.id,
@ -48,152 +47,145 @@ class AutoReactCog(commands.Cog):
reactions=[],
admin=ctx.author.id,
).save()
await ctx.send(f"Autoreact created for {channel.mention}!")
@cog_ext.cog_subcommand(
base="autoreact",
name="delete",
description="Delete an autoreact from a channel",
options=[
create_option(
name="channel",
description="Channel to stop monitoring",
option_type=7,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
async def _autoreact_delete(self, ctx: SlashContext, channel: TextChannel) -> None:
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).delete()
if exists:
await ctx.send(f"Autoreact removed from {channel.mention}")
else:
await ctx.send(f"Autoreact not found on {channel.mention}", hidden=True)
return True, None
@cog_ext.cog_subcommand(
base="autoreact",
name="add",
description="Add an autoreact emote to an existing autoreact",
options=[
create_option(
name="channel",
description="Autoreact channel to add emote to",
option_type=7,
required=True,
),
create_option(
name="emote",
description="Emote to add",
option_type=3,
required=True,
),
],
async def delete_autoreact(self, ctx: InteractionContext, channel: GuildText) -> bool:
"""
Remove an autoreact monitor on a channel.
Args:
ctx: Interaction context of command
channel: Channel to stop monitoring
Returns:
Success?
"""
return Autoreact.objects(guild=ctx.guild.id, channel=channel.id).delete() is not None
@slash_command(
name="autoreact",
sub_cmd_name="add",
sub_cmd_description="Add an autoreact emote to a channel",
)
@admin_or_permissions(manage_guild=True)
async def _autoreact_add(self, ctx: SlashContext, channel: TextChannel, emote: str) -> None:
@slash_option(
name="channel",
description="Autoreact channel to add emote to",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@slash_option(
name="emote", description="Emote to add", opt_type=OptionTypes.STRING, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _autoreact_add(self, ctx: InteractionContext, channel: GuildText, emote: str) -> None:
await ctx.defer()
custom_emoji = self.custom_emote.match(emote)
standard_emoji = emote in emoji_list
if not custom_emoji and not standard_emoji:
await ctx.send(
"Please use either an emote from this server or a unicode emoji.",
hidden=True,
ephemeral=True,
)
return
if custom_emoji:
emoji_id = int(custom_emoji.group(1))
if not find(lambda x: x.id == emoji_id, ctx.guild.emojis):
await ctx.send("Please use a custom emote from this server.", hidden=True)
await ctx.send("Please use a custom emote from this server.", ephemeral=True)
return
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not exists:
await ctx.send(f"Please create autoreact first with /autoreact create {channel.mention}")
return
if emote in exists.reactions:
autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not autoreact:
self.create_autoreact(ctx, channel)
autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if emote in autoreact.reactions:
await ctx.send(
f"Emote already added to {channel.mention} autoreactions.",
hidden=True,
ephemeral=True,
)
return
if len(exists.reactions) >= 5:
if len(autoreact.reactions) >= 5:
await ctx.send(
"Max number of reactions hit. Remove a different one to add this one",
hidden=True,
ephemeral=True,
)
return
exists.reactions.append(emote)
exists.save()
autoreact.reactions.append(emote)
autoreact.save()
await ctx.send(f"Added {emote} to {channel.mention} autoreact.")
@cog_ext.cog_subcommand(
base="autoreact",
name="remove",
description="Remove an autoreact emote from an existing autoreact",
options=[
create_option(
name="channel",
description="Autoreact channel to remove emote from",
option_type=7,
required=True,
),
create_option(
name="emote",
description="Emote to remove",
option_type=3,
required=True,
),
],
@slash_command(
name="autoreact",
sub_cmd_name="remove",
sub_cmd_description="Remove an autoreact emote to a channel",
)
@admin_or_permissions(manage_guild=True)
async def _autoreact_remove(self, ctx: SlashContext, channel: TextChannel, emote: str) -> None:
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not exists:
@slash_option(
name="channel",
description="Autoreact channel to remove emote from",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@slash_option(
name="emote",
description="Emote to remove (use all to delete)",
opt_type=OptionTypes.STRING,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _autoreact_remove(
self, ctx: InteractionContext, channel: GuildText, emote: str
) -> None:
autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not autoreact:
await ctx.send(
f"Please create autoreact first with /autoreact create {channel.mention}",
hidden=True,
f"Please create autoreact first with /autoreact add {channel.mention} {emote}",
ephemeral=True,
)
return
if emote not in exists.reactions:
if emote.lower() == "all":
self.delete_autoreact(ctx, channel)
await ctx.send(f"Autoreact removed from {channel.mention}")
elif emote not in autoreact.reactions:
await ctx.send(
f"{emote} not used in {channel.mention} autoreactions.",
hidden=True,
ephemeral=True,
)
return
exists.reactions.remove(emote)
exists.save()
await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
else:
autoreact.reactions.remove(emote)
autoreact.save()
if len(autoreact.reactions) == 0:
self.delete_autoreact(ctx, channel)
await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
@cog_ext.cog_subcommand(
base="autoreact",
name="list",
description="List all autoreacts on a channel",
options=[
create_option(
name="channel",
description="Autoreact channel to list",
option_type=7,
required=True,
),
],
@slash_command(
name="autoreact",
sub_cmd_name="list",
sub_cmd_description="List all autoreacts on a channel",
)
@admin_or_permissions(manage_guild=True)
async def _autoreact_list(self, ctx: SlashContext, channel: TextChannel) -> None:
@slash_option(
name="channel",
description="Autoreact channel to list",
opt_type=OptionTypes.CHANNEL,
required=True,
)
async def _autoreact_list(self, ctx: InteractionContext, channel: GuildText) -> None:
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not exists:
await ctx.send(
f"Please create autoreact first with /autoreact create {channel.mention}",
hidden=True,
f"Please create autoreact first with /autoreact add {channel.mention} <emote>",
ephemeral=True,
)
return
message = ""
if len(exists.reactions) > 0:
message = f"Current active autoreacts on {channel.mention}:\n" + "\n".join(exists.reactions)
message = f"Current active autoreacts on {channel.mention}:\n" + "\n".join(
exists.reactions
)
else:
message = f"No reactions set on {channel.mention}"
await ctx.send(message)
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add AutoReactCog to J.A.R.V.I.S."""
bot.add_cog(AutoReactCog(bot))
AutoReactCog(bot)

View file

@ -3,22 +3,23 @@ import re
from datetime import datetime, timedelta
import aiohttp
from ButtonPaginator import Paginator
from discord import Member, User
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.model import ButtonStyle
from dis_snek import InteractionContext, Snake
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import Member, User
from dis_snek.models.snek.application_commands import slash_command
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Guess
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)",
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
flags=re.IGNORECASE,
)
@ -26,7 +27,7 @@ invites = re.compile(
class CTCCog(CacheCog):
"""J.A.R.V.I.S. Complete the Code 2 Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
super().__init__(bot)
self._session = aiohttp.ClientSession()
self.url = "https://completethecodetwo.cards/pw"
@ -34,45 +35,48 @@ class CTCCog(CacheCog):
def __del__(self):
self._session.close()
@cog_ext.cog_subcommand(
base="ctc2",
name="about",
description="CTC2 related commands",
guild_ids=guild_ids,
@slash_command(
name="ctc2", sub_cmd_name="about", description="CTC2 related commands", scopes=guild_ids
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _about(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _about(self, ctx: InteractionContext) -> None:
await ctx.send("See https://completethecode.com for more information")
@cog_ext.cog_subcommand(
base="ctc2",
name="pw",
description="Guess a password for https://completethecodetwo.cards",
guild_ids=guild_ids,
@slash_command(
name="ctc2",
sub_cmd_name="pw",
sub_cmd_description="Guess a password for https://completethecodetwo.cards",
scopes=guild_ids,
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _pw(self, ctx: SlashContext, guess: str) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _pw(self, ctx: InteractionContext, guess: str) -> None:
if len(guess) > 800:
await ctx.send(
"Listen here, dipshit. Don't be like <@256110768724901889>. Make your guesses < 800 characters.",
hidden=True,
(
"Listen here, dipshit. Don't be like <@256110768724901889>. "
"Make your guesses < 800 characters."
),
ephemeral=True,
)
return
elif not valid.fullmatch(guess):
await ctx.send(
"Listen here, dipshit. Don't be like <@256110768724901889>. Make your guesses *readable*.",
hidden=True,
(
"Listen here, dipshit. Don't be like <@256110768724901889>. "
"Make your guesses *readable*."
),
ephemeral=True,
)
return
elif invites.search(guess):
await ctx.send(
"Listen here, dipshit. No using this to bypass sending invite links.",
hidden=True,
ephemeral=True,
)
return
guessed = Guess.objects(guess=guess).first()
if guessed:
await ctx.send("Already guessed, dipshit.", hidden=True)
await ctx.send("Already guessed, dipshit.", ephemeral=True)
return
result = await self._session.post(self.url, data=guess)
correct = False
@ -80,30 +84,30 @@ class CTCCog(CacheCog):
await ctx.send(f"{ctx.author.mention} got it! Password is {guess}!")
correct = True
else:
await ctx.send("Nope.", hidden=True)
await ctx.send("Nope.", ephemeral=True)
_ = Guess(guess=guess, user=ctx.author.id, correct=correct).save()
@cog_ext.cog_subcommand(
base="ctc2",
name="guesses",
description="Show guesses made for https://completethecodetwo.cards",
guild_ids=guild_ids,
@slash_command(
name="ctc2",
sub_cmd_name="guesses",
sub_cmd_description="Show guesses made for https://completethecodetwo.cards",
scopes=guild_ids,
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _guesses(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _guesses(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
guesses = Guess.objects().order_by("-correct", "-id")
fields = []
for guess in guesses:
user = ctx.guild.get_member(guess["user"])
user = await ctx.guild.get_member(guess["user"])
if not user:
user = await self.bot.fetch_user(guess["user"])
if not user:
@ -113,7 +117,7 @@ class CTCCog(CacheCog):
name = "Correctly" if guess["correct"] else "Incorrectly"
name += " guessed by: " + user
fields.append(
Field(
EmbedField(
name=name,
value=guess["guess"] + "\n\u200b",
inline=False,
@ -124,7 +128,7 @@ class CTCCog(CacheCog):
embed = build_embed(
title="completethecodetwo.cards guesses",
description=f"{len(fields)} guesses so far",
fields=fields[i : i + 5], # noqa: E203
fields=fields[i : i + 5],
url="https://completethecodetwo.cards",
)
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
@ -134,18 +138,7 @@ class CTCCog(CacheCog):
)
pages.append(embed)
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=pages,
timeout=60 * 5, # 5 minute timeout
only=ctx.author,
disable_after_timeout=True,
use_extend=len(pages) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
@ -155,9 +148,9 @@ class CTCCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add CTCCog to J.A.R.V.I.S."""
bot.add_cog(CTCCog(bot))
CTCCog(bot)

View file

@ -2,26 +2,31 @@
import re
import aiohttp
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.config import get_config
from jarvis.data.dbrand import shipping_lookup
from jarvis.utils import build_embed
from jarvis.utils.field import Field
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
class DbrandCog(commands.Cog):
class DbrandCog(Scale):
"""
dbrand functions for J.A.R.V.I.S.
Mostly support functions. Credit @cpixl for the shipping API
"""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
self.base_url = "https://dbrand.com/"
self._session = aiohttp.ClientSession()
@ -32,134 +37,130 @@ class DbrandCog(commands.Cog):
def __del__(self):
self._session.close()
@cog_ext.cog_subcommand(
base="db",
name="skin",
guild_ids=guild_ids,
description="See what skins are available",
@slash_command(
name="db",
sub_cmd_name="skin",
scopes=guild_ids,
sub_cmd_description="See what skins are available",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _skin(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _skin(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "/skins")
@cog_ext.cog_subcommand(
base="db",
name="robotcamo",
guild_ids=guild_ids,
description="Get some robot camo. Make Tony Stark proud",
@slash_command(
name="db",
sub_cmd_name="robotcamo",
scopes=guild_ids,
sub_cmd_description="Get some robot camo. Make Tony Stark proud",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _camo(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _camo(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "robot-camo")
@cog_ext.cog_subcommand(
base="db",
name="grip",
guild_ids=guild_ids,
description="See devices with Grip support",
@slash_command(
name="db",
sub_cmd_name="grip",
scopes=guild_ids,
sub_cmd_description="See devices with Grip support",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _grip(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _grip(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "grip")
@cog_ext.cog_subcommand(
base="db",
name="contact",
guild_ids=guild_ids,
description="Contact support",
@slash_command(
name="db",
sub_cmd_name="contact",
scopes=guild_ids,
sub_cmd_description="Contact support",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _contact(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _contact(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@cog_ext.cog_subcommand(
base="db",
name="support",
guild_ids=guild_ids,
description="Contact support",
@slash_command(
name="db",
sub_cmd_name="support",
scopes=guild_ids,
sub_cmd_description="Contact support",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _support(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _support(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@cog_ext.cog_subcommand(
base="db",
name="orderstat",
guild_ids=guild_ids,
description="Get your order status",
@slash_command(
name="db",
sub_cmd_name="orderstat",
scopes=guild_ids,
sub_cmd_description="Get your order status",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _orderstat(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _orderstat(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "order-status")
@cog_ext.cog_subcommand(
base="db",
name="orders",
guild_ids=guild_ids,
description="Get your order status",
@slash_command(
name="db",
sub_cmd_name="orders",
scopes=guild_ids,
sub_cmd_description="Get your order status",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _orders(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _orders(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "order-status")
@cog_ext.cog_subcommand(
base="db",
name="status",
guild_ids=guild_ids,
description="dbrand status",
@slash_command(
name="db",
sub_cmd_name="status",
scopes=guild_ids,
sub_cmd_description="dbrand status",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _status(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _status(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "status")
@cog_ext.cog_subcommand(
base="db",
name="buy",
guild_ids=guild_ids,
description="Give us your money!",
@slash_command(
name="db",
sub_cmd_name="buy",
scopes=guild_ids,
sub_cmd_description="Give us your money!",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _buy(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _buy(self, ctx: InteractionContext) -> None:
await ctx.send("Give us your money! " + self.base_url + "shop")
@cog_ext.cog_subcommand(
base="db",
name="extortion",
guild_ids=guild_ids,
description="(not) extortion",
@slash_command(
name="db",
sub_cmd_name="extortion",
scopes=guild_ids,
sub_cmd_description="(not) extortion",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _extort(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _extort(self, ctx: InteractionContext) -> None:
await ctx.send("Be (not) extorted here: " + self.base_url + "not-extortion")
@cog_ext.cog_subcommand(
base="db",
name="wallpapers",
description="Robot Camo Wallpapers",
guild_ids=guild_ids,
@slash_command(
name="db",
sub_cmd_name="wallpapers",
sub_cmd_description="Robot Camo Wallpapers",
scopes=guild_ids,
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _wallpapers(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _wallpapers(self, ctx: InteractionContext) -> None:
await ctx.send("Get robot camo wallpapers here: https://db.io/wallpapers")
@cog_ext.cog_subcommand(
base="db",
name="ship",
description="Get shipping information for your country",
guild_ids=guild_ids,
options=[
(
create_option(
name="search",
description="Country search query (2 character code, country name, emoji)",
option_type=3,
required=True,
)
)
],
@slash_command(
name="db",
sub_cmd_name="ship",
sub_cmd_description="Get shipping information for your country",
scopes=guild_ids,
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _shipping(self, ctx: SlashContext, search: str) -> None:
@slash_option(
name="search",
description="Country search query (2 character code, country name, flag emoji)",
opt_type=OptionTypes.STRING,
required=True,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _shipping(self, ctx: InteractionContext, search: str) -> None:
await ctx.defer()
if not re.match(r"^[A-Z- ]+$", search, re.IGNORECASE):
if re.match(
@ -173,7 +174,6 @@ class DbrandCog(commands.Cog):
elif search == "🏳️":
search = "fr"
else:
print(search)
await ctx.send("Please use text to search for shipping.")
return
if len(search) > 2:
@ -193,14 +193,14 @@ class DbrandCog(commands.Cog):
fields = None
if data is not None and data["is_valid"] and data["shipping_available"]:
fields = []
fields.append(Field(data["short-name"], data["time-title"]))
fields.append(EmbedField(data["short-name"], data["time-title"]))
for service in data["shipping_services_available"][1:]:
service_data = await self._session.get(self.api_url + dest + "/" + service["url"])
if service_data.status > 400:
continue
service_data = await service_data.json()
fields.append(
Field(
EmbedField(
service_data["short-name"],
service_data["time-title"],
)
@ -215,7 +215,7 @@ class DbrandCog(commands.Cog):
)
embed = build_embed(
title="Shipping to {}".format(data["country"]),
description=description,
sub_cmd_description=description,
color="#FFBB00",
fields=fields,
url=self.base_url + "shipping/" + country,
@ -229,8 +229,9 @@ class DbrandCog(commands.Cog):
elif not data["is_valid"]:
embed = build_embed(
title="Check Shipping Times",
description=(
"Country not found.\nYou can [view all shipping " "destinations here](https://dbrand.com/shipping)"
sub_cmd_description=(
"Country not found.\nYou can [view all shipping "
"destinations here](https://dbrand.com/shipping)"
),
fields=[],
url="https://dbrand.com/shipping",
@ -245,7 +246,7 @@ class DbrandCog(commands.Cog):
elif not data["shipping_available"]:
embed = build_embed(
title="Shipping to {}".format(data["country"]),
description=(
sub_cmd_description=(
"No shipping available.\nTime to move to a country"
" that has shipping available.\nYou can [find a new country "
"to live in here](https://dbrand.com/shipping)"
@ -262,6 +263,6 @@ class DbrandCog(commands.Cog):
await ctx.send(embed=embed)
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add dbrandcog to J.A.R.V.I.S."""
bot.add_cog(DbrandCog(bot))
DbrandCog(bot)

View file

@ -8,19 +8,27 @@ from typing import Any, Union
import ulid as ulidpy
from bson import ObjectId
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_choice, create_option
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.utils import build_embed, convert_bytesize
from jarvis.utils.field import Field
supported_hashes = {x for x in hashlib.algorithms_guaranteed if "shake" not in x}
OID_VERIFY = re.compile(r"^([1-9][0-9]{0,3}|0)(\.([1-9][0-9]{0,3}|0)){5,13}$")
URL_VERIFY = re.compile(r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
URL_VERIFY = re.compile(
r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
)
DN_VERIFY = re.compile(
r"^(?:(?P<cn>CN=(?P<name>[^,]*)),)?(?:(?P<path>(?:(?:CN|OU)=[^,]+,?)+),)?(?P<domain>(?:DC=[^,]+,?)+)$"
r"^(?:(?P<cn>CN=(?P<name>[^,]*)),)?(?:(?P<path>(?:(?:CN|OU)=[^,]+,?)+),)?(?P<domain>(?:DC=[^,]+,?)+)$" # noqa: E501
)
ULID_VERIFY = re.compile(r"^[0-9a-z]{26}$", re.IGNORECASE)
UUID_VERIFY = re.compile(
@ -29,7 +37,7 @@ UUID_VERIFY = re.compile(
)
invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)",
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
flags=re.IGNORECASE,
)
@ -47,43 +55,35 @@ def hash_obj(hash: Any, data: Union[str, bytes], text: bool = True) -> str:
BSIZE = 65536
block_idx = 0
while block_idx * BSIZE < len(data):
block = data[BSIZE * block_idx : BSIZE * (block_idx + 1)] # noqa: E203
block = data[BSIZE * block_idx : BSIZE * (block_idx + 1)]
hash.update(block)
block_idx += 1
return hash.hexdigest()
class DevCog(commands.Cog):
class DevCog(Scale):
"""J.A.R.V.I.S. Developer Cog."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@cog_ext.cog_slash(
name="hash",
description="Hash some data",
options=[
create_option(
name="method",
description="Hash method",
option_type=3,
required=True,
choices=[create_choice(name=x, value=x) for x in supported_hashes],
),
create_option(
name="data",
description="Data to hash",
option_type=3,
required=True,
),
],
@slash_command(name="hash", description="Hash some data")
@slash_option(
name="method",
description="Hash method",
opt_type=OptionTypes.STRING,
required=True,
choices=[SlashCommandChoice(name=x, value=x) for x in supported_hashes],
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _hash(self, ctx: SlashContext, method: str, data: str) -> None:
@slash_option(
name="data",
description="Data to hash",
opt_type=OptionTypes.STRING,
required=True,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _hash(self, ctx: InteractionContext, method: str, data: str) -> None:
if not data:
await ctx.send(
"No data to hash",
hidden=True,
ephemeral=True,
)
return
text = True
@ -94,36 +94,31 @@ class DevCog(commands.Cog):
title = data if text else ctx.message.attachments[0].filename
description = "Hashed using " + method
fields = [
Field("Data Size", data_size, False),
Field("Hash", f"`{hex}`", False),
EmbedField("Data Size", data_size, False),
EmbedField("Hash", f"`{hex}`", False),
]
embed = build_embed(title=title, description=description, fields=fields)
await ctx.send(embed=embed)
@cog_ext.cog_slash(
name="uuid",
description="Generate a UUID",
options=[
create_option(
name="version",
description="UUID version",
option_type=3,
required=True,
choices=[create_choice(name=x, value=x) for x in ["3", "4", "5"]],
),
create_option(
name="data",
description="Data for UUID version 3,5",
option_type=3,
required=False,
),
],
@slash_command(name="uuid", description="Generate a UUID")
@slash_option(
name="version",
description="UUID version",
opt_type=OptionTypes.STRING,
required=True,
choices=[SlashCommandChoice(name=x, value=x) for x in ["3", "4", "5"]],
)
async def _uuid(self, ctx: SlashContext, version: str, data: str = None) -> None:
@slash_option(
name="data",
description="Data for UUID version 3,5",
opt_type=OptionTypes.STRING,
required=False,
)
async def _uuid(self, ctx: InteractionContext, version: str, data: str = None) -> None:
version = int(version)
if version in [3, 5] and not data:
await ctx.send(f"UUID{version} requires data.", hidden=True)
await ctx.send(f"UUID{version} requires data.", ephemeral=True)
return
if version == 4:
await ctx.send(f"UUID4: `{uuidpy.uuid4()}`")
@ -139,40 +134,40 @@ class DevCog(commands.Cog):
to_send = UUID_GET[version](uuidpy.NAMESPACE_DNS, data)
await ctx.send(f"UUID{version}: `{to_send}`")
@cog_ext.cog_slash(
@slash_command(
name="objectid",
description="Generate an ObjectID",
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _objectid(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _objectid(self, ctx: InteractionContext) -> None:
await ctx.send(f"ObjectId: `{str(ObjectId())}`")
@cog_ext.cog_slash(
@slash_command(
name="ulid",
description="Generate a ULID",
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _ulid(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _ulid(self, ctx: InteractionContext) -> None:
await ctx.send(f"ULID: `{ulidpy.new().str}`")
@cog_ext.cog_slash(
@slash_command(
name="uuid2ulid",
description="Convert a UUID to a ULID",
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _uuid2ulid(self, ctx: SlashContext, uuid: str) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
if UUID_VERIFY.match(uuid):
u = ulidpy.parse(uuid)
await ctx.send(f"ULID: `{u.str}`")
else:
await ctx.send("Invalid UUID")
@cog_ext.cog_slash(
@slash_command(
name="ulid2uuid",
description="Convert a ULID to a UUID",
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _ulid2uuid(self, ctx: SlashContext, ulid: str) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
if ULID_VERIFY.match(ulid):
ulid = ulidpy.parse(ulid)
await ctx.send(f"UUID: `{ulid.uuid}`")
@ -181,82 +176,71 @@ class DevCog(commands.Cog):
base64_methods = ["b64", "b16", "b32", "a85", "b85"]
@cog_ext.cog_slash(
name="encode",
description="Encode some data",
options=[
create_option(
name="method",
description="Encode method",
option_type=3,
required=True,
choices=[create_choice(name=x, value=x) for x in base64_methods],
),
create_option(
name="data",
description="Data to encode",
option_type=3,
required=True,
),
],
@slash_command(name="encode", description="Encode some data")
@slash_option(
name="method",
description="Encode method",
opt_type=OptionTypes.STRING,
required=True,
choices=[SlashCommandChoice(name=x, value=x) for x in base64_methods],
)
async def _encode(self, ctx: SlashContext, method: str, data: str) -> None:
@slash_option(
name="data",
description="Data to encode",
opt_type=OptionTypes.STRING,
required=True,
)
async def _encode(self, ctx: InteractionContext, method: str, data: str) -> None:
mstr = method
method = getattr(base64, method + "encode")
encoded = method(data.encode("UTF-8")).decode("UTF-8")
fields = [
Field(name="Plaintext", value=f"`{data}`", inline=False),
Field(name=mstr, value=f"`{encoded}`", inline=False),
EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
]
embed = build_embed(title="Decoded Data", description="", fields=fields)
await ctx.send(embed=embed)
@cog_ext.cog_slash(
name="decode",
description="Decode some data",
options=[
create_option(
name="method",
description="Decode method",
option_type=3,
required=True,
choices=[create_choice(name=x, value=x) for x in base64_methods],
),
create_option(
name="data",
description="Data to encode",
option_type=3,
required=True,
),
],
@slash_command(name="decode", description="Decode some data")
@slash_option(
name="method",
description="Decode method",
opt_type=OptionTypes.STRING,
required=True,
choices=[SlashCommandChoice(name=x, value=x) for x in base64_methods],
)
async def _decode(self, ctx: SlashContext, method: str, data: str) -> None:
@slash_option(
name="data",
description="Data to encode",
opt_type=OptionTypes.STRING,
required=True,
)
async def _decode(self, ctx: InteractionContext, method: str, data: str) -> None:
mstr = method
method = getattr(base64, method + "decode")
decoded = method(data.encode("UTF-8")).decode("UTF-8")
if invites.search(decoded):
await ctx.send(
"Please don't use this to bypass invite restrictions",
hidden=True,
ephemeral=True,
)
return
fields = [
Field(name="Plaintext", value=f"`{data}`", inline=False),
Field(name=mstr, value=f"`{decoded}`", inline=False),
EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
EmbedField(name=mstr, value=f"`{decoded}`", inline=False),
]
embed = build_embed(title="Decoded Data", description="", fields=fields)
await ctx.send(embed=embed)
@cog_ext.cog_slash(
name="cloc",
description="Get J.A.R.V.I.S. lines of code",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _cloc(self, ctx: SlashContext) -> None:
output = subprocess.check_output(["tokei", "-C", "--sort", "code"]).decode("UTF-8") # noqa: S603, S607
@slash_command(name="cloc", description="Get J.A.R.V.I.S. lines of code")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _cloc(self, ctx: InteractionContext) -> None:
output = subprocess.check_output( # noqa: S603, S607
["tokei", "-C", "--sort", "code"]
).decode("UTF-8")
await ctx.send(f"```\n{output}\n```")
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add DevCog to J.A.R.V.I.S."""
bot.add_cog(DevCog(bot))
DevCog(bot)

View file

@ -20,7 +20,8 @@ class ErrorHandlerCog(commands.Cog):
return
elif isinstance(error, commands.errors.CommandOnCooldown):
await ctx.send(
"Command on cooldown. " + f"Please wait {error.retry_after:0.2f}s before trying again",
"Command on cooldown. "
f"Please wait {error.retry_after:0.2f}s before trying again",
)
else:
await ctx.send(f"Error processing command:\n```{error}```")
@ -29,19 +30,22 @@ class ErrorHandlerCog(commands.Cog):
@commands.Cog.listener()
async def on_slash_command_error(self, ctx: SlashContext, error: Exception) -> None:
"""discord_slash on_slash_command_error override."""
if isinstance(error, commands.errors.MissingPermissions) or isinstance(error, commands.errors.CheckFailure):
await ctx.send("I'm afraid I can't let you do that.", hidden=True)
if isinstance(error, commands.errors.MissingPermissions) or isinstance(
error, commands.errors.CheckFailure
):
await ctx.send("I'm afraid I can't let you do that.", ephemeral=True)
elif isinstance(error, commands.errors.CommandNotFound):
return
elif isinstance(error, commands.errors.CommandOnCooldown):
await ctx.send(
"Command on cooldown. " + f"Please wait {error.retry_after:0.2f}s before trying again",
hidden=True,
"Command on cooldown. "
f"Please wait {error.retry_after:0.2f}s before trying again",
ephemeral=True,
)
else:
await ctx.send(
f"Error processing command:\n```{error}```",
hidden=True,
ephemeral=True,
)
raise error
slash.commands[ctx.command].reset_cooldown(ctx)
@ -49,4 +53,4 @@ class ErrorHandlerCog(commands.Cog):
def setup(bot: commands.Bot) -> None:
"""Add ErrorHandlerCog to J.A.R.V.I.S."""
bot.add_cog(ErrorHandlerCog(bot))
ErrorHandlerCog(bot)

View file

@ -2,17 +2,19 @@
from datetime import datetime, timedelta
import gitlab
from ButtonPaginator import Paginator
from discord import Embed
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.model import ButtonStyle
from discord_slash.utils.manage_commands import create_choice, create_option
from dis_snek import InteractionContext, Snake
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from jarvis.config import get_config
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
guild_ids = [862402786116763668]
@ -20,25 +22,22 @@ guild_ids = [862402786116763668]
class GitlabCog(CacheCog):
"""J.A.R.V.I.S. GitLab Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
super().__init__(bot)
config = get_config()
self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token)
# J.A.R.V.I.S. GitLab ID is 29
self.project = self._gitlab.projects.get(29)
@cog_ext.cog_subcommand(
base="gl",
name="issue",
description="Get an issue from GitLab",
guild_ids=guild_ids,
options=[create_option(name="id", description="Issue ID", option_type=4, required=True)],
@slash_command(
name="gl", sub_cmd_name="issue", description="Get an issue from GitLab", scopes=guild_ids
)
async def _issue(self, ctx: SlashContext, id: int) -> None:
@slash_option(name="id", description="Issue ID", opt_type=OptionTypes.INTEGER, required=True)
async def _issue(self, ctx: InteractionContext, id: int) -> None:
try:
issue = self.project.issues.get(int(id))
except gitlab.exceptions.GitlabGetError:
await ctx.send("Issue does not exist.", hidden=True)
await ctx.send("Issue does not exist.", ephemeral=True)
return
assignee = issue.assignee
if assignee:
@ -46,7 +45,9 @@ class GitlabCog(CacheCog):
else:
assignee = "None"
created_at = datetime.strptime(issue.created_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S UTC")
created_at = datetime.strptime(issue.created_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
labels = issue.labels
if labels:
@ -55,18 +56,20 @@ class GitlabCog(CacheCog):
labels = "None"
fields = [
Field(name="State", value=issue.state[0].upper() + issue.state[1:]),
Field(name="Assignee", value=assignee),
Field(name="Labels", value=labels),
EmbedField(name="State", value=issue.state[0].upper() + issue.state[1:]),
EmbedField(name="Assignee", value=assignee),
EmbedField(name="Labels", value=labels),
]
color = self.project.labels.get(issue.labels[0]).color
fields.append(Field(name="Created At", value=created_at))
fields.append(EmbedField(name="Created At", value=created_at))
if issue.state == "closed":
closed_at = datetime.strptime(issue.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S UTC")
fields.append(Field(name="Closed At", value=closed_at))
closed_at = datetime.strptime(issue.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
fields.append(EmbedField(name="Closed At", value=closed_at))
if issue.milestone:
fields.append(
Field(
EmbedField(
name="Milestone",
value=f"[{issue.milestone['title']}]({issue.milestone['web_url']})",
inline=False,
@ -83,50 +86,49 @@ class GitlabCog(CacheCog):
)
embed.set_author(
name=issue.author["name"],
icon_url=issue.author["avatar_url"],
icon_url=issue.author["display_avatar"],
url=issue.author["web_url"],
)
embed.set_thumbnail(url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png")
embed.set_thumbnail(
url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png"
)
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(
base="gl",
name="milestone",
@slash_command(
name="gl",
sub_cmd_name="milestone",
description="Get a milestone from GitLab",
guild_ids=guild_ids,
options=[
create_option(
name="id",
description="Milestone ID",
option_type=4,
required=True,
)
],
scopes=guild_ids,
)
async def _milestone(self, ctx: SlashContext, id: int) -> None:
@slash_option(
name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True
)
async def _milestone(self, ctx: InteractionContext, id: int) -> None:
try:
milestone = self.project.milestones.get(int(id))
except gitlab.exceptions.GitlabGetError:
await ctx.send("Milestone does not exist.", hidden=True)
await ctx.send("Milestone does not exist.", ephemeral=True)
return
created_at = datetime.strptime(milestone.created_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S UTC")
created_at = datetime.strptime(milestone.created_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
fields = [
Field(
EmbedField(
name="State",
value=milestone.state[0].upper() + milestone.state[1:],
),
Field(name="Start Date", value=milestone.start_date),
Field(name="Due Date", value=milestone.due_date),
Field(name="Created At", value=created_at),
EmbedField(name="Start Date", value=milestone.start_date),
EmbedField(name="Due Date", value=milestone.due_date),
EmbedField(name="Created At", value=created_at),
]
if milestone.updated_at:
updated_at = datetime.strptime(milestone.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
fields.append(Field(name="Updated At", value=updated_at))
fields.append(EmbedField(name="Updated At", value=updated_at))
if len(milestone.title) > 200:
milestone.title = milestone.title[:200] + "..."
@ -143,28 +145,25 @@ class GitlabCog(CacheCog):
url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
)
embed.set_thumbnail(url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png")
embed.set_thumbnail(
url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png"
)
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(
base="gl",
name="mergerequest",
description="Get an merge request from GitLab",
guild_ids=guild_ids,
options=[
create_option(
name="id",
description="Merge Request ID",
option_type=4,
required=True,
)
],
@slash_command(
name="gl",
sub_cmd_name="mr",
description="Get a merge request from GitLab",
scopes=guild_ids,
)
async def _mergerequest(self, ctx: SlashContext, id: int) -> None:
@slash_option(
name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True
)
async def _mergerequest(self, ctx: InteractionContext, id: int) -> None:
try:
mr = self.project.mergerequests.get(int(id))
except gitlab.exceptions.GitlabGetError:
await ctx.send("Merge request does not exist.", hidden=True)
await ctx.send("Merge request does not exist.", ephemeral=True)
return
assignee = mr.assignee
if assignee:
@ -172,7 +171,9 @@ class GitlabCog(CacheCog):
else:
assignee = "None"
created_at = datetime.strptime(mr.created_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S UTC")
created_at = datetime.strptime(mr.created_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
labels = mr.labels
if labels:
@ -181,24 +182,28 @@ class GitlabCog(CacheCog):
labels = "None"
fields = [
Field(name="State", value=mr.state[0].upper() + mr.state[1:]),
Field(name="Assignee", value=assignee),
Field(name="Labels", value=labels),
EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:]),
EmbedField(name="Assignee", value=assignee),
EmbedField(name="Labels", value=labels),
]
if mr.labels:
color = self.project.labels.get(mr.labels[0]).color
else:
color = "#00FFEE"
fields.append(Field(name="Created At", value=created_at))
fields.append(EmbedField(name="Created At", value=created_at))
if mr.state == "merged":
merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S UTC")
fields.append(Field(name="Merged At", value=merged_at))
merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
fields.append(EmbedField(name="Merged At", value=merged_at))
elif mr.state == "closed":
closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S UTC")
fields.append(Field(name="Closed At", value=closed_at))
closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
fields.append(EmbedField(name="Closed At", value=closed_at))
if mr.milestone:
fields.append(
Field(
EmbedField(
name="Milestone",
value=f"[{mr.milestone['title']}]({mr.milestone['web_url']})",
inline=False,
@ -215,10 +220,12 @@ class GitlabCog(CacheCog):
)
embed.set_author(
name=mr.author["name"],
icon_url=mr.author["avatar_url"],
icon_url=mr.author["display_avatar"],
url=mr.author["web_url"],
)
embed.set_thumbnail(url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png")
embed.set_thumbnail(
url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png"
)
await ctx.send(embed=embed)
def build_embed_page(self, api_list: list, t_state: str, name: str) -> Embed:
@ -230,7 +237,7 @@ class GitlabCog(CacheCog):
fields = []
for item in api_list:
fields.append(
Field(
EmbedField(
name=f"[#{item.iid}] {item.title}",
value=item.description + f"\n\n[View this {name}]({item.web_url})",
inline=False,
@ -248,35 +255,32 @@ class GitlabCog(CacheCog):
url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
)
embed.set_thumbnail(url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png")
embed.set_thumbnail(
url="https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png"
)
return embed
@cog_ext.cog_subcommand(
base="gl",
name="issues",
description="Get open issues from GitLab",
guild_ids=guild_ids,
options=[
create_option(
name="state",
description="State of issues to get",
option_type=3,
required=False,
choices=[
create_choice(name="Open", value="opened"),
create_choice(name="Closed", value="closed"),
create_choice(name="All", value="all"),
],
)
@slash_command(
name="gl", sub_cmd_name="issues", description="Get issues from GitLab", scopes=guild_ids
)
@slash_option(
name="state",
description="State of issues to get",
opt_type=OptionTypes.STRING,
required=False,
choices=[
SlashCommandChoice(name="Open", value="opened"),
SlashCommandChoice(name="Closed", value="closed"),
SlashCommandChoice(name="All", value="all"),
],
)
async def _issues(self, ctx: SlashContext, state: str = "opened") -> None:
async def _issues(self, ctx: InteractionContext, state: str = "opened") -> None:
exists = self.check_cache(ctx, state=state)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
"Please use existing interaction: " + f"{exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
await ctx.defer()
@ -311,20 +315,9 @@ class GitlabCog(CacheCog):
pages = []
t_state = t_state[0].upper() + t_state[1:]
for i in range(0, len(issues), 5):
pages.append(self.build_embed_page(issues[i : i + 5], t_state=t_state, name="issue")) # noqa: E203
pages.append(self.build_embed_page(issues[i : i + 5], t_state=t_state, name="issue"))
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=pages,
only=ctx.author,
timeout=60 * 5, # 5 minute timeout
disable_after_timeout=True,
use_extend=len(pages) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
@ -335,35 +328,32 @@ class GitlabCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)
@cog_ext.cog_subcommand(
base="gl",
name="mergerequests",
description="Get open issues from GitLab",
guild_ids=guild_ids,
options=[
create_option(
name="state",
description="State of issues to get",
option_type=3,
required=False,
choices=[
create_choice(name="Open", value="opened"),
create_choice(name="Closed", value="closed"),
create_choice(name="Merged", value="merged"),
create_choice(name="All", value="all"),
],
)
@slash_command(
name="gl",
sub_cmd_name="mrs",
description="Get merge requests from GitLab",
scopes=guild_ids,
)
@slash_option(
name="state",
description="State of merge requests to get",
opt_type=OptionTypes.STRING,
required=False,
choices=[
SlashCommandChoice(name="Open", value="opened"),
SlashCommandChoice(name="Closed", value="closed"),
SlashCommandChoice(name="All", value="all"),
],
)
async def _mergerequests(self, ctx: SlashContext, state: str = "opened") -> None:
async def _mergerequests(self, ctx: InteractionContext, state: str = "opened") -> None:
exists = self.check_cache(ctx, state=state)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
"Please use existing interaction: " + f"{exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
await ctx.defer()
@ -398,20 +388,11 @@ class GitlabCog(CacheCog):
pages = []
t_state = t_state[0].upper() + t_state[1:]
for i in range(0, len(merges), 5):
pages.append(self.build_embed_page(merges[i : i + 5], t_state=t_state, name="merge request")) # noqa: E203
pages.append(
self.build_embed_page(merges[i : i + 5], t_state=t_state, name="merge request")
)
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=pages,
only=ctx.author,
timeout=60 * 5, # 5 minute timeout
disable_after_timeout=True,
use_extend=len(pages) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
@ -422,21 +403,21 @@ class GitlabCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)
@cog_ext.cog_subcommand(
base="gl",
name="milestones",
description="Get open issues from GitLab",
guild_ids=guild_ids,
@slash_command(
name="gl",
sub_cmd_name="milestones",
description="Get milestones from GitLab",
scopes=guild_ids,
)
async def _milestones(self, ctx: SlashContext) -> None:
async def _milestones(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
await ctx.defer()
@ -463,20 +444,11 @@ class GitlabCog(CacheCog):
pages = []
for i in range(0, len(milestones), 5):
pages.append(self.build_embed_page(milestones[i : i + 5], t_state=None, name="milestone")) # noqa: E203
pages.append(
self.build_embed_page(milestones[i : i + 5], t_state=None, name="milestone")
)
paginator = Paginator(
bot=self.bot,
ctx=ctx,
embeds=pages,
only=ctx.author,
timeout=60 * 5, # 5 minute timeout
disable_after_timeout=True,
use_extend=len(pages) > 2,
left_button_style=ButtonStyle.grey,
right_button_style=ButtonStyle.grey,
basic_buttons=["", ""],
)
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
@ -486,10 +458,10 @@ class GitlabCog(CacheCog):
"paginator": paginator,
}
await paginator.start()
await paginator.send(ctx)
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add GitlabCog to J.A.R.V.I.S. if Gitlab token exists."""
if get_config().gitlab_token:
bot.add_cog(GitlabCog(bot))
GitlabCog(bot)

View file

@ -5,21 +5,23 @@ from io import BytesIO
import aiohttp
import cv2
import numpy as np
from discord import File
from discord.ext import commands
from dis_snek import MessageContext, Scale, Snake, message_command
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.file import File
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.utils import build_embed, convert_bytesize, unconvert_bytesize
from jarvis.utils.field import Field
class ImageCog(commands.Cog):
class ImageCog(Scale):
"""
Image processing functions for J.A.R.V.I.S.
May be categorized under util later
"""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
self._session = aiohttp.ClientSession()
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B)", re.IGNORECASE)
@ -27,7 +29,7 @@ class ImageCog(commands.Cog):
def __del__(self):
self._session.close()
async def _resize(self, ctx: commands.Context, target: str, url: str = None) -> None:
async def _resize(self, ctx: MessageContext, target: str, url: str = None) -> None:
if not target:
await ctx.send("Missing target size, i.e. 200KB.")
return
@ -84,23 +86,23 @@ class ImageCog(commands.Cog):
bufio = BytesIO(file)
accuracy = (len(file) / tgt_size) * 100
fields = [
Field("Original Size", convert_bytesize(size), False),
Field("New Size", convert_bytesize(len(file)), False),
Field("Accuracy", f"{accuracy:.02f}%", False),
EmbedField("Original Size", convert_bytesize(size), False),
EmbedField("New Size", convert_bytesize(len(file)), False),
EmbedField("Accuracy", f"{accuracy:.02f}%", False),
]
embed = build_embed(title=filename, description="", fields=fields)
embed.set_image(url="attachment://resized.png")
await ctx.send(
embed=embed,
file=File(bufio, filename="resized.png"),
file=File(file=bufio, filename="resized.png"),
)
@commands.command(name="resize", help="Resize an image")
@commands.cooldown(1, 60, commands.BucketType.user)
async def _resize_pref(self, ctx: commands.Context, target: str, url: str = None) -> None:
@message_command(name="resize")
@cooldown(bucket=Buckets.USER, rate=1, interval=60)
async def _resize_pref(self, ctx: MessageContext, target: str, url: str = None) -> None:
await self._resize(ctx, target, url)
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add ImageCog to J.A.R.V.I.S."""
bot.add_cog(ImageCog(bot))
ImageCog(bot)

View file

@ -5,31 +5,38 @@ import traceback
from datetime import datetime
from random import randint
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Joke
from jarvis.utils import build_embed
from jarvis.utils.field import Field
class JokeCog(commands.Cog):
class JokeCog(Scale):
"""
Joke library for J.A.R.V.I.S.
May adapt over time to create jokes using machine learning
"""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
# TODO: Make this a command group with subcommands
@cog_ext.cog_slash(
@slash_command(
name="joke",
description="Hear a joke",
)
@commands.cooldown(1, 10, commands.BucketType.channel)
async def _joke(self, ctx: SlashContext, id: str = None) -> None:
@slash_option(name="id", description="Joke ID", required=False, opt_type=OptionTypes.INTEGER)
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=10)
async def _joke(self, ctx: InteractionContext, id: str = None) -> None:
"""Get a joke from the database."""
try:
if randint(1, 100_000) == 5779 and id is None: # noqa: S311
@ -50,7 +57,7 @@ class JokeCog(commands.Cog):
result = Joke.objects().aggregate(pipeline).next()
if result is None:
await ctx.send("Humor module failed. Please try again later.", hidden=True)
await ctx.send("Humor module failed. Please try again later.", ephemeral=True)
return
emotes = re.findall(r"(&#x[a-fA-F0-9]*;)", result["body"])
for match in emotes:
@ -63,7 +70,7 @@ class JokeCog(commands.Cog):
body = ""
for word in result["body"].split(" "):
if len(body) + 1 + len(word) > 1024:
body_chunks.append(Field("", body, False))
body_chunks.append(EmbedField("", body, False))
body = ""
if word == "\n" and body == "":
continue
@ -87,15 +94,15 @@ class JokeCog(commands.Cog):
else:
desc += word + " "
body_chunks.append(Field("", body, False))
body_chunks.append(EmbedField("", body, False))
fields = body_chunks
fields.append(Field("Score", result["score"]))
fields.append(EmbedField("Score", result["score"]))
# Field(
# "Created At",
# str(datetime.fromtimestamp(result["created_utc"])),
# ),
fields.append(Field("ID", result["rid"]))
fields.append(EmbedField("ID", result["rid"]))
embed = build_embed(
title=title,
description=desc,
@ -109,6 +116,6 @@ class JokeCog(commands.Cog):
# await ctx.send(f"**{result['title']}**\n\n{result['body']}")
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add JokeCog to J.A.R.V.I.S."""
bot.add_cog(JokeCog(bot))
JokeCog(bot)

View file

@ -6,6 +6,6 @@ from jarvis.cogs.modlog import command, member, message
def setup(bot: Bot) -> None:
"""Add modlog cogs to J.A.R.V.I.S."""
bot.add_cog(command.ModlogCommandCog(bot))
bot.add_cog(member.ModlogMemberCog(bot))
bot.add_cog(message.ModlogMessageCog(bot))
command.ModlogCommandCog(bot)
member.ModlogMemberCog(bot)
message.ModlogMessageCog(bot)

View file

@ -41,9 +41,8 @@ class ModlogCommandCog(commands.Cog):
fields=fields,
color="#fc9e3f",
)
embed.set_author(
name=ctx.author.name,
icon_url=ctx.author.avatar_url,
embed.set_author(name=ctx.author.username, icon_url=ctx.author.display_avatar.url)
embed.set_footer(
text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}"
)
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}")
await channel.send(embed=embed)

View file

@ -223,7 +223,9 @@ class ModlogMemberCog(commands.Cog):
desc=f"{before.mention} was verified",
)
async def process_rolechange(self, before: discord.Member, after: discord.Member) -> discord.Embed:
async def process_rolechange(
self, before: discord.Member, after: discord.Member
) -> discord.Embed:
"""Process rolechange event."""
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await before.guild.audit_logs(
@ -320,10 +322,7 @@ class ModlogMemberCog(commands.Cog):
fields=fields,
timestamp=log.created_at,
)
embed.set_author(
name=f"{after.name}",
icon_url=after.avatar_url,
)
embed.set_author(name=f"{after.name}", icon_url=after.display_avatar.url)
embed.set_footer(text=f"{after.name}#{after.discriminator} | {after.id}")
elif len(before.roles) != len(after.roles):
# TODO: User got a new role

View file

@ -44,10 +44,12 @@ class ModlogMessageCog(commands.Cog):
)
embed.set_author(
name=before.author.name,
icon_url=before.author.avatar_url,
icon_url=before.author.display_avatar.url,
url=after.jump_url,
)
embed.set_footer(text=f"{before.author.name}#{before.author.discriminator} | {before.author.id}")
embed.set_footer(
text=f"{before.author.name}#{before.author.discriminator} | {before.author.id}"
)
await channel.send(embed=embed)
@commands.Cog.listener()
@ -97,8 +99,10 @@ class ModlogMessageCog(commands.Cog):
embed.set_author(
name=message.author.name,
icon_url=message.author.avatar_url,
icon_url=message.author.display_avatar.url,
url=message.jump_url,
)
embed.set_footer(text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}")
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}"
)
await channel.send(embed=embed)

View file

@ -33,10 +33,7 @@ def modlog_embed(
fields=fields,
timestamp=log.created_at,
)
embed.set_author(
name=f"{member.name}",
icon_url=member.avatar_url,
)
embed.set_author(name=f"{member.name}", icon_url=member.display_avatar.url)
embed.set_footer(text=f"{member.name}#{member.discriminator} | {member.id}")
return embed

View file

@ -1,163 +1,27 @@
"""J.A.R.V.I.S. Owner Cog."""
import os
import sys
import traceback
from inspect import getsource
from time import time
from typing import Any
from dis_snek import MessageContext, Scale, Snake, message_command
from dis_snek.models.discord.user import User
from dis_snek.models.snek.checks import is_owner
from dis_snek.models.snek.command import check
import discord
from discord import DMChannel, User
from discord.ext import commands
import jarvis
from jarvis.config import reload_config
from jarvis.db.models import Config
from jarvis.utils import update
from jarvis.utils.permissions import user_is_bot_admin
class OwnerCog(commands.Cog):
class OwnerCog(Scale):
"""
J.A.R.V.I.S. management cog.
Used by admins to control core J.A.R.V.I.S. systems
"""
def __init__(self, bot: commands.Cog):
def __init__(self, bot: Snake):
self.bot = bot
self.admins = Config.objects(key="admins").first()
@commands.command(name="load", hidden=True)
@user_is_bot_admin()
async def _load_cog(self, ctx: commands.Context, *, cog: str) -> None:
info = await self.bot.application_info()
if ctx.message.author == info.owner or ctx.message.author.id in self.admins.value:
try:
if "jarvis.cogs." not in cog:
cog = "jarvis.cogs." + cog.split(".")[-1]
self.bot.load_extension(cog)
except commands.errors.ExtensionAlreadyLoaded:
await ctx.send(f"Cog `{cog}` already loaded")
except Exception as e:
await ctx.send(f"Failed to load new cog `{cog}`: {type(e).name} - {e}")
else:
await ctx.send(f"Successfully loaded new cog `{cog}`")
else:
await ctx.send("I'm afraid I can't let you do that")
@commands.command(name="unload", hidden=True)
@user_is_bot_admin()
async def _unload_cog(self, ctx: commands.Context, *, cog: str) -> None:
if cog in ["jarvis.cogs.owner", "owner"]:
await ctx.send("Cannot unload `owner` cog")
return
info = await self.bot.application_info()
if ctx.message.author == info.owner or ctx.message.author.id in self.admins.value:
try:
if "jarvis.cogs." not in cog:
cog = "jarvis.cogs." + cog.split(".")[-1]
self.bot.unload_extension(cog)
except commands.errors.ExtensionNotLoaded:
await ctx.send(f"Cog `{cog}` not loaded")
except Exception as e:
await ctx.send(f"Failed to unload cog `{cog}` {type(e).__name__} - {e}")
else:
await ctx.send(f"Successfully unloaded cog `{cog}`")
else:
await ctx.send("I'm afraid I can't let you do that")
@commands.command(name="reload", hidden=True)
@user_is_bot_admin()
async def _cog_reload(self, ctx: commands.Context, *, cog: str) -> None:
if cog in ["jarvis.cogs.owner", "owner"]:
await ctx.send("Cannot reload `owner` cog")
return
info = await self.bot.application_info()
if ctx.message.author == info.owner or ctx.message.author.id in self.admins.value:
try:
if "jarvis.cogs." not in cog:
cog = "jarvis.cogs." + cog.split(".")[-1]
try:
self.bot.load_extension(cog)
except commands.errors.ExtensionNotLoaded:
pass
self.bot.unload_extension(cog)
except Exception as e:
await ctx.send(f"Failed to reload cog `{cog}` {type(e).__name__} - {e}")
else:
await ctx.send(f"Successfully reloaded cog `{cog}`")
else:
await ctx.send("I'm afraid I can't let you do that")
@commands.group(name="system", hidden=True, pass_context=True)
@user_is_bot_admin()
async def _system(self, ctx: commands.Context) -> None:
if ctx.invoked_subcommand is None:
await ctx.send("Usage: `system <subcommand>`\n" + "Subcommands: `restart`, `update`")
@_system.command(name="restart", hidden=True)
@user_is_bot_admin()
async def _restart(self, ctx: commands.Context) -> None:
info = await self.bot.application_info()
if ctx.message.author == info.owner or ctx.message.author.id in self.admins.value:
await ctx.send("Restarting core systems...")
if isinstance(ctx.channel, discord.channel.DMChannel):
jarvis.restart_ctx = {
"user": ctx.message.author.id,
"channel": ctx.channel.id,
}
else:
jarvis.restart_ctx = {
"guild": ctx.message.guild.id,
"channel": ctx.channel.id,
}
await self.bot.close()
else:
await ctx.send("I'm afraid I can't let you do that")
@_system.command(name="update", hidden=True)
@user_is_bot_admin()
async def _update(self, ctx: commands.Context) -> None:
info = await self.bot.application_info()
if ctx.message.author == info.owner or ctx.message.author.id in self.admins.value:
await ctx.send("Updating core systems...")
status = update()
if status == 0:
await ctx.send("Core systems updated. Restarting...")
if isinstance(ctx.channel, discord.channel.DMChannel):
jarvis.restart_ctx = {
"user": ctx.message.author.id,
"channel": ctx.channel.id,
}
else:
jarvis.restart_ctx = {
"guild": ctx.message.guild.id,
"channel": ctx.channel.id,
}
await self.bot.close()
elif status == 1:
await ctx.send("Core systems already up to date.")
elif status == 2:
await ctx.send("Core system update available, but core is dirty.")
else:
await ctx.send("I'm afraid I can't let you do that")
@_system.command(name="refresh", hidden=True)
@user_is_bot_admin()
async def _refresh(self, ctx: commands.Context) -> None:
reload_config()
await ctx.send("System refreshed")
@commands.group(name="admin", hidden=True, pass_context=True)
@commands.is_owner()
async def _admin(self, ctx: commands.Context) -> None:
if ctx.invoked_subcommand is None:
await ctx.send("Usage: `admin <subcommand>`\n" + "Subcommands: `add`, `remove`")
@_admin.command(name="add", hidden=True)
@commands.is_owner()
async def _add(self, ctx: commands.Context, user: User) -> None:
@message_command(name="addadmin")
@check(is_owner())
async def _add(self, ctx: MessageContext, user: User) -> None:
if user.id in self.admins.value:
await ctx.send(f"{user.mention} is already an admin.")
return
@ -166,9 +30,9 @@ class OwnerCog(commands.Cog):
reload_config()
await ctx.send(f"{user.mention} is now an admin. Use this power carefully.")
@_admin.command(name="remove", hidden=True)
@commands.is_owner()
async def _remove(self, ctx: commands.Context, user: User) -> None:
@message_command(name="deladmin")
@is_owner()
async def _remove(self, ctx: MessageContext, user: User) -> None:
if user.id not in self.admins.value:
await ctx.send(f"{user.mention} is not an admin.")
return
@ -177,70 +41,7 @@ class OwnerCog(commands.Cog):
reload_config()
await ctx.send(f"{user.mention} is no longer an admin.")
def resolve_variable(self, variable: Any) -> Any:
"""Resolve a variable from eval."""
if hasattr(variable, "__iter__"):
var_length = len(list(variable))
if (var_length > 100) and (not isinstance(variable, str)):
return f"<a {type(variable).__name__} iterable " + f"with more than 100 values ({var_length})>"
elif not var_length:
return f"<an empty {type(variable).__name__} iterable>"
if (not variable) and (not isinstance(variable, bool)):
return f"<an empty {type(variable).__name__} object>"
return (
variable
if (len(f"{variable}") <= 1000)
else f"<a long {type(variable).__name__} object " + f"with the length of {len(f'{variable}'):,}>"
)
def prepare(self, string: str) -> str:
"""Prepare string for eval."""
arr = string.strip("```").replace("py\n", "").replace("python\n", "").split("\n")
if not arr[::-1][0].replace(" ", "").startswith("return"):
arr[len(arr) - 1] = "return " + arr[::-1][0]
return "".join(f"\n\t{i}" for i in arr)
@commands.command(pass_context=True, aliases=["eval", "exec", "evaluate"])
@user_is_bot_admin()
async def _eval(self, ctx: commands.Context, *, code: str) -> None:
if not isinstance(ctx.message.channel, DMChannel):
return
code = self.prepare(code)
args = {
"discord": discord,
"sauce": getsource,
"sys": sys,
"os": os,
"imp": __import__,
"this": self,
"ctx": ctx,
}
try:
exec( # noqa: S102
f"async def func():{code}",
globals().update(args),
locals(),
)
a = time()
response = await eval("func()", globals().update(args), locals()) # noqa: S307
if response is None or isinstance(response, discord.Message):
del args, code
return
if isinstance(response, str):
response = response.replace("`", "")
await ctx.send(
f"```py\n{self.resolve_variable(response)}```\n`{type(response).__name__} | {(time() - a) / 1000} ms`"
)
except Exception:
await ctx.send(f"Error occurred:```\n{traceback.format_exc()}```")
del args, code
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add OwnerCog to J.A.R.V.I.S."""
bot.add_cog(OwnerCog(bot))
OwnerCog(bot)

View file

@ -5,26 +5,22 @@ from datetime import datetime, timedelta
from typing import List, Optional
from bson import ObjectId
from discord import Embed
from discord.ext.commands import Bot
from discord.ext.tasks import loop
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from discord_slash.utils.manage_components import (
create_actionrow,
create_select,
create_select_option,
wait_for_component,
from dis_snek import InteractionContext, Snake
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from jarvis.db.models import Reminder
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)",
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
flags=re.IGNORECASE,
)
@ -32,49 +28,40 @@ invites = re.compile(
class RemindmeCog(CacheCog):
"""J.A.R.V.I.S. Remind Me Cog."""
def __init__(self, bot: Bot):
def __init__(self, bot: Snake):
super().__init__(bot)
self._remind.start()
@cog_ext.cog_slash(
name="remindme",
description="Set a reminder",
options=[
create_option(
name="message",
description="What to remind you of",
option_type=3,
required=True,
),
create_option(
name="weeks",
description="Number of weeks?",
option_type=4,
required=False,
),
create_option(
name="days",
description="Number of days?",
option_type=4,
required=False,
),
create_option(
name="hours",
description="Number of hours?",
option_type=4,
required=False,
),
create_option(
name="minutes",
description="Number of minutes?",
option_type=4,
required=False,
),
],
@slash_command(name="remindme", description="Set a reminder")
@slash_option(
name="message",
description="What to remind you of?",
opt_type=OptionTypes.STRING,
required=True,
)
@slash_option(
name="weeks",
description="Number of weeks?",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="days", description="Number of days?", opt_type=OptionTypes.INTEGER, required=False
)
@slash_option(
name="hours",
description="Number of hours?",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="minutes",
description="Number of minutes?",
opt_type=OptionTypes.INTEGER,
required=False,
)
async def _remindme(
self,
ctx: SlashContext,
ctx: InteractionContext,
message: Optional[str] = None,
weeks: Optional[int] = 0,
days: Optional[int] = 0,
@ -82,20 +69,20 @@ class RemindmeCog(CacheCog):
minutes: Optional[int] = 0,
) -> None:
if len(message) > 100:
await ctx.send("Reminder cannot be > 100 characters.", hidden=True)
await ctx.send("Reminder cannot be > 100 characters.", ephemeral=True)
return
elif invites.search(message):
await ctx.send(
"Listen, don't use this to try and bypass the rules",
hidden=True,
ephemeral=True,
)
return
elif not valid.fullmatch(message):
await ctx.send("Hey, you should probably make this readable", hidden=True)
await ctx.send("Hey, you should probably make this readable", ephemeral=True)
return
if not any([weeks, days, hours, minutes]):
await ctx.send("At least one time period is required", hidden=True)
await ctx.send("At least one time period is required", ephemeral=True)
return
weeks = abs(weeks)
@ -104,19 +91,19 @@ class RemindmeCog(CacheCog):
minutes = abs(minutes)
if weeks and weeks > 4:
await ctx.send("Cannot be farther than 4 weeks out!", hidden=True)
await ctx.send("Cannot be farther than 4 weeks out!", ephemeral=True)
return
elif days and days > 6:
await ctx.send("Use weeks instead of 7+ days, please.", hidden=True)
await ctx.send("Use weeks instead of 7+ days, please.", ephemeral=True)
return
elif hours and hours > 23:
await ctx.send("Use days instead of 24+ hours, please.", hidden=True)
await ctx.send("Use days instead of 24+ hours, please.", ephemeral=True)
return
elif minutes and minutes > 59:
await ctx.send("Use hours instead of 59+ minutes, please.", hidden=True)
await ctx.send("Use hours instead of 59+ minutes, please.", ephemeral=True)
return
reminders = Reminder.objects(user=ctx.author.id, active=True).count()
@ -124,7 +111,7 @@ class RemindmeCog(CacheCog):
await ctx.send(
"You already have 5 (or more) active reminders. "
"Please either remove an old one, or wait for one to pass",
hidden=True,
ephemeral=True,
)
return
@ -148,8 +135,8 @@ class RemindmeCog(CacheCog):
title="Reminder Set",
description=f"{ctx.author.mention} set a reminder",
fields=[
Field(name="Message", value=message),
Field(
EmbedField(name="Message", value=message),
EmbedField(
name="When",
value=remind_at.strftime("%Y-%m-%d %H:%M UTC"),
inline=False,
@ -158,19 +145,21 @@ class RemindmeCog(CacheCog):
)
embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url,
name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar.url,
)
embed.set_thumbnail(url=ctx.author.avatar_url)
embed.set_thumbnail(url=ctx.author.display_avatar.url)
await ctx.send(embed=embed)
async def get_reminders_embed(self, ctx: SlashContext, reminders: List[Reminder]) -> Embed:
async def get_reminders_embed(
self, ctx: InteractionContext, reminders: List[Reminder]
) -> Embed:
"""Build embed for paginator."""
fields = []
for reminder in reminders:
fields.append(
Field(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=f"{reminder.message}\n\u200b",
inline=False,
@ -184,57 +173,49 @@ class RemindmeCog(CacheCog):
)
embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url,
name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar.url,
)
embed.set_thumbnail(url=ctx.author.avatar_url)
embed.set_thumbnail(url=ctx.author.display_avatar.url)
return embed
@cog_ext.cog_subcommand(
base="reminders",
name="list",
description="List reminders for a user",
)
async def _list(self, ctx: SlashContext) -> None:
@slash_command(name="reminders", sub_cmd_name="list", sub_cmd_description="List reminders")
async def _list(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
await ctx.defer(hidden=True)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True,
ephemeral=True,
)
return
reminders = Reminder.objects(user=ctx.author.id, active=True)
if not reminders:
await ctx.send("You have no reminders set.", hidden=True)
await ctx.send("You have no reminders set.", ephemeral=True)
return
embed = await self.get_reminders_embed(ctx, reminders)
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(
base="reminders",
name="delete",
description="Delete a reminder",
)
async def _delete(self, ctx: SlashContext) -> None:
@slash_command(name="reminders", sub_cmd_name="delete", sub_cmd_description="Delete a reminder")
async def _delete(self, ctx: InteractionContext) -> None:
reminders = Reminder.objects(user=ctx.author.id, active=True)
if not reminders:
await ctx.send("You have no reminders set", hidden=True)
await ctx.send("You have no reminders set", ephemeral=True)
return
options = []
for reminder in reminders:
option = create_select_option(
option = SelectOption(
label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=str(reminder.id),
emoji="",
)
options.append(option)
select = create_select(
select = Select(
options=options,
custom_id="to_delete",
placeholder="Select reminders to delete",
@ -242,7 +223,7 @@ class RemindmeCog(CacheCog):
max_values=len(reminders),
)
components = [create_actionrow(select)]
components = [ActionRow(select)]
embed = await self.get_reminders_embed(ctx, reminders)
message = await ctx.send(
content=f"You have {len(reminders)} reminder(s) set:",
@ -251,23 +232,22 @@ class RemindmeCog(CacheCog):
)
try:
context = await wait_for_component(
self.bot,
check=lambda x: ctx.author.id == x.author_id,
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.context.author.id,
messages=message,
timeout=60 * 5,
)
for to_delete in context.selected_options:
for to_delete in context.context.values:
_ = Reminder.objects(user=ctx.author.id, id=ObjectId(to_delete)).delete()
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
fields = []
for reminder in filter(lambda x: str(x.id) in context.selected_options, reminders):
for reminder in filter(lambda x: str(x.id) in context.context.values, reminders):
fields.append(
Field(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=reminder.message,
inline=False,
@ -280,52 +260,23 @@ class RemindmeCog(CacheCog):
)
embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url,
name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar.url,
)
embed.set_thumbnail(url=ctx.author.avatar_url)
embed.set_thumbnail(url=ctx.author.display_avatar.url)
await context.edit_origin(
content=f"Deleted {len(context.selected_options)} reminder(s)",
await context.context.edit_origin(
content=f"Deleted {len(context.context.values)} reminder(s)",
components=components,
embed=embed,
)
except asyncio.TimeoutError:
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
await message.edit(components=components)
@loop(seconds=15)
async def _remind(self) -> None:
reminders = Reminder.objects(remind_at__lte=datetime.utcnow() + timedelta(seconds=30))
for reminder in reminders:
if reminder.remind_at <= datetime.utcnow():
user = await self.bot.fetch_user(reminder.user)
if not user:
reminder.delete()
continue
embed = build_embed(
title="You have a reminder",
description=reminder.message,
fields=[],
)
embed.set_author(
name=user.name + "#" + user.discriminator,
icon_url=user.avatar_url,
)
embed.set_thumbnail(url=user.avatar_url)
try:
await user.send(embed=embed)
except Exception:
guild = self.bot.fetch_guild(reminder.guild)
channel = guild.get_channel(reminder.channel) if guild else None
if channel:
await channel.send(f"{user.mention}", embed=embed)
finally:
reminder.delete()
def setup(bot: Bot) -> None:
def setup(bot: Snake) -> None:
"""Add RemindmeCog to J.A.R.V.I.S."""
bot.add_cog(RemindmeCog(bot))
RemindmeCog(bot)

View file

@ -1,54 +1,45 @@
"""J.A.R.V.I.S. Role Giver Cog."""
import asyncio
from discord import Role
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from discord_slash.utils.manage_components import (
create_actionrow,
create_select,
create_select_option,
wait_for_component,
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.role import Role
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check, cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Rolegiver
from jarvis.utils import build_embed
from jarvis.utils.field import Field
from jarvis.utils import build_embed, get
from jarvis.utils.permissions import admin_or_permissions
class RolegiverCog(commands.Cog):
class RolegiverCog(Scale):
"""J.A.R.V.I.S. Role Giver Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
@cog_ext.cog_subcommand(
base="rolegiver",
name="add",
description="Add a role to rolegiver",
options=[
create_option(
name="role",
description="Role to add",
option_type=8,
required=True,
)
],
@slash_command(
name="rolegiver", sub_cmd_name="add", sub_cmd_description="Add a role to rolegiver"
)
@admin_or_permissions(manage_guild=True)
async def _rolegiver_add(self, ctx: SlashContext, role: Role) -> None:
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
if setting and role.id in setting.roles:
await ctx.send("Role already in rolegiver", hidden=True)
await ctx.send("Role already in rolegiver", ephemeral=True)
return
if not setting:
setting = Rolegiver(guild=ctx.guild.id, roles=[])
if len(setting.roles) >= 20:
await ctx.send("You can only have 20 roles in the rolegiver", hidden=True)
await ctx.send("You can only have 20 roles in the rolegiver", ephemeral=True)
return
setting.roles.append(role.id)
@ -58,7 +49,7 @@ class RolegiverCog(commands.Cog):
for role_id in setting.roles:
if role_id == role.id:
continue
e_role = ctx.guild.get_role(role_id)
e_role = await ctx.guild.get_role(role_id)
if not e_role:
continue
roles.append(e_role)
@ -67,8 +58,8 @@ class RolegiverCog(commands.Cog):
value = "\n".join([r.mention for r in roles]) if roles else "None"
fields = [
Field(name="New Role", value=f"{role.mention}"),
Field(name="Existing Role(s)", value=value),
EmbedField(name="New Role", value=f"{role.mention}"),
EmbedField(name="Existing Role(s)", value=value),
]
embed = build_embed(
@ -77,61 +68,60 @@ class RolegiverCog(commands.Cog):
fields=fields,
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.avatar_url,
)
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url)
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}")
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}")
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(
base="rolegiver",
name="remove",
description="Remove a role from rolegiver",
@slash_command(
name="rolegiver", sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver"
)
@admin_or_permissions(manage_guild=True)
async def _rolegiver_remove(self, ctx: SlashContext) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", hidden=True)
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
options = []
for role in setting.roles:
role: Role = ctx.guild.get_role(role)
option = create_select_option(label=role.name, value=str(role.id))
role: Role = await ctx.guild.get_role(role)
option = SelectOption(label=role.name, value=str(role.id))
options.append(option)
select = create_select(
select = Select(
options=options,
custom_id="to_delete",
placeholder="Select roles to remove",
min_values=1,
max_values=len(options),
)
components = [create_actionrow(select)]
components = [ActionRow(select)]
message = await ctx.send(content="\u200b", components=components)
try:
context = await wait_for_component(
self.bot,
check=lambda x: ctx.author.id == x.author.id,
message=message,
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.context.author.id,
messages=message,
timeout=60 * 1,
)
for to_delete in context.selected_options:
removed_roles = []
for to_delete in context.context.values:
role = await ctx.guild.get_role(to_delete)
if role:
removed_roles.append(role)
setting.roles.remove(int(to_delete))
setting.save()
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
roles = []
for role_id in setting.roles:
e_role = ctx.guild.get_role(role_id)
e_role = await ctx.guild.get_role(role_id)
if not e_role:
continue
roles.append(e_role)
@ -140,9 +130,10 @@ class RolegiverCog(commands.Cog):
roles.sort(key=lambda x: -x.position)
value = "\n".join([r.mention for r in roles]) if roles else "None"
rvalue = "\n".join([r.mention for r in removed_roles]) if removed_roles else "None"
fields = [
Field(name="Removed Role", value=f"{role.mention}"),
Field(name="Remaining Role(s)", value=value),
EmbedField(name="Removed Role(s)", value=rvalue),
EmbedField(name="Remaining Role(s)", value=value),
]
embed = build_embed(
@ -151,39 +142,34 @@ class RolegiverCog(commands.Cog):
fields=fields,
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.avatar_url,
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url)
embed.set_footer(
text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}"
)
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}")
await context.edit_origin(
content=f"Removed {len(context.selected_options)} role(s)",
await context.context.edit_origin(
content=f"Removed {len(context.context.values)} role(s)",
embed=embed,
components=components,
)
except asyncio.TimeoutError:
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
await message.edit(components=components)
@cog_ext.cog_subcommand(
base="rolegiver",
name="list",
description="List roles rolegiver",
)
async def _rolegiver_list(self, ctx: SlashContext) -> None:
@slash_command(name="rolegiver", sub_cmd_name="list", description="List rolegiver roles")
async def _rolegiver_list(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", hidden=True)
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
roles = []
for role_id in setting.roles:
e_role = ctx.guild.get_role(role_id)
e_role = await ctx.guild.get_role(role_id)
if not e_role:
continue
roles.append(e_role)
@ -199,59 +185,52 @@ class RolegiverCog(commands.Cog):
fields=[],
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.avatar_url,
name=ctx.author.display_name,
icon_url=ctx.author.display_avatar.url,
)
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}")
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}")
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(
base="role",
name="get",
description="Get a role from rolegiver",
)
@commands.cooldown(1, 10, commands.BucketType.user)
async def _role_get(self, ctx: SlashContext) -> None:
@slash_command(name="role", sub_cmd_name="get", sub_cmd_description="Get a role")
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
async def _role_get(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", hidden=True)
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
options = []
for role in setting.roles:
role: Role = ctx.guild.get_role(role)
option = create_select_option(label=role.name, value=str(role.id))
role: Role = await ctx.guild.get_role(role)
option = SelectOption(label=role.name, value=str(role.id))
options.append(option)
select = create_select(
select = Select(
options=options,
custom_id="to_delete",
placeholder="Select roles to add",
min_values=1,
max_values=len(options),
)
components = [create_actionrow(select)]
components = [ActionRow(select)]
message = await ctx.send(content="\u200b", components=components)
try:
context = await wait_for_component(
self.bot,
check=lambda x: ctx.author.id == x.author.id,
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.context.author.id,
messages=message,
timeout=60 * 5,
)
added_roles = []
for role in context.selected_options:
role = ctx.guild.get_role(int(role))
for role in context.context.values:
role = await ctx.guild.get_role(int(role))
added_roles.append(role)
await ctx.author.add_roles(role, reason="Rolegiver")
await ctx.author.add_role(role, reason="Rolegiver")
roles = ctx.author.roles
if roles:
@ -261,101 +240,125 @@ class RolegiverCog(commands.Cog):
avalue = "\n".join([r.mention for r in added_roles]) if added_roles else "None"
value = "\n".join([r.mention for r in roles]) if roles else "None"
fields = [
Field(name="Added Role(s)", value=avalue),
Field(name="Prior Role(s)", value=value),
EmbedField(name="Added Role(s)", value=avalue),
EmbedField(name="Prior Role(s)", value=value),
]
embed = build_embed(
title="User Given Role",
description=f"{role.mention} given to {ctx.author.mention}",
description=f"{len(added_roles)} role(s) given to {ctx.author.mention}",
fields=fields,
)
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.avatar_url,
name=ctx.author.display_name,
icon_url=ctx.author.display_avatar.url,
)
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}")
for row in components:
for component in row["components"]:
component["disabled"] = True
embed.set_footer(
text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}"
)
await message.edit_origin(embed=embed, content="\u200b", components=components)
for row in components:
for component in row.components:
component.disabled = True
await context.context.edit_origin(embed=embed, content="\u200b", components=components)
except asyncio.TimeoutError:
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
await message.edit(components=components)
@cog_ext.cog_subcommand(
base="role",
name="forfeit",
description="Have rolegiver take away role",
options=[
create_option(
name="role",
description="Role to remove",
option_type=8,
required=True,
)
],
)
@commands.cooldown(1, 10, commands.BucketType.user)
async def _role_forfeit(self, ctx: SlashContext, role: Role) -> None:
@slash_command(name="role", sub_cmd_name="remove", sub_cmd_description="Remove a role")
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
async def _role_remove(self, ctx: InteractionContext) -> None:
user_roles = ctx.author.roles
setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", hidden=True)
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
elif role.id not in setting.roles:
await ctx.send("Role not in rolegiver", hidden=True)
return
elif role not in ctx.author.roles:
await ctx.send("You do not have that role", hidden=True)
elif not any(x.id in setting.roles for x in user_roles):
await ctx.send("You have no rolegiver roles", ephemeral=True)
return
await ctx.author.remove_roles(role, reason="Rolegiver")
valid = list(filter(lambda x: x.id in setting.roles, user_roles))
options = []
for role in valid:
option = SelectOption(label=role.name, value=str(role.id))
options.append(option)
roles = ctx.author.roles
if roles:
roles.sort(key=lambda x: -x.position)
_ = roles.pop(-1)
value = "\n".join([r.mention for r in roles]) if roles else "None"
fields = [
Field(name="Taken Role", value=f"{role.mention}"),
Field(name="Remaining Role(s)", value=value),
]
embed = build_embed(
title="User Forfeited Role",
description=f"{role.mention} taken from {ctx.author.mention}",
fields=fields,
select = Select(
options=options,
custom_id="to_remove",
placeholder="Select roles to remove",
min_values=1,
max_values=len(options),
)
components = [ActionRow(select)]
embed.set_thumbnail(url=ctx.guild.icon_url)
embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.avatar_url,
)
message = await ctx.send(content="\u200b", components=components)
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}")
try:
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.context.author.id,
messages=message,
timeout=60 * 5,
)
await ctx.send(embed=embed)
removed_roles = []
for to_remove in context.context.values:
role = get(user_roles, id=int(to_remove))
await ctx.author.remove_role(role, reason="Rolegiver")
user_roles.remove(role)
removed_roles.append(role)
@cog_ext.cog_subcommand(
base="rolegiver",
name="cleanup",
description="Cleanup rolegiver roles",
user_roles.sort(key=lambda x: -x.position)
_ = user_roles.pop(-1)
value = "\n".join([r.mention for r in user_roles]) if user_roles else "None"
rvalue = "\n".join([r.mention for r in removed_roles]) if removed_roles else "None"
fields = [
EmbedField(name="Removed Role(s)", value=rvalue),
EmbedField(name="Remaining Role(s)", value=value),
]
embed = build_embed(
title="User Forfeited Role",
description=f"{len(removed_roles)} role(s) removed from {ctx.author.mention}",
fields=fields,
)
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url)
embed.set_footer(
text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}"
)
for row in components:
for component in row.components:
component.disabled = True
await context.context.edit_origin(embed=embed, components=components, content="\u200b")
except asyncio.TimeoutError:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(components=components)
@slash_command(
name="rolegiver", sub_cmd_name="cleanup", description="Removed deleted roles from rolegiver"
)
@admin_or_permissions(manage_guild=True)
async def _rolegiver_cleanup(self, ctx: SlashContext) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or not setting.roles:
await ctx.send("Rolegiver has no roles", hidden=True)
guild_roles = await ctx.guild.fetch_roles()
guild_role_ids = [x.id for x in guild_roles]
await ctx.send("Rolegiver has no roles", ephemeral=True)
guild_role_ids = [r.id for r in ctx.guild.roles]
for role_id in setting.roles:
if role_id not in guild_role_ids:
setting.roles.remove(role_id)
@ -364,6 +367,6 @@ class RolegiverCog(commands.Cog):
await ctx.send("Rolegiver cleanup finished")
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add RolegiverCog to J.A.R.V.I.S."""
bot.add_cog(RolegiverCog(bot))
RolegiverCog(bot)

View file

@ -1,6 +1,7 @@
"""J.A.R.V.I.S. Settings Management Cog."""
from typing import Any
from dis_snek.models.snek.command import check
from discord import Role, TextChannel
from discord.ext import commands
from discord.utils import find
@ -33,46 +34,24 @@ class SettingsCog(commands.Cog):
"""Delete a guild setting."""
return Setting.objects(setting=setting, guild=guild).delete()
@cog_ext.cog_subcommand(
base="settings",
base_desc="Settings management",
subcommand_group="set",
subcommand_group_description="Set a setting",
name="mute",
description="Set mute role",
options=[
create_option(
name="role",
description="Mute role",
option_type=8,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
async def _set_mute(self, ctx: SlashContext, role: Role) -> None:
await ctx.defer()
self.update_settings("mute", role.id, ctx.guild.id)
await ctx.send(f"Settings applied. New mute role is `{role.name}`")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="modlog",
description="Set modlog channel",
options=[
choices=[
create_option(
name="channel",
description="Modlog channel",
option_type=7,
opt_type=7,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _set_modlog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", hidden=True)
await ctx.send("Channel must be a TextChannel", ephemeral=True)
return
self.update_settings("modlog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New modlog channel is {channel.mention}")
@ -82,19 +61,19 @@ class SettingsCog(commands.Cog):
subcommand_group="set",
name="userlog",
description="Set userlog channel",
options=[
choices=[
create_option(
name="channel",
description="Userlog channel",
option_type=7,
opt_type=7,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _set_userlog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", hidden=True)
await ctx.send("Channel must be a TextChannel", ephemeral=True)
return
self.update_settings("userlog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New userlog channel is {channel.mention}")
@ -104,16 +83,16 @@ class SettingsCog(commands.Cog):
subcommand_group="set",
name="massmention",
description="Set massmention amount",
options=[
choices=[
create_option(
name="amount",
description="Amount of mentions (0 to disable)",
option_type=4,
opt_type=4,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _set_massmention(self, ctx: SlashContext, amount: int) -> None:
await ctx.defer()
self.update_settings("massmention", amount, ctx.guild.id)
@ -124,16 +103,16 @@ class SettingsCog(commands.Cog):
subcommand_group="set",
name="verified",
description="Set verified role",
options=[
choices=[
create_option(
name="role",
description="verified role",
option_type=8,
opt_type=8,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _set_verified(self, ctx: SlashContext, role: Role) -> None:
await ctx.defer()
self.update_settings("verified", role.id, ctx.guild.id)
@ -144,16 +123,16 @@ class SettingsCog(commands.Cog):
subcommand_group="set",
name="unverified",
description="Set unverified role",
options=[
choices=[
create_option(
name="role",
description="Unverified role",
option_type=8,
opt_type=8,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _set_unverified(self, ctx: SlashContext, role: Role) -> None:
await ctx.defer()
self.update_settings("unverified", role.id, ctx.guild.id)
@ -164,41 +143,28 @@ class SettingsCog(commands.Cog):
subcommand_group="set",
name="noinvite",
description="Set if invite deletion should happen",
options=[
choices=[
create_option(
name="active",
description="Active?",
option_type=4,
opt_type=4,
required=True,
)
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _set_invitedel(self, ctx: SlashContext, active: int) -> None:
await ctx.defer()
self.update_settings("noinvite", bool(active), ctx.guild.id)
await ctx.send(f"Settings applied. Automatic invite active: {bool(active)}")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
subcommand_group_description="Unset a setting",
name="mute",
description="Unset mute role",
)
@admin_or_permissions(manage_guild=True)
async def _unset_mute(self, ctx: SlashContext) -> None:
await ctx.defer()
self.delete_settings("mute", ctx.guild.id)
await ctx.send("Setting removed.")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="modlog",
description="Unset modlog channel",
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _unset_modlog(self, ctx: SlashContext) -> None:
self.delete_settings("modlog", ctx.guild.id)
await ctx.send("Setting removed.")
@ -209,7 +175,7 @@ class SettingsCog(commands.Cog):
name="userlog",
description="Unset userlog channel",
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _unset_userlog(self, ctx: SlashContext) -> None:
self.delete_settings("userlog", ctx.guild.id)
await ctx.send("Setting removed.")
@ -220,7 +186,7 @@ class SettingsCog(commands.Cog):
name="massmention",
description="Unet massmention amount",
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _massmention(self, ctx: SlashContext) -> None:
await ctx.defer()
self.delete_settings("massmention", ctx.guild.id)
@ -232,7 +198,7 @@ class SettingsCog(commands.Cog):
name="verified",
description="Unset verified role",
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _verified(self, ctx: SlashContext) -> None:
await ctx.defer()
self.delete_settings("verified", ctx.guild.id)
@ -244,14 +210,14 @@ class SettingsCog(commands.Cog):
name="unverified",
description="Unset unverified role",
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _unverified(self, ctx: SlashContext) -> None:
await ctx.defer()
self.delete_settings("unverified", ctx.guild.id)
await ctx.send("Setting removed.")
@cog_ext.cog_subcommand(base="settings", name="view", description="View settings")
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _view(self, ctx: SlashContext) -> None:
settings = Setting.objects(guild=ctx.guild.id)
@ -272,7 +238,7 @@ class SettingsCog(commands.Cog):
value = "||`[redacted]`||"
elif setting.setting == "rolegiver":
value = ""
for role in setting.value:
for _role in setting.value:
nvalue = find(lambda x: x.id == value, ctx.guild.roles)
if value:
value += "\n" + nvalue.mention
@ -285,7 +251,7 @@ class SettingsCog(commands.Cog):
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(base="settings", name="clear", description="Clear all settings")
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(manage_guild=True))
async def _clear(self, ctx: SlashContext) -> None:
deleted = Setting.objects(guild=ctx.guild.id).delete()
await ctx.send(f"Guild settings cleared: `{deleted is not None}`")
@ -293,4 +259,4 @@ class SettingsCog(commands.Cog):
def setup(bot: commands.Bot) -> None:
"""Add SettingsCog to J.A.R.V.I.S."""
bot.add_cog(SettingsCog(bot))
SettingsCog(bot)

View file

@ -1,20 +1,19 @@
"""J.A.R.V.I.S. Starboard Cog."""
from discord import TextChannel
from discord.ext import commands
from discord.utils import find
from discord_slash import SlashContext, cog_ext
from discord_slash.context import MenuContext
from discord_slash.model import ContextMenuType, SlashMessage
from discord_slash.utils.manage_commands import create_option
from discord_slash.utils.manage_components import (
create_actionrow,
create_select,
create_select_option,
wait_for_component,
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.message import Message
from dis_snek.models.snek.application_commands import (
CommandTypes,
OptionTypes,
context_menu,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Star, Starboard
from jarvis.utils import build_embed
from jarvis.utils import build_embed, find
from jarvis.utils.permissions import admin_or_permissions
supported_images = [
@ -26,19 +25,15 @@ supported_images = [
]
class StarboardCog(commands.Cog):
class StarboardCog(Scale):
"""J.A.R.V.I.S. Starboard Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
@cog_ext.cog_subcommand(
base="starboard",
name="list",
description="Lists all Starboards",
)
@admin_or_permissions(manage_guild=True)
async def _list(self, ctx: SlashContext) -> None:
@slash_command(name="starboard", sub_cmd_name="list", sub_cmd_description="List all starboards")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _list(self, ctx: InteractionContext) -> None:
starboards = Starboard.objects(guild=ctx.guild.id)
if starboards != []:
message = "Available Starboards:\n"
@ -48,39 +43,35 @@ class StarboardCog(commands.Cog):
else:
await ctx.send("No Starboards available.")
@cog_ext.cog_subcommand(
base="starboard",
name="create",
description="Create a starboard",
options=[
create_option(
name="channel",
description="Starboard channel",
option_type=7,
required=True,
),
],
@slash_command(
name="starboard", sub_cmd_name="create", sub_cmd_description="Create a starboard"
)
@admin_or_permissions(manage_guild=True)
async def _create(self, ctx: SlashContext, channel: TextChannel) -> None:
@slash_option(
name="channel",
description="Starboard channel",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _create(self, ctx: InteractionContext, channel: GuildText) -> None:
if channel not in ctx.guild.channels:
await ctx.send(
"Channel not in guild. Choose an existing channel.",
hidden=True,
ephemeral=True,
)
return
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", hidden=True)
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a GuildText", ephemeral=True)
return
exists = Starboard.objects(channel=channel.id, guild=ctx.guild.id).first()
if exists:
await ctx.send(f"Starboard already exists at {channel.mention}.", hidden=True)
await ctx.send(f"Starboard already exists at {channel.mention}.", ephemeral=True)
return
count = Starboard.objects(guild=ctx.guild.id).count()
if count >= 25:
await ctx.send("25 starboard limit reached", hidden=True)
await ctx.send("25 starboard limit reached", ephemeral=True)
return
_ = Starboard(
@ -90,95 +81,89 @@ class StarboardCog(commands.Cog):
).save()
await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
@cog_ext.cog_subcommand(
base="starboard",
name="delete",
description="Delete a starboard",
options=[
create_option(
name="channel",
description="Starboard channel",
option_type=7,
required=True,
),
],
@slash_command(
name="starboard", sub_cmd_name="delete", sub_cmd_description="Delete a starboard"
)
@admin_or_permissions(manage_guild=True)
async def _delete(self, ctx: SlashContext, channel: TextChannel) -> None:
@slash_option(
name="channel",
description="Starboard channel",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _delete(self, ctx: InteractionContext, channel: GuildText) -> None:
deleted = Starboard.objects(channel=channel.id, guild=ctx.guild.id).delete()
if deleted:
_ = Star.objects(starboard=channel.id).delete()
await ctx.send(f"Starboard deleted from {channel.mention}.", hidden=True)
await ctx.send(f"Starboard deleted from {channel.mention}.")
else:
await ctx.send(f"Starboard not found in {channel.mention}.", hidden=True)
await ctx.send(f"Starboard not found in {channel.mention}.", ephemeral=True)
@cog_ext.cog_context_menu(name="Star Message", target=ContextMenuType.MESSAGE)
async def _star_message(self, ctx: MenuContext) -> None:
await self._star_add.invoke(ctx, ctx.target_message)
@context_menu(name="Star Message", context_type=CommandTypes.MESSAGE)
async def _star_message(self, ctx: InteractionContext) -> None:
await self._star_add._can_run(ctx)
await self._star_add.callback(ctx, message=str(ctx.target_id))
@cog_ext.cog_subcommand(
base="star",
name="add",
description="Star a message",
options=[
create_option(
name="message",
description="Message to star",
option_type=3,
required=True,
),
create_option(
name="channel",
description="Channel that has the message, required if different than command message",
option_type=7,
required=False,
),
],
@slash_command(name="star", sub_cmd_name="add", description="Star a message")
@slash_option(
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
)
@admin_or_permissions(manage_guild=True)
@slash_option(
name="channel",
description="Channel that has the message, not required if used in same channel",
opt_type=OptionTypes.CHANNEL,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _star_add(
self,
ctx: SlashContext,
ctx: InteractionContext,
message: str,
channel: TextChannel = None,
channel: GuildText = None,
) -> None:
if not channel:
channel = ctx.channel
starboards = Starboard.objects(guild=ctx.guild.id)
if not starboards:
await ctx.send("No starboards exist.", hidden=True)
await ctx.send("No starboards exist.", ephemeral=True)
return
await ctx.defer()
if not isinstance(message, Message):
if message.startswith("https://"):
message = message.split("/")[-1]
message = await channel.get_message(int(message))
if not message:
await ctx.send("Message not found", ephemeral=True)
return
channel_list = []
for starboard in starboards:
channel_list.append(find(lambda x: x.id == starboard.channel, ctx.guild.channels))
select_channels = [create_select_option(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)]
select_channels = [
SelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)
]
select = create_select(
select = Select(
options=select_channels,
min_values=1,
max_values=1,
)
components = [create_actionrow(select)]
components = [ActionRow(select)]
msg = await ctx.send(content="Choose a starboard", components=components)
com_ctx = await wait_for_component(
self.bot,
com_ctx = await self.bot.wait_for_component(
messages=msg,
components=components,
check=lambda x: x.author.id == ctx.author.id,
check=lambda x: ctx.author.id == x.context.author.id,
)
starboard = channel_list[int(com_ctx.selected_options[0])]
if not isinstance(message, SlashMessage):
if message.startswith("https://"):
message = message.split("/")[-1]
message = await channel.fetch_message(message)
starboard = channel_list[int(com_ctx.context.values[0])]
exists = Star.objects(
message=message.id,
@ -190,7 +175,7 @@ class StarboardCog(commands.Cog):
if exists:
await ctx.send(
f"Message already sent to Starboard {starboard.mention}",
hidden=True,
ephemeral=True,
)
return
@ -215,9 +200,9 @@ class StarboardCog(commands.Cog):
timestamp=message.created_at,
)
embed.set_author(
name=message.author.name,
name=message.author.display_name,
url=message.jump_url,
icon_url=message.author.avatar_url,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(text=message.guild.name + " | " + message.channel.name)
if image_url:
@ -236,47 +221,38 @@ class StarboardCog(commands.Cog):
active=True,
).save()
components[0]["components"][0]["disabled"] = True
components[0].components[0].disabled = True
await com_ctx.edit_origin(
await com_ctx.context.edit_origin(
content=f"Message saved to Starboard.\nSee it in {starboard.mention}",
components=components,
)
@cog_ext.cog_subcommand(
base="star",
name="delete",
description="Delete a starred message",
options=[
create_option(
name="id",
description="Star to delete",
option_type=4,
required=True,
),
create_option(
name="starboard",
description="Starboard to delete star from",
option_type=7,
required=True,
),
],
@slash_command(name="star", sub_cmd_name="delete", description="Delete a starred message")
@slash_option(
name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
)
@admin_or_permissions(manage_guild=True)
@slash_option(
name="starboard",
description="Starboard to delete star from",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _star_delete(
self,
ctx: SlashContext,
ctx: InteractionContext,
id: int,
starboard: TextChannel,
starboard: GuildText,
) -> None:
if not isinstance(starboard, TextChannel):
await ctx.send("Channel must be a TextChannel", hidden=True)
if not isinstance(starboard, GuildText):
await ctx.send("Channel must be a GuildText channel", ephemeral=True)
return
exists = Starboard.objects(channel=starboard.id, guild=ctx.guild.id).first()
if not exists:
await ctx.send(
f"Starboard does not exist in {starboard.mention}. Please create it first",
hidden=True,
ephemeral=True,
)
return
@ -287,19 +263,19 @@ class StarboardCog(commands.Cog):
active=True,
).first()
if not star:
await ctx.send(f"No star exists with id {id}", hidden=True)
await ctx.send(f"No star exists with id {id}", ephemeral=True)
return
message = await starboard.fetch_message(star.star)
message = await starboard.get_message(star.star)
if message:
await message.delete()
star.active = False
star.save()
await ctx.send(f"Star {id} deleted")
await ctx.send(f"Star {id} deleted from {starboard.mention}")
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add StarboardCog to J.A.R.V.I.S."""
bot.add_cog(StarboardCog(bot))
StarboardCog(bot)

View file

@ -1,131 +1,93 @@
"""J.A.R.V.I.S. Twitter Cog."""
import asyncio
import logging
import tweepy
from bson import ObjectId
from discord import TextChannel
from discord.ext import commands
from discord.ext.tasks import loop
from discord.utils import find
from discord_slash import SlashContext, cog_ext
from discord_slash.model import SlashCommandOptionType as COptionType
from discord_slash.utils.manage_commands import create_choice, create_option
from discord_slash.utils.manage_components import (
create_actionrow,
create_select,
create_select_option,
wait_for_component,
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.config import get_config
from jarvis.db.models import Twitter
from jarvis.utils.permissions import admin_or_permissions
logger = logging.getLogger("discord")
class TwitterCog(commands.Cog):
class TwitterCog(Scale):
"""J.A.R.V.I.S. Twitter Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
config = get_config()
auth = tweepy.AppAuthHandler(config.twitter["consumer_key"], config.twitter["consumer_secret"])
auth = tweepy.AppAuthHandler(
config.twitter["consumer_key"], config.twitter["consumer_secret"]
)
self.api = tweepy.API(auth)
self._tweets.start()
self._guild_cache = {}
self._channel_cache = {}
@loop(seconds=30)
async def _tweets(self) -> None:
twitters = Twitter.objects(active=True)
handles = Twitter.objects.distinct("handle")
twitter_data = {}
for handle in handles:
try:
twitter_data[handle] = self.api.user_timeline(screen_name=handle)
except Exception as e:
logger.error(f"Error with fetching: {e}")
for twitter in twitters:
try:
tweets = list(filter(lambda x: x.id > twitter.last_tweet, twitter_data[twitter.handle]))
if tweets:
tweets = sorted(tweets, key=lambda x: x.id)
if twitter.guild not in self._guild_cache:
self._guild_cache[twitter.guild] = await self.bot.fetch_guild(twitter.guild)
guild = self._guild_cache[twitter.guild]
if twitter.channel not in self._channel_cache:
channels = await guild.fetch_channels()
self._channel_cache[twitter.channel] = find(lambda x: x.id == twitter.channel, channels)
channel = self._channel_cache[twitter.channel]
for tweet in tweets:
retweet = "retweeted_status" in tweet.__dict__
if retweet and not twitter.retweets:
continue
timestamp = int(tweet.created_at.timestamp())
url = f"https://twitter.com/{twitter.handle}/status/{tweet.id}"
verb = "re" if retweet else ""
await channel.send(f"`@{twitter.handle}` {verb}tweeted this at <t:{timestamp}:f>: {url}")
newest = max(tweets, key=lambda x: x.id)
twitter.last_tweet = newest.id
twitter.save()
except Exception as e:
logger.error(f"Error with tweets: {e}")
@cog_ext.cog_subcommand(
base="twitter",
base_description="Twitter commands",
name="follow",
description="Follow a Twitter account",
options=[
create_option(name="handle", description="Twitter account", option_type=COptionType.STRING, required=True),
create_option(
name="channel",
description="Channel to post tweets into",
option_type=COptionType.CHANNEL,
required=True,
),
create_option(
name="retweets",
description="Mirror re-tweets?",
option_type=COptionType.STRING,
required=False,
choices=[create_choice(name="Yes", value="Yes"), create_choice(name="No", value="No")],
),
@slash_command(name="twitter", sub_cmd_name="follow", description="Follow a Twitter acount")
@slash_option(
name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="channel",
description="Channel to post tweets to",
opt_type=OptionTypes.CHANNEL,
required=True,
)
@slash_option(
name="retweets",
description="Mirror re-tweets?",
opt_type=OptionTypes.STRING,
required=False,
choices=[
SlashCommandChoice(name="Yes", value="Yes"),
SlashCommandChoice(name="No", value="No"),
],
)
@admin_or_permissions(manage_guild=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_follow(
self, ctx: SlashContext, handle: str, channel: TextChannel, retweets: str = "Yes"
self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: str = "Yes"
) -> None:
handle = handle.lower()
retweets = retweets == "Yes"
if len(handle) > 15:
await ctx.send("Invalid Twitter handle", hidden=True)
await ctx.send("Invalid Twitter handle", ephemeral=True)
return
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a text channel", hidden=True)
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a text channel", ephemeral=True)
return
try:
latest_tweet = self.api.user_timeline(screen_name=handle, count=1)[0]
account = (await asyncio.to_thread(self.api.get_user(screen_name=handle)))[0]
latest_tweet = (await asyncio.to_thread(self.api.user_timeline, screen_name=handle))[0]
except Exception:
await ctx.send("Unable to get user timeline. Are you sure the handle is correct?", hidden=True)
await ctx.send(
"Unable to get user timeline. Are you sure the handle is correct?", ephemeral=True
)
return
count = Twitter.objects(guild=ctx.guild.id).count()
if count >= 12:
await ctx.send("Cannot follow more than 12 Twitter accounts", hidden=True)
await ctx.send("Cannot follow more than 12 Twitter accounts", ephemeral=True)
return
exists = Twitter.objects(handle=handle, guild=ctx.guild.id)
exists = Twitter.objects(twitter_id=account.id, guild=ctx.guild.id)
if exists:
await ctx.send("Twitter handle already being followed in this guild", hidden=True)
await ctx.send("Twitter account already being followed in this guild", ephemeral=True)
return
t = Twitter(
handle=handle,
handle=account.screen_name,
twitter_id=account.id,
guild=ctx.guild.id,
channel=channel.id,
admin=ctx.author.id,
@ -137,27 +99,25 @@ class TwitterCog(commands.Cog):
await ctx.send(f"Now following `@{handle}` in {channel.mention}")
@cog_ext.cog_subcommand(
base="twitter",
name="unfollow",
description="Unfollow Twitter accounts",
)
@admin_or_permissions(manage_guild=True)
async def _twitter_unfollow(self, ctx: SlashContext) -> None:
@slash_command(name="twitter", sub_cmd_name="unfollow", description="Unfollow Twitter accounts")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
twitters = Twitter.objects(guild=ctx.guild.id)
if not twitters:
await ctx.send("You need to follow a Twitter account first", hidden=True)
await ctx.send("You need to follow a Twitter account first", ephemeral=True)
return
options = []
handlemap = {str(x.id): x.handle for x in twitters}
for twitter in twitters:
option = create_select_option(label=twitter.handle, value=str(twitter.id))
option = SelectOption(label=twitter.handle, value=str(twitter.id))
options.append(option)
select = create_select(options=options, custom_id="to_delete", min_values=1, max_values=len(twitters))
select = Select(
options=options, custom_id="to_delete", min_values=1, max_values=len(twitters)
)
components = [create_actionrow(select)]
components = [ActionRow(select)]
block = "\n".join(x.handle for x in twitters)
message = await ctx.send(
content=f"You are following the following accounts:\n```\n{block}\n```\n\n"
@ -166,52 +126,58 @@ class TwitterCog(commands.Cog):
)
try:
context = await wait_for_component(
self.bot, check=lambda x: ctx.author.id == x.author.id, messages=message, timeout=60 * 5
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.context.author.id,
messages=message,
timeout=60 * 5,
)
for to_delete in context.selected_options:
for to_delete in context.context.values:
_ = Twitter.objects(guild=ctx.guild.id, id=ObjectId(to_delete)).delete()
for row in components:
for component in row["components"]:
component["disabled"] = True
block = "\n".join(handlemap[x] for x in context.selected_options)
await context.edit_origin(content=f"Unfollowed the following:\n```\n{block}\n```", components=components)
for component in row.components:
component.disabled = True
block = "\n".join(handlemap[x] for x in context.context.values)
await context.context.edit_origin(
content=f"Unfollowed the following:\n```\n{block}\n```", components=components
)
except asyncio.TimeoutError:
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
await message.edit(components=components)
@cog_ext.cog_subcommand(
base="twitter",
@slash_command(
name="twitter", sub_cmd_name="retweets", description="Modify followed Twitter accounts"
)
@slash_option(
name="retweets",
description="Modify followed Twitter accounts",
options=[
create_option(
name="retweets",
description="Mirror re-tweets?",
option_type=COptionType.STRING,
required=True,
choices=[create_choice(name="Yes", value="Yes"), create_choice(name="No", value="No")],
),
description="Mirror re-tweets?",
opt_type=OptionTypes.STRING,
required=False,
choices=[
SlashCommandChoice(name="Yes", value="Yes"),
SlashCommandChoice(name="No", value="No"),
],
)
@admin_or_permissions(manage_guild=True)
async def _twitter_modify(self, ctx: SlashContext, retweets: str) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_modify(self, ctx: InteractionContext, retweets: str) -> None:
retweets = retweets == "Yes"
twitters = Twitter.objects(guild=ctx.guild.id)
if not twitters:
await ctx.send("You need to follow a Twitter account first", hidden=True)
await ctx.send("You need to follow a Twitter account first", ephemeral=True)
return
options = []
for twitter in twitters:
option = create_select_option(label=twitter.handle, value=str(twitter.id))
option = SelectOption(label=twitter.handle, value=str(twitter.id))
options.append(option)
select = create_select(options=options, custom_id="to_update", min_values=1, max_values=len(twitters))
select = Select(
options=options, custom_id="to_update", min_values=1, max_values=len(twitters)
)
components = [create_actionrow(select)]
components = [ActionRow(select)]
block = "\n".join(x.handle for x in twitters)
message = await ctx.send(
content=f"You are following the following accounts:\n```\n{block}\n```\n\n"
@ -220,30 +186,38 @@ class TwitterCog(commands.Cog):
)
try:
context = await wait_for_component(
self.bot, check=lambda x: ctx.author.id == x.author.id, messages=message, timeout=60 * 5
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.author.id,
messages=message,
timeout=60 * 5,
)
handlemap = {str(x.id): x.handle for x in twitters}
for to_update in context.selected_options:
for to_update in context.context.values:
t = Twitter.objects(guild=ctx.guild.id, id=ObjectId(to_update)).first()
t.retweets = retweets
t.save()
for row in components:
for component in row["components"]:
component["disabled"] = True
block = "\n".join(handlemap[x] for x in context.selected_options)
await context.edit_origin(
content=f"{'Unfollowed' if not retweets else 'Followed'} retweets from the following:"
f"\n```\n{block}\n```",
for component in row.components:
component.disabled = True
block = "\n".join(handlemap[x] for x in context.context.values)
await context.context.edit_origin(
content=(
f"{'Unfollowed' if not retweets else 'Followed'} "
"retweets from the following:"
f"\n```\n{block}\n```"
),
components=components,
)
except asyncio.TimeoutError:
for row in components:
for component in row["components"]:
component["disabled"] = True
for component in row.components:
component.disabled = True
await message.edit(components=components)
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add TwitterCog to J.A.R.V.I.S."""
bot.add_cog(TwitterCog(bot))
TwitterCog(bot)

View file

@ -1,165 +1,148 @@
"""J.A.R.V.I.S. Utility Cog."""
import platform
import re
import secrets
import string
from io import BytesIO
import discord
import discord_slash
import numpy as np
from discord import File, Guild, Role, User
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_choice, create_option
from dis_snek import InteractionContext, Scale, Snake, const
from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.file import File
from dis_snek.models.discord.guild import Guild
from dis_snek.models.discord.role import Role
from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from PIL import Image
import jarvis
from jarvis import jarvis_self
from jarvis.config import get_config
from jarvis.data import pigpen
from jarvis.data.robotcamo import emotes, hk, names
from jarvis.utils import build_embed, convert_bytesize, get_repo_hash
from jarvis.utils.field import Field
from jarvis.utils import build_embed, get_repo_hash
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
class UtilCog(commands.Cog):
class UtilCog(Scale):
"""
Utility functions for J.A.R.V.I.S.
Mostly system utility functions, but may change over time
"""
def __init__(self, bot: commands.Cog):
def __init__(self, bot: Snake):
self.bot = bot
self.config = get_config()
@cog_ext.cog_slash(
name="status",
description="Retrieve J.A.R.V.I.S. status",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _status(self, ctx: SlashContext) -> None:
@slash_command(name="status", description="Retrieve J.A.R.V.I.S. status")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _status(self, ctx: InteractionContext) -> None:
title = "J.A.R.V.I.S. Status"
desc = "All systems online"
color = "#98CCDA"
color = "#3498db"
fields = []
with jarvis_self.oneshot():
fields.append(Field("CPU Usage", jarvis_self.cpu_percent()))
fields.append(
Field(
"RAM Usage",
convert_bytesize(jarvis_self.memory_info().rss),
)
)
fields.append(Field("PID", jarvis_self.pid))
fields.append(Field("discord_slash", discord_slash.__version__))
fields.append(Field("discord.py", discord.__version__))
fields.append(Field("Version", jarvis.__version__, False))
fields.append(Field("Git Hash", get_repo_hash()[:7], False))
embed = build_embed(title=title, description=desc, fields=fields, color=color)
await ctx.send(embed=embed)
@cog_ext.cog_slash(
fields.append(EmbedField(name="dis-snek", value=const.__version__))
fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False))
fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False))
embed = build_embed(title=title, description=desc, fields=fields, color=color)
await ctx.send(embed=embed)
@slash_command(
name="logo",
description="Get the current logo",
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _logo(self, ctx: SlashContext) -> None:
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _logo(self, ctx: InteractionContext) -> None:
with BytesIO() as image_bytes:
JARVIS_LOGO.save(image_bytes, "PNG")
image_bytes.seek(0)
logo = File(image_bytes, filename="logo.png")
logo = File(image_bytes, file_name="logo.png")
await ctx.send(file=logo)
@cog_ext.cog_slash(name="rchk", description="Robot Camo HK416")
async def _rchk(self, ctx: SlashContext) -> None:
@slash_command(name="rchk", description="Robot Camo HK416")
async def _rchk(self, ctx: InteractionContext) -> None:
await ctx.send(content=hk)
@cog_ext.cog_slash(
@slash_command(
name="rcauto",
description="Automates robot camo letters",
options=[
create_option(
name="text",
description="Text to camo-ify",
option_type=3,
required=True,
)
],
)
async def _rcauto(self, ctx: SlashContext, text: str) -> None:
@slash_option(
name="text",
description="Text to camo-ify",
opt_type=OptionTypes.STRING,
required=True,
)
async def _rcauto(self, ctx: InteractionContext, text: str) -> None:
to_send = ""
if len(text) == 1 and not re.match(r"^[A-Z0-9-()$@!?^'#. ]$", text.upper()):
await ctx.send("Please use ASCII characters.", hidden=True)
await ctx.send("Please use ASCII characters.", ephemeral=True)
return
for letter in text.upper():
if letter == " ":
to_send += " "
elif re.match(r"^[A-Z0-9-()$@!?^'#.]$", letter):
id = emotes[letter]
if ctx.author.is_on_mobile():
to_send += f":{names[id]}:"
else:
to_send += f"<:{names[id]}:{id}>"
to_send += f":{names[id]}:"
if len(to_send) > 2000:
await ctx.send("Too long.", hidden=True)
await ctx.send("Too long.", ephemeral=True)
else:
await ctx.send(to_send)
@cog_ext.cog_slash(
name="avatar",
description="Get a user avatar",
options=[
create_option(
name="user",
description="User to view avatar of",
option_type=6,
required=False,
)
],
@slash_command(name="avatar", description="Get a user avatar")
@slash_option(
name="user",
description="User to view avatar of",
opt_type=OptionTypes.USER,
required=False,
)
@commands.cooldown(1, 5, commands.BucketType.user)
async def _avatar(self, ctx: SlashContext, user: User = None) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=5)
async def _avatar(self, ctx: InteractionContext, user: User = None) -> None:
if not user:
user = ctx.author
avatar = user.avatar_url
avatar = user.display_avatar.url
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
embed.set_image(url=avatar)
embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=avatar)
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar)
await ctx.send(embed=embed)
@cog_ext.cog_slash(
@slash_command(
name="roleinfo",
description="Get role info",
options=[
create_option(
name="role",
description="Role to get info of",
option_type=8,
required=True,
)
],
)
async def _roleinfo(self, ctx: SlashContext, role: Role) -> None:
@slash_option(
name="role",
description="Role to get info of",
opt_type=OptionTypes.ROLE,
required=True,
)
async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None:
fields = [
Field(name="ID", value=role.id),
Field(name="Name", value=role.name),
Field(name="Color", value=str(role.color)),
Field(name="Mention", value=f"`{role.mention}`"),
Field(name="Hoisted", value="Yes" if role.hoist else "No"),
Field(name="Position", value=str(role.position)),
Field(name="Mentionable", value="Yes" if role.mentionable else "No"),
EmbedField(name="ID", value=str(role.id), inline=True),
EmbedField(name="Name", value=role.name, inline=True),
EmbedField(name="Color", value=str(role.color.hex), inline=True),
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
EmbedField(name="Position", value=str(role.position), inline=True),
EmbedField(name="Mentionable", value="Yes" if role.mentionable else "No", inline=True),
EmbedField(name="Member Count", value=str(len(role.members)), inline=True),
]
embed = build_embed(
title="",
description="",
fields=fields,
color=str(role.color),
color=role.color,
timestamp=role.created_at,
)
embed.set_footer(text="Role Created")
@ -170,46 +153,47 @@ class UtilCog(commands.Cog):
fill = a > 0
data[..., :-1][fill.T] = list(role.color.to_rgb())
data[..., :-1][fill.T] = list(role.color.rgb)
im = Image.fromarray(data)
with BytesIO() as image_bytes:
im.save(image_bytes, "PNG")
image_bytes.seek(0)
color_show = File(image_bytes, filename="color_show.png")
color_show = File(image_bytes, file_name="color_show.png")
await ctx.send(embed=embed, file=color_show)
@cog_ext.cog_slash(
@slash_command(
name="userinfo",
description="Get user info",
options=[
create_option(
name="user",
description="User to get info of",
option_type=6,
required=False,
)
],
)
async def _userinfo(self, ctx: SlashContext, user: User = None) -> None:
@slash_option(
name="user",
description="User to get info of",
opt_type=OptionTypes.USER,
required=False,
)
async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
if not user:
user = ctx.author
user_roles = user.roles
if user_roles:
user_roles = sorted(user.roles, key=lambda x: -x.position)
_ = user_roles.pop(-1)
format_string = "%a, %b %-d, %Y %-I:%M %p"
if platform.system() == "Windows":
format_string = "%a, %b %#d, %Y %#I:%M %p"
fields = [
Field(
EmbedField(
name="Joined",
value=user.joined_at.strftime("%a, %b %-d, %Y %-I:%M %p"),
value=user.joined_at.strftime(format_string),
),
Field(
EmbedField(
name="Registered",
value=user.created_at.strftime("%a, %b %-d, %Y %-I:%M %p"),
value=user.created_at.strftime(format_string),
),
Field(
EmbedField(
name=f"Roles [{len(user_roles)}]",
value=" ".join([x.mention for x in user_roles]) if user_roles else "None",
inline=False,
@ -220,80 +204,82 @@ class UtilCog(commands.Cog):
title="",
description=user.mention,
fields=fields,
color=str(user_roles[0].color) if user_roles else "#FF0000",
color=str(user_roles[0].color) if user_roles else "#3498db",
)
embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=user.avatar_url)
embed.set_thumbnail(url=user.avatar_url)
embed.set_author(
name=f"{user.display_name}#{user.discriminator}", icon_url=user.display_avatar.url
)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"ID: {user.id}")
await ctx.send(embed=embed)
@cog_ext.cog_slash(name="serverinfo", description="Get server info")
async def _server_info(self, ctx: SlashContext) -> None:
@slash_command(name="serverinfo", description="Get server info")
async def _server_info(self, ctx: InteractionContext) -> None:
guild: Guild = ctx.guild
owner = f"{guild.owner.name}#{guild.owner.discriminator}" if guild.owner else "||`[redacted]`||"
owner = await guild.get_owner()
region = guild.region
categories = len(guild.categories)
text_channels = len(guild.text_channels)
voice_channels = len(guild.voice_channels)
owner = f"{owner.username}#{owner.discriminator}" if owner else "||`[redacted]`||"
categories = len([x for x in guild.channels if isinstance(x, GuildCategory)])
text_channels = len([x for x in guild.channels if isinstance(x, GuildText)])
voice_channels = len([x for x in guild.channels if isinstance(x, GuildVoice)])
threads = len(guild.threads)
members = guild.member_count
roles = len(guild.roles)
role_list = ", ".join(role.name for role in guild.roles)
role_list = sorted(guild.roles, key=lambda x: x.position, reverse=True)
role_list = ", ".join(role.mention for role in role_list)
fields = [
Field(name="Owner", value=owner),
Field(name="Region", value=region),
Field(name="Channel Categories", value=categories),
Field(name="Text Channels", value=text_channels),
Field(name="Voice Channels", value=voice_channels),
Field(name="Members", value=members),
Field(name="Roles", value=roles),
EmbedField(name="Owner", value=owner, inline=True),
EmbedField(name="Channel Categories", value=str(categories), inline=True),
EmbedField(name="Text Channels", value=str(text_channels), inline=True),
EmbedField(name="Voice Channels", value=str(voice_channels), inline=True),
EmbedField(name="Threads", value=str(threads), inline=True),
EmbedField(name="Members", value=str(members), inline=True),
EmbedField(name="Roles", value=str(roles), inline=True),
]
if len(role_list) < 1024:
fields.append(Field(name="Role List", value=role_list, inline=False))
fields.append(EmbedField(name="Role List", value=role_list, inline=False))
embed = build_embed(title="", description="", fields=fields, timestamp=guild.created_at)
embed.set_author(name=guild.name, icon_url=guild.icon_url)
embed.set_thumbnail(url=guild.icon_url)
embed.set_author(name=guild.name, icon_url=guild.icon.url)
embed.set_thumbnail(url=guild.icon.url)
embed.set_footer(text=f"ID: {guild.id} | Server Created")
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(
base="pw",
name="gen",
base_desc="Password utilites",
@slash_command(
name="pw",
sub_cmd_name="gen",
description="Generate a secure password",
guild_ids=[862402786116763668],
options=[
create_option(
name="length",
description="Password length (default 32)",
option_type=4,
required=False,
),
create_option(
name="chars",
description="Characters to include (default last option)",
option_type=4,
required=False,
choices=[
create_choice(name="A-Za-z", value=0),
create_choice(name="A-Fa-f0-9", value=1),
create_choice(name="A-Za-z0-9", value=2),
create_choice(name="A-Za-z0-9!@#$%^&*", value=3),
],
),
scopes=[862402786116763668],
)
@slash_option(
name="length",
description="Password length (default 32)",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="chars",
description="Characters to include (default last option)",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[
SlashCommandChoice(name="A-Za-z", value=0),
SlashCommandChoice(name="A-Fa-f0-9", value=1),
SlashCommandChoice(name="A-Za-z0-9", value=2),
SlashCommandChoice(name="A-Za-z0-9!@#$%^&*", value=3),
],
)
@commands.cooldown(1, 15, type=commands.BucketType.user)
async def _pw_gen(self, ctx: SlashContext, length: int = 32, chars: int = 3) -> None:
@cooldown(bucket=Buckets.USER, rate=1, interval=15)
async def _pw_gen(self, ctx: InteractionContext, length: int = 32, chars: int = 3) -> None:
if length > 256:
await ctx.send("Please limit password to 256 characters", hidden=True)
await ctx.send("Please limit password to 256 characters", ephemeral=True)
return
choices = [
string.ascii_letters,
@ -307,15 +293,14 @@ class UtilCog(commands.Cog):
f"Generated password:\n`{pw}`\n\n"
'**WARNING: Once you press "Dismiss Message", '
"*the password is lost forever***",
hidden=True,
ephemeral=True,
)
@cog_ext.cog_slash(
name="pigpen",
description="Encode a string into pigpen",
options=[create_option(name="text", description="Text to encode", option_type=3, required=True)],
@slash_command(name="pigpen", description="Encode a string into pigpen")
@slash_option(
name="text", description="Text to encode", opt_type=OptionTypes.STRING, required=True
)
async def _pigpen(self, ctx: SlashContext, text: str) -> None:
async def _pigpen(self, ctx: InteractionContext, text: str) -> None:
outp = "`"
for c in text:
c = c.lower()
@ -330,6 +315,6 @@ class UtilCog(commands.Cog):
await ctx.send(outp[:2000])
def setup(bot: commands.Bot) -> None:
def setup(bot: Snake) -> None:
"""Add UtilCog to J.A.R.V.I.S."""
bot.add_cog(UtilCog(bot))
UtilCog(bot)

View file

@ -1,10 +1,12 @@
"""J.A.R.V.I.S. Verify Cog."""
import asyncio
from random import randint
from discord.ext import commands
from discord_slash import ComponentContext, SlashContext, cog_ext
from discord_slash.model import ButtonStyle
from discord_slash.utils import manage_components
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.application_commands import slash_command
from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Setting
@ -16,36 +18,32 @@ def create_layout() -> list:
for i in range(3):
label = "YES" if i == yes else "NO"
id = f"no_{i}" if not i == yes else "yes"
color = ButtonStyle.green if i == yes else ButtonStyle.red
color = ButtonStyles.GREEN if i == yes else ButtonStyles.RED
buttons.append(
manage_components.create_button(
Button(
style=color,
label=label,
custom_id=f"verify_button||{id}",
)
)
action_row = manage_components.spread_to_rows(*buttons, max_in_row=3)
return action_row
return spread_to_rows(*buttons, max_in_row=3)
class VerifyCog(commands.Cog):
class VerifyCog(Scale):
"""J.A.R.V.I.S. Verify Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
@cog_ext.cog_slash(
name="verify",
description="Verify that you've read the rules",
)
@commands.cooldown(1, 15, commands.BucketType.user)
async def _verify(self, ctx: SlashContext) -> None:
@slash_command(name="verify", description="Verify that you've read the rules")
@cooldown(bucket=Buckets.USER, rate=1, interval=15)
async def _verify(self, ctx: InteractionContext) -> None:
await ctx.defer()
role = Setting.objects(guild=ctx.guild.id, setting="verified").first()
if not role:
await ctx.send("This guild has not enabled verification", delete_after=5)
return
if ctx.guild.get_role(role.value) in ctx.author.roles:
if await ctx.guild.get_role(role.value) in ctx.author.roles:
await ctx.send("You are already verified.", delete_after=5)
return
components = create_layout()
@ -53,40 +51,41 @@ class VerifyCog(commands.Cog):
content=f"{ctx.author.mention}, please press the button that says `YES`.",
components=components,
)
await message.delete(delay=15)
@cog_ext.cog_component(components=create_layout())
async def _process(self, ctx: ComponentContext) -> None:
await ctx.defer(edit_origin=True)
try:
if ctx.author.id != ctx.origin_message.mentions[0].id:
return
except Exception:
return
correct = ctx.custom_id.split("||")[-1] == "yes"
if correct:
components = ctx.origin_message.components
for c in components:
for c2 in c["components"]:
c2["disabled"] = True
setting = Setting.objects(guild=ctx.guild.id, setting="verified").first()
role = ctx.guild.get_role(setting.value)
await ctx.author.add_roles(role, reason="Verification passed")
setting = Setting.objects(guild=ctx.guild.id, setting="unverified").first()
if setting:
role = ctx.guild.get_role(setting.value)
await ctx.author.remove_roles(role, reason="Verification passed")
await ctx.edit_origin(
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
components=manage_components.spread_to_rows(*components, max_in_row=5),
)
await ctx.origin_message.delete(delay=5)
else:
await ctx.edit_origin(
content=f"{ctx.author.mention}, incorrect. Please press the button that says `YES`",
context = await self.bot.wait_for_component(
messages=message, check=lambda x: ctx.author.id == x.author.id, timeout=30
)
correct = context.context.custom_id.split("||")[-1] == "yes"
if correct:
for row in components:
for component in row.components:
component.disabled = True
setting = Setting.objects(guild=ctx.guild.id, setting="verified").first()
role = await ctx.guild.get_role(setting.value)
await ctx.author.add_roles(role, reason="Verification passed")
setting = Setting.objects(guild=ctx.guild.id, setting="unverified").first()
if setting:
role = await ctx.guild.get_role(setting.value)
await ctx.author.remove_roles(role, reason="Verification passed")
def setup(bot: commands.Bot) -> None:
await context.context.edit_origin(
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
components=components,
)
await context.context.message.delete(delay=5)
else:
await context.context.edit_origin(
content=(
f"{ctx.author.mention}, incorrect. "
"Please press the button that says `YES`"
)
)
except asyncio.TimeoutError:
await message.delete(delay=30)
def setup(bot: Snake) -> None:
"""Add VerifyCog to J.A.R.V.I.S."""
bot.add_cog(VerifyCog(bot))
VerifyCog(bot)

View file

@ -1,4 +1,6 @@
"""Load the config for J.A.R.V.I.S."""
import os
from pymongo import MongoClient
from yaml import load
@ -27,6 +29,7 @@ class Config(object):
logo: str,
mongo: dict,
urls: dict,
sync: bool = False,
log_level: str = "WARNING",
cogs: list = None,
events: bool = True,
@ -46,6 +49,7 @@ class Config(object):
self.max_messages = max_messages
self.gitlab_token = gitlab_token
self.twitter = twitter
self.sync = sync or os.environ.get("SYNC_COMMANDS", False)
self.__db_loaded = False
self.__mongo = MongoClient(**self.mongo["connect"])
@ -61,8 +65,7 @@ class Config(object):
@classmethod
def from_yaml(cls, y: dict) -> "Config":
"""Load the yaml config file."""
instance = cls(**y)
return instance
return cls(**y)
def get_config(path: str = "config.yaml") -> Config:

View file

@ -222,6 +222,7 @@ class Twitter(Document):
"""Twitter Follow object."""
active = BooleanField(default=True)
twitter_id = IntField(required=True)
handle = StringField(required=True)
channel = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
@ -229,6 +230,7 @@ class Twitter(Document):
retweets = BooleanField(default=True)
admin = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
last_sync = DateTimeField()
meta = {"db_alias": "main"}

View file

@ -1,36 +0,0 @@
"""J.A.R.V.I.S. guild event handler."""
import asyncio
from discord import Guild
from discord.ext.commands import Bot
from discord.utils import find
from jarvis.db.models import Setting
class GuildEventHandler(object):
"""J.A.R.V.I.S. guild event handler."""
def __init__(self, bot: Bot):
self.bot = bot
self.bot.add_listener(self.on_guild_join)
async def on_guild_join(self, guild: Guild) -> None:
"""Handle on_guild_join event."""
general = find(lambda x: x.name == "general", guild.channels)
if general and general.permissions_for(guild.me).send_messages:
user = self.bot.user
await general.send(
f"Allow me to introduce myself. I am {user.mention}, a virtual "
"artificial intelligence, and I'm here to assist you with a "
"variety of tasks as best I can, "
"24 hours a day, seven days a week."
)
await asyncio.sleep(1)
await general.send("Importing all preferences from home interface...")
# Set some default settings
_ = Setting(guild=guild.id, setting="massmention", value=5).save()
_ = Setting(guild=guild.id, setting="noinvite", value=True).save()
await general.send("Systems are now fully operational")

View file

@ -1,6 +1,6 @@
"""J.A.R.V.I.S. Member event handler."""
from discord import Member
from discord.ext.commands import Bot
from dis_snek import Snake, listen
from dis_snek.models.discord.user import Member
from jarvis.db.models import Mute, Setting
@ -8,10 +8,11 @@ from jarvis.db.models import Mute, Setting
class MemberEventHandler(object):
"""J.A.R.V.I.S. Member event handler."""
def __init__(self, bot: Bot):
def __init__(self, bot: Snake):
self.bot = bot
self.bot.add_listener(self.on_member_join)
@listen()
async def on_member_join(self, user: Member) -> None:
"""Handle on_member_join event."""
guild = user.guild

View file

@ -1,17 +1,17 @@
"""J.A.R.V.I.S. Message event handler."""
import re
from discord import DMChannel, Message
from discord.ext.commands import Bot
from discord.utils import find
from dis_snek import Snake, listen
from dis_snek.models.discord.channel import DMChannel
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.message import Message
from jarvis.config import get_config
from jarvis.db.models import Autopurge, Autoreact, Roleping, Setting, Warning
from jarvis.utils import build_embed
from jarvis.utils.field import Field
from jarvis.utils import build_embed, find
invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)",
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
flags=re.IGNORECASE,
)
@ -19,7 +19,7 @@ invites = re.compile(
class MessageEventHandler(object):
"""J.A.R.V.I.S. Message event handler."""
def __init__(self, bot: Bot):
def __init__(self, bot: Snake):
self.bot = bot
self.bot.add_listener(self.on_message)
self.bot.add_listener(self.on_message_edit)
@ -46,7 +46,7 @@ class MessageEventHandler(object):
channel = find(lambda x: x.id == 599068193339736096, message.channel_mentions)
if channel and message.author.id == 293795462752894976:
await channel.send(
content="https://cdn.discordapp.com/attachments/664621130044407838/805218508866453554/tech.gif"
content="https://cdn.discordapp.com/attachments/664621130044407838/805218508866453554/tech.gif" # noqa: E501
)
content = re.sub(r"\s+", "", message.content)
match = invites.search(content)
@ -72,10 +72,10 @@ class MessageEventHandler(object):
user=message.author.id,
).save()
fields = [
Field(
"Reason",
"Sent an invite link",
False,
EmbedField(
name="Reason",
value="Sent an invite link",
inline=False,
)
]
embed = build_embed(
@ -85,9 +85,11 @@ class MessageEventHandler(object):
)
embed.set_author(
name=message.author.nick if message.author.nick else message.author.name,
icon_url=message.author.avatar_url,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}" # noqa: E501
)
embed.set_footer(text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}")
await message.channel.send(embed=embed)
async def massmention(self, message: Message) -> None:
@ -99,7 +101,8 @@ class MessageEventHandler(object):
if (
massmention
and massmention.value > 0 # noqa: W503
and len(message.mentions) - (1 if message.author in message.mentions else 0) # noqa: W503
and len(message.mentions) # noqa: W503
- (1 if message.author in message.mentions else 0) # noqa: W503
> massmention.value # noqa: W503
):
_ = Warning(
@ -110,7 +113,7 @@ class MessageEventHandler(object):
reason="Mass Mention",
user=message.author.id,
).save()
fields = [Field("Reason", "Mass Mention", False)]
fields = [EmbedField(name="Reason", value="Mass Mention", inline=False)]
embed = build_embed(
title="Warning",
description=f"{message.author.mention} has been warned",
@ -118,9 +121,11 @@ class MessageEventHandler(object):
)
embed.set_author(
name=message.author.nick if message.author.nick else message.author.name,
icon_url=message.author.avatar_url,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}"
)
embed.set_footer(text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}")
await message.channel.send(embed=embed)
async def roleping(self, message: Message) -> None:
@ -173,10 +178,10 @@ class MessageEventHandler(object):
user=message.author.id,
).save()
fields = [
Field(
"Reason",
"Pinged a blocked role/user with a blocked role",
False,
EmbedField(
name="Reason",
value="Pinged a blocked role/user with a blocked role",
inline=False,
)
]
embed = build_embed(
@ -186,11 +191,14 @@ class MessageEventHandler(object):
)
embed.set_author(
name=message.author.nick if message.author.nick else message.author.name,
icon_url=message.author.avatar_url,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}"
)
embed.set_footer(text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}")
await message.channel.send(embed=embed)
@listen()
async def on_message(self, message: Message) -> None:
"""Handle on_message event. Calls other event handlers."""
if not isinstance(message.channel, DMChannel) and not message.author.bot:
@ -200,6 +208,7 @@ class MessageEventHandler(object):
await self.autopurge(message)
await self.checks(message)
@listen()
async def on_message_edit(self, before: Message, after: Message) -> None:
"""Handle on_message_edit event. Calls other event handlers."""
if not isinstance(after.channel, DMChannel) and not after.author.bot:
@ -208,3 +217,10 @@ class MessageEventHandler(object):
await self.checks(after)
await self.roleping(after)
await self.checks(after)
"""Handle on_message_edit event. Calls other event handlers."""
if not isinstance(after.channel, DMChannel) and not after.author.bot:
await self.massmention(after)
await self.roleping(after)
await self.checks(after)
await self.roleping(after)
await self.checks(after)

View file

@ -1,10 +1,10 @@
"""J.A.R.V.I.S. background task handlers."""
from jarvis.tasks import unban, unlock, unmute, unwarn
from jarvis.tasks import twitter, unban, unlock, unwarn
def init() -> None:
"""Start the background task handlers."""
unban.unban.start()
unlock.unlock.start()
unmute.unmute.start()
unwarn.unwarn.start()
twitter.tweets.start()

45
jarvis/tasks/reminder.py Normal file
View file

@ -0,0 +1,45 @@
"""J.A.R.V.I.S. reminder background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.db.models import Reminder
from jarvis.utils import build_embed
async def _remind() -> None:
"""J.A.R.V.I.S. reminder blocking task."""
reminders = Reminder.objects(remind_at__lte=datetime.utcnow() + timedelta(seconds=30))
for reminder in reminders:
if reminder.remind_at <= datetime.utcnow():
user = await jarvis.jarvis.fetch_user(reminder.user)
if not user:
reminder.delete()
continue
embed = build_embed(
title="You have a reminder",
description=reminder.message,
fields=[],
)
embed.set_author(
name=user.name + "#" + user.discriminator, icon_url=user.display_avatar.url
)
embed.set_thumbnail(url=user.display_avatar.url)
try:
await user.send(embed=embed)
except Exception:
guild = jarvis.jarvis.fetch_guild(reminder.guild)
channel = guild.get_channel(reminder.channel) if guild else None
if channel:
await channel.send(f"{user.mention}", embed=embed)
finally:
reminder.delete()
@Task.create(trigger=IntervalTrigger(seconds=15))
async def remind() -> None:
"""J.A.R.V.I.S. reminder background task."""
await to_thread(_remind)

70
jarvis/tasks/twitter.py Normal file
View file

@ -0,0 +1,70 @@
"""J.A.R.V.I.S. twitter background task handler."""
import logging
from asyncio import to_thread
from datetime import datetime, timedelta
import tweepy
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.config import get_config
from jarvis.db.models import Twitter
logger = logging.getLogger("jarvis")
__auth = tweepy.AppAuthHandler(
get_config().twitter["consumer_key"], get_config().twitter["consumer_secret"]
)
__api = tweepy.API(__auth)
async def _tweets() -> None:
"""J.A.R.V.I.S. twitter blocking task."""
guild_cache = {}
channel_cache = {}
twitters = Twitter.objects(active=True)
for twitter in twitters:
try:
if not twitter.twitter_id or not twitter.last_sync:
user = __api.get_user(screen_name=twitter.handle)
twitter.twitter_id = user.id
twitter.handle = user.screen_name
twitter.last_sync = datetime.now()
if twitter.last_sync + timedelta(hours=1) <= datetime.now():
user = __api.get_user(id=twitter.twitter_id)
twitter.handle = user.screen_name
twitter.last_sync = datetime.now()
if tweets := __api.user_timeline(id=twitter.twitter_id):
guild_id = twitter.guild
channel_id = twitter.channel
tweets = sorted(tweets, key=lambda x: x.id)
if guild_id not in guild_cache:
guild_cache[guild_id] = await jarvis.jarvis.get_guild(guild_id)
guild = guild_cache[twitter.guild]
if channel_id not in channel_cache:
channel_cache[channel_id] = await guild.fetch_channel(channel_id)
channel = channel_cache[channel_id]
for tweet in tweets:
retweet = "retweeted_status" in tweet.__dict__
if retweet and not twitter.retweets:
continue
timestamp = int(tweet.created_at.timestamp())
url = f"https://twitter.com/{twitter.handle}/status/{tweet.id}"
verb = "re" if retweet else ""
await channel.send(
f"`@{twitter.handle}` {verb}tweeted this at <t:{timestamp}:f>: {url}"
)
newest = max(tweets, key=lambda x: x.id)
twitter.last_tweet = newest.id
twitter.save()
except Exception as e:
logger.error(f"Error with tweets: {e}")
@Task.create(trigger=IntervalTrigger(minutes=1))
async def tweets() -> None:
"""J.A.R.V.I.S. twitter background task."""
await to_thread(_tweets)

View file

@ -1,7 +1,9 @@
"""J.A.R.V.I.S. unban background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from discord.ext.tasks import loop
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.config import get_config
@ -10,17 +12,18 @@ from jarvis.db.models import Ban, Unban
jarvis_id = get_config().client_id
@loop(minutes=10)
async def unban() -> None:
"""J.A.R.V.I.S. unban background task."""
async def _unban() -> None:
"""J.A.R.V.I.S. unban blocking task."""
bans = Ban.objects(type="temp", active=True)
unbans = []
for ban in bans:
if ban.created_at + timedelta(hours=ban.duration) < datetime.utcnow() + timedelta(minutes=10):
guild = await jarvis.jarvis.fetch_guild(ban.guild)
user = await jarvis.jarvis.fetch_user(ban.user)
if ban.created_at + timedelta(hours=ban.duration) < datetime.utcnow() + timedelta(
minutes=10
):
guild = await jarvis.jarvis.get_guild(ban.guild)
user = await jarvis.jarvis.get_user(ban.user)
if user:
guild.unban(user)
await guild.unban(user=user, reason="Ban expired")
ban.active = False
ban.save()
unbans.append(
@ -34,4 +37,10 @@ async def unban() -> None:
)
)
if unbans:
Ban.objects().insert(unbans)
Unban.objects().insert(unbans)
@Task.create(IntervalTrigger(minutes=10))
async def unban() -> None:
"""J.A.R.V.I.S. unban background task."""
await to_thread(_unban)

View file

@ -1,25 +1,37 @@
"""J.A.R.V.I.S. unlock background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from discord.ext.tasks import loop
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.db.models import Lock
@loop(minutes=1)
async def _unlock() -> None:
"""J.A.R.V.I.S. unlock blocking task."""
locks = Lock.objects(active=True)
# Block execution for now
# TODO: Reevaluate with admin/lock[down]
if False:
for lock in locks:
if lock.created_at + timedelta(minutes=lock.duration) < datetime.utcnow():
guild = await jarvis.jarvis.get_guild(lock.guild)
channel = await jarvis.jarvis.get_guild(lock.channel)
if channel:
roles = await guild.fetch_roles()
for role in roles:
overrides = channel.overwrites_for(role)
overrides.send_messages = None
await channel.set_permissions(
role, overwrite=overrides, reason="Lock expired"
)
lock.active = False
lock.save()
@Task.create(IntervalTrigger(minutes=1))
async def unlock() -> None:
"""J.A.R.V.I.S. unlock background task."""
locks = Lock.objects(active=True)
for lock in locks:
if lock.created_at + timedelta(minutes=lock.duration) < datetime.utcnow():
guild = await jarvis.jarvis.fetch_guild(lock.guild)
channel = await jarvis.jarvis.fetch_channel(lock.channel)
if channel:
roles = await guild.fetch_roles()
for role in roles:
overrides = channel.overwrites_for(role)
overrides.send_messages = None
await channel.set_permissions(role, overwrite=overrides, reason="Lock expired")
lock.active = False
lock.save()
await to_thread(_unlock)

View file

@ -1,27 +0,0 @@
"""J.A.R.V.I.S. unmute background task handler."""
from datetime import datetime, timedelta
from discord.ext.tasks import loop
import jarvis
from jarvis.db.models import Mute, Setting
@loop(minutes=1)
async def unmute() -> None:
"""J.A.R.V.I.S. unmute background task."""
mutes = Mute.objects(duration__gt=0, active=True)
mute_roles = Setting.objects(setting="mute")
for mute in mutes:
if mute.created_at + timedelta(minutes=mute.duration) < datetime.utcnow():
mute_role = [x.value for x in mute_roles if x.guild == mute.guild][0]
guild = await jarvis.jarvis.fetch_guild(mute.guild)
role = guild.get_role(mute_role)
user = await guild.fetch_member(mute.user)
if user:
if role in user.roles:
await user.remove_roles(role, reason="Mute expired")
# Objects can't handle bulk_write, so handle it via raw methods
mute.active = False
mute.save

View file

@ -1,16 +1,23 @@
"""J.A.R.V.I.S. unwarn background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from discord.ext.tasks import loop
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
from jarvis.db.models import Warning
@loop(hours=1)
async def unwarn() -> None:
"""J.A.R.V.I.S. unwarn background task."""
async def _unwarn() -> None:
"""J.A.R.V.I.S. unwarn blocking task."""
warns = Warning.objects(active=True)
for warn in warns:
if warn.created_at + timedelta(hours=warn.duration) < datetime.utcnow():
warn.active = False
warn.save()
@Task.create(IntervalTrigger(hours=1))
async def unwarn() -> None:
"""J.A.R.V.I.S. unwarn background task."""
await to_thread(_unwarn)

View file

@ -1,10 +1,10 @@
"""J.A.R.V.I.S. Utility Functions."""
from datetime import datetime
from pkgutil import iter_modules
from typing import Any, Callable, Iterable, List, Optional, TypeVar
import git
from discord import Color, Embed, Message
from discord.ext import commands
from dis_snek.models.discord.embed import Embed
import jarvis.cogs
import jarvis.db
@ -12,6 +12,31 @@ from jarvis.config import get_config
__all__ = ["field", "db", "cachecog", "permissions"]
T = TypeVar("T")
def build_embed(
title: str,
description: str,
fields: list,
color: str = "#FF0000",
timestamp: datetime = None,
**kwargs: dict,
) -> Embed:
"""Embed builder utility function."""
if not timestamp:
timestamp = datetime.now()
embed = Embed(
title=title,
description=description,
color=color,
timestamp=timestamp,
**kwargs,
)
for field in fields:
embed.add_field(**field.to_dict())
return embed
def convert_bytesize(b: int) -> str:
"""Convert bytes amount to human readable."""
@ -34,15 +59,6 @@ def unconvert_bytesize(size: int, ending: str) -> int:
return round(size * (1024 ** sizes.index(ending)))
def get_prefix(bot: commands.Bot, message: Message) -> list:
"""Get bot prefixes."""
prefixes = ["!", "-", "%"]
# if not message.guild:
# return "?"
return commands.when_mentioned_or(*prefixes)(bot, message)
def get_extensions(path: str = jarvis.cogs.__path__) -> list:
"""Get J.A.R.V.I.S. cogs."""
config = get_config()
@ -50,36 +66,6 @@ def get_extensions(path: str = jarvis.cogs.__path__) -> list:
return ["jarvis.cogs.{}".format(x) for x in vals]
def parse_color_hex(hex: str) -> Color:
"""Convert a hex color to a d.py Color."""
hex = hex.lstrip("#")
rgb = tuple(int(hex[i : i + 2], 16) for i in (0, 2, 4)) # noqa: E203
return Color.from_rgb(*rgb)
def build_embed(
title: str,
description: str,
fields: list,
color: str = "#FF0000",
timestamp: datetime = None,
**kwargs: dict,
) -> Embed:
"""Embed builder utility function."""
if not timestamp:
timestamp = datetime.utcnow()
embed = Embed(
title=title,
description=description,
color=parse_color_hex(color),
timestamp=timestamp,
**kwargs,
)
for field in fields:
embed.add_field(**field.to_dict())
return embed
def update() -> int:
"""J.A.R.V.I.S. update utility."""
repo = git.Repo(".")
@ -99,3 +85,103 @@ def get_repo_hash() -> str:
"""J.A.R.V.I.S. current branch hash."""
repo = git.Repo(".")
return repo.head.object.hexsha
def find(predicate: Callable, sequence: Iterable) -> Optional[Any]:
"""
Find the first element in a sequence that matches the predicate.
??? Hint "Example Usage:"
```python
member = find(lambda m: m.name == "UserName", guild.members)
```
Args:
predicate: A callable that returns a boolean value
sequence: A sequence to be searched
Returns:
A match if found, otherwise None
"""
for el in sequence:
if predicate(el):
return el
return None
def find_all(predicate: Callable, sequence: Iterable) -> List[Any]:
"""
Find all elements in a sequence that match the predicate.
??? Hint "Example Usage:"
```python
members = find_all(lambda m: m.name == "UserName", guild.members)
```
Args:
predicate: A callable that returns a boolean value
sequence: A sequence to be searched
Returns:
A list of matches
"""
matches = []
for el in sequence:
if predicate(el):
matches.append(el)
return matches
def get(sequence: Iterable, **kwargs: Any) -> Optional[Any]:
"""
Find the first element in a sequence that matches all attrs.
??? Hint "Example Usage:"
```python
channel = get(guild.channels, nsfw=False, category="General")
```
Args:
sequence: A sequence to be searched
kwargs: Keyword arguments to search the sequence for
Returns:
A match if found, otherwise None
"""
if not kwargs:
return sequence[0]
for el in sequence:
if any(not hasattr(el, attr) for attr in kwargs.keys()):
continue
if all(getattr(el, attr) == value for attr, value in kwargs.items()):
return el
return None
def get_all(sequence: Iterable, **kwargs: Any) -> List[Any]:
"""
Find all elements in a sequence that match all attrs.
??? Hint "Example Usage:"
```python
channels = get_all(guild.channels, nsfw=False, category="General")
```
Args:
sequence: A sequence to be searched
kwargs: Keyword arguments to search the sequence for
Returns:
A list of matches
"""
if not kwargs:
return sequence
matches = []
for el in sequence:
if any(not hasattr(el, attr) for attr in kwargs.keys()):
continue
if all(getattr(el, attr) == value for attr, value in kwargs.items()):
matches.append(el)
return matches

View file

@ -1,21 +1,22 @@
"""Cog wrapper for command caching."""
from datetime import datetime, timedelta
from discord.ext import commands
from discord.ext.tasks import loop
from discord.utils import find
from discord_slash import SlashContext
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
from jarvis.utils import find
class CacheCog(commands.Cog):
class CacheCog(Scale):
"""Cog wrapper for command caching."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
self.cache = {}
self._expire_interaction.start()
def check_cache(self, ctx: SlashContext, **kwargs: dict) -> dict:
def check_cache(self, ctx: InteractionContext, **kwargs: dict) -> dict:
"""Check the cache."""
if not kwargs:
kwargs = {}
@ -27,7 +28,7 @@ class CacheCog(commands.Cog):
self.cache.values(),
)
@loop(minutes=1)
@Task.create(IntervalTrigger(minutes=1))
async def _expire_interaction(self) -> None:
keys = list(self.cache.keys())
for key in keys:

View file

@ -1,16 +0,0 @@
"""Embed field helper."""
from dataclasses import dataclass
from typing import Any
@dataclass
class Field:
"""Embed Field."""
name: Any
value: Any
inline: bool = True
def to_dict(self) -> dict:
"""Convert Field to d.py field dict."""
return {"name": self.name, "value": self.value, "inline": self.inline}

View file

@ -1,5 +1,5 @@
"""Permissions wrappers."""
from discord.ext import commands
from dis_snek import InteractionContext, Permissions
from jarvis.config import get_config
@ -7,22 +7,23 @@ from jarvis.config import get_config
def user_is_bot_admin() -> bool:
"""Check if a user is a J.A.R.V.I.S. admin."""
def predicate(ctx: commands.Context) -> bool:
async def predicate(ctx: InteractionContext) -> bool:
"""Command check predicate."""
if getattr(get_config(), "admins", None):
return ctx.author.id in get_config().admins
else:
return False
return commands.check(predicate)
return predicate
def admin_or_permissions(**perms: dict) -> bool:
def admin_or_permissions(*perms: list) -> bool:
"""Check if a user is an admin or has other perms."""
original = commands.has_permissions(**perms).predicate
async def extended_check(ctx: commands.Context) -> bool:
async def predicate(ctx: InteractionContext) -> bool:
"""Extended check predicate.""" # noqa: D401
return await commands.has_permissions(administrator=True).predicate(ctx) or await original(ctx)
is_admin = ctx.author.has_permission(Permissions.ADMINISTRATOR)
has_other = any(ctx.author.has_permission(perm) for perm in perms)
return is_admin or has_other
return commands.check(extended_check)
return predicate

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

1568
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

27
pyproject.toml Normal file
View file

@ -0,0 +1,27 @@
[tool.poetry]
name = "jarvis"
version = "2.0.0a0"
description = "J.A.R.V.I.S. admin bot"
authors = ["Zevaryx <zevaryx@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.10"
PyYAML = "^6.0"
dis-snek = "^5.0.0"
GitPython = "^3.1.26"
mongoengine = "^0.23.1"
opencv-python = "^4.5.5"
Pillow = "^9.0.0"
psutil = "^5.9.0"
python-gitlab = "^3.1.1"
ulid-py = "^1.1.0"
tweepy = "^4.5.0"
orjson = "^3.6.6"
[tool.poetry.dev-dependencies]
python-lsp-server = {extras = ["all"], version = "^1.3.3"}
black = "^22.1.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

118
run.py
View file

@ -1,117 +1,5 @@
#!/bin/python3
# flake8: noqa
from importlib import reload as ireload
from multiprocessing import Process, Value, freeze_support
from pathlib import Path
from time import sleep
import git
import jarvis
from jarvis.config import get_config
def run():
ctx = None
while True:
ireload(jarvis)
ctx = jarvis.run(ctx)
def restart():
global jarvis_process
Path(get_pid_file()).unlink()
jarvis_process.kill()
jarvis_process = Process(target=run, name="jarvis")
jarvis_process.start()
def update():
repo = git.Repo(".")
dirty = repo.is_dirty()
if dirty:
print(" Local system has uncommitted changes.")
current_hash = repo.head.object.hexsha
origin = repo.remotes.origin
origin.fetch()
if current_hash != origin.refs["main"].object.hexsha:
if dirty:
return 2
origin.pull()
return 0
return 1
def get_pid_file():
return f"jarvis.{get_pid()}.pid"
def get_pid():
global jarvis_process
return jarvis_process.pid
def cli():
pfile = Path(get_pid_file())
while not pfile.exists():
sleep(0.2)
print(
"""
All systems online.
Command List:
(R)eload
(U)pdate
(Q)uit
"""
)
while True:
cmd = input("> ")
if cmd.lower() in ["q", "quit", "e", "exit"]:
print(" Shutting down core systems...")
pfile.unlink()
break
if cmd.lower() in ["u", "update"]:
print(" Updating core systems...")
status = update()
if status == 0:
restart()
pfile = Path(get_pid_file())
while not pfile.exists():
sleep(0.2)
print(" Core systems successfully updated.")
elif status == 1:
print(" No core updates available.")
elif status == 2:
print(" Core system update available, but core is dirty.")
if cmd.lower() in ["r", "reload"]:
print(" Reloading core systems...")
restart()
pfile = Path(get_pid_file())
while not pfile.exists():
sleep(0.2)
print(" All systems reloaded.")
"""Main run file for J.A.R.V.I.S."""
from jarvis import run
if __name__ == "__main__":
freeze_support()
config = get_config()
pid_file = Value("i", 0)
jarvis_process = Process(target=run, name="jarvis")
logo = jarvis.logo.get_logo(config.logo)
print(logo)
print("Initializing....")
print(" Updating core systems...")
status = update()
if status == 0:
print(" Core systems successfully updated")
elif status == 1:
print(" No core updates available.")
elif status == 2:
print(" Core updates available, but not applied.")
print(" Starting core systems...")
jarvis_process.start()
cli()
if jarvis_process.is_alive():
jarvis_process.kill()
print("All systems shut down.")
run()