36 Commits

Author SHA1 Message Date
4afcbc93d5 Merge pull request 'Fixed minor issues' (#6) from dev into main
Reviewed-on: #6
2024-06-23 13:14:08 +03:00
4f610fc55c Fixed minor issues 2024-06-23 12:12:34 +02:00
72ccaa04a4 Merge pull request 'v0.1.0-rc.1' (#5) from dev into main
Reviewed-on: #5
2024-06-23 13:06:12 +03:00
f67375ff4f Closes #2 2024-06-23 12:05:03 +02:00
d168821fb5 Merge branch 'renovate/apscheduler-3.x' into dev 2024-06-23 11:24:42 +02:00
f01047cc11 Merge pull request 'Update dependency aiofiles to v23.2.1' (#4) from renovate/aiofiles-23.x into dev
Reviewed-on: #4
2024-06-23 12:19:43 +03:00
ed190bc07e Update dependency aiofiles to v23.2.1 2024-06-23 12:17:14 +03:00
e14c93f07f Update dependency apscheduler to v3.10.4 2024-06-23 12:17:10 +03:00
ffb3153a47 WIP: Analytics validation 2023-05-08 19:27:33 +02:00
64630e0ab7 Fixed sticker object type 2023-05-08 19:27:22 +02:00
967d5e981e Improved typing 2023-05-08 16:39:46 +02:00
42b4578d36 Slightly changes naming 2023-05-08 15:48:15 +02:00
daae3db305 WIP: Validation 2023-05-08 15:45:16 +02:00
786bc95eeb Changed admin check 2023-05-08 15:45:00 +02:00
35ee903abb Added /action command 2023-05-07 11:01:28 +02:00
16f3d4bc56 Improved analytics 2023-05-07 09:57:35 +02:00
1d902c75e0 Removed deprecated shit 2023-05-07 09:42:21 +02:00
8b6bab9c31 Fixed formatting 2023-05-06 19:17:40 +02:00
df1732fa97 Added custom activity types 2023-05-06 19:06:04 +02:00
5ff49ed052 Sorted, formatted, removed unused imports 2023-05-06 18:56:01 +02:00
640ab65e7a Improved data usage handling 2023-05-06 18:48:04 +02:00
9d7a13f473 Fixed not working config 2023-05-06 18:31:23 +02:00
f9a8d6ddf6 Fixed unexpected behavior 2023-05-06 18:31:07 +02:00
89bb57b2fc Removed data export from imports 2023-05-06 18:10:40 +02:00
445bbd6f93 Imported all cogs 2023-05-06 17:56:33 +02:00
35bb16e564 Added data export 2023-05-06 17:56:25 +02:00
ed232b3cdb Added admin commands 2023-05-06 17:56:15 +02:00
c9f2da5834 Added analytics 2023-05-06 17:52:46 +02:00
7fdf37e35a Added tmp to ignore 2023-05-06 17:51:27 +02:00
0eea4e67a3 Added missing keys 2023-05-06 17:51:18 +02:00
9f5c59f376 Added json indent 2023-05-06 17:51:07 +02:00
e59b36fcd1 Improved custom channels 2023-05-06 17:09:06 +02:00
6015592df5 Replaced logger 2023-05-06 17:08:52 +02:00
32c7ec7d44 Added logger thats adds users to DB 2023-05-06 17:08:35 +02:00
b3a78816f7 Added new config keys 2023-05-06 17:08:16 +02:00
22a19c27f8 Changed discord.utils import 2023-05-06 15:21:15 +02:00
20 changed files with 904 additions and 188 deletions

4
.gitignore vendored
View File

@@ -166,4 +166,6 @@ venv
venv_linux venv_linux
venv_windows venv_windows
config.json config.json
tmp

View File

@@ -1,15 +1,13 @@
from datetime import datetime, timezone import logging
from random import randint from typing import Any, Union
from typing import Union, Any
import discord import discord
import discord.member import discord.member
from modules.utils import config_get
from modules.database import col_users, col_warnings
try: from modules.database import col_users, col_warnings
from typing import Literal from modules.utils import config_get
except ImportError:
from typing_extensions import Literal logger = logging.getLogger(__name__)
class NotEnoughMoneyError(Exception): class NotEnoughMoneyError(Exception):
@@ -54,108 +52,10 @@ class HoloUser:
self.db_id = jav_user["_id"] self.db_id = jav_user["_id"]
# self.xp = jav_user["xp"]
# self.xp_next = jav_user["xp_next"]
# self.level = jav_user["level"]
# self.work_xp = jav_user["work_xp"]
# self.balance = jav_user["balance"]
self.customrole = jav_user["customrole"] self.customrole = jav_user["customrole"]
self.customchannel = jav_user["customchannel"] self.customchannel = jav_user["customchannel"]
# self.married = jav_user["married"]
# self.marriage_request = jav_user["marriage_request"]
# self.marriage_request_sent = jav_user["marriage_request_sent"]
# self.cooldown = jav_user["cooldown"]
self.warnings = self.warns() self.warnings = self.warns()
# def xp_add(self, amount: int = 1) -> None:
# """Add some amount of XP points
# ### Args:
# * `amount` (int, optional): Amount of XP points to give. Defaults to 1.
# """
# self.xp += amount
# col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { "xp": self.xp } })
# def xp_level_up(self) -> None:
# """Add 1 to the current XP level"""
# xp_diff = int(self.xp - self.xp_next)
# xp_next = int(self.xp_next*configGet("multiplier", "leveling")+configGet("addition", "leveling"))
# self.xp = xp_diff
# self.xp_next = xp_next
# self.level += 1
# col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { "xp": xp_diff } })
# col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { "xp_next": xp_next } })
# col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { "level": self.level } })
# def balance_set(self, amount: int) -> None:
# """Set the balance to amount
# ### Args:
# * `amount` (int): Amount of currency to be set
# """
# self.balance = amount
# col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { "balance": self.balance } })
# def balance_add(self, amount: int) -> None:
# """Add amount to the balance
# ### Args:
# * `amount` (int): Amount to be added
# """
# self.balance_set(self.balance+amount)
# def balance_take(self, amount: int) -> bool:
# """Take amount from the balance
# ### Args:
# * `amount` (int): Amount to be taken
# ### Returns:
# * `bool`: True if successful and False if not
# """
# if self.balance >= amount:
# self.balance_set(self.balance-amount)
# return True
# else:
# return False
# #raise NotEnoughMoneyError()
# def balance_transfer(self, amount: int, destination: Union[Any, int]) -> None:
# """Transfer money to another user
# ### Args:
# * `amount` (int): Amount to be transferred
# * `destination` (Union[Any, int]): Destination user of the transfer (should have attribute "id" or be id itself if int)
# ### Raises:
# * `NotEnoughMoneyError`: Not enough money to perform this transaction
# """
# if self.balance >= amount:
# if isinstance(destination, int):
# destination = HoloUser(destination)
# self.balance_take(amount)
# destination.balance_add(amount) # type: ignore
# else:
# raise NotEnoughMoneyError()
# def salary_get(self) -> int:
# """Get the salary level depending on work_xp
# ### Returns:
# * `int`: Amount of money to be earned
# """
# if self.work_xp >= 100:
# return randint(configGet("min", "work", "level", "4"), configGet("max", "work", "level", "4"))
# elif self.work_xp >= 50:
# return randint(configGet("min", "work", "level", "3"), configGet("max", "work", "level", "3"))
# elif self.work_xp > 10:
# return randint(configGet("min", "work", "level", "2"), configGet("max", "work", "level", "2"))
# else:
# return randint(configGet("min", "work", "level", "1"), configGet("max", "work", "level", "1"))
# def work_xp_add(self) -> None:
# self.set("work_xp", self.work_xp+1)
def warns(self) -> int: def warns(self) -> int:
"""Get number of warnings user has """Get number of warnings user has
@@ -182,16 +82,7 @@ class HoloUser:
) )
else: else:
col_warnings.insert_one(document={"user": self.id, "warns": count}) col_warnings.insert_one(document={"user": self.id, "warns": count})
logWrite(f"User {self.id} was warned {count} times due to: {reason}") logger.info(f"User {self.id} was warned {count} times due to: {reason}")
# def cooldown_go(self, kind: Literal["work", "daily", "weekly", "monthly", "steal"]) -> None:
# """Set cooldown start of kind now
# ### Args:
# * `kind` (Literal["work", "daily", "weekly", "monthly", "steal"]): Kind of a cooldown
# """
# self.cooldown[kind] = datetime.now(tz=timezone.utc)
# col_users.update_one(filter={"_id": self.db_id}, update={ "$set": { "cooldown": self.cooldown } })
def set(self, key: str, value: Any) -> None: def set(self, key: str, value: Any) -> None:
"""Set attribute data and save it into database """Set attribute data and save it into database
@@ -206,28 +97,48 @@ class HoloUser:
col_users.update_one( col_users.update_one(
filter={"_id": self.db_id}, update={"$set": {key: value}}, upsert=True filter={"_id": self.db_id}, update={"$set": {key: value}}, upsert=True
) )
logWrite(f"Set attribute {key} of user {self.id} to {value}") logger.info(f"Set attribute {key} of user {self.id} to {value}")
def purge(self) -> None: async def is_moderator(
"""Completely remove data from database. Will not remove transactions logs and warnings.""" self, member: Union[discord.User, discord.Member, discord.member.Member]
col_users.delete_one(filter={"_id": self.db_id}) ) -> bool:
self.unauthorize() """Check if user is moderator or council member
def unauthorize(self) -> None: ### Args:
"""Cancel Oauth2 authorization""" * `member` (Union[discord.User, discord.Member, discord.member.Member]): Member object
col_authorized.find_one_and_delete({"user": self.id})
# def is_authorized(self) -> bool: ### Returns:
# """Check if user provided Oauth2 authorization `bool`: `True` if member is a moderator or member of council and `False` if not
"""
if isinstance(member, discord.User):
return False
moderator_role = await config_get("moderators", "roles")
council_role = await config_get("council", "roles")
for role in member.roles:
if role.id == moderator_role or role.id == council_role:
return True
return False
# ### Returns: async def is_council(
# * `bool`: True if yes and False if no self, member: Union[discord.User, discord.Member, discord.member.Member]
# """ ) -> bool:
# if configGet("mode") == "secure": """Check if user is a member of council
# authorized = col_authorized.find_one({"user": self.id})
# if authorized is not None: ### Args:
# return True * `member` (Union[discord.User, discord.Member, discord.member.Member]): Member object
# else:
# return False ### Returns:
# else: `bool`: `True` if member is a member of council and `False` if not
# return True """
if isinstance(member, discord.User):
return False
council_role = await config_get("council", "roles")
for role in member.roles:
if role.id == council_role:
return True
return False
# def purge(self) -> None:
# """Completely remove data from database. Will not remove transactions logs and warnings."""
# col_users.delete_one(filter={"_id": self.db_id})
# self.unauthorize()

