Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cogs/verification/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .cog import Verification
from bot import Bot

__all__ = ("setup",)

async def setup(bot: Bot) -> None:
await bot.add_cog(Verification(bot))
174 changes: 174 additions & 0 deletions cogs/verification/cog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from typing import TYPE_CHECKING, Optional
import discord
from discord.ext import commands

from utils.wiki import Wiki

from .errors import AlreadyVerified, OwnershipNotProved, RequirementsNotSatsified
from .models import Account, Binding, RequirementsCheckResult, RequirementsCheckResultStatus, User
from config import config, strings as _strings
from utils.converters import AccountConverter

if TYPE_CHECKING:
from bot import Bot
from utils.context import WhiteContext

strings = _strings.verification

class Verification(commands.Cog):
def __init__(self, bot: "Bot"):
self.bot = bot

async def fetch_user_from_db(
self,
fandom_name: Optional[str] = None,
discord_id: Optional[int] = None,
trusted_only: bool = True
) -> Optional["User"]:
"""Fetches fandom-discord bindings from the database and returns Account object with that data.

Arguments:
fandom_name (Optional[str]): Account name on Fandom
discord_id (Optional[int]): Account id on Discord
trusted_only (bool): Whether to fetch only trusted bindings

Returns:
An User object or None when no bindings satsify the given parameters.
"""
conditions = []
args = []
if fandom_name is not None:
conditions.append(f"fandom_name = ${len(conditions) + 1}")
args.append(fandom_name)
if discord_id is not None:
conditions.append(f"discord_id = ${len(conditions) + 1}")
args.append(discord_id)
if trusted_only:
conditions.append(f"trusted = true")

async with self.bot.pool.acquire() as conn:
results = await conn.fetch(f"""
WITH RECURSIVE tmp AS (
SELECT *
FROM users
WHERE {" AND ".join(conditions)}
UNION
SELECT users.fandom_name, users.discord_id, users.guild_id, users.trusted, users.active
FROM users
JOIN tmp ON users.fandom_name = tmp.fandom_name or users.discord_id = tmp.discord_id
{"WHERE users.trusted = true" if trusted_only else ""}
) SELECT * FROM tmp;
""", *args)

if len(results) == 0:
return None

bindings = [
Binding(
fandom_account=Account(name=result["fandom_name"], wiki=Wiki.from_dot_notation("ru.c")),
discord_account=discord.Object(id=result["discord_id"]),
trusted=result["trusted"],
active=result["active"]
) for result in results
]
return User(_bot=self.bot, bindings=bindings)

async def is_verified(self, ctx: "WhiteContext", member: discord.Member) -> bool:
"""Checks whether the given account is verified in the given context.

Arguments:
ctx (WhiteContext): The context to check in
member (discord.Member): The member to check

Returns:
True if the account is verified, False otherwise.
"""
pass

async def check_requirements(self, ctx: "WhiteContext", user: User) -> RequirementsCheckResult:
"""Checks whether the user satsifies verification requirements in the given context.

Arguments:
ctx (WhiteContext): The context to check requirements in
user (User): The user to check requirements for

Returns:
ReqiurementsCheckResult object.
"""

async def verify(self, ctx: "WhiteContext", member: discord.Member, account: Account):
"""Verifies the user in the given context under the given account.

This method performs real actions, like giving roles and changing nickname.
It does not perform any checks or create bindings. It's up to the caller to do so.

Arguments:
ctx (WhiteContext): The context
member (discord.Member): The member to verify
account (Account): The account that will be used for verification

Returns:
None

Raises:
MissingPermissions: The bot doesn't have permissions to verify that user.
"""

@commands.hybrid_command(name="verify")
@discord.app_commands.guild_only()
@commands.guild_only()
async def verify_command(self, ctx: "WhiteContext", *, account: Account = commands.param(converter=AccountConverter)):
"""Верифицирует Вас на сервере.

Аргументы:
account: Имя Вашего аккаунта на Фэндоме
"""
await ctx.defer()

# a few asserts to make type checker happy
assert isinstance(ctx.author, discord.Member)
assert ctx.guild is not None

if await self.is_verified(ctx, ctx.author):
raise AlreadyVerified()

user = await self.fetch_user_from_db(discord_id=ctx.author.id, trusted_only=True)
if user is None or account not in user.fandom_accounts:
if account.discord_tag != str(ctx.author):
raise OwnershipNotProved()

if user is None:
user = User(_bot=self.bot)
await user.add_account(
fandom_account=account.name,
discord_id=ctx.author.id,
guild_id=ctx.guild.id,
trusted=True
)

