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: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.1.0
hooks: hooks:
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
args: [--unsafe]
- id: check-merge-conflict - id: check-merge-conflict
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: end-of-file-fixer - id: end-of-file-fixer
- id: debug-statements
language_version: python3.10
- id: trailing-whitespace - id: trailing-whitespace
args: [--markdown-linebreak-ext=md] args: [--markdown-linebreak-ext=md]
@ -16,23 +19,31 @@ repos:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.7b0 rev: 22.1.0
hooks: hooks:
- id: black - 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 - repo: https://github.com/pre-commit/mirrors-isort
rev: V5.9.3 rev: V5.10.1
hooks: hooks:
- id: isort - id: isort
args: ["--profile", "black"] args: ["--profile", "black"]
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: 3.9.2 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
- flake8-annotations~=2.0 - flake8-annotations~=2.0
- flake8-bandit~=2.1 - flake8-bandit~=2.1
- flake8-docstrings~=1.5 - 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)]() [![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.) [![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) [![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. 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. **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. Join the [Stark R&D Department Discord server](https://discord.gg/VtgZntXcnZ) to be kept up-to-date on code updates and issues.
## Requirements ## Requirements
- MongoDB 4.4 or higher - MongoDB 5.0 or higher
- Python 3.8 or higher - Python 3.10 or higher
- [tokei](https://github.com/XAMPPRocky/tokei) 12.1 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: 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` - `psutil>=5.8, <6`
- `GitPython>=3.1, <4` - `GitPython>=3.1, <4`
- `PyYaml>=5.4, <6` - `PyYaml>=5.4, <6`
- `discord-py-slash-command>=2.3.2, <3`
- `pymongo>=3.12.0, <4` - `pymongo>=3.12.0, <4`
- `opencv-python>=4.5, <5` - `opencv-python>=4.5, <5`
- `ButtonPaginator>=0.0.3`
- `Pillow>=8.2.0, <9` - `Pillow>=8.2.0, <9`
- `python-gitlab>=2.9.0, <3` - `python-gitlab>=2.9.0, <3`
- `ulid-py>=1.1.0, <2` - `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> <defs>
<style> <style>
.a { .a {
fill: #3498DB; fill: #3498db;
} }
</style> </style>
</defs> </defs>
<title>logotests</title> <title>jarvis</title>
<g> <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="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="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="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="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="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="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="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> </g>
</svg> </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.""" """Main J.A.R.V.I.S. package."""
import asyncio
import logging import logging
from pathlib import Path
from typing import Optional
from discord import Intents from dis_snek import Intents, Snake, listen
from discord.ext import commands
from discord.utils import find
from discord_slash import SlashCommand
from mongoengine import connect 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 import tasks, utils
from jarvis.config import get_config from jarvis.config import get_config
from jarvis.events import guild, member, message from jarvis.events import member, message
jconfig = get_config() 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")) file_handler.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s][%(name)s] %(message)s"))
logger.addHandler(file_handler) logger.addHandler(file_handler)
if asyncio.get_event_loop().is_closed(): intents = Intents.DEFAULT
asyncio.set_event_loop(asyncio.new_event_loop())
intents = Intents.default()
intents.members = True intents.members = True
restart_ctx = None restart_ctx = None
jarvis = commands.Bot( jarvis = Snake(intents=intents, default_prefix="!", sync_interactions=jconfig.sync)
command_prefix=utils.get_prefix,
intents=intents,
help_command=None,
max_messages=jconfig.max_messages,
)
slash = SlashCommand(jarvis, sync_commands=False, sync_on_cog_reload=True) __version__ = "2.0.0a0"
jarvis_self = Process()
__version__ = "1.11.4"
@jarvis.event @listen()
async def on_ready() -> None: async def on_ready() -> None:
"""d.py on_ready override.""" """Lepton on_ready override."""
global restart_ctx global restart_ctx
print(" Logged in as {0.user}".format(jarvis)) print(" Logged in as {0.user}".format(jarvis)) # noqa: T001
print(" Connected to {} guild(s)".format(len(jarvis.guilds))) print(" Connected to {} guild(s)".format(len(jarvis.guilds))) # noqa: T001
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
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.""" """Run J.A.R.V.I.S."""
global restart_ctx
if ctx:
restart_ctx = ctx
connect( connect(
db="ctc2", db="ctc2",
alias="ctc2", alias="ctc2",
@ -84,27 +56,21 @@ def run(ctx: dict = None) -> Optional[dict]:
**jconfig.mongo["connect"], **jconfig.mongo["connect"],
) )
jconfig.get_db_config() jconfig.get_db_config()
for extension in utils.get_extensions(): for extension in utils.get_extensions():
jarvis.load_extension(extension) jarvis.load_extension(extension)
print(
print( # noqa: T001
" https://discord.com/api/oauth2/authorize?client_id=" " 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 jarvis.max_messages = jconfig.max_messages
tasks.init()
# Add event listeners # Add event listeners
if jconfig.events: if jconfig.events:
_ = [ _ = [
guild.GuildEventHandler(jarvis),
member.MemberEventHandler(jarvis), member.MemberEventHandler(jarvis),
message.MessageEventHandler(jarvis), message.MessageEventHandler(jarvis),
] ]
jarvis.run(jconfig.token, bot=True, reconnect=True) jarvis.start(jconfig.token)
for cog in jarvis.cogs:
session = getattr(cog, "_session", None)
if session:
session.close()
if restart_ctx:
return restart_ctx

View file

@ -1,16 +1,16 @@
"""J.A.R.V.I.S. Admin Cogs.""" """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.""" """Add admin cogs to J.A.R.V.I.S."""
bot.add_cog(ban.BanCog(bot)) ban.BanCog(bot)
bot.add_cog(kick.KickCog(bot)) kick.KickCog(bot)
bot.add_cog(lock.LockCog(bot)) # lock.LockCog(bot)
bot.add_cog(lockdown.LockdownCog(bot)) # lockdown.LockdownCog(bot)
bot.add_cog(mute.MuteCog(bot)) mute.MuteCog(bot)
bot.add_cog(purge.PurgeCog(bot)) purge.PurgeCog(bot)
bot.add_cog(roleping.RolepingCog(bot)) roleping.RolepingCog(bot)
bot.add_cog(warning.WarningCog(bot)) warning.WarningCog(bot)

View file

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

View file

@ -1,53 +1,38 @@
"""J.A.R.V.I.S. KickCog.""" """J.A.R.V.I.S. KickCog."""
from discord import User from dis_snek import InteractionContext, Permissions, Scale
from discord.ext.commands import Bot from dis_snek.models.discord.embed import EmbedField
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.user import User
from discord_slash.utils.manage_commands import create_option 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.db.models import Kick
from jarvis.utils import build_embed 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 from jarvis.utils.permissions import admin_or_permissions
class KickCog(CacheCog): class KickCog(Scale):
"""J.A.R.V.I.S. KickCog.""" """J.A.R.V.I.S. KickCog."""
def __init__(self, bot: Bot): @slash_command(name="kick", description="Kick a user")
super().__init__(bot) @slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True)
@slash_option(
@cog_ext.cog_slash( name="reason", description="Kick reason", opt_type=OptionTypes.STRING, required=True
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,
),
],
) )
@admin_or_permissions(kick_members=True) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _kick(self, ctx: SlashContext, user: User, reason: str = None) -> None: async def _kick(self, ctx: InteractionContext, user: User, reason: str) -> None:
if not user or user == ctx.author: 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 return
if user == self.bot.user: 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 return
if len(reason) > 100: 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 return
if not reason:
reason = "Mr. Stark is displeased with your presence. Please leave."
guild_name = ctx.guild.name guild_name = ctx.guild.name
embed = build_embed( embed = build_embed(
title=f"You have been kicked from {guild_name}", title=f"You have been kicked from {guild_name}",
@ -56,10 +41,10 @@ class KickCog(CacheCog):
) )
embed.set_author( embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator, name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url, 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 send_failed = False
try: try:
@ -68,19 +53,16 @@ class KickCog(CacheCog):
send_failed = True send_failed = True
await ctx.guild.kick(user, reason=reason) 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( embed = build_embed(
title="User Kicked", title="User Kicked",
description=f"Reason: {reason}", description=f"Reason: {reason}",
fields=fields, fields=fields,
) )
embed.set_author( embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
name=user.nick if user.nick else user.name, embed.set_thumbnail(url=user.display_avatar.url)
icon_url=user.avatar_url, embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)
_ = Kick( _ = Kick(

View file

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

View file

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

View file

@ -1,132 +1,125 @@
"""J.A.R.V.I.S. MuteCog.""" """J.A.R.V.I.S. MuteCog."""
from discord import Member from datetime import datetime
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 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 import build_embed
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class MuteCog(commands.Cog): class MuteCog(Scale):
"""J.A.R.V.I.S. MuteCog.""" """J.A.R.V.I.S. MuteCog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
@cog_ext.cog_slash( @slash_command(name="mute", description="Mute a user")
name="mute", @slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True)
description="Mute a user", @slash_option(
options=[
create_option(
name="user",
description="User to mute",
option_type=6,
required=True,
),
create_option(
name="reason", name="reason",
description="Reason for mute", description="Reason for mute",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
), )
create_option( @slash_option(
name="duration", name="time",
description="Duration of mute in minutes, default 30", description="Duration of mute, default 1",
option_type=4, opt_type=OptionTypes.INTEGER,
required=False, 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) @check(
async def _mute(self, ctx: SlashContext, user: Member, reason: str, duration: int = 30) -> None: 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: if user == ctx.author:
await ctx.send("You cannot mute yourself.", hidden=True) await ctx.send("You cannot mute yourself.", ephemeral=True)
return return
if user == self.bot.user: 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 return
if len(reason) > 100: 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 return
mute_setting = Setting.objects(guild=ctx.guild.id, setting="mute").first()
if not mute_setting: # Max 4 weeks (2419200 seconds) per API
await ctx.send( duration = time * scale
"Please configure a mute role with /settings mute <role> first", if duration > 2419200:
hidden=True, await ctx.send("Mute must be less than 4 weeks (2419200 seconds)", ephemeral=True)
)
return return
role = get(ctx.guild.roles, id=mute_setting.value)
if role in user.roles: await user.timeout(communication_disabled_until=duration, reason=reason)
await ctx.send("User already muted", hidden=True)
return
await user.add_roles(role, reason=reason)
if duration < 0 or duration > 300:
duration = -1
_ = Mute( _ = Mute(
user=user.id, user=user.id,
reason=reason, reason=reason,
admin=ctx.author.id, admin=ctx.author.id,
guild=ctx.guild.id, guild=ctx.guild.id,
duration=duration, duration=duration,
active=True if duration >= 0 else False, active=True,
).save() ).save()
embed = build_embed( embed = build_embed(
title="User Muted", title="User Muted",
description=f"{user.mention} has been muted", description=f"{user.mention} has been muted",
fields=[Field(name="Reason", value=reason)], fields=[EmbedField(name="Reason", value=reason)],
) )
embed.set_author( embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
name=user.nick if user.nick else user.name, embed.set_thumbnail(url=user.display_avatar.url)
icon_url=user.avatar_url, embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
)
embed.set_thumbnail(url=user.avatar_url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_slash( @slash_command(name="unmute", description="Unmute a user")
name="unmute", @slash_option(
description="Unmute a user", name="user", description="User to unmute", opt_type=OptionTypes.USER, required=True
options=[
create_option(
name="user",
description="User to unmute",
option_type=6,
required=True,
) )
], @check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
) )
@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,
) )
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 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( embed = build_embed(
title="User Unmuted", title="User Unmuted",
description=f"{user.mention} has been unmuted", description=f"{user.mention} has been unmuted",
fields=[], fields=[],
) )
embed.set_author( embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
name=user.nick if user.nick else user.name, embed.set_thumbnail(url=user.display_avatar.url)
icon_url=user.avatar_url, embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
) await ctx.send(embed=embed)
embed.set_thumbnail(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}") embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)

View file

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

View file

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

View file

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

View file

@ -1,46 +1,45 @@
"""J.A.R.V.I.S. Autoreact Cog.""" """J.A.R.V.I.S. Autoreact Cog."""
import re import re
from typing import Optional, Tuple
from discord import TextChannel from dis_snek import InteractionContext, Permissions, Scale, Snake
from discord.ext import commands from dis_snek.models.discord.channel import GuildText
from discord.utils import find from dis_snek.models.snek.application_commands import (
from discord_slash import SlashContext, cog_ext OptionTypes,
from discord_slash.utils.manage_commands import create_option slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.data.unicode import emoji_list from jarvis.data.unicode import emoji_list
from jarvis.db.models import Autoreact from jarvis.db.models import Autoreact
from jarvis.utils import find
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class AutoReactCog(commands.Cog): class AutoReactCog(Scale):
"""J.A.R.V.I.S. Autoreact Cog.""" """J.A.R.V.I.S. Autoreact Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.custom_emote = re.compile(r"^<:\w+:(\d+)>$") self.custom_emote = re.compile(r"^<:\w+:(\d+)>$")
@cog_ext.cog_subcommand( async def create_autoreact(
base="autoreact", self, ctx: InteractionContext, channel: GuildText
name="create", ) -> Tuple[bool, Optional[str]]:
description="Add an autoreact to a channel", """
options=[ Create an autoreact monitor on a channel.
create_option(
name="channel", Args:
description="Channel to monitor", ctx: Interaction context of command
option_type=7, channel: Channel to monitor
required=True,
) Returns:
], Tuple of success? and error message
) """
@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
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first() exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if exists: if exists:
await ctx.send(f"Autoreact already exists for {channel.mention}.", hidden=True) return False, f"Autoreact already exists for {channel.mention}."
return
_ = Autoreact( _ = Autoreact(
guild=ctx.guild.id, guild=ctx.guild.id,
@ -48,152 +47,145 @@ class AutoReactCog(commands.Cog):
reactions=[], reactions=[],
admin=ctx.author.id, admin=ctx.author.id,
).save() ).save()
await ctx.send(f"Autoreact created for {channel.mention}!")
@cog_ext.cog_subcommand( return True, None
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)
@cog_ext.cog_subcommand( async def delete_autoreact(self, ctx: InteractionContext, channel: GuildText) -> bool:
base="autoreact", """
name="add", Remove an autoreact monitor on a channel.
description="Add an autoreact emote to an existing autoreact",
options=[ Args:
create_option( 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",
)
@slash_option(
name="channel", name="channel",
description="Autoreact channel to add emote to", description="Autoreact channel to add emote to",
option_type=7, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
),
create_option(
name="emote",
description="Emote to add",
option_type=3,
required=True,
),
],
) )
@admin_or_permissions(manage_guild=True) @slash_option(
async def _autoreact_add(self, ctx: SlashContext, channel: TextChannel, emote: str) -> None: 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() await ctx.defer()
custom_emoji = self.custom_emote.match(emote) custom_emoji = self.custom_emote.match(emote)
standard_emoji = emote in emoji_list standard_emoji = emote in emoji_list
if not custom_emoji and not standard_emoji: if not custom_emoji and not standard_emoji:
await ctx.send( await ctx.send(
"Please use either an emote from this server or a unicode emoji.", "Please use either an emote from this server or a unicode emoji.",
hidden=True, ephemeral=True,
) )
return return
if custom_emoji: if custom_emoji:
emoji_id = int(custom_emoji.group(1)) emoji_id = int(custom_emoji.group(1))
if not find(lambda x: x.id == emoji_id, ctx.guild.emojis): 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 return
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first() autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not exists: if not autoreact:
await ctx.send(f"Please create autoreact first with /autoreact create {channel.mention}") self.create_autoreact(ctx, channel)
return autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if emote in exists.reactions: if emote in autoreact.reactions:
await ctx.send( await ctx.send(
f"Emote already added to {channel.mention} autoreactions.", f"Emote already added to {channel.mention} autoreactions.",
hidden=True, ephemeral=True,
) )
return return
if len(exists.reactions) >= 5: if len(autoreact.reactions) >= 5:
await ctx.send( await ctx.send(
"Max number of reactions hit. Remove a different one to add this one", "Max number of reactions hit. Remove a different one to add this one",
hidden=True, ephemeral=True,
) )
return return
exists.reactions.append(emote) autoreact.reactions.append(emote)
exists.save() autoreact.save()
await ctx.send(f"Added {emote} to {channel.mention} autoreact.") await ctx.send(f"Added {emote} to {channel.mention} autoreact.")
@cog_ext.cog_subcommand( @slash_command(
base="autoreact", name="autoreact",
name="remove", sub_cmd_name="remove",
description="Remove an autoreact emote from an existing autoreact", sub_cmd_description="Remove an autoreact emote to a channel",
options=[ )
create_option( @slash_option(
name="channel", name="channel",
description="Autoreact channel to remove emote from", description="Autoreact channel to remove emote from",
option_type=7, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
),
create_option(
name="emote",
description="Emote to remove",
option_type=3,
required=True,
),
],
) )
@admin_or_permissions(manage_guild=True) @slash_option(
async def _autoreact_remove(self, ctx: SlashContext, channel: TextChannel, emote: str) -> None: name="emote",
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first() description="Emote to remove (use all to delete)",
if not exists: 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( await ctx.send(
f"Please create autoreact first with /autoreact create {channel.mention}", f"Please create autoreact first with /autoreact add {channel.mention} {emote}",
hidden=True, ephemeral=True,
) )
return 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( await ctx.send(
f"{emote} not used in {channel.mention} autoreactions.", f"{emote} not used in {channel.mention} autoreactions.",
hidden=True, ephemeral=True,
) )
return return
exists.reactions.remove(emote) else:
exists.save() 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.") await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
@cog_ext.cog_subcommand( @slash_command(
base="autoreact", name="autoreact",
name="list", sub_cmd_name="list",
description="List all autoreacts on a channel", sub_cmd_description="List all autoreacts on a channel",
options=[ )
create_option( @slash_option(
name="channel", name="channel",
description="Autoreact channel to list", description="Autoreact channel to list",
option_type=7, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
),
],
) )
@admin_or_permissions(manage_guild=True) async def _autoreact_list(self, ctx: InteractionContext, channel: GuildText) -> None:
async def _autoreact_list(self, ctx: SlashContext, channel: TextChannel) -> None:
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first() exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
if not exists: if not exists:
await ctx.send( await ctx.send(
f"Please create autoreact first with /autoreact create {channel.mention}", f"Please create autoreact first with /autoreact add {channel.mention} <emote>",
hidden=True, ephemeral=True,
) )
return return
message = "" message = ""
if len(exists.reactions) > 0: 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: else:
message = f"No reactions set on {channel.mention}" message = f"No reactions set on {channel.mention}"
await ctx.send(message) await ctx.send(message)
def setup(bot: commands.Bot) -> None: def setup(bot: Snake) -> None:
"""Add AutoReactCog to J.A.R.V.I.S.""" """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 from datetime import datetime, timedelta
import aiohttp import aiohttp
from ButtonPaginator import Paginator from dis_snek import InteractionContext, Snake
from discord import Member, User from dis_snek.ext.paginators import Paginator
from discord.ext import commands from dis_snek.models.discord.embed import EmbedField
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.user import Member, User
from discord_slash.model import ButtonStyle 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.db.models import Guess
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668] guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*") valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = 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, flags=re.IGNORECASE,
) )
@ -26,7 +27,7 @@ invites = re.compile(
class CTCCog(CacheCog): class CTCCog(CacheCog):
"""J.A.R.V.I.S. Complete the Code 2 Cog.""" """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) super().__init__(bot)
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
self.url = "https://completethecodetwo.cards/pw" self.url = "https://completethecodetwo.cards/pw"
@ -34,45 +35,48 @@ class CTCCog(CacheCog):
def __del__(self): def __del__(self):
self._session.close() self._session.close()
@cog_ext.cog_subcommand( @slash_command(
base="ctc2", name="ctc2", sub_cmd_name="about", description="CTC2 related commands", scopes=guild_ids
name="about",
description="CTC2 related commands",
guild_ids=guild_ids,
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _about(self, ctx: SlashContext) -> None: async def _about(self, ctx: InteractionContext) -> None:
await ctx.send("See https://completethecode.com for more information") await ctx.send("See https://completethecode.com for more information")
@cog_ext.cog_subcommand( @slash_command(
base="ctc2", name="ctc2",
name="pw", sub_cmd_name="pw",
description="Guess a password for https://completethecodetwo.cards", sub_cmd_description="Guess a password for https://completethecodetwo.cards",
guild_ids=guild_ids, scopes=guild_ids,
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _pw(self, ctx: SlashContext, guess: str) -> None: async def _pw(self, ctx: InteractionContext, guess: str) -> None:
if len(guess) > 800: if len(guess) > 800:
await ctx.send( 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 return
elif not valid.fullmatch(guess): elif not valid.fullmatch(guess):
await ctx.send( 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 return
elif invites.search(guess): elif invites.search(guess):
await ctx.send( await ctx.send(
"Listen here, dipshit. No using this to bypass sending invite links.", "Listen here, dipshit. No using this to bypass sending invite links.",
hidden=True, ephemeral=True,
) )
return return
guessed = Guess.objects(guess=guess).first() guessed = Guess.objects(guess=guess).first()
if guessed: if guessed:
await ctx.send("Already guessed, dipshit.", hidden=True) await ctx.send("Already guessed, dipshit.", ephemeral=True)
return return
result = await self._session.post(self.url, data=guess) result = await self._session.post(self.url, data=guess)
correct = False correct = False
@ -80,30 +84,30 @@ class CTCCog(CacheCog):
await ctx.send(f"{ctx.author.mention} got it! Password is {guess}!") await ctx.send(f"{ctx.author.mention} got it! Password is {guess}!")
correct = True correct = True
else: else:
await ctx.send("Nope.", hidden=True) await ctx.send("Nope.", ephemeral=True)
_ = Guess(guess=guess, user=ctx.author.id, correct=correct).save() _ = Guess(guess=guess, user=ctx.author.id, correct=correct).save()
@cog_ext.cog_subcommand( @slash_command(
base="ctc2", name="ctc2",
name="guesses", sub_cmd_name="guesses",
description="Show guesses made for https://completethecodetwo.cards", sub_cmd_description="Show guesses made for https://completethecodetwo.cards",
guild_ids=guild_ids, scopes=guild_ids,
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _guesses(self, ctx: SlashContext) -> None: async def _guesses(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx) exists = self.check_cache(ctx)
if exists: if exists:
await ctx.defer(hidden=True) await ctx.defer(ephemeral=True)
await ctx.send( await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}", f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True, ephemeral=True,
) )
return return
guesses = Guess.objects().order_by("-correct", "-id") guesses = Guess.objects().order_by("-correct", "-id")
fields = [] fields = []
for guess in guesses: for guess in guesses:
user = ctx.guild.get_member(guess["user"]) user = await ctx.guild.get_member(guess["user"])
if not user: if not user:
user = await self.bot.fetch_user(guess["user"]) user = await self.bot.fetch_user(guess["user"])
if not user: if not user:
@ -113,7 +117,7 @@ class CTCCog(CacheCog):
name = "Correctly" if guess["correct"] else "Incorrectly" name = "Correctly" if guess["correct"] else "Incorrectly"
name += " guessed by: " + user name += " guessed by: " + user
fields.append( fields.append(
Field( EmbedField(
name=name, name=name,
value=guess["guess"] + "\n\u200b", value=guess["guess"] + "\n\u200b",
inline=False, inline=False,
@ -124,7 +128,7 @@ class CTCCog(CacheCog):
embed = build_embed( embed = build_embed(
title="completethecodetwo.cards guesses", title="completethecodetwo.cards guesses",
description=f"{len(fields)} guesses so far", description=f"{len(fields)} guesses so far",
fields=fields[i : i + 5], # noqa: E203 fields=fields[i : i + 5],
url="https://completethecodetwo.cards", url="https://completethecodetwo.cards",
) )
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png") embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
@ -134,18 +138,7 @@ class CTCCog(CacheCog):
) )
pages.append(embed) pages.append(embed)
paginator = Paginator( paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
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=["", ""],
)
self.cache[hash(paginator)] = { self.cache[hash(paginator)] = {
"guild": ctx.guild.id, "guild": ctx.guild.id,
@ -155,9 +148,9 @@ class CTCCog(CacheCog):
"paginator": paginator, "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.""" """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 re
import aiohttp import aiohttp
from discord.ext import commands from dis_snek import InteractionContext, Scale, Snake
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.embed import EmbedField
from discord_slash.utils.manage_commands import create_option 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.config import get_config
from jarvis.data.dbrand import shipping_lookup from jarvis.data.dbrand import shipping_lookup
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.field import Field
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668] guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
class DbrandCog(commands.Cog): class DbrandCog(Scale):
""" """
dbrand functions for J.A.R.V.I.S. dbrand functions for J.A.R.V.I.S.
Mostly support functions. Credit @cpixl for the shipping API Mostly support functions. Credit @cpixl for the shipping API
""" """
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.base_url = "https://dbrand.com/" self.base_url = "https://dbrand.com/"
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
@ -32,134 +37,130 @@ class DbrandCog(commands.Cog):
def __del__(self): def __del__(self):
self._session.close() self._session.close()
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="skin", sub_cmd_name="skin",
guild_ids=guild_ids, scopes=guild_ids,
description="See what skins are available", sub_cmd_description="See what skins are available",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _skin(self, ctx: SlashContext) -> None: async def _skin(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "/skins") await ctx.send(self.base_url + "/skins")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="robotcamo", sub_cmd_name="robotcamo",
guild_ids=guild_ids, scopes=guild_ids,
description="Get some robot camo. Make Tony Stark proud", sub_cmd_description="Get some robot camo. Make Tony Stark proud",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _camo(self, ctx: SlashContext) -> None: async def _camo(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "robot-camo") await ctx.send(self.base_url + "robot-camo")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="grip", sub_cmd_name="grip",
guild_ids=guild_ids, scopes=guild_ids,
description="See devices with Grip support", sub_cmd_description="See devices with Grip support",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _grip(self, ctx: SlashContext) -> None: async def _grip(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "grip") await ctx.send(self.base_url + "grip")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="contact", sub_cmd_name="contact",
guild_ids=guild_ids, scopes=guild_ids,
description="Contact support", sub_cmd_description="Contact support",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _contact(self, ctx: SlashContext) -> None: async def _contact(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact") await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="support", sub_cmd_name="support",
guild_ids=guild_ids, scopes=guild_ids,
description="Contact support", sub_cmd_description="Contact support",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _support(self, ctx: SlashContext) -> None: async def _support(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact") await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="orderstat", sub_cmd_name="orderstat",
guild_ids=guild_ids, scopes=guild_ids,
description="Get your order status", sub_cmd_description="Get your order status",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _orderstat(self, ctx: SlashContext) -> None: async def _orderstat(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "order-status") await ctx.send(self.base_url + "order-status")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="orders", sub_cmd_name="orders",
guild_ids=guild_ids, scopes=guild_ids,
description="Get your order status", sub_cmd_description="Get your order status",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _orders(self, ctx: SlashContext) -> None: async def _orders(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "order-status") await ctx.send(self.base_url + "order-status")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="status", sub_cmd_name="status",
guild_ids=guild_ids, scopes=guild_ids,
description="dbrand status", sub_cmd_description="dbrand status",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _status(self, ctx: SlashContext) -> None: async def _status(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "status") await ctx.send(self.base_url + "status")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="buy", sub_cmd_name="buy",
guild_ids=guild_ids, scopes=guild_ids,
description="Give us your money!", sub_cmd_description="Give us your money!",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _buy(self, ctx: SlashContext) -> None: async def _buy(self, ctx: InteractionContext) -> None:
await ctx.send("Give us your money! " + self.base_url + "shop") await ctx.send("Give us your money! " + self.base_url + "shop")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="extortion", sub_cmd_name="extortion",
guild_ids=guild_ids, scopes=guild_ids,
description="(not) extortion", sub_cmd_description="(not) extortion",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _extort(self, ctx: SlashContext) -> None: async def _extort(self, ctx: InteractionContext) -> None:
await ctx.send("Be (not) extorted here: " + self.base_url + "not-extortion") await ctx.send("Be (not) extorted here: " + self.base_url + "not-extortion")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="wallpapers", sub_cmd_name="wallpapers",
description="Robot Camo Wallpapers", sub_cmd_description="Robot Camo Wallpapers",
guild_ids=guild_ids, scopes=guild_ids,
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _wallpapers(self, ctx: SlashContext) -> None: async def _wallpapers(self, ctx: InteractionContext) -> None:
await ctx.send("Get robot camo wallpapers here: https://db.io/wallpapers") await ctx.send("Get robot camo wallpapers here: https://db.io/wallpapers")
@cog_ext.cog_subcommand( @slash_command(
base="db", name="db",
name="ship", sub_cmd_name="ship",
description="Get shipping information for your country", sub_cmd_description="Get shipping information for your country",
guild_ids=guild_ids, scopes=guild_ids,
options=[ )
( @slash_option(
create_option(
name="search", name="search",
description="Country search query (2 character code, country name, emoji)", description="Country search query (2 character code, country name, flag emoji)",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
) )
) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
], async def _shipping(self, ctx: InteractionContext, search: str) -> None:
)
@commands.cooldown(1, 2, commands.BucketType.user)
async def _shipping(self, ctx: SlashContext, search: str) -> None:
await ctx.defer() await ctx.defer()
if not re.match(r"^[A-Z- ]+$", search, re.IGNORECASE): if not re.match(r"^[A-Z- ]+$", search, re.IGNORECASE):
if re.match( if re.match(
@ -173,7 +174,6 @@ class DbrandCog(commands.Cog):
elif search == "🏳️": elif search == "🏳️":
search = "fr" search = "fr"
else: else:
print(search)
await ctx.send("Please use text to search for shipping.") await ctx.send("Please use text to search for shipping.")
return return
if len(search) > 2: if len(search) > 2:
@ -193,14 +193,14 @@ class DbrandCog(commands.Cog):
fields = None fields = None
if data is not None and data["is_valid"] and data["shipping_available"]: if data is not None and data["is_valid"] and data["shipping_available"]:
fields = [] 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:]: for service in data["shipping_services_available"][1:]:
service_data = await self._session.get(self.api_url + dest + "/" + service["url"]) service_data = await self._session.get(self.api_url + dest + "/" + service["url"])
if service_data.status > 400: if service_data.status > 400:
continue continue
service_data = await service_data.json() service_data = await service_data.json()
fields.append( fields.append(
Field( EmbedField(
service_data["short-name"], service_data["short-name"],
service_data["time-title"], service_data["time-title"],
) )
@ -215,7 +215,7 @@ class DbrandCog(commands.Cog):
) )
embed = build_embed( embed = build_embed(
title="Shipping to {}".format(data["country"]), title="Shipping to {}".format(data["country"]),
description=description, sub_cmd_description=description,
color="#FFBB00", color="#FFBB00",
fields=fields, fields=fields,
url=self.base_url + "shipping/" + country, url=self.base_url + "shipping/" + country,
@ -229,8 +229,9 @@ class DbrandCog(commands.Cog):
elif not data["is_valid"]: elif not data["is_valid"]:
embed = build_embed( embed = build_embed(
title="Check Shipping Times", title="Check Shipping Times",
description=( sub_cmd_description=(
"Country not found.\nYou can [view all shipping " "destinations here](https://dbrand.com/shipping)" "Country not found.\nYou can [view all shipping "
"destinations here](https://dbrand.com/shipping)"
), ),
fields=[], fields=[],
url="https://dbrand.com/shipping", url="https://dbrand.com/shipping",
@ -245,7 +246,7 @@ class DbrandCog(commands.Cog):
elif not data["shipping_available"]: elif not data["shipping_available"]:
embed = build_embed( embed = build_embed(
title="Shipping to {}".format(data["country"]), title="Shipping to {}".format(data["country"]),
description=( sub_cmd_description=(
"No shipping available.\nTime to move to a country" "No shipping available.\nTime to move to a country"
" that has shipping available.\nYou can [find a new country " " that has shipping available.\nYou can [find a new country "
"to live in here](https://dbrand.com/shipping)" "to live in here](https://dbrand.com/shipping)"
@ -262,6 +263,6 @@ class DbrandCog(commands.Cog):
await ctx.send(embed=embed) 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.""" """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 import ulid as ulidpy
from bson import ObjectId from bson import ObjectId
from discord.ext import commands from dis_snek import InteractionContext, Scale, Snake
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.embed import EmbedField
from discord_slash.utils.manage_commands import create_choice, create_option 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 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} 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}$") 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( 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) ULID_VERIFY = re.compile(r"^[0-9a-z]{26}$", re.IGNORECASE)
UUID_VERIFY = re.compile( UUID_VERIFY = re.compile(
@ -29,7 +37,7 @@ UUID_VERIFY = re.compile(
) )
invites = 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, flags=re.IGNORECASE,
) )
@ -47,43 +55,35 @@ def hash_obj(hash: Any, data: Union[str, bytes], text: bool = True) -> str:
BSIZE = 65536 BSIZE = 65536
block_idx = 0 block_idx = 0
while block_idx * BSIZE < len(data): 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) hash.update(block)
block_idx += 1 block_idx += 1
return hash.hexdigest() return hash.hexdigest()
class DevCog(commands.Cog): class DevCog(Scale):
"""J.A.R.V.I.S. Developer Cog.""" """J.A.R.V.I.S. Developer Cog."""
def __init__(self, bot: commands.Bot): @slash_command(name="hash", description="Hash some data")
self.bot = bot @slash_option(
@cog_ext.cog_slash(
name="hash",
description="Hash some data",
options=[
create_option(
name="method", name="method",
description="Hash method", description="Hash method",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
choices=[create_choice(name=x, value=x) for x in supported_hashes], choices=[SlashCommandChoice(name=x, value=x) for x in supported_hashes],
), )
create_option( @slash_option(
name="data", name="data",
description="Data to hash", description="Data to hash",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
),
],
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _hash(self, ctx: SlashContext, method: str, data: str) -> None: async def _hash(self, ctx: InteractionContext, method: str, data: str) -> None:
if not data: if not data:
await ctx.send( await ctx.send(
"No data to hash", "No data to hash",
hidden=True, ephemeral=True,
) )
return return
text = True text = True
@ -94,36 +94,31 @@ class DevCog(commands.Cog):
title = data if text else ctx.message.attachments[0].filename title = data if text else ctx.message.attachments[0].filename
description = "Hashed using " + method description = "Hashed using " + method
fields = [ fields = [
Field("Data Size", data_size, False), EmbedField("Data Size", data_size, False),
Field("Hash", f"`{hex}`", False), EmbedField("Hash", f"`{hex}`", False),
] ]
embed = build_embed(title=title, description=description, fields=fields) embed = build_embed(title=title, description=description, fields=fields)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_slash( @slash_command(name="uuid", description="Generate a UUID")
name="uuid", @slash_option(
description="Generate a UUID",
options=[
create_option(
name="version", name="version",
description="UUID version", description="UUID version",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
choices=[create_choice(name=x, value=x) for x in ["3", "4", "5"]], choices=[SlashCommandChoice(name=x, value=x) for x in ["3", "4", "5"]],
), )
create_option( @slash_option(
name="data", name="data",
description="Data for UUID version 3,5", description="Data for UUID version 3,5",
option_type=3, opt_type=OptionTypes.STRING,
required=False, required=False,
),
],
) )
async def _uuid(self, ctx: SlashContext, version: str, data: str = None) -> None: async def _uuid(self, ctx: InteractionContext, version: str, data: str = None) -> None:
version = int(version) version = int(version)
if version in [3, 5] and not data: 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 return
if version == 4: if version == 4:
await ctx.send(f"UUID4: `{uuidpy.uuid4()}`") await ctx.send(f"UUID4: `{uuidpy.uuid4()}`")
@ -139,40 +134,40 @@ class DevCog(commands.Cog):
to_send = UUID_GET[version](uuidpy.NAMESPACE_DNS, data) to_send = UUID_GET[version](uuidpy.NAMESPACE_DNS, data)
await ctx.send(f"UUID{version}: `{to_send}`") await ctx.send(f"UUID{version}: `{to_send}`")
@cog_ext.cog_slash( @slash_command(
name="objectid", name="objectid",
description="Generate an ObjectID", description="Generate an ObjectID",
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _objectid(self, ctx: SlashContext) -> None: async def _objectid(self, ctx: InteractionContext) -> None:
await ctx.send(f"ObjectId: `{str(ObjectId())}`") await ctx.send(f"ObjectId: `{str(ObjectId())}`")
@cog_ext.cog_slash( @slash_command(
name="ulid", name="ulid",
description="Generate a ULID", description="Generate a ULID",
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _ulid(self, ctx: SlashContext) -> None: async def _ulid(self, ctx: InteractionContext) -> None:
await ctx.send(f"ULID: `{ulidpy.new().str}`") await ctx.send(f"ULID: `{ulidpy.new().str}`")
@cog_ext.cog_slash( @slash_command(
name="uuid2ulid", name="uuid2ulid",
description="Convert a UUID to a ULID", description="Convert a UUID to a ULID",
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _uuid2ulid(self, ctx: SlashContext, uuid: str) -> None: async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
if UUID_VERIFY.match(uuid): if UUID_VERIFY.match(uuid):
u = ulidpy.parse(uuid) u = ulidpy.parse(uuid)
await ctx.send(f"ULID: `{u.str}`") await ctx.send(f"ULID: `{u.str}`")
else: else:
await ctx.send("Invalid UUID") await ctx.send("Invalid UUID")
@cog_ext.cog_slash( @slash_command(
name="ulid2uuid", name="ulid2uuid",
description="Convert a ULID to a UUID", description="Convert a ULID to a UUID",
) )
@commands.cooldown(1, 2, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _ulid2uuid(self, ctx: SlashContext, ulid: str) -> None: async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
if ULID_VERIFY.match(ulid): if ULID_VERIFY.match(ulid):
ulid = ulidpy.parse(ulid) ulid = ulidpy.parse(ulid)
await ctx.send(f"UUID: `{ulid.uuid}`") await ctx.send(f"UUID: `{ulid.uuid}`")
@ -181,82 +176,71 @@ class DevCog(commands.Cog):
base64_methods = ["b64", "b16", "b32", "a85", "b85"] base64_methods = ["b64", "b16", "b32", "a85", "b85"]
@cog_ext.cog_slash( @slash_command(name="encode", description="Encode some data")
name="encode", @slash_option(
description="Encode some data",
options=[
create_option(
name="method", name="method",
description="Encode method", description="Encode method",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
choices=[create_choice(name=x, value=x) for x in base64_methods], choices=[SlashCommandChoice(name=x, value=x) for x in base64_methods],
), )
create_option( @slash_option(
name="data", name="data",
description="Data to encode", description="Data to encode",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
),
],
) )
async def _encode(self, ctx: SlashContext, method: str, data: str) -> None: async def _encode(self, ctx: InteractionContext, method: str, data: str) -> None:
mstr = method mstr = method
method = getattr(base64, method + "encode") method = getattr(base64, method + "encode")
encoded = method(data.encode("UTF-8")).decode("UTF-8") encoded = method(data.encode("UTF-8")).decode("UTF-8")
fields = [ fields = [
Field(name="Plaintext", value=f"`{data}`", inline=False), EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
Field(name=mstr, value=f"`{encoded}`", inline=False), EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
] ]
embed = build_embed(title="Decoded Data", description="", fields=fields) embed = build_embed(title="Decoded Data", description="", fields=fields)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_slash( @slash_command(name="decode", description="Decode some data")
name="decode", @slash_option(
description="Decode some data",
options=[
create_option(
name="method", name="method",
description="Decode method", description="Decode method",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
choices=[create_choice(name=x, value=x) for x in base64_methods], choices=[SlashCommandChoice(name=x, value=x) for x in base64_methods],
), )
create_option( @slash_option(
name="data", name="data",
description="Data to encode", description="Data to encode",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
),
],
) )
async def _decode(self, ctx: SlashContext, method: str, data: str) -> None: async def _decode(self, ctx: InteractionContext, method: str, data: str) -> None:
mstr = method mstr = method
method = getattr(base64, method + "decode") method = getattr(base64, method + "decode")
decoded = method(data.encode("UTF-8")).decode("UTF-8") decoded = method(data.encode("UTF-8")).decode("UTF-8")
if invites.search(decoded): if invites.search(decoded):
await ctx.send( await ctx.send(
"Please don't use this to bypass invite restrictions", "Please don't use this to bypass invite restrictions",
hidden=True, ephemeral=True,
) )
return return
fields = [ fields = [
Field(name="Plaintext", value=f"`{data}`", inline=False), EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
Field(name=mstr, value=f"`{decoded}`", inline=False), EmbedField(name=mstr, value=f"`{decoded}`", inline=False),
] ]
embed = build_embed(title="Decoded Data", description="", fields=fields) embed = build_embed(title="Decoded Data", description="", fields=fields)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_slash( @slash_command(name="cloc", description="Get J.A.R.V.I.S. lines of code")
name="cloc", @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
description="Get J.A.R.V.I.S. lines of code", async def _cloc(self, ctx: InteractionContext) -> None:
) output = subprocess.check_output( # noqa: S603, S607
@commands.cooldown(1, 30, commands.BucketType.channel) ["tokei", "-C", "--sort", "code"]
async def _cloc(self, ctx: SlashContext) -> None: ).decode("UTF-8")
output = subprocess.check_output(["tokei", "-C", "--sort", "code"]).decode("UTF-8") # noqa: S603, S607
await ctx.send(f"```\n{output}\n```") 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.""" """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 return
elif isinstance(error, commands.errors.CommandOnCooldown): elif isinstance(error, commands.errors.CommandOnCooldown):
await ctx.send( 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: else:
await ctx.send(f"Error processing command:\n```{error}```") await ctx.send(f"Error processing command:\n```{error}```")
@ -29,19 +30,22 @@ class ErrorHandlerCog(commands.Cog):
@commands.Cog.listener() @commands.Cog.listener()
async def on_slash_command_error(self, ctx: SlashContext, error: Exception) -> None: async def on_slash_command_error(self, ctx: SlashContext, error: Exception) -> None:
"""discord_slash on_slash_command_error override.""" """discord_slash on_slash_command_error override."""
if isinstance(error, commands.errors.MissingPermissions) or isinstance(error, commands.errors.CheckFailure): if isinstance(error, commands.errors.MissingPermissions) or isinstance(
await ctx.send("I'm afraid I can't let you do that.", hidden=True) 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): elif isinstance(error, commands.errors.CommandNotFound):
return return
elif isinstance(error, commands.errors.CommandOnCooldown): elif isinstance(error, commands.errors.CommandOnCooldown):
await ctx.send( await ctx.send(
"Command on cooldown. " + f"Please wait {error.retry_after:0.2f}s before trying again", "Command on cooldown. "
hidden=True, f"Please wait {error.retry_after:0.2f}s before trying again",
ephemeral=True,
) )
else: else:
await ctx.send( await ctx.send(
f"Error processing command:\n```{error}```", f"Error processing command:\n```{error}```",
hidden=True, ephemeral=True,
) )
raise error raise error
slash.commands[ctx.command].reset_cooldown(ctx) slash.commands[ctx.command].reset_cooldown(ctx)
@ -49,4 +53,4 @@ class ErrorHandlerCog(commands.Cog):
def setup(bot: commands.Bot) -> None: def setup(bot: commands.Bot) -> None:
"""Add ErrorHandlerCog to J.A.R.V.I.S.""" """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 from datetime import datetime, timedelta
import gitlab import gitlab
from ButtonPaginator import Paginator from dis_snek import InteractionContext, Snake
from discord import Embed from dis_snek.ext.paginators import Paginator
from discord.ext import commands from dis_snek.models.discord.embed import Embed, EmbedField
from discord_slash import SlashContext, cog_ext from dis_snek.models.snek.application_commands import (
from discord_slash.model import ButtonStyle OptionTypes,
from discord_slash.utils.manage_commands import create_choice, create_option SlashCommandChoice,
slash_command,
slash_option,
)
from jarvis.config import get_config from jarvis.config import get_config
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
guild_ids = [862402786116763668] guild_ids = [862402786116763668]
@ -20,25 +22,22 @@ guild_ids = [862402786116763668]
class GitlabCog(CacheCog): class GitlabCog(CacheCog):
"""J.A.R.V.I.S. GitLab Cog.""" """J.A.R.V.I.S. GitLab Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
super().__init__(bot) super().__init__(bot)
config = get_config() config = get_config()
self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token) self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token)
# J.A.R.V.I.S. GitLab ID is 29 # J.A.R.V.I.S. GitLab ID is 29
self.project = self._gitlab.projects.get(29) self.project = self._gitlab.projects.get(29)
@cog_ext.cog_subcommand( @slash_command(
base="gl", name="gl", sub_cmd_name="issue", description="Get an issue from GitLab", scopes=guild_ids
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)],
) )
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: try:
issue = self.project.issues.get(int(id)) issue = self.project.issues.get(int(id))
except gitlab.exceptions.GitlabGetError: except gitlab.exceptions.GitlabGetError:
await ctx.send("Issue does not exist.", hidden=True) await ctx.send("Issue does not exist.", ephemeral=True)
return return
assignee = issue.assignee assignee = issue.assignee
if assignee: if assignee:
@ -46,7 +45,9 @@ class GitlabCog(CacheCog):
else: else:
assignee = "None" 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 labels = issue.labels
if labels: if labels:
@ -55,18 +56,20 @@ class GitlabCog(CacheCog):
labels = "None" labels = "None"
fields = [ fields = [
Field(name="State", value=issue.state[0].upper() + issue.state[1:]), EmbedField(name="State", value=issue.state[0].upper() + issue.state[1:]),
Field(name="Assignee", value=assignee), EmbedField(name="Assignee", value=assignee),
Field(name="Labels", value=labels), EmbedField(name="Labels", value=labels),
] ]
color = self.project.labels.get(issue.labels[0]).color 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": 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") closed_at = datetime.strptime(issue.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
fields.append(Field(name="Closed At", value=closed_at)) "%Y-%m-%d %H:%M:%S UTC"
)
fields.append(EmbedField(name="Closed At", value=closed_at))
if issue.milestone: if issue.milestone:
fields.append( fields.append(
Field( EmbedField(
name="Milestone", name="Milestone",
value=f"[{issue.milestone['title']}]({issue.milestone['web_url']})", value=f"[{issue.milestone['title']}]({issue.milestone['web_url']})",
inline=False, inline=False,
@ -83,50 +86,49 @@ class GitlabCog(CacheCog):
) )
embed.set_author( embed.set_author(
name=issue.author["name"], name=issue.author["name"],
icon_url=issue.author["avatar_url"], icon_url=issue.author["display_avatar"],
url=issue.author["web_url"], 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) await ctx.send(embed=embed)
@cog_ext.cog_subcommand( @slash_command(
base="gl", name="gl",
name="milestone", sub_cmd_name="milestone",
description="Get a milestone from GitLab", description="Get a milestone from GitLab",
guild_ids=guild_ids, scopes=guild_ids,
options=[
create_option(
name="id",
description="Milestone ID",
option_type=4,
required=True,
) )
], @slash_option(
name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True
) )
async def _milestone(self, ctx: SlashContext, id: int) -> None: async def _milestone(self, ctx: InteractionContext, id: int) -> None:
try: try:
milestone = self.project.milestones.get(int(id)) milestone = self.project.milestones.get(int(id))
except gitlab.exceptions.GitlabGetError: except gitlab.exceptions.GitlabGetError:
await ctx.send("Milestone does not exist.", hidden=True) await ctx.send("Milestone does not exist.", ephemeral=True)
return 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 = [ fields = [
Field( EmbedField(
name="State", name="State",
value=milestone.state[0].upper() + milestone.state[1:], value=milestone.state[0].upper() + milestone.state[1:],
), ),
Field(name="Start Date", value=milestone.start_date), EmbedField(name="Start Date", value=milestone.start_date),
Field(name="Due Date", value=milestone.due_date), EmbedField(name="Due Date", value=milestone.due_date),
Field(name="Created At", value=created_at), EmbedField(name="Created At", value=created_at),
] ]
if milestone.updated_at: if milestone.updated_at:
updated_at = datetime.strptime(milestone.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime( updated_at = datetime.strptime(milestone.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC" "%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: if len(milestone.title) > 200:
milestone.title = milestone.title[:200] + "..." milestone.title = milestone.title[:200] + "..."
@ -143,28 +145,25 @@ class GitlabCog(CacheCog):
url="https://git.zevaryx.com/jarvis", url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png", 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) await ctx.send(embed=embed)
@cog_ext.cog_subcommand( @slash_command(
base="gl", name="gl",
name="mergerequest", sub_cmd_name="mr",
description="Get an merge request from GitLab", description="Get a merge request from GitLab",
guild_ids=guild_ids, scopes=guild_ids,
options=[
create_option(
name="id",
description="Merge Request ID",
option_type=4,
required=True,
) )
], @slash_option(
name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True
) )
async def _mergerequest(self, ctx: SlashContext, id: int) -> None: async def _mergerequest(self, ctx: InteractionContext, id: int) -> None:
try: try:
mr = self.project.mergerequests.get(int(id)) mr = self.project.mergerequests.get(int(id))
except gitlab.exceptions.GitlabGetError: 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 return
assignee = mr.assignee assignee = mr.assignee
if assignee: if assignee:
@ -172,7 +171,9 @@ class GitlabCog(CacheCog):
else: else:
assignee = "None" 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 labels = mr.labels
if labels: if labels:
@ -181,24 +182,28 @@ class GitlabCog(CacheCog):
labels = "None" labels = "None"
fields = [ fields = [
Field(name="State", value=mr.state[0].upper() + mr.state[1:]), EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:]),
Field(name="Assignee", value=assignee), EmbedField(name="Assignee", value=assignee),
Field(name="Labels", value=labels), EmbedField(name="Labels", value=labels),
] ]
if mr.labels: if mr.labels:
color = self.project.labels.get(mr.labels[0]).color color = self.project.labels.get(mr.labels[0]).color
else: else:
color = "#00FFEE" color = "#00FFEE"
fields.append(Field(name="Created At", value=created_at)) fields.append(EmbedField(name="Created At", value=created_at))
if mr.state == "merged": 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") merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
fields.append(Field(name="Merged At", value=merged_at)) "%Y-%m-%d %H:%M:%S UTC"
)
fields.append(EmbedField(name="Merged At", value=merged_at))
elif mr.state == "closed": 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") closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
fields.append(Field(name="Closed At", value=closed_at)) "%Y-%m-%d %H:%M:%S UTC"
)
fields.append(EmbedField(name="Closed At", value=closed_at))
if mr.milestone: if mr.milestone:
fields.append( fields.append(
Field( EmbedField(
name="Milestone", name="Milestone",
value=f"[{mr.milestone['title']}]({mr.milestone['web_url']})", value=f"[{mr.milestone['title']}]({mr.milestone['web_url']})",
inline=False, inline=False,
@ -215,10 +220,12 @@ class GitlabCog(CacheCog):
) )
embed.set_author( embed.set_author(
name=mr.author["name"], name=mr.author["name"],
icon_url=mr.author["avatar_url"], icon_url=mr.author["display_avatar"],
url=mr.author["web_url"], 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) await ctx.send(embed=embed)
def build_embed_page(self, api_list: list, t_state: str, name: str) -> Embed: def build_embed_page(self, api_list: list, t_state: str, name: str) -> Embed:
@ -230,7 +237,7 @@ class GitlabCog(CacheCog):
fields = [] fields = []
for item in api_list: for item in api_list:
fields.append( fields.append(
Field( EmbedField(
name=f"[#{item.iid}] {item.title}", name=f"[#{item.iid}] {item.title}",
value=item.description + f"\n\n[View this {name}]({item.web_url})", value=item.description + f"\n\n[View this {name}]({item.web_url})",
inline=False, inline=False,
@ -248,35 +255,32 @@ class GitlabCog(CacheCog):
url="https://git.zevaryx.com/jarvis", url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png", 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 return embed
@cog_ext.cog_subcommand( @slash_command(
base="gl", name="gl", sub_cmd_name="issues", description="Get issues from GitLab", scopes=guild_ids
name="issues", )
description="Get open issues from GitLab", @slash_option(
guild_ids=guild_ids,
options=[
create_option(
name="state", name="state",
description="State of issues to get", description="State of issues to get",
option_type=3, opt_type=OptionTypes.STRING,
required=False, required=False,
choices=[ choices=[
create_choice(name="Open", value="opened"), SlashCommandChoice(name="Open", value="opened"),
create_choice(name="Closed", value="closed"), SlashCommandChoice(name="Closed", value="closed"),
create_choice(name="All", value="all"), SlashCommandChoice(name="All", value="all"),
], ],
) )
], async def _issues(self, ctx: InteractionContext, state: str = "opened") -> None:
)
async def _issues(self, ctx: SlashContext, state: str = "opened") -> None:
exists = self.check_cache(ctx, state=state) exists = self.check_cache(ctx, state=state)
if exists: if exists:
await ctx.defer(hidden=True) await ctx.defer(ephemeral=True)
await ctx.send( await ctx.send(
"Please use existing interaction: " + f"{exists['paginator']._message.jump_url}", "Please use existing interaction: " + f"{exists['paginator']._message.jump_url}",
hidden=True, ephemeral=True,
) )
return return
await ctx.defer() await ctx.defer()
@ -311,20 +315,9 @@ class GitlabCog(CacheCog):
pages = [] pages = []
t_state = t_state[0].upper() + t_state[1:] t_state = t_state[0].upper() + t_state[1:]
for i in range(0, len(issues), 5): 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( paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
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=["", ""],
)
self.cache[hash(paginator)] = { self.cache[hash(paginator)] = {
"user": ctx.author.id, "user": ctx.author.id,
@ -335,35 +328,32 @@ class GitlabCog(CacheCog):
"paginator": paginator, "paginator": paginator,
} }
await paginator.start() await paginator.send(ctx)
@cog_ext.cog_subcommand( @slash_command(
base="gl", name="gl",
name="mergerequests", sub_cmd_name="mrs",
description="Get open issues from GitLab", description="Get merge requests from GitLab",
guild_ids=guild_ids, scopes=guild_ids,
options=[ )
create_option( @slash_option(
name="state", name="state",
description="State of issues to get", description="State of merge requests to get",
option_type=3, opt_type=OptionTypes.STRING,
required=False, required=False,
choices=[ choices=[
create_choice(name="Open", value="opened"), SlashCommandChoice(name="Open", value="opened"),
create_choice(name="Closed", value="closed"), SlashCommandChoice(name="Closed", value="closed"),
create_choice(name="Merged", value="merged"), SlashCommandChoice(name="All", value="all"),
create_choice(name="All", value="all"),
], ],
) )
], async def _mergerequests(self, ctx: InteractionContext, state: str = "opened") -> None:
)
async def _mergerequests(self, ctx: SlashContext, state: str = "opened") -> None:
exists = self.check_cache(ctx, state=state) exists = self.check_cache(ctx, state=state)
if exists: if exists:
await ctx.defer(hidden=True) await ctx.defer(ephemeral=True)
await ctx.send( await ctx.send(
"Please use existing interaction: " + f"{exists['paginator']._message.jump_url}", "Please use existing interaction: " + f"{exists['paginator']._message.jump_url}",
hidden=True, ephemeral=True,
) )
return return
await ctx.defer() await ctx.defer()
@ -398,21 +388,12 @@ class GitlabCog(CacheCog):
pages = [] pages = []
t_state = t_state[0].upper() + t_state[1:] t_state = t_state[0].upper() + t_state[1:]
for i in range(0, len(merges), 5): 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)] = { self.cache[hash(paginator)] = {
"user": ctx.author.id, "user": ctx.author.id,
"guild": ctx.guild.id, "guild": ctx.guild.id,
@ -422,21 +403,21 @@ class GitlabCog(CacheCog):
"paginator": paginator, "paginator": paginator,
} }
await paginator.start() await paginator.send(ctx)
@cog_ext.cog_subcommand( @slash_command(
base="gl", name="gl",
name="milestones", sub_cmd_name="milestones",
description="Get open issues from GitLab", description="Get milestones from GitLab",
guild_ids=guild_ids, scopes=guild_ids,
) )
async def _milestones(self, ctx: SlashContext) -> None: async def _milestones(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx) exists = self.check_cache(ctx)
if exists: if exists:
await ctx.defer(hidden=True) await ctx.defer(ephemeral=True)
await ctx.send( await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}", f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True, ephemeral=True,
) )
return return
await ctx.defer() await ctx.defer()
@ -463,21 +444,12 @@ class GitlabCog(CacheCog):
pages = [] pages = []
for i in range(0, len(milestones), 5): 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)] = { self.cache[hash(paginator)] = {
"user": ctx.author.id, "user": ctx.author.id,
"guild": ctx.guild.id, "guild": ctx.guild.id,
@ -486,10 +458,10 @@ class GitlabCog(CacheCog):
"paginator": paginator, "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.""" """Add GitlabCog to J.A.R.V.I.S. if Gitlab token exists."""
if get_config().gitlab_token: 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 aiohttp
import cv2 import cv2
import numpy as np import numpy as np
from discord import File from dis_snek import MessageContext, Scale, Snake, message_command
from discord.ext import commands 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 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. Image processing functions for J.A.R.V.I.S.
May be categorized under util later May be categorized under util later
""" """
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B)", re.IGNORECASE) 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): def __del__(self):
self._session.close() 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: if not target:
await ctx.send("Missing target size, i.e. 200KB.") await ctx.send("Missing target size, i.e. 200KB.")
return return
@ -84,23 +86,23 @@ class ImageCog(commands.Cog):
bufio = BytesIO(file) bufio = BytesIO(file)
accuracy = (len(file) / tgt_size) * 100 accuracy = (len(file) / tgt_size) * 100
fields = [ fields = [
Field("Original Size", convert_bytesize(size), False), EmbedField("Original Size", convert_bytesize(size), False),
Field("New Size", convert_bytesize(len(file)), False), EmbedField("New Size", convert_bytesize(len(file)), False),
Field("Accuracy", f"{accuracy:.02f}%", False), EmbedField("Accuracy", f"{accuracy:.02f}%", False),
] ]
embed = build_embed(title=filename, description="", fields=fields) embed = build_embed(title=filename, description="", fields=fields)
embed.set_image(url="attachment://resized.png") embed.set_image(url="attachment://resized.png")
await ctx.send( await ctx.send(
embed=embed, embed=embed,
file=File(bufio, filename="resized.png"), file=File(file=bufio, filename="resized.png"),
) )
@commands.command(name="resize", help="Resize an image") @message_command(name="resize")
@commands.cooldown(1, 60, commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=60)
async def _resize_pref(self, ctx: commands.Context, target: str, url: str = None) -> None: async def _resize_pref(self, ctx: MessageContext, target: str, url: str = None) -> None:
await self._resize(ctx, target, url) 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.""" """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 datetime import datetime
from random import randint from random import randint
from discord.ext import commands from dis_snek import InteractionContext, Scale, Snake
from discord_slash import SlashContext, cog_ext 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.db.models import Joke
from jarvis.utils import build_embed 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. Joke library for J.A.R.V.I.S.
May adapt over time to create jokes using machine learning May adapt over time to create jokes using machine learning
""" """
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
# TODO: Make this a command group with subcommands # TODO: Make this a command group with subcommands
@cog_ext.cog_slash( @slash_command(
name="joke", name="joke",
description="Hear a joke", description="Hear a joke",
) )
@commands.cooldown(1, 10, commands.BucketType.channel) @slash_option(name="id", description="Joke ID", required=False, opt_type=OptionTypes.INTEGER)
async def _joke(self, ctx: SlashContext, id: str = None) -> None: @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=10)
async def _joke(self, ctx: InteractionContext, id: str = None) -> None:
"""Get a joke from the database.""" """Get a joke from the database."""
try: try:
if randint(1, 100_000) == 5779 and id is None: # noqa: S311 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() result = Joke.objects().aggregate(pipeline).next()
if result is None: 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 return
emotes = re.findall(r"(&#x[a-fA-F0-9]*;)", result["body"]) emotes = re.findall(r"(&#x[a-fA-F0-9]*;)", result["body"])
for match in emotes: for match in emotes:
@ -63,7 +70,7 @@ class JokeCog(commands.Cog):
body = "" body = ""
for word in result["body"].split(" "): for word in result["body"].split(" "):
if len(body) + 1 + len(word) > 1024: if len(body) + 1 + len(word) > 1024:
body_chunks.append(Field("", body, False)) body_chunks.append(EmbedField("", body, False))
body = "" body = ""
if word == "\n" and body == "": if word == "\n" and body == "":
continue continue
@ -87,15 +94,15 @@ class JokeCog(commands.Cog):
else: else:
desc += word + " " desc += word + " "
body_chunks.append(Field("", body, False)) body_chunks.append(EmbedField("", body, False))
fields = body_chunks fields = body_chunks
fields.append(Field("Score", result["score"])) fields.append(EmbedField("Score", result["score"]))
# Field( # Field(
# "Created At", # "Created At",
# str(datetime.fromtimestamp(result["created_utc"])), # str(datetime.fromtimestamp(result["created_utc"])),
# ), # ),
fields.append(Field("ID", result["rid"])) fields.append(EmbedField("ID", result["rid"]))
embed = build_embed( embed = build_embed(
title=title, title=title,
description=desc, description=desc,
@ -109,6 +116,6 @@ class JokeCog(commands.Cog):
# await ctx.send(f"**{result['title']}**\n\n{result['body']}") # 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.""" """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: def setup(bot: Bot) -> None:
"""Add modlog cogs to J.A.R.V.I.S.""" """Add modlog cogs to J.A.R.V.I.S."""
bot.add_cog(command.ModlogCommandCog(bot)) command.ModlogCommandCog(bot)
bot.add_cog(member.ModlogMemberCog(bot)) member.ModlogMemberCog(bot)
bot.add_cog(message.ModlogMessageCog(bot)) message.ModlogMessageCog(bot)

View file

@ -41,9 +41,8 @@ class ModlogCommandCog(commands.Cog):
fields=fields, fields=fields,
color="#fc9e3f", color="#fc9e3f",
) )
embed.set_author( embed.set_author(name=ctx.author.username, icon_url=ctx.author.display_avatar.url)
name=ctx.author.name, embed.set_footer(
icon_url=ctx.author.avatar_url, 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) await channel.send(embed=embed)

View file

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

View file

@ -44,10 +44,12 @@ class ModlogMessageCog(commands.Cog):
) )
embed.set_author( embed.set_author(
name=before.author.name, name=before.author.name,
icon_url=before.author.avatar_url, icon_url=before.author.display_avatar.url,
url=after.jump_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) await channel.send(embed=embed)
@commands.Cog.listener() @commands.Cog.listener()
@ -97,8 +99,10 @@ class ModlogMessageCog(commands.Cog):
embed.set_author( embed.set_author(
name=message.author.name, name=message.author.name,
icon_url=message.author.avatar_url, icon_url=message.author.display_avatar.url,
url=message.jump_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) await channel.send(embed=embed)

View file

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

View file

@ -1,163 +1,27 @@
"""J.A.R.V.I.S. Owner Cog.""" """J.A.R.V.I.S. Owner Cog."""
import os from dis_snek import MessageContext, Scale, Snake, message_command
import sys from dis_snek.models.discord.user import User
import traceback from dis_snek.models.snek.checks import is_owner
from inspect import getsource from dis_snek.models.snek.command import check
from time import time
from typing import Any
import discord
from discord import DMChannel, User
from discord.ext import commands
import jarvis
from jarvis.config import reload_config from jarvis.config import reload_config
from jarvis.db.models import 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. J.A.R.V.I.S. management cog.
Used by admins to control core J.A.R.V.I.S. systems 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.bot = bot
self.admins = Config.objects(key="admins").first() self.admins = Config.objects(key="admins").first()
@commands.command(name="load", hidden=True) @message_command(name="addadmin")
@user_is_bot_admin() @check(is_owner())
async def _load_cog(self, ctx: commands.Context, *, cog: str) -> None: async def _add(self, ctx: MessageContext, user: User) -> 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:
if user.id in self.admins.value: if user.id in self.admins.value:
await ctx.send(f"{user.mention} is already an admin.") await ctx.send(f"{user.mention} is already an admin.")
return return
@ -166,9 +30,9 @@ class OwnerCog(commands.Cog):
reload_config() reload_config()
await ctx.send(f"{user.mention} is now an admin. Use this power carefully.") await ctx.send(f"{user.mention} is now an admin. Use this power carefully.")
@_admin.command(name="remove", hidden=True) @message_command(name="deladmin")
@commands.is_owner() @is_owner()
async def _remove(self, ctx: commands.Context, user: User) -> None: async def _remove(self, ctx: MessageContext, user: User) -> None:
if user.id not in self.admins.value: if user.id not in self.admins.value:
await ctx.send(f"{user.mention} is not an admin.") await ctx.send(f"{user.mention} is not an admin.")
return return
@ -177,70 +41,7 @@ class OwnerCog(commands.Cog):
reload_config() reload_config()
await ctx.send(f"{user.mention} is no longer an admin.") 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)): def setup(bot: Snake) -> None:
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:
"""Add OwnerCog to J.A.R.V.I.S.""" """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 typing import List, Optional
from bson import ObjectId from bson import ObjectId
from discord import Embed from dis_snek import InteractionContext, Snake
from discord.ext.commands import Bot from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from discord.ext.tasks import loop from dis_snek.models.discord.embed import Embed, EmbedField
from discord_slash import SlashContext, cog_ext from dis_snek.models.snek.application_commands import (
from discord_slash.utils.manage_commands import create_option OptionTypes,
from discord_slash.utils.manage_components import ( slash_command,
create_actionrow, slash_option,
create_select,
create_select_option,
wait_for_component,
) )
from jarvis.db.models import Reminder from jarvis.db.models import Reminder
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*") valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = 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, flags=re.IGNORECASE,
) )
@ -32,49 +28,40 @@ invites = re.compile(
class RemindmeCog(CacheCog): class RemindmeCog(CacheCog):
"""J.A.R.V.I.S. Remind Me Cog.""" """J.A.R.V.I.S. Remind Me Cog."""
def __init__(self, bot: Bot): def __init__(self, bot: Snake):
super().__init__(bot) super().__init__(bot)
self._remind.start()
@cog_ext.cog_slash( @slash_command(name="remindme", description="Set a reminder")
name="remindme", @slash_option(
description="Set a reminder",
options=[
create_option(
name="message", name="message",
description="What to remind you of", description="What to remind you of?",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
), )
create_option( @slash_option(
name="weeks", name="weeks",
description="Number of weeks?", description="Number of weeks?",
option_type=4, opt_type=OptionTypes.INTEGER,
required=False, required=False,
), )
create_option( @slash_option(
name="days", name="days", description="Number of days?", opt_type=OptionTypes.INTEGER, required=False
description="Number of days?", )
option_type=4, @slash_option(
required=False,
),
create_option(
name="hours", name="hours",
description="Number of hours?", description="Number of hours?",
option_type=4, opt_type=OptionTypes.INTEGER,
required=False, required=False,
), )
create_option( @slash_option(
name="minutes", name="minutes",
description="Number of minutes?", description="Number of minutes?",
option_type=4, opt_type=OptionTypes.INTEGER,
required=False, required=False,
),
],
) )
async def _remindme( async def _remindme(
self, self,
ctx: SlashContext, ctx: InteractionContext,
message: Optional[str] = None, message: Optional[str] = None,
weeks: Optional[int] = 0, weeks: Optional[int] = 0,
days: Optional[int] = 0, days: Optional[int] = 0,
@ -82,20 +69,20 @@ class RemindmeCog(CacheCog):
minutes: Optional[int] = 0, minutes: Optional[int] = 0,
) -> None: ) -> None:
if len(message) > 100: 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 return
elif invites.search(message): elif invites.search(message):
await ctx.send( await ctx.send(
"Listen, don't use this to try and bypass the rules", "Listen, don't use this to try and bypass the rules",
hidden=True, ephemeral=True,
) )
return return
elif not valid.fullmatch(message): 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 return
if not any([weeks, days, hours, minutes]): 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 return
weeks = abs(weeks) weeks = abs(weeks)
@ -104,19 +91,19 @@ class RemindmeCog(CacheCog):
minutes = abs(minutes) minutes = abs(minutes)
if weeks and weeks > 4: 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 return
elif days and days > 6: 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 return
elif hours and hours > 23: 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 return
elif minutes and minutes > 59: 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 return
reminders = Reminder.objects(user=ctx.author.id, active=True).count() reminders = Reminder.objects(user=ctx.author.id, active=True).count()
@ -124,7 +111,7 @@ class RemindmeCog(CacheCog):
await ctx.send( await ctx.send(
"You already have 5 (or more) active reminders. " "You already have 5 (or more) active reminders. "
"Please either remove an old one, or wait for one to pass", "Please either remove an old one, or wait for one to pass",
hidden=True, ephemeral=True,
) )
return return
@ -148,8 +135,8 @@ class RemindmeCog(CacheCog):
title="Reminder Set", title="Reminder Set",
description=f"{ctx.author.mention} set a reminder", description=f"{ctx.author.mention} set a reminder",
fields=[ fields=[
Field(name="Message", value=message), EmbedField(name="Message", value=message),
Field( EmbedField(
name="When", name="When",
value=remind_at.strftime("%Y-%m-%d %H:%M UTC"), value=remind_at.strftime("%Y-%m-%d %H:%M UTC"),
inline=False, inline=False,
@ -158,19 +145,21 @@ class RemindmeCog(CacheCog):
) )
embed.set_author( embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator, name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url, 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) 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.""" """Build embed for paginator."""
fields = [] fields = []
for reminder in reminders: for reminder in reminders:
fields.append( fields.append(
Field( EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=f"{reminder.message}\n\u200b", value=f"{reminder.message}\n\u200b",
inline=False, inline=False,
@ -184,57 +173,49 @@ class RemindmeCog(CacheCog):
) )
embed.set_author( embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator, name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url, 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 return embed
@cog_ext.cog_subcommand( @slash_command(name="reminders", sub_cmd_name="list", sub_cmd_description="List reminders")
base="reminders", async def _list(self, ctx: InteractionContext) -> None:
name="list",
description="List reminders for a user",
)
async def _list(self, ctx: SlashContext) -> None:
exists = self.check_cache(ctx) exists = self.check_cache(ctx)
if exists: if exists:
await ctx.defer(hidden=True) await ctx.defer(ephemeral=True)
await ctx.send( await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}", f"Please use existing interaction: {exists['paginator']._message.jump_url}",
hidden=True, ephemeral=True,
) )
return return
reminders = Reminder.objects(user=ctx.author.id, active=True) reminders = Reminder.objects(user=ctx.author.id, active=True)
if not reminders: if not reminders:
await ctx.send("You have no reminders set.", hidden=True) await ctx.send("You have no reminders set.", ephemeral=True)
return return
embed = await self.get_reminders_embed(ctx, reminders) embed = await self.get_reminders_embed(ctx, reminders)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_subcommand( @slash_command(name="reminders", sub_cmd_name="delete", sub_cmd_description="Delete a reminder")
base="reminders", async def _delete(self, ctx: InteractionContext) -> None:
name="delete",
description="Delete a reminder",
)
async def _delete(self, ctx: SlashContext) -> None:
reminders = Reminder.objects(user=ctx.author.id, active=True) reminders = Reminder.objects(user=ctx.author.id, active=True)
if not reminders: if not reminders:
await ctx.send("You have no reminders set", hidden=True) await ctx.send("You have no reminders set", ephemeral=True)
return return
options = [] options = []
for reminder in reminders: for reminder in reminders:
option = create_select_option( option = SelectOption(
label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=str(reminder.id), value=str(reminder.id),
emoji="", emoji="",
) )
options.append(option) options.append(option)
select = create_select( select = Select(
options=options, options=options,
custom_id="to_delete", custom_id="to_delete",
placeholder="Select reminders to delete", placeholder="Select reminders to delete",
@ -242,7 +223,7 @@ class RemindmeCog(CacheCog):
max_values=len(reminders), max_values=len(reminders),
) )
components = [create_actionrow(select)] components = [ActionRow(select)]
embed = await self.get_reminders_embed(ctx, reminders) embed = await self.get_reminders_embed(ctx, reminders)
message = await ctx.send( message = await ctx.send(
content=f"You have {len(reminders)} reminder(s) set:", content=f"You have {len(reminders)} reminder(s) set:",
@ -251,23 +232,22 @@ class RemindmeCog(CacheCog):
) )
try: try:
context = await wait_for_component( context = await self.bot.wait_for_component(
self.bot, check=lambda x: ctx.author.id == x.context.author.id,
check=lambda x: ctx.author.id == x.author_id,
messages=message, messages=message,
timeout=60 * 5, 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() _ = Reminder.objects(user=ctx.author.id, id=ObjectId(to_delete)).delete()
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
fields = [] 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( fields.append(
Field( EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=reminder.message, value=reminder.message,
inline=False, inline=False,
@ -280,52 +260,23 @@ class RemindmeCog(CacheCog):
) )
embed.set_author( embed.set_author(
name=ctx.author.name + "#" + ctx.author.discriminator, name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.avatar_url, 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( await context.context.edit_origin(
content=f"Deleted {len(context.selected_options)} reminder(s)", content=f"Deleted {len(context.context.values)} reminder(s)",
components=components, components=components,
embed=embed, embed=embed,
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
await message.edit(components=components) 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: Snake) -> None:
def setup(bot: Bot) -> None:
"""Add RemindmeCog to J.A.R.V.I.S.""" """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.""" """J.A.R.V.I.S. Role Giver Cog."""
import asyncio import asyncio
from discord import Role from dis_snek import InteractionContext, Permissions, Scale, Snake
from discord.ext import commands from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.embed import EmbedField
from discord_slash.utils.manage_commands import create_option from dis_snek.models.discord.role import Role
from discord_slash.utils.manage_components import ( from dis_snek.models.snek.application_commands import (
create_actionrow, OptionTypes,
create_select, slash_command,
create_select_option, slash_option,
wait_for_component,
) )
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.db.models import Rolegiver
from jarvis.utils import build_embed from jarvis.utils import build_embed, get
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class RolegiverCog(commands.Cog): class RolegiverCog(Scale):
"""J.A.R.V.I.S. Role Giver Cog.""" """J.A.R.V.I.S. Role Giver Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
@cog_ext.cog_subcommand( @slash_command(
base="rolegiver", name="rolegiver", sub_cmd_name="add", sub_cmd_description="Add a role to rolegiver"
name="add",
description="Add a role to rolegiver",
options=[
create_option(
name="role",
description="Role to add",
option_type=8,
required=True,
) )
], @slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
@admin_or_permissions(manage_guild=True) async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
async def _rolegiver_add(self, ctx: SlashContext, role: Role) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first() setting = Rolegiver.objects(guild=ctx.guild.id).first()
if setting and role.id in setting.roles: 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 return
if not setting: if not setting:
setting = Rolegiver(guild=ctx.guild.id, roles=[]) setting = Rolegiver(guild=ctx.guild.id, roles=[])
if len(setting.roles) >= 20: 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 return
setting.roles.append(role.id) setting.roles.append(role.id)
@ -58,7 +49,7 @@ class RolegiverCog(commands.Cog):
for role_id in setting.roles: for role_id in setting.roles:
if role_id == role.id: if role_id == role.id:
continue continue
e_role = ctx.guild.get_role(role_id) e_role = await ctx.guild.get_role(role_id)
if not e_role: if not e_role:
continue continue
roles.append(e_role) 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" value = "\n".join([r.mention for r in roles]) if roles else "None"
fields = [ fields = [
Field(name="New Role", value=f"{role.mention}"), EmbedField(name="New Role", value=f"{role.mention}"),
Field(name="Existing Role(s)", value=value), EmbedField(name="Existing Role(s)", value=value),
] ]
embed = build_embed( embed = build_embed(
@ -77,61 +68,60 @@ class RolegiverCog(commands.Cog):
fields=fields, fields=fields,
) )
embed.set_thumbnail(url=ctx.guild.icon_url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author( embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url)
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.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) await ctx.send(embed=embed)
@cog_ext.cog_subcommand( @slash_command(
base="rolegiver", name="rolegiver", sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver"
name="remove",
description="Remove a role from rolegiver",
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_remove(self, ctx: SlashContext) -> None: async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first() setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles): 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 return
options = [] options = []
for role in setting.roles: for role in setting.roles:
role: Role = ctx.guild.get_role(role) role: Role = await ctx.guild.get_role(role)
option = create_select_option(label=role.name, value=str(role.id)) option = SelectOption(label=role.name, value=str(role.id))
options.append(option) options.append(option)
select = create_select( select = Select(
options=options, options=options,
custom_id="to_delete", custom_id="to_delete",
placeholder="Select roles to remove", placeholder="Select roles to remove",
min_values=1, min_values=1,
max_values=len(options), max_values=len(options),
) )
components = [create_actionrow(select)] components = [ActionRow(select)]
message = await ctx.send(content="\u200b", components=components) message = await ctx.send(content="\u200b", components=components)
try: try:
context = await wait_for_component( context = await self.bot.wait_for_component(
self.bot, check=lambda x: ctx.author.id == x.context.author.id,
check=lambda x: ctx.author.id == x.author.id, messages=message,
message=message,
timeout=60 * 1, 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.roles.remove(int(to_delete))
setting.save() setting.save()
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
roles = [] roles = []
for role_id in setting.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: if not e_role:
continue continue
roles.append(e_role) roles.append(e_role)
@ -140,9 +130,10 @@ class RolegiverCog(commands.Cog):
roles.sort(key=lambda x: -x.position) roles.sort(key=lambda x: -x.position)
value = "\n".join([r.mention for r in roles]) if roles else "None" 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 = [ fields = [
Field(name="Removed Role", value=f"{role.mention}"), EmbedField(name="Removed Role(s)", value=rvalue),
Field(name="Remaining Role(s)", value=value), EmbedField(name="Remaining Role(s)", value=value),
] ]
embed = build_embed( embed = build_embed(
@ -151,39 +142,34 @@ class RolegiverCog(commands.Cog):
fields=fields, fields=fields,
) )
embed.set_thumbnail(url=ctx.guild.icon_url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author( embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url)
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.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.context.edit_origin(
content=f"Removed {len(context.context.values)} role(s)",
await context.edit_origin(
content=f"Removed {len(context.selected_options)} role(s)",
embed=embed, embed=embed,
components=components, components=components,
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@cog_ext.cog_subcommand( @slash_command(name="rolegiver", sub_cmd_name="list", description="List rolegiver roles")
base="rolegiver", async def _rolegiver_list(self, ctx: InteractionContext) -> None:
name="list",
description="List roles rolegiver",
)
async def _rolegiver_list(self, ctx: SlashContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first() setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles): 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 return
roles = [] roles = []
for role_id in setting.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: if not e_role:
continue continue
roles.append(e_role) roles.append(e_role)
@ -199,59 +185,52 @@ class RolegiverCog(commands.Cog):
fields=[], fields=[],
) )
embed.set_thumbnail(url=ctx.guild.icon_url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author( embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name, name=ctx.author.display_name,
icon_url=ctx.author.avatar_url, 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) await ctx.send(embed=embed)
@cog_ext.cog_subcommand( @slash_command(name="role", sub_cmd_name="get", sub_cmd_description="Get a role")
base="role", @cooldown(bucket=Buckets.USER, rate=1, interval=10)
name="get", async def _role_get(self, ctx: InteractionContext) -> None:
description="Get a role from rolegiver",
)
@commands.cooldown(1, 10, commands.BucketType.user)
async def _role_get(self, ctx: SlashContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first() setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles): 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 return
options = [] options = []
for role in setting.roles: for role in setting.roles:
role: Role = ctx.guild.get_role(role) role: Role = await ctx.guild.get_role(role)
option = create_select_option(label=role.name, value=str(role.id)) option = SelectOption(label=role.name, value=str(role.id))
options.append(option) options.append(option)
select = create_select( select = Select(
options=options, options=options,
custom_id="to_delete",
placeholder="Select roles to add", placeholder="Select roles to add",
min_values=1, min_values=1,
max_values=len(options), max_values=len(options),
) )
components = [create_actionrow(select)] components = [ActionRow(select)]
message = await ctx.send(content="\u200b", components=components) message = await ctx.send(content="\u200b", components=components)
try: try:
context = await self.bot.wait_for_component(
context = await wait_for_component( check=lambda x: ctx.author.id == x.context.author.id,
self.bot,
check=lambda x: ctx.author.id == x.author.id,
messages=message, messages=message,
timeout=60 * 5, timeout=60 * 5,
) )
added_roles = [] added_roles = []
for role in context.selected_options: for role in context.context.values:
role = ctx.guild.get_role(int(role)) role = await ctx.guild.get_role(int(role))
added_roles.append(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 roles = ctx.author.roles
if 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" 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" value = "\n".join([r.mention for r in roles]) if roles else "None"
fields = [ fields = [
Field(name="Added Role(s)", value=avalue), EmbedField(name="Added Role(s)", value=avalue),
Field(name="Prior Role(s)", value=value), EmbedField(name="Prior Role(s)", value=value),
] ]
embed = build_embed( embed = build_embed(
title="User Given Role", 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, fields=fields,
) )
embed.set_thumbnail(url=ctx.guild.icon_url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author( embed.set_author(
name=ctx.author.nick if ctx.author.nick else ctx.author.name, name=ctx.author.display_name,
icon_url=ctx.author.avatar_url, icon_url=ctx.author.display_avatar.url,
) )
embed.set_footer(text=f"{ctx.author.name}#{ctx.author.discriminator} | {ctx.author.id}") embed.set_footer(
for row in components: text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}"
for component in row["components"]: )
component["disabled"] = True
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: except asyncio.TimeoutError:
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@cog_ext.cog_subcommand( @slash_command(name="role", sub_cmd_name="remove", sub_cmd_description="Remove a role")
base="role", @cooldown(bucket=Buckets.USER, rate=1, interval=10)
name="forfeit", async def _role_remove(self, ctx: InteractionContext) -> None:
description="Have rolegiver take away role", user_roles = ctx.author.roles
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:
setting = Rolegiver.objects(guild=ctx.guild.id).first() setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or (setting and not setting.roles): 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 return
elif role.id not in setting.roles: elif not any(x.id in setting.roles for x in user_roles):
await ctx.send("Role not in rolegiver", hidden=True) await ctx.send("You have no rolegiver roles", ephemeral=True)
return
elif role not in ctx.author.roles:
await ctx.send("You do not have that role", hidden=True)
return 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 select = Select(
if roles: options=options,
roles.sort(key=lambda x: -x.position) custom_id="to_remove",
_ = roles.pop(-1) placeholder="Select roles to remove",
min_values=1,
max_values=len(options),
)
components = [ActionRow(select)]
value = "\n".join([r.mention for r in roles]) if roles else "None" message = await ctx.send(content="\u200b", components=components)
try:
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.context.author.id,
messages=message,
timeout=60 * 5,
)
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)
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 = [ fields = [
Field(name="Taken Role", value=f"{role.mention}"), EmbedField(name="Removed Role(s)", value=rvalue),
Field(name="Remaining Role(s)", value=value), EmbedField(name="Remaining Role(s)", value=value),
] ]
embed = build_embed( embed = build_embed(
title="User Forfeited Role", title="User Forfeited Role",
description=f"{role.mention} taken from {ctx.author.mention}", description=f"{len(removed_roles)} role(s) removed from {ctx.author.mention}",
fields=fields, fields=fields,
) )
embed.set_thumbnail(url=ctx.guild.icon_url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author( embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url)
name=ctx.author.nick if ctx.author.nick else ctx.author.name,
icon_url=ctx.author.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}") for row in components:
for component in row.components:
component.disabled = True
await ctx.send(embed=embed) await context.context.edit_origin(embed=embed, components=components, content="\u200b")
@cog_ext.cog_subcommand( except asyncio.TimeoutError:
base="rolegiver", for row in components:
name="cleanup", for component in row.components:
description="Cleanup rolegiver roles", 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) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_cleanup(self, ctx: SlashContext) -> None: async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first() setting = Rolegiver.objects(guild=ctx.guild.id).first()
if not setting or not setting.roles: if not setting or not setting.roles:
await ctx.send("Rolegiver has no roles", hidden=True) await ctx.send("Rolegiver has no roles", ephemeral=True)
guild_roles = await ctx.guild.fetch_roles() guild_role_ids = [r.id for r in ctx.guild.roles]
guild_role_ids = [x.id for x in guild_roles]
for role_id in setting.roles: for role_id in setting.roles:
if role_id not in guild_role_ids: if role_id not in guild_role_ids:
setting.roles.remove(role_id) setting.roles.remove(role_id)
@ -364,6 +367,6 @@ class RolegiverCog(commands.Cog):
await ctx.send("Rolegiver cleanup finished") 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.""" """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.""" """J.A.R.V.I.S. Settings Management Cog."""
from typing import Any from typing import Any
from dis_snek.models.snek.command import check
from discord import Role, TextChannel from discord import Role, TextChannel
from discord.ext import commands from discord.ext import commands
from discord.utils import find from discord.utils import find
@ -33,46 +34,24 @@ class SettingsCog(commands.Cog):
"""Delete a guild setting.""" """Delete a guild setting."""
return Setting.objects(setting=setting, guild=guild).delete() 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( @cog_ext.cog_subcommand(
base="settings", base="settings",
subcommand_group="set", subcommand_group="set",
name="modlog", name="modlog",
description="Set modlog channel", description="Set modlog channel",
options=[ choices=[
create_option( create_option(
name="channel", name="channel",
description="Modlog channel", description="Modlog channel",
option_type=7, opt_type=7,
required=True, 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: async def _set_modlog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel): 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 return
self.update_settings("modlog", channel.id, ctx.guild.id) self.update_settings("modlog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New modlog channel is {channel.mention}") await ctx.send(f"Settings applied. New modlog channel is {channel.mention}")
@ -82,19 +61,19 @@ class SettingsCog(commands.Cog):
subcommand_group="set", subcommand_group="set",
name="userlog", name="userlog",
description="Set userlog channel", description="Set userlog channel",
options=[ choices=[
create_option( create_option(
name="channel", name="channel",
description="Userlog channel", description="Userlog channel",
option_type=7, opt_type=7,
required=True, 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: async def _set_userlog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel): 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 return
self.update_settings("userlog", channel.id, ctx.guild.id) self.update_settings("userlog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New userlog channel is {channel.mention}") await ctx.send(f"Settings applied. New userlog channel is {channel.mention}")
@ -104,16 +83,16 @@ class SettingsCog(commands.Cog):
subcommand_group="set", subcommand_group="set",
name="massmention", name="massmention",
description="Set massmention amount", description="Set massmention amount",
options=[ choices=[
create_option( create_option(
name="amount", name="amount",
description="Amount of mentions (0 to disable)", description="Amount of mentions (0 to disable)",
option_type=4, opt_type=4,
required=True, 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: async def _set_massmention(self, ctx: SlashContext, amount: int) -> None:
await ctx.defer() await ctx.defer()
self.update_settings("massmention", amount, ctx.guild.id) self.update_settings("massmention", amount, ctx.guild.id)
@ -124,16 +103,16 @@ class SettingsCog(commands.Cog):
subcommand_group="set", subcommand_group="set",
name="verified", name="verified",
description="Set verified role", description="Set verified role",
options=[ choices=[
create_option( create_option(
name="role", name="role",
description="verified role", description="verified role",
option_type=8, opt_type=8,
required=True, 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: async def _set_verified(self, ctx: SlashContext, role: Role) -> None:
await ctx.defer() await ctx.defer()
self.update_settings("verified", role.id, ctx.guild.id) self.update_settings("verified", role.id, ctx.guild.id)
@ -144,16 +123,16 @@ class SettingsCog(commands.Cog):
subcommand_group="set", subcommand_group="set",
name="unverified", name="unverified",
description="Set unverified role", description="Set unverified role",
options=[ choices=[
create_option( create_option(
name="role", name="role",
description="Unverified role", description="Unverified role",
option_type=8, opt_type=8,
required=True, 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: async def _set_unverified(self, ctx: SlashContext, role: Role) -> None:
await ctx.defer() await ctx.defer()
self.update_settings("unverified", role.id, ctx.guild.id) self.update_settings("unverified", role.id, ctx.guild.id)
@ -164,41 +143,28 @@ class SettingsCog(commands.Cog):
subcommand_group="set", subcommand_group="set",
name="noinvite", name="noinvite",
description="Set if invite deletion should happen", description="Set if invite deletion should happen",
options=[ choices=[
create_option( create_option(
name="active", name="active",
description="Active?", description="Active?",
option_type=4, opt_type=4,
required=True, 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: async def _set_invitedel(self, ctx: SlashContext, active: int) -> None:
await ctx.defer() await ctx.defer()
self.update_settings("noinvite", bool(active), ctx.guild.id) self.update_settings("noinvite", bool(active), ctx.guild.id)
await ctx.send(f"Settings applied. Automatic invite active: {bool(active)}") 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( @cog_ext.cog_subcommand(
base="settings", base="settings",
subcommand_group="unset", subcommand_group="unset",
name="modlog", name="modlog",
description="Unset modlog channel", 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: async def _unset_modlog(self, ctx: SlashContext) -> None:
self.delete_settings("modlog", ctx.guild.id) self.delete_settings("modlog", ctx.guild.id)
await ctx.send("Setting removed.") await ctx.send("Setting removed.")
@ -209,7 +175,7 @@ class SettingsCog(commands.Cog):
name="userlog", name="userlog",
description="Unset userlog channel", 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: async def _unset_userlog(self, ctx: SlashContext) -> None:
self.delete_settings("userlog", ctx.guild.id) self.delete_settings("userlog", ctx.guild.id)
await ctx.send("Setting removed.") await ctx.send("Setting removed.")
@ -220,7 +186,7 @@ class SettingsCog(commands.Cog):
name="massmention", name="massmention",
description="Unet massmention amount", description="Unet massmention amount",
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(manage_guild=True))
async def _massmention(self, ctx: SlashContext) -> None: async def _massmention(self, ctx: SlashContext) -> None:
await ctx.defer() await ctx.defer()
self.delete_settings("massmention", ctx.guild.id) self.delete_settings("massmention", ctx.guild.id)
@ -232,7 +198,7 @@ class SettingsCog(commands.Cog):
name="verified", name="verified",
description="Unset verified role", description="Unset verified role",
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(manage_guild=True))
async def _verified(self, ctx: SlashContext) -> None: async def _verified(self, ctx: SlashContext) -> None:
await ctx.defer() await ctx.defer()
self.delete_settings("verified", ctx.guild.id) self.delete_settings("verified", ctx.guild.id)
@ -244,14 +210,14 @@ class SettingsCog(commands.Cog):
name="unverified", name="unverified",
description="Unset unverified role", description="Unset unverified role",
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(manage_guild=True))
async def _unverified(self, ctx: SlashContext) -> None: async def _unverified(self, ctx: SlashContext) -> None:
await ctx.defer() await ctx.defer()
self.delete_settings("unverified", ctx.guild.id) self.delete_settings("unverified", ctx.guild.id)
await ctx.send("Setting removed.") await ctx.send("Setting removed.")
@cog_ext.cog_subcommand(base="settings", name="view", description="View settings") @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: async def _view(self, ctx: SlashContext) -> None:
settings = Setting.objects(guild=ctx.guild.id) settings = Setting.objects(guild=ctx.guild.id)
@ -272,7 +238,7 @@ class SettingsCog(commands.Cog):
value = "||`[redacted]`||" value = "||`[redacted]`||"
elif setting.setting == "rolegiver": elif setting.setting == "rolegiver":
value = "" value = ""
for role in setting.value: for _role in setting.value:
nvalue = find(lambda x: x.id == value, ctx.guild.roles) nvalue = find(lambda x: x.id == value, ctx.guild.roles)
if value: if value:
value += "\n" + nvalue.mention value += "\n" + nvalue.mention
@ -285,7 +251,7 @@ class SettingsCog(commands.Cog):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_subcommand(base="settings", name="clear", description="Clear all settings") @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: async def _clear(self, ctx: SlashContext) -> None:
deleted = Setting.objects(guild=ctx.guild.id).delete() deleted = Setting.objects(guild=ctx.guild.id).delete()
await ctx.send(f"Guild settings cleared: `{deleted is not None}`") 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: def setup(bot: commands.Bot) -> None:
"""Add SettingsCog to J.A.R.V.I.S.""" """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.""" """J.A.R.V.I.S. Starboard Cog."""
from discord import TextChannel from dis_snek import InteractionContext, Permissions, Scale, Snake
from discord.ext import commands from dis_snek.models.discord.channel import GuildText
from discord.utils import find from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.message import Message
from discord_slash.context import MenuContext from dis_snek.models.snek.application_commands import (
from discord_slash.model import ContextMenuType, SlashMessage CommandTypes,
from discord_slash.utils.manage_commands import create_option OptionTypes,
from discord_slash.utils.manage_components import ( context_menu,
create_actionrow, slash_command,
create_select, slash_option,
create_select_option,
wait_for_component,
) )
from dis_snek.models.snek.command import check
from jarvis.db.models import Star, Starboard 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 from jarvis.utils.permissions import admin_or_permissions
supported_images = [ supported_images = [
@ -26,19 +25,15 @@ supported_images = [
] ]
class StarboardCog(commands.Cog): class StarboardCog(Scale):
"""J.A.R.V.I.S. Starboard Cog.""" """J.A.R.V.I.S. Starboard Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
@cog_ext.cog_subcommand( @slash_command(name="starboard", sub_cmd_name="list", sub_cmd_description="List all starboards")
base="starboard", @check(admin_or_permissions(Permissions.MANAGE_GUILD))
name="list", async def _list(self, ctx: InteractionContext) -> None:
description="Lists all Starboards",
)
@admin_or_permissions(manage_guild=True)
async def _list(self, ctx: SlashContext) -> None:
starboards = Starboard.objects(guild=ctx.guild.id) starboards = Starboard.objects(guild=ctx.guild.id)
if starboards != []: if starboards != []:
message = "Available Starboards:\n" message = "Available Starboards:\n"
@ -48,39 +43,35 @@ class StarboardCog(commands.Cog):
else: else:
await ctx.send("No Starboards available.") await ctx.send("No Starboards available.")
@cog_ext.cog_subcommand( @slash_command(
base="starboard", name="starboard", sub_cmd_name="create", sub_cmd_description="Create a starboard"
name="create", )
description="Create a starboard", @slash_option(
options=[
create_option(
name="channel", name="channel",
description="Starboard channel", description="Starboard channel",
option_type=7, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
),
],
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _create(self, ctx: SlashContext, channel: TextChannel) -> None: async def _create(self, ctx: InteractionContext, channel: GuildText) -> None:
if channel not in ctx.guild.channels: if channel not in ctx.guild.channels:
await ctx.send( await ctx.send(
"Channel not in guild. Choose an existing channel.", "Channel not in guild. Choose an existing channel.",
hidden=True, ephemeral=True,
) )
return return
if not isinstance(channel, TextChannel): if not isinstance(channel, GuildText):
await ctx.send("Channel must be a TextChannel", hidden=True) await ctx.send("Channel must be a GuildText", ephemeral=True)
return return
exists = Starboard.objects(channel=channel.id, guild=ctx.guild.id).first() exists = Starboard.objects(channel=channel.id, guild=ctx.guild.id).first()
if exists: 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 return
count = Starboard.objects(guild=ctx.guild.id).count() count = Starboard.objects(guild=ctx.guild.id).count()
if count >= 25: if count >= 25:
await ctx.send("25 starboard limit reached", hidden=True) await ctx.send("25 starboard limit reached", ephemeral=True)
return return
_ = Starboard( _ = Starboard(
@ -90,95 +81,89 @@ class StarboardCog(commands.Cog):
).save() ).save()
await ctx.send(f"Starboard created. Check it out at {channel.mention}.") await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
@cog_ext.cog_subcommand( @slash_command(
base="starboard", name="starboard", sub_cmd_name="delete", sub_cmd_description="Delete a starboard"
name="delete", )
description="Delete a starboard", @slash_option(
options=[
create_option(
name="channel", name="channel",
description="Starboard channel", description="Starboard channel",
option_type=7, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
),
],
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _delete(self, ctx: SlashContext, channel: TextChannel) -> None: async def _delete(self, ctx: InteractionContext, channel: GuildText) -> None:
deleted = Starboard.objects(channel=channel.id, guild=ctx.guild.id).delete() deleted = Starboard.objects(channel=channel.id, guild=ctx.guild.id).delete()
if deleted: if deleted:
_ = Star.objects(starboard=channel.id).delete() _ = 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: 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) @context_menu(name="Star Message", context_type=CommandTypes.MESSAGE)
async def _star_message(self, ctx: MenuContext) -> None: async def _star_message(self, ctx: InteractionContext) -> None:
await self._star_add.invoke(ctx, ctx.target_message) await self._star_add._can_run(ctx)
await self._star_add.callback(ctx, message=str(ctx.target_id))
@cog_ext.cog_subcommand( @slash_command(name="star", sub_cmd_name="add", description="Star a message")
base="star", @slash_option(
name="add", name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
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,
),
],
) )
@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( async def _star_add(
self, self,
ctx: SlashContext, ctx: InteractionContext,
message: str, message: str,
channel: TextChannel = None, channel: GuildText = None,
) -> None: ) -> None:
if not channel: if not channel:
channel = ctx.channel channel = ctx.channel
starboards = Starboard.objects(guild=ctx.guild.id) starboards = Starboard.objects(guild=ctx.guild.id)
if not starboards: if not starboards:
await ctx.send("No starboards exist.", hidden=True) await ctx.send("No starboards exist.", ephemeral=True)
return return
await ctx.defer() 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 = [] channel_list = []
for starboard in starboards: for starboard in starboards:
channel_list.append(find(lambda x: x.id == starboard.channel, ctx.guild.channels)) 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, options=select_channels,
min_values=1, min_values=1,
max_values=1, max_values=1,
) )
components = [create_actionrow(select)] components = [ActionRow(select)]
msg = await ctx.send(content="Choose a starboard", components=components) msg = await ctx.send(content="Choose a starboard", components=components)
com_ctx = await wait_for_component( com_ctx = await self.bot.wait_for_component(
self.bot,
messages=msg, messages=msg,
components=components, 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])] starboard = channel_list[int(com_ctx.context.values[0])]
if not isinstance(message, SlashMessage):
if message.startswith("https://"):
message = message.split("/")[-1]
message = await channel.fetch_message(message)
exists = Star.objects( exists = Star.objects(
message=message.id, message=message.id,
@ -190,7 +175,7 @@ class StarboardCog(commands.Cog):
if exists: if exists:
await ctx.send( await ctx.send(
f"Message already sent to Starboard {starboard.mention}", f"Message already sent to Starboard {starboard.mention}",
hidden=True, ephemeral=True,
) )
return return
@ -215,9 +200,9 @@ class StarboardCog(commands.Cog):
timestamp=message.created_at, timestamp=message.created_at,
) )
embed.set_author( embed.set_author(
name=message.author.name, name=message.author.display_name,
url=message.jump_url, 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) embed.set_footer(text=message.guild.name + " | " + message.channel.name)
if image_url: if image_url:
@ -236,47 +221,38 @@ class StarboardCog(commands.Cog):
active=True, active=True,
).save() ).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}", content=f"Message saved to Starboard.\nSee it in {starboard.mention}",
components=components, components=components,
) )
@cog_ext.cog_subcommand( @slash_command(name="star", sub_cmd_name="delete", description="Delete a starred message")
base="star", @slash_option(
name="delete", name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
description="Delete a starred message", )
options=[ @slash_option(
create_option(
name="id",
description="Star to delete",
option_type=4,
required=True,
),
create_option(
name="starboard", name="starboard",
description="Starboard to delete star from", description="Starboard to delete star from",
option_type=7, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
),
],
) )
@admin_or_permissions(manage_guild=True) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _star_delete( async def _star_delete(
self, self,
ctx: SlashContext, ctx: InteractionContext,
id: int, id: int,
starboard: TextChannel, starboard: GuildText,
) -> None: ) -> None:
if not isinstance(starboard, TextChannel): if not isinstance(starboard, GuildText):
await ctx.send("Channel must be a TextChannel", hidden=True) await ctx.send("Channel must be a GuildText channel", ephemeral=True)
return return
exists = Starboard.objects(channel=starboard.id, guild=ctx.guild.id).first() exists = Starboard.objects(channel=starboard.id, guild=ctx.guild.id).first()
if not exists: if not exists:
await ctx.send( await ctx.send(
f"Starboard does not exist in {starboard.mention}. Please create it first", f"Starboard does not exist in {starboard.mention}. Please create it first",
hidden=True, ephemeral=True,
) )
return return
@ -287,19 +263,19 @@ class StarboardCog(commands.Cog):
active=True, active=True,
).first() ).first()
if not star: 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 return
message = await starboard.fetch_message(star.star) message = await starboard.get_message(star.star)
if message: if message:
await message.delete() await message.delete()
star.active = False star.active = False
star.save() 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.""" """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.""" """J.A.R.V.I.S. Twitter Cog."""
import asyncio import asyncio
import logging
import tweepy import tweepy
from bson import ObjectId from bson import ObjectId
from discord import TextChannel from dis_snek import InteractionContext, Permissions, Scale, Snake
from discord.ext import commands from dis_snek.models.discord.channel import GuildText
from discord.ext.tasks import loop from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from discord.utils import find from dis_snek.models.snek.application_commands import (
from discord_slash import SlashContext, cog_ext OptionTypes,
from discord_slash.model import SlashCommandOptionType as COptionType SlashCommandChoice,
from discord_slash.utils.manage_commands import create_choice, create_option slash_command,
from discord_slash.utils.manage_components import ( slash_option,
create_actionrow,
create_select,
create_select_option,
wait_for_component,
) )
from dis_snek.models.snek.command import check
from jarvis.config import get_config from jarvis.config import get_config
from jarvis.db.models import Twitter from jarvis.db.models import Twitter
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
logger = logging.getLogger("discord")
class TwitterCog(Scale):
class TwitterCog(commands.Cog):
"""J.A.R.V.I.S. Twitter Cog.""" """J.A.R.V.I.S. Twitter Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
config = get_config() 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.api = tweepy.API(auth)
self._tweets.start()
self._guild_cache = {} self._guild_cache = {}
self._channel_cache = {} self._channel_cache = {}
@loop(seconds=30) @slash_command(name="twitter", sub_cmd_name="follow", description="Follow a Twitter acount")
async def _tweets(self) -> None: @slash_option(
twitters = Twitter.objects(active=True) name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True
handles = Twitter.objects.distinct("handle") )
twitter_data = {} @slash_option(
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", name="channel",
description="Channel to post tweets into", description="Channel to post tweets to",
option_type=COptionType.CHANNEL, opt_type=OptionTypes.CHANNEL,
required=True, required=True,
), )
create_option( @slash_option(
name="retweets", name="retweets",
description="Mirror re-tweets?", description="Mirror re-tweets?",
option_type=COptionType.STRING, opt_type=OptionTypes.STRING,
required=False, required=False,
choices=[create_choice(name="Yes", value="Yes"), create_choice(name="No", value="No")], 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( 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: ) -> None:
handle = handle.lower()
retweets = retweets == "Yes" retweets = retweets == "Yes"
if len(handle) > 15: if len(handle) > 15:
await ctx.send("Invalid Twitter handle", hidden=True) await ctx.send("Invalid Twitter handle", ephemeral=True)
return return
if not isinstance(channel, TextChannel): if not isinstance(channel, GuildText):
await ctx.send("Channel must be a text channel", hidden=True) await ctx.send("Channel must be a text channel", ephemeral=True)
return return
try: 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: 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 return
count = Twitter.objects(guild=ctx.guild.id).count() count = Twitter.objects(guild=ctx.guild.id).count()
if count >= 12: 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 return
exists = Twitter.objects(handle=handle, guild=ctx.guild.id) exists = Twitter.objects(twitter_id=account.id, guild=ctx.guild.id)
if exists: 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 return
t = Twitter( t = Twitter(
handle=handle, handle=account.screen_name,
twitter_id=account.id,
guild=ctx.guild.id, guild=ctx.guild.id,
channel=channel.id, channel=channel.id,
admin=ctx.author.id, admin=ctx.author.id,
@ -137,27 +99,25 @@ class TwitterCog(commands.Cog):
await ctx.send(f"Now following `@{handle}` in {channel.mention}") await ctx.send(f"Now following `@{handle}` in {channel.mention}")
@cog_ext.cog_subcommand( @slash_command(name="twitter", sub_cmd_name="unfollow", description="Unfollow Twitter accounts")
base="twitter", @check(admin_or_permissions(Permissions.MANAGE_GUILD))
name="unfollow", async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
description="Unfollow Twitter accounts",
)
@admin_or_permissions(manage_guild=True)
async def _twitter_unfollow(self, ctx: SlashContext) -> None:
twitters = Twitter.objects(guild=ctx.guild.id) twitters = Twitter.objects(guild=ctx.guild.id)
if not twitters: 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 return
options = [] options = []
handlemap = {str(x.id): x.handle for x in twitters} handlemap = {str(x.id): x.handle for x in twitters}
for twitter 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) 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) block = "\n".join(x.handle for x in twitters)
message = await ctx.send( message = await ctx.send(
content=f"You are following the following accounts:\n```\n{block}\n```\n\n" content=f"You are following the following accounts:\n```\n{block}\n```\n\n"
@ -166,52 +126,58 @@ class TwitterCog(commands.Cog):
) )
try: try:
context = await wait_for_component( context = await self.bot.wait_for_component(
self.bot, check=lambda x: ctx.author.id == x.author.id, messages=message, timeout=60 * 5 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() _ = Twitter.objects(guild=ctx.guild.id, id=ObjectId(to_delete)).delete()
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True 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) 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: except asyncio.TimeoutError:
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@cog_ext.cog_subcommand( @slash_command(
base="twitter", name="twitter", sub_cmd_name="retweets", description="Modify followed Twitter accounts"
name="retweets", )
description="Modify followed Twitter accounts", @slash_option(
options=[
create_option(
name="retweets", name="retweets",
description="Mirror re-tweets?", description="Mirror re-tweets?",
option_type=COptionType.STRING, opt_type=OptionTypes.STRING,
required=True, required=False,
choices=[create_choice(name="Yes", value="Yes"), create_choice(name="No", value="No")], 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_modify(self, ctx: SlashContext, retweets: str) -> None: async def _twitter_modify(self, ctx: InteractionContext, retweets: str) -> None:
retweets = retweets == "Yes" retweets = retweets == "Yes"
twitters = Twitter.objects(guild=ctx.guild.id) twitters = Twitter.objects(guild=ctx.guild.id)
if not twitters: 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 return
options = [] options = []
for twitter 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) 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) block = "\n".join(x.handle for x in twitters)
message = await ctx.send( message = await ctx.send(
content=f"You are following the following accounts:\n```\n{block}\n```\n\n" content=f"You are following the following accounts:\n```\n{block}\n```\n\n"
@ -220,30 +186,38 @@ class TwitterCog(commands.Cog):
) )
try: try:
context = await wait_for_component( context = await self.bot.wait_for_component(
self.bot, check=lambda x: ctx.author.id == x.author.id, messages=message, timeout=60 * 5 check=lambda x: ctx.author.id == x.author.id,
messages=message,
timeout=60 * 5,
) )
handlemap = {str(x.id): x.handle for x in twitters} 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 = Twitter.objects(guild=ctx.guild.id, id=ObjectId(to_update)).first()
t.retweets = retweets t.retweets = retweets
t.save() t.save()
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
block = "\n".join(handlemap[x] for x in context.selected_options)
await context.edit_origin( block = "\n".join(handlemap[x] for x in context.context.values)
content=f"{'Unfollowed' if not retweets else 'Followed'} retweets from the following:" await context.context.edit_origin(
f"\n```\n{block}\n```", content=(
f"{'Unfollowed' if not retweets else 'Followed'} "
"retweets from the following:"
f"\n```\n{block}\n```"
),
components=components, components=components,
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
for row in components: for row in components:
for component in row["components"]: for component in row.components:
component["disabled"] = True component.disabled = True
await message.edit(components=components) 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.""" """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.""" """J.A.R.V.I.S. Utility Cog."""
import platform
import re import re
import secrets import secrets
import string import string
from io import BytesIO from io import BytesIO
import discord
import discord_slash
import numpy as np import numpy as np
from discord import File, Guild, Role, User from dis_snek import InteractionContext, Scale, Snake, const
from discord.ext import commands from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice
from discord_slash import SlashContext, cog_ext from dis_snek.models.discord.embed import EmbedField
from discord_slash.utils.manage_commands import create_choice, create_option 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 from PIL import Image
import jarvis import jarvis
from jarvis import jarvis_self
from jarvis.config import get_config from jarvis.config import get_config
from jarvis.data import pigpen from jarvis.data import pigpen
from jarvis.data.robotcamo import emotes, hk, names from jarvis.data.robotcamo import emotes, hk, names
from jarvis.utils import build_embed, convert_bytesize, get_repo_hash from jarvis.utils import build_embed, get_repo_hash
from jarvis.utils.field import Field
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA") 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. Utility functions for J.A.R.V.I.S.
Mostly system utility functions, but may change over time Mostly system utility functions, but may change over time
""" """
def __init__(self, bot: commands.Cog): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.config = get_config() self.config = get_config()
@cog_ext.cog_slash( @slash_command(name="status", description="Retrieve J.A.R.V.I.S. status")
name="status", @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
description="Retrieve J.A.R.V.I.S. status", async def _status(self, ctx: InteractionContext) -> None:
)
@commands.cooldown(1, 30, commands.BucketType.channel)
async def _status(self, ctx: SlashContext) -> None:
title = "J.A.R.V.I.S. Status" title = "J.A.R.V.I.S. Status"
desc = "All systems online" desc = "All systems online"
color = "#98CCDA" color = "#3498db"
fields = [] fields = []
with jarvis_self.oneshot():
fields.append(Field("CPU Usage", jarvis_self.cpu_percent())) fields.append(EmbedField(name="dis-snek", value=const.__version__))
fields.append( fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False))
Field( fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False))
"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) embed = build_embed(title=title, description=desc, fields=fields, color=color)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_slash( @slash_command(
name="logo", name="logo",
description="Get the current logo", description="Get the current logo",
) )
@commands.cooldown(1, 30, commands.BucketType.channel) @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _logo(self, ctx: SlashContext) -> None: async def _logo(self, ctx: InteractionContext) -> None:
with BytesIO() as image_bytes: with BytesIO() as image_bytes:
JARVIS_LOGO.save(image_bytes, "PNG") JARVIS_LOGO.save(image_bytes, "PNG")
image_bytes.seek(0) image_bytes.seek(0)
logo = File(image_bytes, filename="logo.png") logo = File(image_bytes, file_name="logo.png")
await ctx.send(file=logo) await ctx.send(file=logo)
@cog_ext.cog_slash(name="rchk", description="Robot Camo HK416") @slash_command(name="rchk", description="Robot Camo HK416")
async def _rchk(self, ctx: SlashContext) -> None: async def _rchk(self, ctx: InteractionContext) -> None:
await ctx.send(content=hk) await ctx.send(content=hk)
@cog_ext.cog_slash( @slash_command(
name="rcauto", name="rcauto",
description="Automates robot camo letters", description="Automates robot camo letters",
options=[ )
create_option( @slash_option(
name="text", name="text",
description="Text to camo-ify", description="Text to camo-ify",
option_type=3, opt_type=OptionTypes.STRING,
required=True, required=True,
) )
], async def _rcauto(self, ctx: InteractionContext, text: str) -> None:
)
async def _rcauto(self, ctx: SlashContext, text: str) -> None:
to_send = "" to_send = ""
if len(text) == 1 and not re.match(r"^[A-Z0-9-()$@!?^'#. ]$", text.upper()): 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 return
for letter in text.upper(): for letter in text.upper():
if letter == " ": if letter == " ":
to_send += " " to_send += " "
elif re.match(r"^[A-Z0-9-()$@!?^'#.]$", letter): elif re.match(r"^[A-Z0-9-()$@!?^'#.]$", letter):
id = emotes[letter] id = emotes[letter]
if ctx.author.is_on_mobile():
to_send += f":{names[id]}:" to_send += f":{names[id]}:"
else:
to_send += f"<:{names[id]}:{id}>"
if len(to_send) > 2000: if len(to_send) > 2000:
await ctx.send("Too long.", hidden=True) await ctx.send("Too long.", ephemeral=True)
else: else:
await ctx.send(to_send) await ctx.send(to_send)
@cog_ext.cog_slash( @slash_command(name="avatar", description="Get a user avatar")
name="avatar", @slash_option(
description="Get a user avatar",
options=[
create_option(
name="user", name="user",
description="User to view avatar of", description="User to view avatar of",
option_type=6, opt_type=OptionTypes.USER,
required=False, required=False,
) )
], @cooldown(bucket=Buckets.USER, rate=1, interval=5)
) async def _avatar(self, ctx: InteractionContext, user: User = None) -> None:
@commands.cooldown(1, 5, commands.BucketType.user)
async def _avatar(self, ctx: SlashContext, user: User = None) -> None:
if not user: if not user:
user = ctx.author user = ctx.author
avatar = user.avatar_url avatar = user.display_avatar.url
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE") embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
embed.set_image(url=avatar) 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) await ctx.send(embed=embed)
@cog_ext.cog_slash( @slash_command(
name="roleinfo", name="roleinfo",
description="Get role info", description="Get role info",
options=[ )
create_option( @slash_option(
name="role", name="role",
description="Role to get info of", description="Role to get info of",
option_type=8, opt_type=OptionTypes.ROLE,
required=True, required=True,
) )
], async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None:
)
async def _roleinfo(self, ctx: SlashContext, role: Role) -> None:
fields = [ fields = [
Field(name="ID", value=role.id), EmbedField(name="ID", value=str(role.id), inline=True),
Field(name="Name", value=role.name), EmbedField(name="Name", value=role.name, inline=True),
Field(name="Color", value=str(role.color)), EmbedField(name="Color", value=str(role.color.hex), inline=True),
Field(name="Mention", value=f"`{role.mention}`"), EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
Field(name="Hoisted", value="Yes" if role.hoist else "No"), EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
Field(name="Position", value=str(role.position)), EmbedField(name="Position", value=str(role.position), inline=True),
Field(name="Mentionable", value="Yes" if role.mentionable else "No"), 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( embed = build_embed(
title="", title="",
description="", description="",
fields=fields, fields=fields,
color=str(role.color), color=role.color,
timestamp=role.created_at, timestamp=role.created_at,
) )
embed.set_footer(text="Role Created") embed.set_footer(text="Role Created")
@ -170,46 +153,47 @@ class UtilCog(commands.Cog):
fill = a > 0 fill = a > 0
data[..., :-1][fill.T] = list(role.color.to_rgb()) data[..., :-1][fill.T] = list(role.color.rgb)
im = Image.fromarray(data) im = Image.fromarray(data)
with BytesIO() as image_bytes: with BytesIO() as image_bytes:
im.save(image_bytes, "PNG") im.save(image_bytes, "PNG")
image_bytes.seek(0) 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) await ctx.send(embed=embed, file=color_show)
@cog_ext.cog_slash( @slash_command(
name="userinfo", name="userinfo",
description="Get user info", description="Get user info",
options=[ )
create_option( @slash_option(
name="user", name="user",
description="User to get info of", description="User to get info of",
option_type=6, opt_type=OptionTypes.USER,
required=False, required=False,
) )
], async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
)
async def _userinfo(self, ctx: SlashContext, user: User = None) -> None:
if not user: if not user:
user = ctx.author user = ctx.author
user_roles = user.roles user_roles = user.roles
if user_roles: if user_roles:
user_roles = sorted(user.roles, key=lambda x: -x.position) 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 = [ fields = [
Field( EmbedField(
name="Joined", 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", 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)}]", name=f"Roles [{len(user_roles)}]",
value=" ".join([x.mention for x in user_roles]) if user_roles else "None", value=" ".join([x.mention for x in user_roles]) if user_roles else "None",
inline=False, inline=False,
@ -220,80 +204,82 @@ class UtilCog(commands.Cog):
title="", title="",
description=user.mention, description=user.mention,
fields=fields, 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_author(
embed.set_thumbnail(url=user.avatar_url) 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}") embed.set_footer(text=f"ID: {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_slash(name="serverinfo", description="Get server info") @slash_command(name="serverinfo", description="Get server info")
async def _server_info(self, ctx: SlashContext) -> None: async def _server_info(self, ctx: InteractionContext) -> None:
guild: Guild = ctx.guild 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 owner = f"{owner.username}#{owner.discriminator}" if owner else "||`[redacted]`||"
categories = len(guild.categories)
text_channels = len(guild.text_channels) categories = len([x for x in guild.channels if isinstance(x, GuildCategory)])
voice_channels = len(guild.voice_channels) 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 members = guild.member_count
roles = len(guild.roles) 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 = [ fields = [
Field(name="Owner", value=owner), EmbedField(name="Owner", value=owner, inline=True),
Field(name="Region", value=region), EmbedField(name="Channel Categories", value=str(categories), inline=True),
Field(name="Channel Categories", value=categories), EmbedField(name="Text Channels", value=str(text_channels), inline=True),
Field(name="Text Channels", value=text_channels), EmbedField(name="Voice Channels", value=str(voice_channels), inline=True),
Field(name="Voice Channels", value=voice_channels), EmbedField(name="Threads", value=str(threads), inline=True),
Field(name="Members", value=members), EmbedField(name="Members", value=str(members), inline=True),
Field(name="Roles", value=roles), EmbedField(name="Roles", value=str(roles), inline=True),
] ]
if len(role_list) < 1024: 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 = build_embed(title="", description="", fields=fields, timestamp=guild.created_at)
embed.set_author(name=guild.name, icon_url=guild.icon_url) embed.set_author(name=guild.name, icon_url=guild.icon.url)
embed.set_thumbnail(url=guild.icon_url) embed.set_thumbnail(url=guild.icon.url)
embed.set_footer(text=f"ID: {guild.id} | Server Created") embed.set_footer(text=f"ID: {guild.id} | Server Created")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_subcommand( @slash_command(
base="pw", name="pw",
name="gen", sub_cmd_name="gen",
base_desc="Password utilites",
description="Generate a secure password", description="Generate a secure password",
guild_ids=[862402786116763668], scopes=[862402786116763668],
options=[ )
create_option( @slash_option(
name="length", name="length",
description="Password length (default 32)", description="Password length (default 32)",
option_type=4, opt_type=OptionTypes.INTEGER,
required=False, required=False,
), )
create_option( @slash_option(
name="chars", name="chars",
description="Characters to include (default last option)", description="Characters to include (default last option)",
option_type=4, opt_type=OptionTypes.INTEGER,
required=False, required=False,
choices=[ choices=[
create_choice(name="A-Za-z", value=0), SlashCommandChoice(name="A-Za-z", value=0),
create_choice(name="A-Fa-f0-9", value=1), SlashCommandChoice(name="A-Fa-f0-9", value=1),
create_choice(name="A-Za-z0-9", value=2), SlashCommandChoice(name="A-Za-z0-9", value=2),
create_choice(name="A-Za-z0-9!@#$%^&*", value=3), SlashCommandChoice(name="A-Za-z0-9!@#$%^&*", value=3),
],
),
], ],
) )
@commands.cooldown(1, 15, type=commands.BucketType.user) @cooldown(bucket=Buckets.USER, rate=1, interval=15)
async def _pw_gen(self, ctx: SlashContext, length: int = 32, chars: int = 3) -> None: async def _pw_gen(self, ctx: InteractionContext, length: int = 32, chars: int = 3) -> None:
if length > 256: 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 return
choices = [ choices = [
string.ascii_letters, string.ascii_letters,
@ -307,15 +293,14 @@ class UtilCog(commands.Cog):
f"Generated password:\n`{pw}`\n\n" f"Generated password:\n`{pw}`\n\n"
'**WARNING: Once you press "Dismiss Message", ' '**WARNING: Once you press "Dismiss Message", '
"*the password is lost forever***", "*the password is lost forever***",
hidden=True, ephemeral=True,
) )
@cog_ext.cog_slash( @slash_command(name="pigpen", description="Encode a string into pigpen")
name="pigpen", @slash_option(
description="Encode a string into pigpen", name="text", description="Text to encode", opt_type=OptionTypes.STRING, required=True
options=[create_option(name="text", description="Text to encode", option_type=3, required=True)],
) )
async def _pigpen(self, ctx: SlashContext, text: str) -> None: async def _pigpen(self, ctx: InteractionContext, text: str) -> None:
outp = "`" outp = "`"
for c in text: for c in text:
c = c.lower() c = c.lower()
@ -330,6 +315,6 @@ class UtilCog(commands.Cog):
await ctx.send(outp[:2000]) 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.""" """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.""" """J.A.R.V.I.S. Verify Cog."""
import asyncio
from random import randint from random import randint
from discord.ext import commands from dis_snek import InteractionContext, Scale, Snake
from discord_slash import ComponentContext, SlashContext, cog_ext from dis_snek.models.application_commands import slash_command
from discord_slash.model import ButtonStyle from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows
from discord_slash.utils import manage_components from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Setting from jarvis.db.models import Setting
@ -16,36 +18,32 @@ def create_layout() -> list:
for i in range(3): for i in range(3):
label = "YES" if i == yes else "NO" label = "YES" if i == yes else "NO"
id = f"no_{i}" if not i == yes else "yes" 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( buttons.append(
manage_components.create_button( Button(
style=color, style=color,
label=label, label=label,
custom_id=f"verify_button||{id}", custom_id=f"verify_button||{id}",
) )
) )
action_row = manage_components.spread_to_rows(*buttons, max_in_row=3) return spread_to_rows(*buttons, max_in_row=3)
return action_row
class VerifyCog(commands.Cog): class VerifyCog(Scale):
"""J.A.R.V.I.S. Verify Cog.""" """J.A.R.V.I.S. Verify Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
@cog_ext.cog_slash( @slash_command(name="verify", description="Verify that you've read the rules")
name="verify", @cooldown(bucket=Buckets.USER, rate=1, interval=15)
description="Verify that you've read the rules", async def _verify(self, ctx: InteractionContext) -> None:
)
@commands.cooldown(1, 15, commands.BucketType.user)
async def _verify(self, ctx: SlashContext) -> None:
await ctx.defer() await ctx.defer()
role = Setting.objects(guild=ctx.guild.id, setting="verified").first() role = Setting.objects(guild=ctx.guild.id, setting="verified").first()
if not role: if not role:
await ctx.send("This guild has not enabled verification", delete_after=5) await ctx.send("This guild has not enabled verification", delete_after=5)
return 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) await ctx.send("You are already verified.", delete_after=5)
return return
components = create_layout() components = create_layout()
@ -53,40 +51,41 @@ class VerifyCog(commands.Cog):
content=f"{ctx.author.mention}, please press the button that says `YES`.", content=f"{ctx.author.mention}, please press the button that says `YES`.",
components=components, 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: try:
if ctx.author.id != ctx.origin_message.mentions[0].id: context = await self.bot.wait_for_component(
return messages=message, check=lambda x: ctx.author.id == x.author.id, timeout=30
except Exception: )
return
correct = ctx.custom_id.split("||")[-1] == "yes" correct = context.context.custom_id.split("||")[-1] == "yes"
if correct: if correct:
components = ctx.origin_message.components for row in components:
for c in components: for component in row.components:
for c2 in c["components"]: component.disabled = True
c2["disabled"] = True
setting = Setting.objects(guild=ctx.guild.id, setting="verified").first() setting = Setting.objects(guild=ctx.guild.id, setting="verified").first()
role = ctx.guild.get_role(setting.value) role = await ctx.guild.get_role(setting.value)
await ctx.author.add_roles(role, reason="Verification passed") await ctx.author.add_roles(role, reason="Verification passed")
setting = Setting.objects(guild=ctx.guild.id, setting="unverified").first() setting = Setting.objects(guild=ctx.guild.id, setting="unverified").first()
if setting: if setting:
role = ctx.guild.get_role(setting.value) role = await ctx.guild.get_role(setting.value)
await ctx.author.remove_roles(role, reason="Verification passed") await ctx.author.remove_roles(role, reason="Verification passed")
await ctx.edit_origin(
await context.context.edit_origin(
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.", content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
components=manage_components.spread_to_rows(*components, max_in_row=5), components=components,
) )
await ctx.origin_message.delete(delay=5) await context.context.message.delete(delay=5)
else: else:
await ctx.edit_origin( await context.context.edit_origin(
content=f"{ctx.author.mention}, incorrect. Please press the button that says `YES`", content=(
f"{ctx.author.mention}, incorrect. "
"Please press the button that says `YES`"
) )
)
except asyncio.TimeoutError:
await message.delete(delay=30)
def setup(bot: commands.Bot) -> None: def setup(bot: Snake) -> None:
"""Add VerifyCog to J.A.R.V.I.S.""" """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.""" """Load the config for J.A.R.V.I.S."""
import os
from pymongo import MongoClient from pymongo import MongoClient
from yaml import load from yaml import load
@ -27,6 +29,7 @@ class Config(object):
logo: str, logo: str,
mongo: dict, mongo: dict,
urls: dict, urls: dict,
sync: bool = False,
log_level: str = "WARNING", log_level: str = "WARNING",
cogs: list = None, cogs: list = None,
events: bool = True, events: bool = True,
@ -46,6 +49,7 @@ class Config(object):
self.max_messages = max_messages self.max_messages = max_messages
self.gitlab_token = gitlab_token self.gitlab_token = gitlab_token
self.twitter = twitter self.twitter = twitter
self.sync = sync or os.environ.get("SYNC_COMMANDS", False)
self.__db_loaded = False self.__db_loaded = False
self.__mongo = MongoClient(**self.mongo["connect"]) self.__mongo = MongoClient(**self.mongo["connect"])
@ -61,8 +65,7 @@ class Config(object):
@classmethod @classmethod
def from_yaml(cls, y: dict) -> "Config": def from_yaml(cls, y: dict) -> "Config":
"""Load the yaml config file.""" """Load the yaml config file."""
instance = cls(**y) return cls(**y)
return instance
def get_config(path: str = "config.yaml") -> Config: def get_config(path: str = "config.yaml") -> Config:

View file

@ -222,6 +222,7 @@ class Twitter(Document):
"""Twitter Follow object.""" """Twitter Follow object."""
active = BooleanField(default=True) active = BooleanField(default=True)
twitter_id = IntField(required=True)
handle = StringField(required=True) handle = StringField(required=True)
channel = SnowflakeField(required=True) channel = SnowflakeField(required=True)
guild = SnowflakeField(required=True) guild = SnowflakeField(required=True)
@ -229,6 +230,7 @@ class Twitter(Document):
retweets = BooleanField(default=True) retweets = BooleanField(default=True)
admin = SnowflakeField(required=True) admin = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow) created_at = DateTimeField(default=datetime.utcnow)
last_sync = DateTimeField()
meta = {"db_alias": "main"} 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.""" """J.A.R.V.I.S. Member event handler."""
from discord import Member from dis_snek import Snake, listen
from discord.ext.commands import Bot from dis_snek.models.discord.user import Member
from jarvis.db.models import Mute, Setting from jarvis.db.models import Mute, Setting
@ -8,10 +8,11 @@ from jarvis.db.models import Mute, Setting
class MemberEventHandler(object): class MemberEventHandler(object):
"""J.A.R.V.I.S. Member event handler.""" """J.A.R.V.I.S. Member event handler."""
def __init__(self, bot: Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.bot.add_listener(self.on_member_join) self.bot.add_listener(self.on_member_join)
@listen()
async def on_member_join(self, user: Member) -> None: async def on_member_join(self, user: Member) -> None:
"""Handle on_member_join event.""" """Handle on_member_join event."""
guild = user.guild guild = user.guild

View file

@ -1,17 +1,17 @@
"""J.A.R.V.I.S. Message event handler.""" """J.A.R.V.I.S. Message event handler."""
import re import re
from discord import DMChannel, Message from dis_snek import Snake, listen
from discord.ext.commands import Bot from dis_snek.models.discord.channel import DMChannel
from discord.utils import find from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.message import Message
from jarvis.config import get_config from jarvis.config import get_config
from jarvis.db.models import Autopurge, Autoreact, Roleping, Setting, Warning from jarvis.db.models import Autopurge, Autoreact, Roleping, Setting, Warning
from jarvis.utils import build_embed from jarvis.utils import build_embed, find
from jarvis.utils.field import Field
invites = 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, flags=re.IGNORECASE,
) )
@ -19,7 +19,7 @@ invites = re.compile(
class MessageEventHandler(object): class MessageEventHandler(object):
"""J.A.R.V.I.S. Message event handler.""" """J.A.R.V.I.S. Message event handler."""
def __init__(self, bot: Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.bot.add_listener(self.on_message) self.bot.add_listener(self.on_message)
self.bot.add_listener(self.on_message_edit) 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) channel = find(lambda x: x.id == 599068193339736096, message.channel_mentions)
if channel and message.author.id == 293795462752894976: if channel and message.author.id == 293795462752894976:
await channel.send( 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) content = re.sub(r"\s+", "", message.content)
match = invites.search(content) match = invites.search(content)
@ -72,10 +72,10 @@ class MessageEventHandler(object):
user=message.author.id, user=message.author.id,
).save() ).save()
fields = [ fields = [
Field( EmbedField(
"Reason", name="Reason",
"Sent an invite link", value="Sent an invite link",
False, inline=False,
) )
] ]
embed = build_embed( embed = build_embed(
@ -85,9 +85,11 @@ class MessageEventHandler(object):
) )
embed.set_author( embed.set_author(
name=message.author.nick if message.author.nick else message.author.name, 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) await message.channel.send(embed=embed)
async def massmention(self, message: Message) -> None: async def massmention(self, message: Message) -> None:
@ -99,7 +101,8 @@ class MessageEventHandler(object):
if ( if (
massmention massmention
and massmention.value > 0 # noqa: W503 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 > massmention.value # noqa: W503
): ):
_ = Warning( _ = Warning(
@ -110,7 +113,7 @@ class MessageEventHandler(object):
reason="Mass Mention", reason="Mass Mention",
user=message.author.id, user=message.author.id,
).save() ).save()
fields = [Field("Reason", "Mass Mention", False)] fields = [EmbedField(name="Reason", value="Mass Mention", inline=False)]
embed = build_embed( embed = build_embed(
title="Warning", title="Warning",
description=f"{message.author.mention} has been warned", description=f"{message.author.mention} has been warned",
@ -118,9 +121,11 @@ class MessageEventHandler(object):
) )
embed.set_author( embed.set_author(
name=message.author.nick if message.author.nick else message.author.name, 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) await message.channel.send(embed=embed)
async def roleping(self, message: Message) -> None: async def roleping(self, message: Message) -> None:
@ -173,10 +178,10 @@ class MessageEventHandler(object):
user=message.author.id, user=message.author.id,
).save() ).save()
fields = [ fields = [
Field( EmbedField(
"Reason", name="Reason",
"Pinged a blocked role/user with a blocked role", value="Pinged a blocked role/user with a blocked role",
False, inline=False,
) )
] ]
embed = build_embed( embed = build_embed(
@ -186,11 +191,14 @@ class MessageEventHandler(object):
) )
embed.set_author( embed.set_author(
name=message.author.nick if message.author.nick else message.author.name, 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) await message.channel.send(embed=embed)
@listen()
async def on_message(self, message: Message) -> None: async def on_message(self, message: Message) -> None:
"""Handle on_message event. Calls other event handlers.""" """Handle on_message event. Calls other event handlers."""
if not isinstance(message.channel, DMChannel) and not message.author.bot: if not isinstance(message.channel, DMChannel) and not message.author.bot:
@ -200,6 +208,7 @@ class MessageEventHandler(object):
await self.autopurge(message) await self.autopurge(message)
await self.checks(message) await self.checks(message)
@listen()
async def on_message_edit(self, before: Message, after: Message) -> None: async def on_message_edit(self, before: Message, after: Message) -> None:
"""Handle on_message_edit event. Calls other event handlers.""" """Handle on_message_edit event. Calls other event handlers."""
if not isinstance(after.channel, DMChannel) and not after.author.bot: if not isinstance(after.channel, DMChannel) and not after.author.bot:
@ -208,3 +217,10 @@ class MessageEventHandler(object):
await self.checks(after) await self.checks(after)
await self.roleping(after) await self.roleping(after)
await self.checks(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.""" """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: def init() -> None:
"""Start the background task handlers.""" """Start the background task handlers."""
unban.unban.start() unban.unban.start()
unlock.unlock.start() unlock.unlock.start()
unmute.unmute.start()
unwarn.unwarn.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.""" """J.A.R.V.I.S. unban background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta 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 import jarvis
from jarvis.config import get_config from jarvis.config import get_config
@ -10,17 +12,18 @@ from jarvis.db.models import Ban, Unban
jarvis_id = get_config().client_id jarvis_id = get_config().client_id
@loop(minutes=10) async def _unban() -> None:
async def unban() -> None: """J.A.R.V.I.S. unban blocking task."""
"""J.A.R.V.I.S. unban background task."""
bans = Ban.objects(type="temp", active=True) bans = Ban.objects(type="temp", active=True)
unbans = [] unbans = []
for ban in bans: for ban in bans:
if ban.created_at + timedelta(hours=ban.duration) < datetime.utcnow() + timedelta(minutes=10): if ban.created_at + timedelta(hours=ban.duration) < datetime.utcnow() + timedelta(
guild = await jarvis.jarvis.fetch_guild(ban.guild) minutes=10
user = await jarvis.jarvis.fetch_user(ban.user) ):
guild = await jarvis.jarvis.get_guild(ban.guild)
user = await jarvis.jarvis.get_user(ban.user)
if user: if user:
guild.unban(user) await guild.unban(user=user, reason="Ban expired")
ban.active = False ban.active = False
ban.save() ban.save()
unbans.append( unbans.append(
@ -34,4 +37,10 @@ async def unban() -> None:
) )
) )
if unbans: 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.""" """J.A.R.V.I.S. unlock background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta 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 import jarvis
from jarvis.db.models import Lock from jarvis.db.models import Lock
@loop(minutes=1) async def _unlock() -> None:
async def unlock() -> None: """J.A.R.V.I.S. unlock blocking task."""
"""J.A.R.V.I.S. unlock background task."""
locks = Lock.objects(active=True) locks = Lock.objects(active=True)
# Block execution for now
# TODO: Reevaluate with admin/lock[down]
if False:
for lock in locks: for lock in locks:
if lock.created_at + timedelta(minutes=lock.duration) < datetime.utcnow(): if lock.created_at + timedelta(minutes=lock.duration) < datetime.utcnow():
guild = await jarvis.jarvis.fetch_guild(lock.guild) guild = await jarvis.jarvis.get_guild(lock.guild)
channel = await jarvis.jarvis.fetch_channel(lock.channel) channel = await jarvis.jarvis.get_guild(lock.channel)
if channel: if channel:
roles = await guild.fetch_roles() roles = await guild.fetch_roles()
for role in roles: for role in roles:
overrides = channel.overwrites_for(role) overrides = channel.overwrites_for(role)
overrides.send_messages = None overrides.send_messages = None
await channel.set_permissions(role, overwrite=overrides, reason="Lock expired") await channel.set_permissions(
role, overwrite=overrides, reason="Lock expired"
)
lock.active = False lock.active = False
lock.save() lock.save()
@Task.create(IntervalTrigger(minutes=1))
async def unlock() -> None:
"""J.A.R.V.I.S. unlock background task."""
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.""" """J.A.R.V.I.S. unwarn background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta 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 from jarvis.db.models import Warning
@loop(hours=1) async def _unwarn() -> None:
async def unwarn() -> None: """J.A.R.V.I.S. unwarn blocking task."""
"""J.A.R.V.I.S. unwarn background task."""
warns = Warning.objects(active=True) warns = Warning.objects(active=True)
for warn in warns: for warn in warns:
if warn.created_at + timedelta(hours=warn.duration) < datetime.utcnow(): if warn.created_at + timedelta(hours=warn.duration) < datetime.utcnow():
warn.active = False warn.active = False
warn.save() 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.""" """J.A.R.V.I.S. Utility Functions."""
from datetime import datetime from datetime import datetime
from pkgutil import iter_modules from pkgutil import iter_modules
from typing import Any, Callable, Iterable, List, Optional, TypeVar
import git import git
from discord import Color, Embed, Message from dis_snek.models.discord.embed import Embed
from discord.ext import commands
import jarvis.cogs import jarvis.cogs
import jarvis.db import jarvis.db
@ -12,6 +12,31 @@ from jarvis.config import get_config
__all__ = ["field", "db", "cachecog", "permissions"] __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: def convert_bytesize(b: int) -> str:
"""Convert bytes amount to human readable.""" """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))) 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: def get_extensions(path: str = jarvis.cogs.__path__) -> list:
"""Get J.A.R.V.I.S. cogs.""" """Get J.A.R.V.I.S. cogs."""
config = get_config() 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] 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: def update() -> int:
"""J.A.R.V.I.S. update utility.""" """J.A.R.V.I.S. update utility."""
repo = git.Repo(".") repo = git.Repo(".")
@ -99,3 +85,103 @@ def get_repo_hash() -> str:
"""J.A.R.V.I.S. current branch hash.""" """J.A.R.V.I.S. current branch hash."""
repo = git.Repo(".") repo = git.Repo(".")
return repo.head.object.hexsha 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.""" """Cog wrapper for command caching."""
from datetime import datetime, timedelta from datetime import datetime, timedelta
from discord.ext import commands from dis_snek import InteractionContext, Scale, Snake
from discord.ext.tasks import loop from dis_snek.ext.tasks.task import Task
from discord.utils import find from dis_snek.ext.tasks.triggers import IntervalTrigger
from discord_slash import SlashContext
from jarvis.utils import find
class CacheCog(commands.Cog): class CacheCog(Scale):
"""Cog wrapper for command caching.""" """Cog wrapper for command caching."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.cache = {} self.cache = {}
self._expire_interaction.start() 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.""" """Check the cache."""
if not kwargs: if not kwargs:
kwargs = {} kwargs = {}
@ -27,7 +28,7 @@ class CacheCog(commands.Cog):
self.cache.values(), self.cache.values(),
) )
@loop(minutes=1) @Task.create(IntervalTrigger(minutes=1))
async def _expire_interaction(self) -> None: async def _expire_interaction(self) -> None:
keys = list(self.cache.keys()) keys = list(self.cache.keys())
for key in 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.""" """Permissions wrappers."""
from discord.ext import commands from dis_snek import InteractionContext, Permissions
from jarvis.config import get_config from jarvis.config import get_config
@ -7,22 +7,23 @@ from jarvis.config import get_config
def user_is_bot_admin() -> bool: def user_is_bot_admin() -> bool:
"""Check if a user is a J.A.R.V.I.S. admin.""" """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.""" """Command check predicate."""
if getattr(get_config(), "admins", None): if getattr(get_config(), "admins", None):
return ctx.author.id in get_config().admins return ctx.author.id in get_config().admins
else: else:
return False 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.""" """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 """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 """Main run file for J.A.R.V.I.S."""
# flake8: noqa from jarvis import run
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.")
if __name__ == "__main__": if __name__ == "__main__":
freeze_support() run()
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.")