207
cogs/admin.py Normal file
View File

@@ -0,0 +1,207 @@
import logging
from discord import ApplicationContext, Embed, User, option, slash_command
from discord import utils as ds_utils
from discord.ext import commands
from libbot.pycord.classes import PycordBot
from enums.colors import Color
from modules.scheduled import scheduler
from modules.utils import config_get
from modules.utils_sync import config_get_sync, guild_name
logger = logging.getLogger(__name__)
class Admin(commands.Cog):
def __init__(self, client: PycordBot):
self.client = client
# @slash_command(
# name="warning",
# description="Попередити юзера про порушення правил",
# guild_ids=[config_get_sync("guild")],
# )
# @option("user", description="Користувач")
# @option("reason", description="Причина")
# async def warn_cmd(
# self,
# ctx: ApplicationContext,
# user: User,
# reason: str = "Не вказана",
# ):
# logging.info(f"User {ctx.user.id} warned {user.id} for {reason}")
# await ctx.defer()
# jav_user = HoloUser(user)
# if ctx.user.id in await config_get("admins"):
# logging.info(
# f"Moderator {guild_name(ctx.user)} warned {guild_name(user)} for {reason} (has {jav_user.warnings} warns)"
# )
# if jav_user.warnings >= 5:
# logging.info(
# f"User {guild_name(user)} was banned due to a big amount of warns ({jav_user.warnings})"
# )
# await user.send(
# embed=Embed(
# title="Перманентне блокування",
# description=f"Вас було заблоковано за неодноразове порушення правил сервера.",
# color=Color.fail,
# )
# )
# await user.ban(reason=reason)
# elif jav_user.warnings >= 2:
# logging.info(
# f"User {guild_name(user)} was muted due to a big amount of warns ({jav_user.warnings})"
# )
# jav_user.warn(reason=reason)
# await user.send(
# embed=Embed(
# title="Тимчасове блокування",
# description=f"Причина: `{reason}`\n\nНа вашому рахунку вже {jav_user.warnings} попереджень. Вас було тимчасово заблоковано на **1 годину**.\n\nЯкщо Ви продовжите порушувати правила сервера згодом Вас заблокують.",
# color=0xDED56B,
# )
# )
# await user.timeout_for(timedelta(hours=1), reason=reason)
# else:
# jav_user.warn()
# await ctx.respond(
# embed=Embed(
# title="Попередження",
# description=f"{user.mention} Будь ласка, не порушуйте правила. Ви отримали попередження з причини `{reason}`.\n\nЯкщо Ви продовжите порушувати правила це може призвести до блокування в спільноті.",
# color=0xDED56B,
# )
# )
# else:
# logging.warning(
# f"User {guild_name(ctx.user)} tried to use /warn but permission denied"
# )
# await ctx.respond(
# embed=Embed(
# title="Відмовлено в доступі",
# description="Здається, це команда лише для модераторів",
# color=Color.fail,
# )
# )
# mod_role = ds_utils.get(
# ctx.user.guild.roles, id=await config_get("moderators", "roles")
# )
# admin_chan = ds_utils.get(
# ctx.user.guild.channels,
# id=await config_get("adminchat", "channels", "text"),
# )
# await admin_chan.send(
# content=f"{mod_role.mention}",
# embed=Embed(
# title="Неавторизований запит",
# description=f"Користувач {ctx.user.mention} запитав у каналі {ctx.channel.mention} команду, до якої не повинен мати доступу/бачити.",
# color=Color.fail,
# ),
# )
@slash_command(
name="clear",
description="Видалити деяку кількість повідомлень в каналі",
guild_ids=[config_get_sync("guild")],
)
@option("amount", description="Кількість")
@option("user", description="Користувач", default=None)
async def clear_cmd(
self,
ctx: ApplicationContext,
amount: int,
user: User,
):
if ctx.user.id in self.client.owner_ids:
logging.info(
"User %s removed %s message(s) in %s",
ctx.user.id,
amount,
ctx.channel.id,
)
await ctx.respond(
embed=Embed(description="Видаляю..."), ephemeral=True, delete_after=2.0
)
if user == None:
await ctx.channel.purge(limit=amount)
else:
await ctx.channel.purge(
limit=amount, check=lambda msg: msg.author == user
)
else:
logging.warning(
"User %s tried to use /clear but permission denied",
guild_name(ctx.user),
)
await ctx.respond(
embed=Embed(
title="Відмовлено в доступі",
description="Здається, це команда лише для модераторів",
color=Color.fail,
)
)
mod_role = ds_utils.get(
ctx.user.guild.roles, id=await config_get("moderators", "roles")
)
admin_chan = ds_utils.get(
ctx.user.guild.channels,
id=await config_get("adminchat", "channels", "text"),
)
await admin_chan.send(
content=f"{mod_role.mention}",
embed=Embed(
title="Неавторизований запит",
description=f"Користувач {ctx.user.mention} запитав у каналі {ctx.channel.mention} команду, до якої не повинен мати доступу/бачити.",
color=Color.fail,
),
)
@slash_command(
name="reboot",
description="Перезапустити бота",
guild_ids=[config_get_sync("guild")],
)
async def reboot_cmd(self, ctx: ApplicationContext):
await ctx.defer(ephemeral=True)
if ctx.user.id in self.client.owner_ids:
logging.info("Calling shutdown initiated by %s", guild_name(ctx.user))
await ctx.respond(
embed=Embed(
title="Вимикаюсь...",
description="Спробую перезавантажитись за 5 секунд",
)
)
scheduler.shutdown()
await self.client.close()
exit()
else:
logging.warning(
"User %s tried to use /reboot but permission denied",
guild_name(ctx.user),
)
await ctx.respond(
embed=Embed(
title="Відмовлено в доступі",
description="Здається, це команда лише для модераторів",
color=Color.fail,
)
)
mod_role = ds_utils.get(
ctx.user.guild.roles, id=await config_get("moderators", "roles")
)
admin_chan = ds_utils.get(
ctx.user.guild.channels,
id=await config_get("adminchat", "channels", "text"),
)
await admin_chan.send(
content=f"{mod_role.mention}",
embed=Embed(
title="Неавторизований запит",
description=f"Користувач {ctx.user.mention} запитав у каналі {ctx.channel.mention} команду, до якої не повинен мати доступу/бачити.",
color=Color.fail,
),
)
def setup(client: PycordBot):
client.add_cog(Admin(client))