req_check_result = await self.check_requirements(ctx, user)
if req_check_result.status is RequirementsCheckResultStatus.failed:
raise RequirementsNotSatsified(req_check_result)

await self.verify(ctx, ctx.author, account)

em = discord.Embed(
description=strings.verification_successful.format(guild=ctx.guild.name),
color=discord.Color.green(),
timestamp=discord.utils.utcnow()
)
em.set_author(
name=account.name,
url=account.page_url,
icon_url=account.avatar_url
)
em.add_field(
name=strings.account_header,
value=f"{config.emojis.success} {strings.ownership_confirmed}",
)
em.add_field(
name=strings.requirements_header,
value=f"{config.emojis.success} {strings.requirements_satsified}"
)
await ctx.send(embed=em)

21 changes: 21 additions & 0 deletions cogs/verification/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dataclasses import dataclass
from utils.errors import WhiteException

class VerificationError(WhiteException):
pass

class AlreadyVerified(VerificationError):
pass

class OwnershipNotProved(VerificationError):
pass

class RequirementsNotSatsified(VerificationError):
pass

@dataclass
class MissingPermissions(VerificationError):
add_role: bool
remove_role: bool
change_nickname: bool
create_role: bool
72 changes: 72 additions & 0 deletions cogs/verification/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from dataclasses import dataclass, field
import enum
from typing import TYPE_CHECKING, List, Optional

import discord

if TYPE_CHECKING:
from bot import Bot
from utils.wiki import Wiki

@dataclass
class Account:
name: str
wiki: "Wiki"
id: Optional[int] = None
discord_tag: Optional[str] = None

@property
def page_url(self):
return self.wiki.url_to("User:" + self.name)

@property
def avatar_url(self):
return f"https://services.fandom.com/user-avatar/user/{self.id}/avatar"

def __hash__(self) -> int:
return hash(self.name)

def __eq__(self, other: "Account") -> bool:
return self.name == other.name

@dataclass
class Binding:
fandom_account: Account
discord_account: discord.abc.Snowflake
trusted: bool
active: bool

@dataclass
class User:
"""Represents an user.

The user is a person that can have multiple accounts both on Fandom an Discord.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: and, not an

"""
_bot: "Bot"
bindings: List[Binding] = field(default_factory=list)

async def add_account(
self,
*,
guild_id: int,
fandom_account: Optional[str] = None,
discord_id: Optional[int] = None,
trusted: bool = True
):
pass

@property
def fandom_accounts(self) -> List[Account]:
return list(set(b.fandom_account for b in self.bindings))

@property
def discord_accounts(self) -> List[discord.abc.Snowflake]:
return list(set(b.discord_account for b in self.bindings))

class RequirementsCheckResultStatus(enum.Enum):
ok = True
failed = False

@dataclass
class RequirementsCheckResult:
status: RequirementsCheckResultStatus
1 change: 1 addition & 0 deletions config-default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ cogs:
# - cogs.help
- cogs.info
- cogs.fandom
- cogs.verification
- cogs.wikilinks

emojis:
Expand Down
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@ class BotEmojis:
if Path("config.yml").exists():
with open("config.yml") as f:
config.update(yaml.safe_load(f))

with open("resources/strings.yml") as f:
strings = _BotConfigImpl(yaml.safe_load(f))
14 changes: 14 additions & 0 deletions db/migrations/20220730054441_create_users_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- migrate:up
CREATE TABLE users (
fandom_name text,
discord_id bigint,
guild_id bigint,
trusted boolean,
active boolean
);

CREATE INDEX ON users (fandom_name, discord_id, guild_id);

-- migrate:down
DROP TABLE users;

23 changes: 22 additions & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ CREATE TABLE public.schema_migrations (
);


--
-- Name: users; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE public.users (
fandom_name text,
discord_id bigint,
guild_id bigint,
trusted boolean,
active boolean
);


--
-- Name: wh_guilds; Type: TABLE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -68,6 +81,13 @@ ALTER TABLE ONLY public.wh_wikilink_webhooks
ADD CONSTRAINT wh_wikilink_webhooks_pkey PRIMARY KEY (channel_id);


--
-- Name: users_fandom_name_discord_id_guild_id_idx; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX users_fandom_name_discord_id_guild_id_idx ON public.users USING btree (fandom_name, discord_id, guild_id);


--
-- PostgreSQL database dump complete
--
Expand All @@ -81,4 +101,5 @@ INSERT INTO public.schema_migrations (version) VALUES
('20210519201710'),
('20211006194734'),
('20211222113737'),
('20211226164418');
('20211226164418'),
('20220730054441');
Loading