diff --git a/README.md b/README.md index f38c384..a081545 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ -### C4GT Discord Bot +## C4GT Discord Bot -Features +### Running the Bot locally +1. Clone the repository onto your local system +2. [Optional+Recommended] Set up a python [virtual environment](https://docs.python.org/3/library/venv.html#:~:text=Creating%20virtual-,environments,-%C2%B6) and [activate](https://python.land/virtual-environments/virtualenv#Python_venv_activation) it before installing dependencies. A Python venv is an independent collection of python packages and is used for creating replicable dev environments and preventing versioning conflicts. +3. Use the [`pip install -r requirements.txt`](https://learnpython.com/blog/python-requirements-file/#:~:text=document%20and%20exit!-,Installing,-Python%20Packages%20From) command to install all dependencies. +4. Add the requisite `.env` file in the repository root. +5. Run the bot using `python3 main.py` or `python main.py` command in the terminal. + +### Reference +[how to build a simple discord bot](https://realpython.com/how-to-make-a-discord-bot-python/) +[discord.py](https://discordpy.readthedocs.io/en/stable/) + + +### Features - [ ] Allow tagging a `discordId` to a `githubId` so that contributions can be managed. Everyone coming to the server will be required to register themselves on the bot. Ask every contributor to connect their Github to Discord. The bot will take this publicly available info and add it to the database. - [ ] Seeing the list of projects and their quick links. Should allow for navigation to projects. - [ ] Documentation links diff --git a/cogs/listeners.py b/cogs/listeners.py new file mode 100644 index 0000000..65c1f24 --- /dev/null +++ b/cogs/listeners.py @@ -0,0 +1,39 @@ +from discord.ext import commands +import discord +from utils.db import SupabaseInterface + +class Listeners(commands.Cog): + def __init__(self, bot) -> None: + super().__init__() + self.bot = bot + + async def grantVerifiedRole(self, member: discord.Member): + verifiedContributorRoleID = 1123967402175119482 + try: + verifiedContributorRole = member.guild.get_role(verifiedContributorRoleID) + if verifiedContributorRole: + if verifiedContributorRole not in member.roles: + await member.add_roles(verifiedContributorRole, reason="Completed Auth and Introduction") + else: + print("Verified Contributor Role not found") + except Exception as e: + print("Exception while granting Role:", e) + + async def isAuthenticated(self, memberID: int) -> bool: + if SupabaseInterface("contributors").read("discord_id", memberID): + return True + else: + return False + + + @commands.Cog.listener("on_message") + async def listenForIntroduction(self, message: discord.Message): + if message.channel.id == 1107343423167541328: #intro channel + if await self.isAuthenticated(message.author.id): + await self.grantVerifiedRole(message.author) + else: + return + + +async def setup(bot: commands.Bot): + await bot.add_cog(Listeners(bot)) diff --git a/cogs/user_interactions.py b/cogs/user_interactions.py index 8247259..bc08c61 100644 --- a/cogs/user_interactions.py +++ b/cogs/user_interactions.py @@ -3,7 +3,6 @@ from discord.ext import commands, tasks import time, csv from utils.db import SupabaseInterface -from utils.api import GithubAPI VERIFIED_CONTRIBUTOR_ROLE_ID = 1123967402175119482 NON_CONTRIBUTOR_ROLES = [973852321870118914, 976345770477387788, 973852439054782464] @@ -54,9 +53,12 @@ async def create_embed(self): #This is a Discord View that is a set of UI elements that can be sent together in a message in discord. #This view send a link to Github Auth through c4gt flask app in the form of a button. +class RegistrationModal(discord.ui.Modal, title="Contributor Registration"): + name = discord.ui.TextInput(label='Name') class AuthenticationView(discord.ui.View): def __init__(self, discord_userdata): super().__init__() + self.timeout = None button = discord.ui.Button(label='Authenticate Github', style=discord.ButtonStyle.url, url=f'https://github-app.c4gt.samagra.io/authenticate/{discord_userdata}') self.add_item(button) self.message = None @@ -64,24 +66,26 @@ def __init__(self, discord_userdata): class UserHandler(commands.Cog): def __init__(self, bot) -> None: self.bot = bot - self.update_contributors.start() + # self.update_contributors.start() + + #Executing this command sends a link to Github OAuth App via a Flask Server in the DM channel of the one executing the command - @commands.command(aliases=['join']) - async def join_as_contributor(self, ctx): - #create a direct messaging channel with the one who executed the command - if isinstance(ctx.channel, discord.DMChannel): - userdata = str(ctx.author.id) - view = AuthenticationView(userdata) - await ctx.send("Please authenticate your github account to register in the C4GT Community", view=view) - # Command logic for DMs - else: - # Command logic for other channels (e.g., servers, groups) - await ctx.send("Please use this command in Bot DMs.") - # Command logic for DMs - userdata = str(ctx.author.id) - view = AuthenticationView(userdata) - # await dmchannel.send("Please authenticate your github account to register for Code for GovTech 2023", view=view) + # @commands.command(aliases=['join']) + # async def join_as_contributor(self, ctx): + # #create a direct messaging channel with the one who executed the command + # if isinstance(ctx.channel, discord.DMChannel): + # userdata = str(ctx.author.id) + # view = AuthenticationView(userdata) + # await ctx.send("Please authenticate your github account to register in the C4GT Community", view=view) + # # Command logic for DMs + # else: + # # Command logic for other channels (e.g., servers, groups) + # await ctx.send("Please use this command in Bot DMs.") + # # Command logic for DMs + # userdata = str(ctx.author.id) + # view = AuthenticationView(userdata) + # # await dmchannel.send("Please authenticate your github account to register for Code for GovTech 2023", view=view) @commands.command(aliases=["badges"]) async def list_badges(self, ctx): diff --git a/config.json b/config.json index 66ed946..2c69179 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,12 @@ { + "channels": { + "introduction": { + "id": "", + "name": "" + } + }, + "roles": [], + "CONTRIBUTOR_ROLE_ID": 973852365188907048, "INTRODUCTIONS_CHANNEL_ID": 1107343423167541328, "ERROR_CHANNEL_ID": 0, diff --git a/docker-compose.yml b/docker-compose.yml index 392dfdf..e142d0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,13 @@ services: container_name: discord-bot image: ghcr.io/code4govtech/discord-bot:main restart: always + logging: + driver: syslog + options: + syslog-address: "udp://172.26.0.1:12201" + tag: discord-bot + networks: + - logstash_common environment: TOKEN: ${TOKEN} SERVER_ID: ${SERVER_ID} @@ -12,3 +19,7 @@ services: FLASK_HOST: ${FLASK_HOST} SUPABASE_URL: ${SUPABASE_URL} SUPABASE_KEY: ${SUPABASE_KEY} + +networks: + logstash_common: + external: true diff --git a/main.py b/main.py index 4df3b53..512d9b6 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,187 @@ +from typing import Optional, Union import discord from discord.ext import commands import os, sys import asyncio -import dotenv +from discord.utils import MISSING +import dotenv, aiohttp, json +from utils.db import SupabaseInterface #Since there are user defined packages, adding current directory to python path current_directory = os.getcwd() sys.path.append(current_directory) dotenv.load_dotenv(".env") -intents = discord.Intents.all() -client = commands.Bot(command_prefix='!', intents=intents) +# class GithubAuthModal(discord.ui.Modal): +# def __init__(self, *,userID, title: str = None, timeout: float | None = None, custom_id: str = None) -> None: +# super().__init__(title=title, timeout=timeout, custom_id=custom_id) +# self.add_item(discord.ui.Button(label='Authenticate Github', style=discord.ButtonStyle.url, url=f'https://github-app.c4gt.samagra.io/authenticate/{userID}')) +class AuthenticationView(discord.ui.View): + def __init__(self, discord_userdata): + super().__init__() + button = discord.ui.Button(label='Authenticate Github', style=discord.ButtonStyle.url, url=f'https://github-app.c4gt.samagra.io/authenticate/{discord_userdata}') + self.add_item(button) + self.message = None + +# class ChapterSelect(discord.ui.Select): +# def __init__(self, affiliation, data): +# collegeOptions = [discord.SelectOption(label=option["label"], emoji=option["emoji"] ) for option in [ +# { +# "label": "NIT Kurukshetra", +# "emoji": "\N{GRADUATION CAP}" +# }, +# { +# "label": "ITER, Siksha 'O' Anusandhan", +# "emoji": "\N{GRADUATION CAP}" +# }, +# { +# "label": "IIITDM Jabalpur", +# "emoji": "\N{GRADUATION CAP}" +# }, +# { +# "label": "KIIT, Bhubaneswar", +# "emoji": "\N{GRADUATION CAP}" +# } + +# ]] +# corporateOptions = [] +# self.data = data +# super().__init__(placeholder="Please select your institute",max_values=1,min_values=1,options=collegeOptions if affiliation=="College Chapter" else corporateOptions) +# async def callback(self, interaction:discord.Interaction): +# self.data["chapter"] = self.values[0] +# self.data["discord_id"]= interaction.user.id + +# await interaction.response.send_message("Now please Authenticate using Github so we can start awarding your points!",view=AuthenticationView(interaction.user.id), ephemeral=True) + +# class AffiliationSelect(discord.ui.Select): +# def __init__(self, data): +# options = [discord.SelectOption(label=option["label"], emoji=option["emoji"] ) for option in [ +# { +# "label": "College Chapter", +# "emoji": "\N{OPEN BOOK}" +# }, +# { +# "label": "Corporate Chapter", +# "emoji": "\N{OFFICE BUILDING}" +# }, +# { +# "label": "Individual Contributor", +# "emoji": "\N{BRIEFCASE}" +# } +# ]] +# super().__init__(placeholder="Please select applicable affliliation",max_values=1,min_values=1,options=options) +# self.data = data +# async def callback(self, interaction:discord.Interaction): +# self.data["affiliation"] = self.values[0] +# if self.values[0] == "College Chapter": +# chapterView = discord.ui.View() +# chapterView.add_item(ChapterSelect(self.values[0], self.data)) +# await interaction.response.send_message("Please select your institute!", view=chapterView, ephemeral=True) +# elif self.values[0] == "Corporate Chapter": +# await interaction.response.send_message("We currently don't have any active Corporate Chapters!", ephemeral=True) +# elif self.values[0] == "Individual Contributor": +# await interaction.response.send_message("Now please Authenticate using Github so we can start awarding your points!",view=AuthenticationView(interaction.user.id), ephemeral=True) + +# class AffiliationView(discord.ui.View): + # def __init__(self, data): + # super().__init__() + # self.timeout = None + # self.add_item(AffiliationSelect(data)) + +class RegistrationModal(discord.ui.Modal): + def __init__(self, *, title: str = None, timeout: Union[float, None] = None, custom_id: str = None) -> None: + super().__init__(title=title, timeout=timeout, custom_id=custom_id) + + async def post_data(self, data): + url = 'https://kcavhjwafgtoqkqbbqrd.supabase.co/rest/v1/contributor_names' + headers = { + "apikey": f"{os.getenv('SUPABASE_KEY')}", + "Authorization": f"Bearer {os.getenv('SUPABASE_KEY')}", + "Content-Type": "application/json", + "Prefer": "return=minimal" + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, data=json.dumps(data)) as response: + if response.status == 200: + print("Data posted successfully") + else: + print("Failed to post data") + print("Status Code:", response.status) + + + name = discord.ui.TextInput(label='Please Enter Your Name', placeholder='To give you the recognition you deserve, could you please share your full name for the certificates!') + async def on_submit(self, interaction: discord.Interaction): + user = interaction.user + await interaction.response.send_message("Thanks! Now please sign in via Github!",view=AuthenticationView(user.id), ephemeral=True) + await self.post_data( + { + "name": self.name.value, + "discord_id": user.id + } + ) + + verifiedContributorRoleID = 1123967402175119482 + print("User:", type(user)) + if verifiedContributorRoleID in [role.id for role in user.roles]: + return + else: + async def hasIntroduced(): + print("Checking...") + authentication = SupabaseInterface("contributors").read("discord_id", user.id) + while not authentication: + await asyncio.sleep(30) + print("Found!") + discordEngagement = SupabaseInterface("discord_engagement").read("contributor", user.id)[0] + return discordEngagement["has_introduced"] + try: + await asyncio.wait_for(hasIntroduced(), timeout=1000) + verifiedContributorRole = user.guild.get_role(verifiedContributorRoleID) + if verifiedContributorRole: + if verifiedContributorRole not in user.roles: + await user.add_roles(verifiedContributorRole, reason="Completed Auth and Introduction") + except asyncio.TimeoutError: + print("Timed out waiting for authentication") + + + +class RegistrationView(discord.ui.View): + def __init__(self): + super().__init__(timeout = None) + + @discord.ui.button(label="Register", style=discord.enums.ButtonStyle.blurple, custom_id='registration_view:blurple') + async def reg(self, interaction: discord.Interaction, button: discord.ui.Button): + modal = RegistrationModal(title="Contributor Registration", custom_id="registration:modal") + await interaction.response.send_modal(modal) + +class C4GTBot(commands.Bot): + def __init__(self): + intents = discord.Intents.all() + intents.message_content = True + + super().__init__(command_prefix=commands.when_mentioned_or('!'), intents=intents) + + async def setup_hook(self) -> None: + # Register the persistent view for listening here. + # Note that this does not send the view to any message. + # In order to do this you need to first send a message with the View, which is shown below. + # If you have the message_id you can also pass it as a keyword argument, but for this example + # we don't have one. + self.add_view(RegistrationView()) + +client = C4GTBot() + +@client.command(aliases=['registration']) +async def registerAsContributor(ctx, channel: discord.TextChannel): + # guild = ctx.guild + # channelID = 1167054801385820240 + # channel = guild.get_channel_or_thread(channelID) + await channel.send("Please register using Github to sign up as a C4GT Contributor", view=RegistrationView()) + #alert message on commandline that bot has successfully logged in + @client.event async def on_ready(): print(f'We have logged in as {client.user}')