61
cogs/analytics.py Normal file
View File

@@ -0,0 +1,61 @@
import logging
from discord import Cog, Message
from discord.ext import commands
from libbot.pycord.classes import PycordBot
from modules.database import col_analytics
logger = logging.getLogger(__name__)
class Analytics(commands.Cog):
def __init__(self, client: PycordBot):
self.client = client
@Cog.listener()
async def on_message(self, message: Message):
if (
(message.author != self.client.user)
and (message.author.bot == False)
and (message.author.system == False)
):
stickers = []
for sticker in message.stickers:
stickers.append(
{
"id": sticker.id,
"name": sticker.name,
"format": sticker.format,
"url": sticker.url,
}
)
attachments = []
for attachment in message.attachments:
attachments.append(
{
"content_type": attachment.content_type,
"description": attachment.description,
"filename": attachment.filename,
"is_spoiler": attachment.is_spoiler(),
"size": attachment.size,
"url": attachment.url,
"width": attachment.width,
"height": attachment.height,
}
)
col_analytics.insert_one(
{
"user": message.author.id,
"channel": message.channel.id,
"content": message.content,
"stickers": stickers,
"attachments": attachments,
}
)
def setup(client: PycordBot):
client.add_cog(Analytics(client))

View File

@@ -1,27 +1,38 @@
from discord import ApplicationContext, option, utils, Embed from discord import ApplicationContext, Embed, option
from discord.ext import commands from discord import utils as ds_utils
from discord.abc import GuildChannel
from discord.commands import SlashCommandGroup from discord.commands import SlashCommandGroup
from discord.ext import commands
from libbot.pycord.classes import PycordBot
from classes.holo_user import HoloUser from classes.holo_user import HoloUser
from enums.colors import Color from enums.colors import Color
from modules.database import col_users
from modules.utils import config_get from modules.utils import config_get
from modules.utils_sync import config_get_sync, guild_name from modules.utils_sync import config_get_sync, guild_name
class CustomChannels(commands.Cog): class CustomChannels(commands.Cog):
def __init__(self, client): def __init__(self, client: PycordBot):
self.client = client self.client = client
@commands.Cog.listener()
async def on_guild_channel_delete(self, channel: GuildChannel):
col_users.find_one_and_update(
{"customchannel": channel.id}, {"$set": {"customchannel": None}}
)
customchannel = SlashCommandGroup("customchannel", "Керування особистим каналом") customchannel = SlashCommandGroup("customchannel", "Керування особистим каналом")
@customchannel.command( @customchannel.command(
name="buy", name="get",
description="Отримати персональний текстовий канал", description="Отримати персональний текстовий канал",
guild_ids=[config_get_sync("guild")], guild_ids=[config_get_sync("guild")],
) )
@option("name", description="Назва каналу") @option("name", description="Назва каналу")
@option("reactions", description="Дозволити реакції") @option("reactions", description="Дозволити реакції")
@option("threads", description="Дозволити гілки") @option("threads", description="Дозволити гілки")
async def customchannel_buy_cmd( async def customchannel_get_cmd(
self, ctx: ApplicationContext, name: str, reactions: bool, threads: bool self, ctx: ApplicationContext, name: str, reactions: bool, threads: bool
): ):
holo_user_ctx = HoloUser(ctx.user) holo_user_ctx = HoloUser(ctx.user)
@@ -30,8 +41,8 @@ class CustomChannels(commands.Cog):
await ctx.defer() await ctx.defer()
created_channel = await ctx.user.guild.create_text_channel( created_channel = await ctx.user.guild.create_text_channel(
name=name, name=name,
reason=f"Користувач {guild_name(ctx.user)} купив канал", reason=f"Користувач {guild_name(ctx.user)} отримав власний приватний канал",
category=utils.get( category=ds_utils.get(
ctx.author.guild.categories, ctx.author.guild.categories,
id=await config_get("customchannels", "categories"), id=await config_get("customchannels", "categories"),
), ),
@@ -62,7 +73,7 @@ class CustomChannels(commands.Cog):
bots = await config_get("bots") bots = await config_get("bots")
for bot in bots: for bot in bots:
await created_channel.set_permissions( await created_channel.set_permissions(
utils.get(ctx.user.guild.roles, id=bots[bot]["role"]), ds_utils.get(ctx.user.guild.roles, id=bots[bot]["role"]),
view_channel=False, view_channel=False,
) )
else: else:
@@ -70,7 +81,7 @@ class CustomChannels(commands.Cog):
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
title="Помилка виконання", title="Помилка виконання",
description=f"У вас вже є особистий канал.\nДля редагування каналу є `/customchannel edit` або просто відкрийте меню керування вашим каналом.", description="У вас вже є особистий канал.\nДля редагування каналу є `/customchannel edit` або просто відкрийте меню керування вашим каналом.",
color=Color.fail, color=Color.fail,
) )
) )
@@ -88,12 +99,14 @@ class CustomChannels(commands.Cog):
): ):
holo_user_ctx = HoloUser(ctx.user) holo_user_ctx = HoloUser(ctx.user)
custom_channel = utils.get(ctx.guild.channels, id=holo_user_ctx.customchannel) custom_channel = ds_utils.get(
ctx.guild.channels, id=holo_user_ctx.customchannel
)
if custom_channel is None: if custom_channel is None:
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
title="Канал не знайдено", title="Канал не знайдено",
description=f"Канал, вказаний як ваш, не існує. Можливо, його було вручну видалено раніше.", description="Канал, вказаний як ваш, не існує. Можливо, його було вручну видалено раніше.",
color=Color.fail, color=Color.fail,
) )
) )
@@ -115,34 +128,46 @@ class CustomChannels(commands.Cog):
) )
@customchannel.command( @customchannel.command(
name="refund", name="remove",
description="Відібрати канал, знищуючи його, та частково повернути кошти", description="Відібрати канал, знищуючи його, та частково повернути кошти",
guild_ids=[config_get_sync("guild")], guild_ids=[config_get_sync("guild")],
) )
async def customchannel_refund_cmd(self, ctx: ApplicationContext): @option("confirm", description="Підтвердження операції")
async def customchannel_remove_cmd(
self, ctx: ApplicationContext, confirm: bool = False
):
holo_user_ctx = HoloUser(ctx.user) holo_user_ctx = HoloUser(ctx.user)
if holo_user_ctx.customchannel is not None: if holo_user_ctx.customchannel is not None:
await ctx.defer() await ctx.defer()
custom_channel = utils.get( custom_channel = ds_utils.get(
ctx.guild.channels, id=holo_user_ctx.customchannel ctx.guild.channels, id=holo_user_ctx.customchannel
) )
if custom_channel is None: if custom_channel is None:
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
title="Канал не знайдено", title="Канал не знайдено",
description=f"Канал, вказаний як ваш, не існує. Можливо, його було вручну видалено раніше.", description="Канал, вказаний як ваш, не існує. Можливо, його було вручну видалено раніше.",
color=Color.fail, color=Color.fail,
) )
) )
holo_user_ctx.set("customchannel", None) holo_user_ctx.set("customchannel", None)
return return
await custom_channel.delete(reason="Повернення коштів") if not confirm:
await ctx.respond(
embed=Embed(
title="Підтвердження не надано",
description="Для підтвердження операції додайте до команди параметр `confirm` зі значенням `True`.",
color=Color.fail,
)
)
return
await custom_channel.delete(reason="Власник запросив видалення")
holo_user_ctx.set("customchannel", None) holo_user_ctx.set("customchannel", None)
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
title="Канал знищено", title="Канал знищено",
description=f"Ви відмовились від каналу.", description="Ви відмовились від каналу та видалили його.",
color=Color.default, color=Color.default,
) )
) )
@@ -151,7 +176,11 @@ class CustomChannels(commands.Cog):
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
title="Помилка виконання", title="Помилка виконання",
description=f"У вас немає особистого каналу.", description="У вас немає особистого каналу.",
color=Color.fail, color=Color.fail,
) )
) )
def setup(client: PycordBot):
client.add_cog(CustomChannels(client))

167
cogs/data.py Normal file
View File

@@ -0,0 +1,167 @@
import logging
from os import makedirs
from pathlib import Path
from uuid import uuid4
from discord import ApplicationContext, Embed, File, option
from discord import utils as ds_utils
from discord.commands import SlashCommandGroup
from discord.ext import commands
from libbot.pycord.classes import PycordBot
from classes.holo_user import HoloUser
from enums.colors import Color
from modules.database import col_users
from modules.utils import config_get
from modules.utils_sync import config_get_sync, guild_name, json_write_sync
logger = logging.getLogger(__name__)
class Data(commands.Cog):
def __init__(self, client: PycordBot):
self.client = client
data = SlashCommandGroup("data", "Керування даними користувачів")
@data.command(
name="export",
description="Експортувати дані",
guild_ids=[config_get_sync("guild")],
)
@option(
"kind", description="Тип даних, які треба експортувати", choices=["Користувачі"]
)
async def data_export_cmd(self, ctx: ApplicationContext, kind: str):
await ctx.defer()
holo_user = HoloUser(ctx.author)
if (ctx.user.id in self.client.owner_ids) or (
await holo_user.is_council(ctx.author)
):
logging.info(
"Moderator %s exported current users list", guild_name(ctx.user)
)
makedirs("tmp", exist_ok=True)
uuid = str(uuid4())
if kind == "Користувачі":
users = []
for member in ctx.guild.members:
users.append(
{
"id": member.id,
"nick": member.nick,
"username": f"{member.name}#{member.discriminator}",
"bot": member.bot,
}
)
json_write_sync(users, str(Path(f"tmp/{uuid}")))
await ctx.respond(
file=File(str(Path(f"tmp/{uuid}")), filename="users.json")
)
else:
logging.info(
"User %s tried to use /export but permission denied",
guild_name(ctx.user),
)
await ctx.respond(
embed=Embed(
title="Відмовлено в доступі",
description="Здається, це команда лише для модераторів",
color=Color.fail,
)
)
mod_role = ds_utils.get(
ctx.user.guild.roles, id=await config_get("moderators", "roles")
)
admin_chan = ds_utils.get(
ctx.user.guild.channels,
id=await config_get("adminchat", "channels", "text"),
)
await admin_chan.send(
content=f"{mod_role.mention}",
embed=Embed(
title="Неавторизований запит",
description=f"Користувач {ctx.user.mention} запитав у каналі {ctx.channel.mention} команду, до якої не повинен мати доступу/бачити.",
color=Color.fail,
),
)
@data.command(
name="migrate",
description="Мігрувати всіх користувачів до бази",
guild_ids=[config_get_sync("guild")],
)
@option(
"kind", description="Тип даних, які треба експортувати", choices=["Користувачі"]
)
async def data_migrate_cmd(self, ctx: ApplicationContext, kind: str):
await ctx.defer()
holo_user = HoloUser(ctx.author)
if (ctx.user.id in self.client.owner_ids) or (
await holo_user.is_council(ctx.author)
):
logging.info(
"Moderator %s started migration of all members to the database",
guild_name(ctx.user),
)
if kind == "Користувачі":
for member in ctx.guild.members:
if member.bot:
continue
if col_users.find_one({"user": member.id}) is None:
user = {}
defaults = await config_get("user", "defaults")
user["user"] = member.id
for key in defaults:
user[key] = defaults[key]
col_users.insert_one(document=user)
logging.info(
"Added DB record for user %s during migration", member.id
)
await ctx.respond(
embed=Embed(
title="Міграцію завершено",
description="Всім користувачам сервера було створено записи в базі даних.",
color=Color.success,
)
)
else:
logging.info(
"User %s tried to use /migrate but permission denied",
guild_name(ctx.user),
)
await ctx.respond(
embed=Embed(
title="Відмовлено в доступі",
description="Здається, це команда лише для модераторів",
color=Color.fail,
)
)
mod_role = ds_utils.get(
ctx.user.guild.roles, id=await config_get("moderators", "roles")
)
admin_chan = ds_utils.get(
ctx.user.guild.channels,
id=await config_get("adminchat", "channels", "text"),
)
await admin_chan.send(
content=f"{mod_role.mention}",
embed=Embed(
title="Неавторизований запит",
description=f"Користувач {ctx.user.mention} запитав у каналі {ctx.channel.mention} команду, до якої не повинен мати доступу/бачити.",
color=Color.fail,
),
)
def setup(client: PycordBot):
client.add_cog(Data(client))

59
cogs/fun.py Normal file
View File

@@ -0,0 +1,59 @@
import logging
from discord import ApplicationContext, Embed, User, option, slash_command
from discord.ext import commands
from libbot.pycord.classes import PycordBot
from WaifuPicsPython import WaifuAsync
from modules.utils import config_get
from modules.utils_sync import config_get_sync, guild_name
logger = logging.getLogger(__name__)
wafiu_pics = WaifuAsync()
class Fun(commands.Cog):
def __init__(self, client: PycordBot):
self.client = client
@slash_command(
name="action",
description="Провести над користувачем РП дію",
guild_ids=[config_get_sync("guild")],
)
@option(
"type",
description="Тип дії, яку хочете провести з користувачем",
choices=config_get_sync("actions").keys(),
)
@option("user", description="Користувач")
async def action_cmd(self, ctx: ApplicationContext, type: str, user: User):
await ctx.defer()
action = await config_get("category", "actions", type)
action_verb = await config_get("action", "actions", type)
image = await wafiu_pics.sfw(action)
logger.info(
"User %s (%s) %s %s (%s) with image %s",
guild_name(ctx.user),
ctx.user.id,
action_verb,
guild_name(user),
user.id,
image,
)
embed = Embed(
description=f"**{guild_name(ctx.user)}** {action_verb} **{guild_name(user)}**",
color=0x2F3136,
)
embed.set_image(url=image)
await ctx.respond(embed=embed)
def setup(client: PycordBot):
client.add_cog(Fun(client))

67
cogs/logger.py Normal file
View File

@@ -0,0 +1,67 @@
from discord import Member, Message
from discord import utils as ds_utils
from discord.ext import commands
from libbot.pycord.classes import PycordBot
from modules.database import col_users
from modules.utils import config_get
class Logger(commands.Cog):
def __init__(self, client: PycordBot):
self.client = client
@commands.Cog.listener()
async def on_message(self, message: Message):
if (
(message.author != self.client.user)
and (message.author.bot == False)
and (message.author.system == False)
):
if col_users.find_one({"user": message.author.id}) is None:
user = {}
defaults = await config_get("user", "defaults")
user["user"] = message.author.id
for key in defaults:
user[key] = defaults[key]
col_users.insert_one(document=user)
@commands.Cog.listener()
async def on_member_join(self, member: Member):
welcome_chan = ds_utils.get(
self.client.get_guild(await config_get("guild")).channels,
id=await config_get("welcome", "channels", "text"),
)
rules_chan = ds_utils.get(
self.client.get_guild(await config_get("guild")).channels,
id=await config_get("rules", "channels", "text"),
)
if (
(member != self.client.user)
and (member.bot == False)
and (member.system == False)
):
await welcome_chan.send(
content=(await config_get("welcome", "messages")).format(
mention=member.mention, rules=rules_chan.mention
)
)
if col_users.find_one({"user": member.id}) is None:
user = {}
defaults = await config_get("user", "defaults")
user["user"] = member.id
for key in defaults:
user[key] = defaults[key]
col_users.insert_one(document=user)
def setup(client: PycordBot):
client.add_cog(Logger(client))

View File

@@ -1,9 +1,20 @@
{ {
"token": "", "locale": "en",
"owner": 0, "debug": false,
"guild": 0, "guild": 0,
"admins": [], "bot": {
"status": "crying clowns", "owners": [
0
],
"debug_guilds": [
0
],
"bot_token": ""
},
"status": {
"type": "playing",
"message": "on your nerves"
},
"database": { "database": {
"user": null, "user": null,
"password": null, "password": null,
@@ -15,8 +26,63 @@
"size": 512, "size": 512,
"location": "logs" "location": "logs"
}, },
"defaults": {
"user": {
"customrole": null,
"customchannel": null
}
},
"categories": { "categories": {
"customchannels": 0 "customchannels": 0
}, },
"bots": {} "channels": {
"text": {
"rules": 0,
"welcome": 0,
"adminchat": 0
},
"voice": {}
},
"roles": {
"council": 0,
"moderators": 0
},
"bots": {},
"messages": {
"welcome": "Вітаємо {mention} на сервері HoloUA! Будь ласка, ознайомся з правилами серверу на каналі {rules}. Сподіваємося, тобі тут сподобається!"
},
"actions": {
"Вкусити": {
"category": "bite",
"action": "робить кусь"
},
"Обійняти": {
"category": "hug",
"action": "обіймає"
},
"Поцілувати": {
"category": "kiss",
"action": "цілує"
},
"Лизнути": {
"category": "lick",
"action": "лиже"
},
"Погладити": {
"category": "pat",
"action": "гладить"
},
"Тикнути": {
"category": "poke",
"action": "тикає в"
},
"Помахати": {
"category": "wave",
"action": "махає"
},
"Підморгнути": {
"category": "wink",
"action": "підморгує"
}
}
} }

9
locale/en.json Normal file
View File

@@ -0,0 +1,9 @@
{
"metadata": {
"flag": "🇬🇧",
"name": "English",
"codes": [
"en"
]
}
}

9
locale/uk.json Normal file
View File

@@ -0,0 +1,9 @@
{
"metadata": {
"flag": "🇺🇦",
"name": "Українська",
"codes": [
"uk"
]
}
}

46
main.py
View File

@@ -1,15 +1,12 @@
import asyncio
import logging import logging
from discord import Activity, ActivityType from discord import Activity, ActivityType
from modules.scheduled import scheduler
from modules.client import client from modules.client import client
from modules.scheduled import scheduler
from modules.utils import config_get from modules.utils import config_get
from modules.utils_sync import config_get_sync from modules.utils_sync import config_get_sync
from cogs.custom_channels import CustomChannels
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s", format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
@@ -28,18 +25,49 @@ except ImportError:
@client.event @client.event
async def on_ready(): async def on_ready():
logger.info(f"Logged in as {client.user}") logger.info("Logged in as %s", client.user)
await client.change_presence(
activity=Activity(type=ActivityType.listening, name=await config_get("status")) activity_type = await config_get("type", "status")
activity_message = await config_get("message", "status")
if activity_type == "playing":
await client.change_presence(
activity=Activity(type=ActivityType.playing, name=activity_message)
)
elif activity_type == "watching":
await client.change_presence(
activity=Activity(type=ActivityType.watching, name=activity_message)
)
elif activity_type == "listening":
await client.change_presence(
activity=Activity(type=ActivityType.listening, name=activity_message)
)
elif activity_type == "streaming":
await client.change_presence(
activity=Activity(type=ActivityType.streaming, name=activity_message)
)
elif activity_type == "competing":
await client.change_presence(
activity=Activity(type=ActivityType.competing, name=activity_message)
)
elif activity_type == "custom":
await client.change_presence(
activity=Activity(type=ActivityType.custom, name=activity_message)
)
else:
return
logger.info(
"Set activity type to %s with message %s", activity_type, activity_message
) )
def main(): def main():
client.add_cog(CustomChannels(client)) client.load_extension("cogs")
try: try:
scheduler.start() scheduler.start()
client.run(config_get_sync("token")) client.run(config_get_sync("bot_token", "bot"))
except KeyboardInterrupt: except KeyboardInterrupt:
scheduler.shutdown() scheduler.shutdown()
exit() exit()

View File

@@ -1,5 +1,6 @@
from discord import Intents, Bot from discord import Intents
from libbot.pycord.classes import PycordBot
intents = Intents().all() intents = Intents().all()
intents.members = True intents.members = True
client = Bot(intents=intents) client = PycordBot(intents=intents)

View File

@@ -18,13 +18,10 @@ db = db_client.get_database(name=db_config["name"])
collections = db.list_collection_names() collections = db.list_collection_names()
for collection in ["users", "warnings", "scheduler"]: for collection in ["users", "warnings", "scheduler", "analytics"]:
if not collection in collections: if not collection in collections:
db.create_collection(collection) db.create_collection(collection)
col_users = db.get_collection("users") col_users = db.get_collection("users")
col_warnings = db.get_collection("warnings") col_warnings = db.get_collection("warnings")
# col_checkouts = db.get_collection("checkouts") col_analytics = db.get_collection("analytics")
# col_trackings = db.get_collection("trackings")
# col_authorized = db.get_collection("authorized")
# col_transactions = db.get_collection("transactions")

View File

@@ -12,7 +12,9 @@ async def json_read(path: str) -> Any:
async def json_write(data: Any, path: str) -> None: async def json_write(data: Any, path: str) -> None:
async with aiofiles.open(path, mode="w", encoding="utf-8") as f: async with aiofiles.open(path, mode="w", encoding="utf-8") as f:
await f.write(dumps(data, ensure_ascii=False, escape_forward_slashes=False)) await f.write(
dumps(data, ensure_ascii=False, escape_forward_slashes=False, indent=4)
)
async def config_get(key: str, *path: str) -> Any: async def config_get(key: str, *path: str) -> Any:

View File

@@ -1,6 +1,6 @@
from typing import Any from typing import Any, Union
from discord import Member from discord import Member, User
from ujson import dumps, loads from ujson import dumps, loads
@@ -12,7 +12,7 @@ def json_read_sync(path: str) -> Any:
def json_write_sync(data: Any, path: str) -> None: def json_write_sync(data: Any, path: str) -> None:
with open(path, mode="w", encoding="utf-8") as f: with open(path, mode="w", encoding="utf-8") as f:
f.write(dumps(data, ensure_ascii=False, escape_forward_slashes=False)) f.write(dumps(data, ensure_ascii=False, escape_forward_slashes=False, indent=4))
def config_get_sync(key: str, *path: str) -> Any: def config_get_sync(key: str, *path: str) -> Any:
@@ -36,7 +36,9 @@ def config_set_sync(key: str, value: Any, *path: str) -> None:
return return
def guild_name(member: Member): def guild_name(member: Union[Member, User]) -> str:
if isinstance(member, User):
return member.name
if member.nick == None: if member.nick == None:
return member.name return member.name
else: else:

View File

@@ -1,5 +1,8 @@
aiofiles==23.1.0 aiofiles==23.2.1
apscheduler==3.10.1 apscheduler==3.10.4
py-cord[speed]==2.4.1 pymongo~=4.7.3
pymongo==4.3.3 requests~=2.32.3
ujson==5.7.0 ujson~=5.10.0
WaifuPicsPython==0.2.0
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
libbot[speed,pycord]==3.2.2

54
validation/analytics.json Normal file
View File

@@ -0,0 +1,54 @@
{
"$jsonSchema": {
"required": [
"user",
"channel",
"content",
"stickers",
"attachments"
],
"properties": {
"user": {
"bsonType": "long",
"description": "Discord ID of user"
},
"channel": {
"bsonType": "int",
"description": "Discord ID of a channel"
},
"content": {
"bsonType": ["null", "string"],
"description": "Text of the message"
},
"stickers": {
"bsonType": "array",
"items": {
"bsonType": "object",
"required": [
"id",
"name",
"format",
"url"
],
"properties": {
"id": {
"bsonType": "int"
},
"name": {
"bsonType": "string"
},
"format": {
"bsonType": "array"
},
"user": {
"bsonType": "string"
}
}
}
},
"attachments": {
"bsonType": "array"
}
}
}
}

24
validation/users.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$jsonSchema": {
"required": [
"user",
"customrole",
"customchannel"
],
"properties": {
"user": {
"bsonType": "long",
"description": "Discord ID of user"
},
"customrole": {
"bsonType": ["null", "long"],
"description": "Discord ID of custom role or 'null' if not set"
},
"customchannel": {
"bsonType": ["null", "long"],
"description": "Discord ID of custom channel or 'null' if not set"
}
}
}
}

18
validation/warnings.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$jsonSchema": {
"required": [
"user",
"warns"
],
"properties": {
"user": {
"bsonType": "long",
"description": "Discord ID of user"
},
"warns": {
"bsonType": "int",
"description": "Number of warnings on count"
}
}
}
}