diff --git a/.env.sample b/.env.sample index dc1fa81..1a8a62d 100644 --- a/.env.sample +++ b/.env.sample @@ -11,4 +11,3 @@ SUPABASE_URL="" SUPABASE_KEY="" GithubPAT="" - diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..8077cd0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 120 +max-complexity = 10 +exclude = .git,__pycache__,.venv, .env +extend-ignore = E203 diff --git a/.gitignore b/.gitignore index 5dfbc50..e8d75a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.venv #environment variables and cache *.env -*/__pycache__/* +__pycache__/ *.csv +*.DS_Store /local_only diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..30d16c9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 23.11.0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + language_version: python3 diff --git a/Dockerfile b/Dockerfile index c5d6530..cf1fc0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,4 @@ RUN pip3 install -r requirements.txt COPY . . -CMD ["python3", "main.py"] \ No newline at end of file +CMD ["python3", "main.py"] diff --git a/README.md b/README.md index a081545..da32a87 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 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/) +[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/) @@ -40,7 +40,7 @@ ### Registration Steps (WIP) #### Step 1 -Connect your Github with Discord. +Connect your Github with Discord. #### Step 2 Got to [this URL](https://discord.com/api/oauth2/authorize?client_id=982859834355499088&redirect_uri=https%3A%2F%2Fbot.c4gt.samagra.io&response_type=code&scope=identify%20connections%20email) to register yourself. This URL allows the discord bot to get @@ -48,4 +48,3 @@ Got to [this URL](https://discord.com/api/oauth2/authorize?client_id=98285983435 - identity - connections (githubId) This registeres a token with us that we can use to check your GithubId that was connected in step 1. - diff --git a/cogs/badges.py b/cogs/badges.py index 9910af9..e024133 100644 --- a/cogs/badges.py +++ b/cogs/badges.py @@ -1,11 +1,15 @@ -from typing import Optional -from discord.ext import commands -import discord -from utils.db import SupabaseInterface import asyncio -class BadgeModal(discord.ui.Modal, title = "Your Badges"): - select = discord.ui.Select(placeholder="Choose Badge Type", options=["Points Based", "Achievement Based"]) +import discord +from discord.ext import commands + +from helpers.supabaseClient import SupabaseClient + + +class BadgeModal(discord.ui.Modal, title="Your Badges"): + select = discord.ui.Select( + placeholder="Choose Badge Type", options=["Points Based", "Achievement Based"] + ) async def on_timeout(self, interaction): return @@ -13,130 +17,175 @@ async def on_timeout(self, interaction): class BadgeContents: def __init__(self, name) -> None: - apprentinceDesc = f'''Welcome *{name}*!! - - -Congratulations! πŸŽ‰ You have taken the first step to join & introduce yourself to this awesome community and earned the **Apprentice Badge**! πŸŽ“ This badge shows that you are eager to learn and grow with our community! 😎 We are so happy to have you here and we can’t wait to see what you will create and solve! πŸš€''' - converseDesc = f'''Well done *{name}*! πŸ‘ - You have engaged on the C4GT discord community with 10 or more messages and earned the **Converser Badge!** πŸ’¬ This badge shows that you are a friendly and helpful member of our community! 😊 ''' - rockstarDesc = f'''Amazing *{name}*! πŸ™Œ - - You have received 5 upvotes on your message and earned the **Rockstar Badge!** 🌟 You add so much value to our community and we are grateful for your contribution! πŸ’– - + apprentinceDesc = f"""Welcome *{name}*!! + + +Congratulations! πŸŽ‰ You have taken the first step to join & introduce yourself to this awesome community and earned the **Apprentice Badge**! πŸŽ“ This badge shows that you are eager to learn and grow with our community! 😎 We are so happy to have you here and we can’t wait to see what you will create and solve! πŸš€""" + converseDesc = f"""Well done *{name}*! πŸ‘ + You have engaged on the C4GT discord community with 10 or more messages and earned the **Converser Badge!** πŸ’¬ This badge shows that you are a friendly and helpful member of our community! 😊 """ + rockstarDesc = f"""Amazing *{name}*! πŸ™Œ + + You have received 5 upvotes on your message and earned the **Rockstar Badge!** 🌟 You add so much value to our community and we are grateful for your contribution! πŸ’– + Please keep up the good work and share your expertise with us! πŸ™Œ - ''' - enthusiastDesc = f'''Wohoo *{name}*!!! + """ + enthusiastDesc = f"""Wohoo *{name}*!!! You have solved your first C4GT ticket ! -You have earned merged your first pull request and earned a** C4GT Enthusiast Badge**!πŸ₯³ +You have earned merged your first pull request and earned a** C4GT Enthusiast Badge**!πŸ₯³ This badge shows that you are a valuable member of our community and that you are ready to take on more challenges! 😎 But don’t stop here! There are more badges and rewards waiting for you! 🎁 The next badge is **Rising Star**, and you can get it by solving more issues and winning 30 points! πŸ’― -''' - risingStarDesc = f'''Hey *{name}*!!! +""" + risingStarDesc = f"""Hey *{name}*!!! You are on fire! πŸ”₯ You have earned 50 DPG points and reached a new level of excellence! πŸ™Œ You have earned the **C4GT Rising Star badge!** 🌟 - + This badge shows that you are a brilliant problem-solver and a leader in our community! 😎 You have impressed us all with your skills and passion! πŸ₯° But there’s more to come! There are more badges and rewards for you to unlock! 🎁 The next badge is **Wizard**, and you can get it by earning 60 points! πŸ’― - ''' + """ - dicordXGithubDesc = f'''Hey *{name}* + dicordXGithubDesc = f"""Hey *{name}* You have taken the first step towards becoming an active contributor by linking your Discord & Github handles!!πŸ™Œ Explore the C4GT Community Projects and get coding to earn more badges & pointsπŸš€πŸš€ -''' - wizardBadgeDesc = f'''Hey *{name}*!!! +""" + wizardBadgeDesc = f"""Hey *{name}*!!! Woah, you have acquired a total of 100 DPG points and reached great heights! You have earned the the **C4GT Wizard Badge**πŸ§™β€β™€οΈ Awesome job πŸŽ‰ Earning 100 points is no small feat. Your skills and dedication are impressive. Keep up the fantastic work! βœ¨πŸš€ You need 75 more points to reach the next level and unlock more benefits! The next badge is the **Ninja** badge, and we can’t wait to see you earn it soon πŸ™‚ -''' - ninjaBadgeDesc = f'''Congratulations, *{name}*! πŸŽ‰ -You have acquired a 175 DPG points with your active contribution to the C4GT community tickets. You're now a **C4GT Ninja Badge**πŸ₯· +""" + ninjaBadgeDesc = f"""Congratulations, *{name}*! πŸŽ‰ +You have acquired a 175 DPG points with your active contribution to the C4GT community tickets. You're now a **C4GT Ninja Badge**πŸ₯· Keep soaring high and setting benchmarks for open-source contributions. Your skills and determination to learn is amazing!🌟 Want to get on to the next level?πŸƒ Earn 100 more points to get awarded the **Warrior** Badge. Don’t wait up and get coding -''' - warriorBadgeDesc = f'''Woahh, *{name}*! πŸŽ‰ +""" + warriorBadgeDesc = f"""Woahh, *{name}*! πŸŽ‰ You are killing it! You have earned yourself 275 DPG Points with your stellar contribution and earned yourself the **C4GT Warrior Badge** βš”οΈ This really showcases your exceptional skills and abilities.πŸ› οΈ You have reached the highest level yet in the C4GT Community. πŸ”₯ Keep creating impact through meaningful contribution to open-source contribution to DPGs πŸ“ˆ -''' - - - self.apprenticeBadge = discord.Embed(title="Apprentice Badge", description=apprentinceDesc) - self.converserBadge = discord.Embed(title="Converser Badge", description=converseDesc) - self.rockstarBadge = discord.Embed(title="Rockstar Badge", description=rockstarDesc) - self.enthusiastBadge = discord.Embed(title="Enthusiast Badge", description=enthusiastDesc) - self.risingStarBadge = discord.Embed(title="Rising Star Badge", description=risingStarDesc) - self.discordXGithubBadge = discord.Embed(title="Discord X Github Badge", description=dicordXGithubDesc) - self.wizardBadge = discord.Embed(title="Wizard Badge",description=wizardBadgeDesc) +""" + + self.apprenticeBadge = discord.Embed( + title="Apprentice Badge", description=apprentinceDesc + ) + self.converserBadge = discord.Embed( + title="Converser Badge", description=converseDesc + ) + self.rockstarBadge = discord.Embed( + title="Rockstar Badge", description=rockstarDesc + ) + self.enthusiastBadge = discord.Embed( + title="Enthusiast Badge", description=enthusiastDesc + ) + self.risingStarBadge = discord.Embed( + title="Rising Star Badge", description=risingStarDesc + ) + self.discordXGithubBadge = discord.Embed( + title="Discord X Github Badge", description=dicordXGithubDesc + ) + self.wizardBadge = discord.Embed( + title="Wizard Badge", description=wizardBadgeDesc + ) self.ninjaBadge = discord.Embed(title="Ninja Badge", description=ninjaBadgeDesc) - self.warriorBadge = discord.Embed(title="Warrior Badge", description=warriorBadgeDesc) - - self.apprenticeBadge.set_image(url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Apprentice.png") - self.converserBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Converser.png') - self.rockstarBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Rockstar.png') - self.enthusiastBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Enthusiast.png') - self.risingStarBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/RisingStar.png') - self.discordXGithubBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Discord+Github.png') - self.wizardBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Wizard.jpeg') - self.ninjaBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Ninja.jpg') - self.warriorBadge.set_image(url='https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Warrior.jpeg') - + self.warriorBadge = discord.Embed( + title="Warrior Badge", description=warriorBadgeDesc + ) + + self.apprenticeBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Apprentice.png" + ) + self.converserBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Converser.png" + ) + self.rockstarBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Rockstar.png" + ) + self.enthusiastBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Enthusiast.png" + ) + self.risingStarBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/RisingStar.png" + ) + self.discordXGithubBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Discord+Github.png" + ) + self.wizardBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Wizard.jpeg" + ) + self.ninjaBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Ninja.jpg" + ) + self.warriorBadge.set_image( + url="https://raw.githubusercontent.com/Code4GovTech/discord-bot/main/assets/Warrior.jpeg" + ) + def get_user_badges(self, discord_id): - userBadges = { - "points": [], - "achievements": [] - } - if len(SupabaseInterface("contributors_registration").read(query_key="discord_id", query_value=discord_id))>0: + userBadges = {"points": [], "achievements": []} + if ( + len( + SupabaseClient().read( + "contributors_registration", + query_key="discord_id", + query_value=discord_id, + ) + ) + > 0 + ): userBadges["achievements"].append(self.discordXGithubBadge) - discordMemberData = SupabaseInterface("discord_engagement").read("contributor", discord_id) + discordMemberData = SupabaseClient().read( + "discord_engagement", "contributor", discord_id + ) if discordMemberData: - if discordMemberData[0]["total_message_count"]>10: + if discordMemberData[0]["total_message_count"] > 10: userBadges["achievements"].append(self.converserBadge) - if discordMemberData[0]["total_reaction_count"]>5: + if discordMemberData[0]["total_reaction_count"] > 5: userBadges["achievements"].append(self.rockstarBadge) if discordMemberData[0]["has_introduced"]: userBadges["achievements"].append(self.apprenticeBadge) - contributorData = SupabaseInterface("contributors_registration").read(query_key="discord_id", query_value=discord_id) + contributorData = SupabaseClient().read( + "contributors_registration", query_key="discord_id", query_value=discord_id + ) if contributorData: github_id = contributorData[0]["github_id"] prData = { - "raised": SupabaseInterface(table="pull_requests").read(query_key="raised_by", query_value=github_id), - "merged":SupabaseInterface(table="pull_requests").read(query_key="merged_by", query_value=github_id) + "raised": SupabaseClient().read( + table="pull_requests", query_key="raised_by", query_value=github_id + ), + "merged": SupabaseClient(table="pull_requests").read( + table="pull_requests", query_key="merged_by", query_value=github_id + ), } points = 0 for action in prData.keys(): prs = prData[action] for pr in prs: - points+=pr["points"] - if len(prData["raised"])+len(prData["merged"])>0: + points += pr["points"] + if len(prData["raised"]) + len(prData["merged"]) > 0: userBadges["points"].append(self.enthusiastBadge) - if points>=50: + if points >= 50: userBadges["points"].append(self.risingStarBadge) - if points>=100: + if points >= 100: userBadges["points"].append(self.wizardBadge) - if points>=175: + if points >= 175: userBadges["points"].append(self.ninjaBadge) - if points>=275: + if points >= 275: userBadges["points"].append(self.warriorBadge) if not discordMemberData and not contributorData: return None return userBadges - - class Badges(commands.Cog): def __init__(self, bot) -> None: self.bot = bot @@ -145,87 +194,115 @@ def __init__(self, bot) -> None: async def show_badges(self, ctx): # only works in DM channels if isinstance(ctx.channel, discord.DMChannel): - #Information abt the point system - infoEmbed = discord.Embed(title="Point System", description='If you want to understand more about the points & badge system, check out this [link](https://github.com/Code4GovTech/C4GT/wiki/Point-System-for-Contributors).') - await ctx.send(embed = infoEmbed) + # Information abt the point system + infoEmbed = discord.Embed( + title="Point System", + description="If you want to understand more about the points & badge system, check out this [link](https://github.com/Code4GovTech/C4GT/wiki/Point-System-for-Contributors).", + ) + await ctx.send(embed=infoEmbed) name = ctx.author.name user_badges = { - "points": [BadgeContents(name).enthusiastBadge, BadgeContents(name).rockstarBadge, BadgeContents(name).wizardBadge], - "achievements": [BadgeContents(name).apprenticeBadge, BadgeContents(name).converserBadge] + "points": [ + BadgeContents(name).enthusiastBadge, + BadgeContents(name).rockstarBadge, + BadgeContents(name).wizardBadge, + ], + "achievements": [ + BadgeContents(name).apprenticeBadge, + BadgeContents(name).converserBadge, + ], } - embed = discord.Embed(title="Badge Type", description="What badge type do you want to view?", color=discord.Color.blue()) - embed.set_footer(text="Please react with πŸ“ˆ for Points Based Badges or πŸ₯³ Achievements Based Badges .") + embed = discord.Embed( + title="Badge Type", + description="What badge type do you want to view?", + color=discord.Color.blue(), + ) + embed.set_footer( + text="Please react with πŸ“ˆ for Points Based Badges or πŸ₯³ Achievements Based Badges ." + ) message = await ctx.send(embed=embed) - await message.add_reaction('πŸ“ˆ') - await message.add_reaction('πŸ₯³') + await message.add_reaction("πŸ“ˆ") + await message.add_reaction("πŸ₯³") + def check(reaction, user): - return user == ctx.message.author and str(reaction.emoji) in ['πŸ“ˆ', 'πŸ₯³'] + return user == ctx.message.author and str(reaction.emoji) in ["πŸ“ˆ", "πŸ₯³"] + try: - reaction, user = await self.bot.wait_for('reaction_add', timeout=60.0, check=check) + reaction, user = await self.bot.wait_for( + "reaction_add", timeout=60.0, check=check + ) except asyncio.TimeoutError: await ctx.send("You took too long to respond.") else: - if str(reaction.emoji) == 'πŸ“ˆ': + if str(reaction.emoji) == "πŸ“ˆ": for badge in user_badges["points"]: await ctx.send(embed=badge) - elif str(reaction.emoji) == 'πŸ₯³': + elif str(reaction.emoji) == "πŸ₯³": for badge in user_badges["achievements"]: await ctx.send(embed=badge) - - - @commands.command() async def my_badges(self, ctx): - #Check if this happens in DM + # Check if this happens in DM if isinstance(ctx.channel, discord.DMChannel): - infoEmbed = discord.Embed(title="Point System", description='If you want to understand more about the points & badge system, check out this [link](https://github.com/Code4GovTech/C4GT/wiki/Point-System-for-Contributors).') - await ctx.send(embed = infoEmbed) - #Get available badges + infoEmbed = discord.Embed( + title="Point System", + description="If you want to understand more about the points & badge system, check out this [link](https://github.com/Code4GovTech/C4GT/wiki/Point-System-for-Contributors).", + ) + await ctx.send(embed=infoEmbed) + # Get available badges user_badges = BadgeContents(ctx.author.name).get_user_badges(ctx.author.id) if not user_badges: await ctx.channel.send("Oops! It seems you aren' registered!") else: if user_badges["points"] and user_badges["achievements"]: - embed = discord.Embed(title="Badge Type", description="What badge type do you want to view?", color=discord.Color.blue()) - embed.set_footer(text="Please react with πŸ“ˆ for Points Based Badges or πŸ₯³ Achievements Based Badges .") + embed = discord.Embed( + title="Badge Type", + description="What badge type do you want to view?", + color=discord.Color.blue(), + ) + embed.set_footer( + text="Please react with πŸ“ˆ for Points Based Badges or πŸ₯³ Achievements Based Badges ." + ) message = await ctx.send(embed=embed) - await message.add_reaction('πŸ“ˆ') - await message.add_reaction('πŸ₯³') + await message.add_reaction("πŸ“ˆ") + await message.add_reaction("πŸ₯³") + def check(reaction, user): - return user == ctx.message.author and str(reaction.emoji) in ['πŸ“ˆ', 'πŸ₯³'] + return user == ctx.message.author and str(reaction.emoji) in [ + "πŸ“ˆ", + "πŸ₯³", + ] + try: - reaction, user = await self.bot.wait_for('reaction_add', timeout=60.0, check=check) + reaction, user = await self.bot.wait_for( + "reaction_add", timeout=60.0, check=check + ) except asyncio.TimeoutError: await ctx.send("You took too long to respond.") else: - if str(reaction.emoji) == 'πŸ“ˆ': + if str(reaction.emoji) == "πŸ“ˆ": for badge in user_badges["points"]: await ctx.send(embed=badge) - elif str(reaction.emoji) == 'πŸ₯³': + elif str(reaction.emoji) == "πŸ₯³": for badge in user_badges["achievements"]: await ctx.send(embed=badge) else: if user_badges["points"]: for badge in user_badges["points"]: - await ctx.send(embed=badge) + await ctx.send(embed=badge) if user_badges["achievements"]: for badge in user_badges["achievements"]: - await ctx.send(embed=badge) + await ctx.send(embed=badge) else: - await ctx.send(f"Hey {ctx.author.name}\n\nYou have not earned any badges yet. Keep contributing and engaging on our community to earn more badges!!") + await ctx.send( + f"Hey {ctx.author.name}\n\nYou have not earned any badges yet. Keep contributing and engaging on our community to earn more badges!!" + ) else: ctx.send("This command is only usable by DMing the bot") - - - - - - - async def setup(bot): - await bot.add_cog(Badges(bot)) \ No newline at end of file + await bot.add_cog(Badges(bot)) diff --git a/cogs/discordDataScraper.py b/cogs/discordDataScraper.py new file mode 100644 index 0000000..236610f --- /dev/null +++ b/cogs/discordDataScraper.py @@ -0,0 +1,281 @@ +import json +import os +import sys +from asyncio import sleep +from datetime import datetime + +import discord +from discord import Member +from discord.channel import TextChannel +from discord.ext import commands, tasks + +from helpers.supabaseClient import SupabaseClient + +with open("config.json") as config_file: + config_data = json.load(config_file) + +# CONSTANTS +CONTRIBUTOR_ROLE_ID = config_data["CONTRIBUTOR_ROLE_ID"] +INTRODUCTIONS_CHANNEL_ID = config_data["INTRODUCTIONS_CHANNEL_ID"] +ERROR_CHANNEL_ID = config_data["ERROR_CHANNEL_ID"] +TIME_DURATION = config_data["TIME_DURATION"] + + +class DiscordDataScaper(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + + @commands.Cog.listener() + async def on_message(self, message): + contributor = SupabaseClient().read( + "discord_engagement", "contributor", message.author.id + ) + print("message", len(message.content)) + if not contributor: + SupabaseClient().insert( + "discord_engagement", + { + "contributor": message.author.id, + "has_introduced": False, + "total_message_count": 1, + "total_reaction_count": 0, + }, + ) + return + if len(message.content) > 20: + if message.channel.id == INTRODUCTIONS_CHANNEL_ID: + print("intro") + SupabaseClient().update( + "discord_engagement", + {"has_introduced": True}, + "contributor", + message.author.id, + ) + SupabaseClient("discord_engagement").update( + "discord_engagement", + {"total_message_count": contributor[0]["total_message_count"] + 1}, + "contributor", + message.author.id, + ) + + @commands.Cog.listener() + async def on_reaction_add(self, reaction, user): + message = reaction.message + contributor = SupabaseClient().read( + "discord_engagement", "contributor", message.author.id + )[0] + if not contributor: + SupabaseClient().insert( + "discord_engagement", + { + "contributor": message.author.id, + "has_introduced": False, + "total_message_count": 0, + "total_reaction_count": 1, + }, + ) + return + print("reaction") + SupabaseClient().update( + "discord_engagement", + {"total_reaction_count": contributor["total_reaction_count"] + 1}, + "contributor", + message.author.id, + ) + + @commands.command() + async def add_engagement(self, ctx): + await ctx.channel.send("started") + + def addEngagmentData(data): + client = SupabaseClient() + client.insert("discord_engagement", data) + return + + guild = await self.bot.fetch_guild( + os.getenv("SERVER_ID") + ) # SERVER_ID Should be C4GT Server ID + channels = await guild.fetch_channels() + engagmentData = {} + + async for member in guild.fetch_members(limit=None): + memberData = { + "contributor": member.id, + "has_introduced": False, + "total_message_count": 0, + "total_reaction_count": 0, + } + engagmentData[member.id] = memberData + + for channel in channels: + print(channel.name) + if isinstance( + channel, TextChannel + ): # See Channel Types for info on text channels https://discordpy.readthedocs.io/en/stable/api.html?highlight=guild#discord.ChannelType + async for message in channel.history(limit=None): + if message.author.id not in engagmentData.keys(): + engagmentData[message.author.id] = { + "contributor": message.author.id, + "has_introduced": False, + "total_message_count": 0, + "total_reaction_count": 0, + } + if message.content == "": + continue + if len(message.content) > 20: + engagmentData[message.author.id]["total_message_count"] += 1 + if message.channel.id == INTRODUCTIONS_CHANNEL_ID: + engagmentData[message.author.id]["has_introduced"] = True + if message.reactions: + engagmentData[message.author.id]["total_reaction_count"] += len( + message.reactions + ) + addEngagmentData(list(engagmentData.values())) + print("Complete!", file=sys.stderr) + return + + def valid_user(ctx): + return ctx.author.id == 476285280811483140 + + @commands.command() + @commands.check(valid_user) + async def enable_webhook(self, ctx): + guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) + channels = await guild.fetch_channels() + enabled = [ + channel["channel_id"] + for channel in SupabaseClient().read_all("discord_channels") + ] + for channel in channels: + try: + feedback = f"""Channel: {channel.name}\nCategory: {channel.category} """ + await ctx.send(feedback) + if isinstance(channel, TextChannel) and channel.id not in enabled: + sleep(120) + webhook = await channel.create_webhook(name="New Ticket Alert") + feedback = f"""URL: {webhook.url}\n Token:{"Yes" if webhook.token else "No"}""" + await ctx.send(feedback) + SupabaseClient().insert( + "discord_channels", + { + "channel_id": channel.id, + "channel_name": channel.name, + "webhook": webhook.url, + }, + ) + except Exception as e: + await ctx.send(e) + continue + + @commands.command() + async def update_applicants(self, ctx): + try: + applicants_channel = self.bot.get_channel(1125359312370405396) + await ctx.send("Channel Identified:" + applicants_channel.name) + members = applicants_channel.members + await ctx.send("Member List Count: " + str(len(members))) + for member in members: + try: + SupabaseClient().insert( + "applicant", + {"sheet_username": member.name, "discord_id": member.id}, + ) + except Exception as e: + print(e) + continue + except Exception as e: + await ctx.send(e) + await ctx.send("Completed") + + # command to run the message collector + @commands.command() + async def start_collecting_messages(self, ctx): + if not self.collect_all_messages.is_running(): + self.collect_all_messages.start() + print("Initiating message collection") + await ctx.send("Message collection started.") + else: + await ctx.send("Message collection already in progress.") + + # command to stop the message collector + @commands.command() + async def stop_collecting_messages(self, ctx): + if self.collect_all_messages.is_running(): + self.collect_all_messages.cancel() + print("Stopping message collection") + await ctx.send("Message collection stopped.") + else: + await ctx.send("Message collection is not running.") + + # recurring job to collect all the messages + @tasks.loop(seconds=TIME_DURATION) + async def collect_all_messages(self): + print(f"Collecting all messages as of {datetime.now()}") + await self.add_messages() + + async def add_messages(self): + def addMessageData(data): + client = SupabaseClient() + client.insert("unstructured discord data", data) + return + + def getLastMessageObject(channelId): + last_message = SupabaseClient().read_by_order_limit( + table="unstructured discord data", + query_key="channel", + query_value=channelId, + order_column="id.desc", + ) # fetching the record for the lastest message downloaded from a particular channel, the most recent message has the largest message_id + if len(last_message) > 0: + print(f"Last message details for {channelId} is {last_message[0]}") + return discord.Object(id=last_message[0]["id"]) + else: + print(f"No previous messages obtained for {channelId}") + return None + + try: + guild = await self.bot.fetch_guild( + os.getenv("SERVER_ID") + ) # SERVER_ID Should be C4GT Server ID + channels = await guild.fetch_channels() + + for channel in channels: + print(f"Downloading messages for '{channel.name}' channel") + if isinstance( + channel, TextChannel + ): # See Channel Types for info on text channels https://discordpy.readthedocs.io/en/stable/api.html?highlight=guild#discord.ChannelType + messages = [] + last_message_object = getLastMessageObject(channel.id) + # fetching only the messages after the last message id, if None, then all the messages are fetched + async for message in channel.history( + limit=None, after=last_message_object + ): + if message.content == "": + continue + msg_data = { + "channel": channel.id, + "channel_name": channel.name, + "text": message.content, + "author": message.author.id, + "author_name": message.author.name, + "author_roles": message.author.roles + if isinstance(message.author, Member) + else [], + "sent_at": str(message.created_at), + "id": message.id, + } + messages.append(msg_data) + print(f"{len(messages)} new messages found ") + addMessageData(messages) + else: + print(f"{channel.name} not a text channel") + print(f"Downloaded all messages as of {datetime.now()}") + except Exception as e: + error_channel = await guild.fetch_channel(ERROR_CHANNEL_ID) + error_message = f"Error occurred while downloading messages: {e}" + await error_channel.send(error_message) + print(error_message) + + +async def setup(bot): + await bot.add_cog(DiscordDataScaper(bot)) diff --git a/cogs/discord_data_scraper.py b/cogs/discord_data_scraper.py deleted file mode 100644 index ac3e611..0000000 --- a/cogs/discord_data_scraper.py +++ /dev/null @@ -1,232 +0,0 @@ -from discord.ext import commands, tasks -from discord.channel import TextChannel -from discord import Member -import discord -import os, dateutil, json, sys -from datetime import datetime -from asyncio import sleep -from utils.db import SupabaseInterface -from utils.api import GithubAPI -import csv - -with open('config.json') as config_file: - config_data = json.load(config_file) - -#CONSTANTS -CONTRIBUTOR_ROLE_ID = config_data['CONTRIBUTOR_ROLE_ID'] -INTRODUCTIONS_CHANNEL_ID =config_data['INTRODUCTIONS_CHANNEL_ID'] -ERROR_CHANNEL_ID = config_data['ERROR_CHANNEL_ID'] -TIME_DURATION = config_data['TIME_DURATION'] - - - -class DiscordDataScaper(commands.Cog): - def __init__(self, bot) -> None: - self.bot = bot - - @commands.Cog.listener() - async def on_message(self, message): - contributor = SupabaseInterface("discord_engagement").read("contributor", message.author.id) - print("message", len(message.content)) - if not contributor: - SupabaseInterface("discord_engagement").insert({ - "contributor": message.author.id, - "has_introduced": False, - "total_message_count": 1, - "total_reaction_count": 0}) - return - if len(message.content)>20: - if message.channel.id == INTRODUCTIONS_CHANNEL_ID: - print("intro") - SupabaseInterface("discord_engagement").update({"has_introduced":True}, "contributor", message.author.id) - SupabaseInterface("discord_engagement").update({"total_message_count":contributor[0]["total_message_count"]+1}, "contributor", message.author.id) - - @commands.Cog.listener() - async def on_reaction_add(self, reaction, user): - message = reaction.message - contributor = SupabaseInterface("discord_engagement").read("contributor", message.author.id)[0] - if not contributor: - SupabaseInterface("discord_engagement").insert({ - "contributor": message.author.id, - "has_introduced": False, - "total_message_count": 0, - "total_reaction_count": 1}) - return - print("reaction") - SupabaseInterface("discord_engagement").update({"total_reaction_count":contributor["total_reaction_count"]+1}, "contributor", message.author.id) - - @commands.command() - async def add_engagement(self, ctx): - await ctx.channel.send("started") - def addEngagmentData(data): - client = SupabaseInterface("discord_engagement") - client.insert(data) - return - guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) #SERVER_ID Should be C4GT Server ID - channels = await guild.fetch_channels() - engagmentData = {} - - - - async for member in guild.fetch_members(limit=None): - memberData = { - "contributor": member.id, - "has_introduced": False, - "total_message_count": 0, - "total_reaction_count": 0 - - } - engagmentData[member.id]= memberData - - for channel in channels: - print(channel.name) - if isinstance(channel, TextChannel): #See Channel Types for info on text channels https://discordpy.readthedocs.io/en/stable/api.html?highlight=guild#discord.ChannelType - async for message in channel.history(limit=None): - if message.author.id not in engagmentData.keys(): - engagmentData[message.author.id]= { - "contributor": message.author.id, - "has_introduced": False, - "total_message_count": 0, - "total_reaction_count": 0} - if message.content=='': - continue - if len(message.content)>20: - engagmentData[message.author.id]["total_message_count"]+=1 - if message.channel.id == INTRODUCTIONS_CHANNEL_ID: - engagmentData[message.author.id]["has_introduced"] =True - if message.reactions: - engagmentData[message.author.id]["total_reaction_count"]+=len(message.reactions) - addEngagmentData(list(engagmentData.values())) - print("Complete!", file=sys.stderr) - return - - def valid_user(ctx): - return ctx.author.id == 476285280811483140 - - - @commands.command() - @commands.check(valid_user) - async def enable_webhook(self, ctx): - guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) - channels = await guild.fetch_channels() - enabled = [channel["channel_id"] for channel in SupabaseInterface("discord_channels").read_all()] - for channel in channels: - try: - feedback = f'''Channel: {channel.name}\nCategory: {channel.category} ''' - await ctx.send(feedback) - if isinstance(channel, TextChannel) and channel.id not in enabled: - sleep(120) - webhook = await channel.create_webhook(name = 'New Ticket Alert') - feedback = f'''URL: {webhook.url}\n Token:{"Yes" if webhook.token else "No"}''' - await ctx.send(feedback) - SupabaseInterface("discord_channels").insert({ - "channel_id": channel.id, - "channel_name": channel.name, - "webhook": webhook.url - }) - except Exception as e: - await ctx.send(e) - continue - - - - - @commands.command() - async def update_applicants(self,ctx): - - try: - applicants_channel = self.bot.get_channel(1125359312370405396) - await ctx.send("Channel Identified:"+applicants_channel.name) - members = applicants_channel.members - await ctx.send("Member List Count: " + str(len(members))) - for member in members: - try: - SupabaseInterface("applicant").insert({'sheet_username':member.name, 'discord_id':member.id}) - except Exception as e: - print - continue - except Exception as e: - await ctx.send(e) - await ctx.send("Completed") - - - # command to run the message collector - @commands.command() - async def start_collecting_messages(self, ctx): - if not self.collect_all_messages.is_running(): - self.collect_all_messages.start() - print("Initiating message collection") - await ctx.send("Message collection started.") - else: - await ctx.send("Message collection already in progress.") - - # command to stop the message collector - @commands.command() - async def stop_collecting_messages(self, ctx): - if self.collect_all_messages.is_running(): - self.collect_all_messages.cancel() - print("Stopping message collection") - await ctx.send("Message collection stopped.") - else: - await ctx.send("Message collection is not running.") - - # recurring job to collect all the messages - @tasks.loop(seconds=TIME_DURATION) - async def collect_all_messages(self): - print(f"Collecting all messages as of {datetime.now()}") - await self.add_messages() - - async def add_messages(self): - - def addMessageData(data): - client = SupabaseInterface("unstructured discord data") - client.insert(data) - return - - def getLastMessageObject(channelId): - last_message = SupabaseInterface("unstructured discord data").read_by_order_limit(query_key="channel",query_value=channelId,order_column="id.desc") # fetching the record for the lastest message downloaded from a particular channel, the most recent message has the largest message_id - if len(last_message)>0: - print(f"Last message details for {channelId} is {last_message[0]}") - return discord.Object(id=last_message[0]['id'] ) - else: - print(f"No previous messages obtained for {channelId}") - return None - - try: - guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) #SERVER_ID Should be C4GT Server ID - channels = await guild.fetch_channels() - - for channel in channels: - print(f"Downloading messages for '{channel.name}' channel") - if isinstance(channel, TextChannel): #See Channel Types for info on text channels https://discordpy.readthedocs.io/en/stable/api.html?highlight=guild#discord.ChannelType - messages = [] - last_message_object = getLastMessageObject(channel.id) - # fetching only the messages after the last message id, if None, then all the messages are fetched - async for message in channel.history(limit=None, after=last_message_object): - if message.content=='': - continue - msg_data = { - "channel": channel.id, - "channel_name": channel.name, - "text": message.content, - "author": message.author.id, - "author_name": message.author.name, - "author_roles": message.author.roles if isinstance(message.author, Member) else [], - "sent_at":str(message.created_at), - "id": message.id - } - messages.append(msg_data) - print(f"{len(messages)} new messages found ") - addMessageData(messages) - else: - print(f"{channel.name} not a text channel") - print(f"Downloaded all messages as of {datetime.now()}") - except Exception as e: - error_channel = await guild.fetch_channel(ERROR_CHANNEL_ID) - error_message = f'Error occurred while downloading messages: {e}' - await error_channel.send(error_message) - print(error_message) - - -async def setup(bot): - await bot.add_cog(DiscordDataScaper(bot)) diff --git a/cogs/github_data_scraper.py b/cogs/github_data_scraper.py deleted file mode 100644 index 2348fe7..0000000 --- a/cogs/github_data_scraper.py +++ /dev/null @@ -1,26 +0,0 @@ -from discord.ext import commands - -class GithubDataScraper(commands.Cog): - def __init__(self, bot) -> None: - self.bot = bot - - - @commands.command() - async def update_prs(self, ctx): - return - - @commands.command() - async def update_issues(self, ctx): - return - - @commands.command() - async def update_commits(self, ctx): - return - - - - - - -async def setup(bot): - await bot.add_cog(GithubDataScraper(bot)) \ No newline at end of file diff --git a/cogs/listeners.py b/cogs/listeners.py deleted file mode 100644 index 65c1f24..0000000 --- a/cogs/listeners.py +++ /dev/null @@ -1,39 +0,0 @@ -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/listeners/member_events_cog.py b/cogs/listeners/member_events_cog.py new file mode 100644 index 0000000..b743de2 --- /dev/null +++ b/cogs/listeners/member_events_cog.py @@ -0,0 +1,27 @@ +import discord +from discord.ext import commands + +from helpers.supabaseClient import SupabaseClient + + +class MemberEventsListener(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + super().__init__() + + @commands.Cog.listener("on_member_join") + async def on_member_join(self, member: discord.Member): + SupabaseClient().updateContributor(member) + + @commands.Cog.listener("on_member_remove") + async def on_member_remove(self, member: discord.Member): + # Members leaving the discord server is not defined behavior as of now, but should be defined eventually + pass + + @commands.Cog.listener("on_member_update") + async def on_member_update(self, before: discord.Member, after: discord.Member): + SupabaseClient().updateContributor(after) + + +async def setup(bot: commands.Bot): + await bot.add_cog(MemberEventsListener(bot)) diff --git a/cogs/listeners/message_events_cog.py b/cogs/listeners/message_events_cog.py new file mode 100644 index 0000000..0fb4462 --- /dev/null +++ b/cogs/listeners/message_events_cog.py @@ -0,0 +1,42 @@ +import discord +from discord.ext import commands + +from config.server import ServerConfig +from helpers.supabaseClient import SupabaseClient + +serverConfig = ServerConfig() +supabaseClient = SupabaseClient() + + +async def grantVerifiedRole(member: discord.Member): + try: + verifiedContributorRole = member.guild.get_role( + serverConfig.Roles.CONTRIBUTOR_ROLE + ) + 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) + + +class MessageEventsListener(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + + @commands.Cog.listener("on_message") + async def on_message(self, message: discord.Message): + # Listen for Introduction + if message.channel.id == serverConfig.Channels.INTRODUCTION_CHANNEL: + if await supabaseClient.memberIsAuthenticated(message.author): + await grantVerifiedRole(message.author) + else: + return + + +async def setup(bot: commands.Bot): + await bot.add_cog(MessageEventsListener(bot)) diff --git a/cogs/listeners/role_events_cog.py b/cogs/listeners/role_events_cog.py new file mode 100644 index 0000000..36d3d34 --- /dev/null +++ b/cogs/listeners/role_events_cog.py @@ -0,0 +1,37 @@ +import discord +from discord.ext import commands + +from helpers.supabaseClient import SupabaseClient + + +class RoleEventsListener(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + super().__init__() + + @commands.Cog.listener() + async def on_guild_role_create(self, role: discord.Role): + if role.name.startswith("College:"): + orgName = role.name[len("College: ") :] + SupabaseClient().addChapter(orgName=orgName, type="COLLEGE") + elif role.name.startswith("Corporate:"): + orgName = role.name[len("Corporate: ") :] + SupabaseClient().addChapter(orgName=orgName, type="CORPORATE") + + @commands.Cog.listener() + async def on_guild_role_delete(self, role: discord.Role): + # Role Removal is not being handled this version, but it should be defined eventually + pass + + @commands.Cog.listener() + async def on_guild_role_update(self, before: discord.Role, after: discord.Role): + if after.name.startswith("College:"): + orgName = after.name[len("College:") :] + SupabaseClient().addChapter(orgName=orgName, type="COLLEGE") + elif after.name.startswith("Corporate:"): + orgName = after.name[len("Corporate: ") :] + SupabaseClient().addChapter(orgName=orgName, type="CORPORATE") + + +async def setup(bot: commands.Bot): + await bot.add_cog(RoleEventsListener(bot)) diff --git a/cogs/logger.py b/cogs/logger.py new file mode 100644 index 0000000..897aa93 --- /dev/null +++ b/cogs/logger.py @@ -0,0 +1,28 @@ +import os + +import aiohttp +import discord +from discord.ext import commands + +""" +Webhook Logger +Purpose: Create persistent logs for Exceptions raised in discord bot +""" + + +class WebhookLogger(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + self.url = os.getenv("WEBHOOK_URL") + + async def saveToSupabase(self): + pass + + async def log(self, content: str, username=None, embeds=[]): + async with aiohttp.ClientSession() as session: + webhook = discord.Webhook.from_url(url=self.url, session=session) + await webhook.send(content=content, username=username, embeds=embeds) + + +async def setup(bot: commands.Bot): + await bot.add_cog(WebhookLogger(bot)) diff --git a/cogs/metrics_tracker.py b/cogs/metrics_tracker.py deleted file mode 100644 index b23f194..0000000 --- a/cogs/metrics_tracker.py +++ /dev/null @@ -1,253 +0,0 @@ -# #Track metrics on github and discord and update the database accordingly -# #Implement using: https://discordpy.readthedocs.io/en/stable/ext/tasks/index.html?highlight=tasks# -from discord.ext import commands, tasks -from discord import Embed -import aiohttp, json -from utils.db import SupabaseInterface -# from discord import Member -# from discord.channel import TextChannel -# from datetime import time, datetime -# from models.product import Product -# from models.project import Project -# from utils.api import GithubAPI -# from utils.db import SupabaseInterface -# import requests, json -# import os, dateutil.parser - - -# async def getCetificate(name, badge): -# url = 'http://139.59.20.91:5000/rcw/credential' -# headers = { -# 'Content-Type': 'application/json' -# } -# data = { -# "type": ["Test Credential"], -# "subject": { -# "id": "did:C4GT:test", -# "username": f"{name}", -# "badge": f"{badge}" -# }, -# "schema": "cllbzgfor000ytj151nu7km4t", -# "tags": ["Tag 1"], -# "templateId": "cllbzglwa0010tj15104dtgc8" -# } - -# async with aiohttp.ClientSession() as session: -# async with session.post(url, headers=headers, json=data) as response: -# if response.status == 201: -# resp_data = await response.json() -# return json.loads(resp_data) -# else: -# print(f"Failed to fetch credential. Status code: {response.status}") -class MetricsTracker(commands.Cog): - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - -# @commands.command() -# async def my_certificates(self, ctx): - -# noCertsEmbed = Embed(title='', description=f'''Hey {ctx.author.name} - -# You have currently not earned any C4GT certificates yet! -# But don’t worry, all you need to do is collect 50 DPG points and get a Rising Star :stars: badge by solving issue tickets to become eligible for your first certificate. **Get coding now!!**:computer: - -# **Discover issue tickets [here](https://www.codeforgovtech.in/community-program-projects).** -# ''') -# oneCertEmbed = Embed( -# title="Congratulations!", -# description=f"Hey {ctx.author.mention}\n\n" -# "You have earned a C4GT certificate for being an **active DPG contributor and earning 50 DPG points!** :partying_face:\n\n" -# "Click [here](http://139.59.20.91:9000/c4gt/Kanav%20Dwevedi_Enthusiast%20Badge.pdf) to access your certificate :page_with_curl:", -# color=0x00ff00 # You can choose any color -# ) - -# contributor_data = SupabaseInterface("contributors").read("discord_id", ctx.author.id) -# if len(contributor_data)==0: -# ctx.send("Use the !join command to register to obtain certificates") -# return - -# [contributor] = contributor_data -# name = contributor["github_url"].split('/')[-1] - -# user = SupabaseInterface("github_profile_data").read("discord_id", ctx.author.id) -# data = user[0] -# data["points"] = 70 -# if data["points"] <50 : -# await ctx.send(embed=noCertsEmbed) -# elif data["points"]>=50 and data["points"]<100: - -# # resp = await getCetificate(name, "Enthusiast Badge") -# oneCertEmbed = Embed( -# title="Congratulations!", -# description=f"Hey {ctx.author.mention}\n\n" -# "You have earned a C4GT certificate for being an **active DPG contributor and earning 50 DPG points!** :partying_face:\n\n" -# "Click [here](http://139.59.20.91:9000/c4gt/Kanav%20Dwevedi_Enthusiast%20Badge.pdf) to access your certificate :page_with_curl:", -# color=0x00ff00 # You can choose any color -# ) -# await ctx.send(embed=oneCertEmbed) - - - - - @commands.command() - async def points(self, ctx): - await ctx.send(f'''Hey {ctx.author} - -**You have a total of 140 points**🌟 - -▢️ **Points Basis PRs accepted - 60 points**πŸ”₯ - -Number of tickets solved - 3 -Points on tickets with low complexity - 10 points -Points on tickets with medium complexity - 20 points -Points of tickets with high complexity - 30 points - -▢️ **Points as per PRs reviewed - 80 points**πŸ™Œ - -Number of tickets reviewed - 4 -Points on tickets with low complexity - 10 points -Points on tickets with medium complexity - 40 points -Points of tickets with high complexity - 30 points - -Get coding and earn more points to get a spot on the leaderboardπŸ“ˆ''') - - - - - -# #Command to assign a channel to a product -# @commands.command(aliases=['product','assign', 'assign channel', 'add channel']) -# #@commands.has_any_role([]) -# @commands.has_permissions(administrator=True) -# async def assign_channel_to_product(self, ctx, product_name=None): - -# #Check if product name was given -# if product_name is None: -# await ctx.channel.send("This command expects the name of the product as an argument like '!assign '") -# return - -# #Check if channel is a valid type -# if str(ctx.channel.type) not in ['text']: -# await ctx.channel.send("Only text channels may be assigned to products") -# return - - -# #Check if given product name -# if not Product.is_product(product_name): -# await ctx.channel.send(f"{product_name} is not a valid product name. Please try again.") -# return - - -# product = Product(name=product_name) -# product.assign_channel(ctx.channel.id) -# await ctx.channel.send(f"Channel successfully assigned to product {product_name}") -# return - -# #error handling for assigning channel to product -# @assign_channel_to_product.error -# async def handle_assignment_error(self, ctx, error): -# pass - -# # async def get_discord_metrics(self): -# # # print(1) -# # products = Product.get_all_products() - -# # print(products) - -# # discord_metrics = { -# # "measured_at": datetime.now(), -# # "metrics": dict() -# # } - -# # # print(2) - -# # for product in products: -# # # print(3) -# # discord_metrics["metrics"][product['name']] = { -# # "mentor_messages": 0, -# # "contributor_messages": 0 -# # } -# # channel_id = product["channel"] -# # channel = await self.bot.fetch_channel(channel_id) - -# # async for message in channel.history(limit=None): -# # # print(4) -# # if not isinstance(message.author, Member): -# # # print(5) -# # continue -# # if any(role.name.lower() == 'mentor' for role in message.author.roles): -# # discord_metrics["metrics"][product["name"]]['mentor_messages'] +=1 - -# # if any(role.name.lower() == 'contributor' for role in message.author.roles): -# # discord_metrics["metrics"][product['name']]['contributor_messages'] +=1 -# # # print(6) - -# # r = requests.post(f"""{os.getenv("FLASK_HOST")}/metrics/discord""", json=json.dumps(discord_metrics, indent=4, default=str)) -# # # print(r.json()) - -# # #Store metrics - - - -# # async def get_github_metrics(self): - -# # #Get all projects in the db -# # projects = Project.get_all_projects() - -# # github_metrics = { -# # "updated_at": datetime.now(), -# # "metrics": dict() -# # } - -# # for project in projects: -# # url_components = str(project['repository']).split('/') -# # url_components = [component for component in url_components if component != ''] -# # # print(url_components) -# # [protocol, host, repo_owner, repo_name] = url_components -# # api = GithubAPI(owner=repo_owner, repo=repo_name) - -# # (open_prs, closed_prs) = api.get_pull_request_count() -# # (open_issues, closed_issues) = api.get_issue_count() - - -# # github_metrics["metrics"][project["product"]] = { -# # "project": project["name"], -# # "repository": project["repository"], -# # "number_of_commits": api.get_commit_count(), -# # "open_prs": open_prs, -# # "closed_prs": closed_prs, -# # "open_issues": open_issues, -# # "closed_issues": closed_issues -# # } -# # r = requests.post(f"""{os.getenv("FLASK_HOST")}/metrics/github""", json=json.dumps(github_metrics, indent=4, default=str)) -# # # print(r.json()) - -# # # await ctx.channel.send(github_metrics) - -# # return - -# # @tasks.loop(seconds=20.0) -# # async def record_metrics(self): -# # # print('recording started') -# # await self.get_discord_metrics() -# # # print('discord done') -# # # await self.get_github_metrics() -# # # print('metrics recorded') - -# # @commands.command(aliases=['metrics']) -# # # @tasks.loop(seconds=10.0) -# # async def update_metrics_periodically(self, ctx, args): -# # if args == 'start': -# # self.record_metrics.start() -# # # await self.get_github_metrics() - -# # elif args == 'stop': -# # self.record_metrics.stop() - - -# # return - - - -async def setup(bot): - await bot.add_cog(MetricsTracker(bot)) diff --git a/cogs/serverManagement.py b/cogs/serverManagement.py new file mode 100644 index 0000000..00f9117 --- /dev/null +++ b/cogs/serverManagement.py @@ -0,0 +1,64 @@ +from datetime import datetime + +from discord.ext import commands + +from config.server import ServerConfig +from helpers.supabaseClient import SupabaseClient + +serverConfig = ServerConfig() + + +class ServerManagement(commands.Cog): + def __init__(self, bot): + self.bot: commands.Bot = bot + + def validUser(self, ctx): + authorised_users = [ + 1042682119035568178, + 1120262151676895274, + 1107555866422562926, + 1107555866422562926, + 599878601143222282, + ] # bhavya, devaraj, navaneeth, venkatesh, sukhpreet + return ctx.author.id in authorised_users + + @commands.command(aliaes=["initiate"]) + async def initiateServerData(self, ctx): + # add all chapters + guild = self.bot.get_guild(serverConfig.SERVER) + for role in guild.roles: + if role.name.startswith("College:"): + orgName = role.name[len("College: ") :] + SupabaseClient().addChapter(orgName=orgName, type="COLLEGE") + elif role.name.startswith("Corporate:"): + orgName = role.name[len("Corporate: ") :] + SupabaseClient().addChapter(orgName=orgName, type="CORPORATE") + + async for member in guild.fetch_members( + after=datetime(2023, 12, 1, 0, 0, 0), limit=None + ): + print(member.name) + SupabaseClient().updateContributor(member) + print("Done") + + # async def notifs_on(self,ctx,channel: discord.TextChannel): + # try: + # SupabaseClient("discord_channels").update({"should_notify": True}, "channel_id", channel.id) + # await ctx.send(f"Notifications have been turned on for {channel.name}") + # except Exception as e: + # print(e) + # await ctx.send("An unexpected error occured") + + # async def notifs_off(self, ctx, channel: discord.TextChannel): + # try: + # SupabaseClient("discord_channels").update({"should_notify": False}, "channel_id", channel.id) + # await ctx.send(f"Notifications have been turned on for {channel.name}") + # except Exception as e: + # print(e) + # await ctx.send("An unexpected error occured") + + # async def + + +async def setup(bot): + await bot.add_cog(ServerManagement(bot)) diff --git a/cogs/server_management.py b/cogs/server_management.py deleted file mode 100644 index 2da2e1b..0000000 --- a/cogs/server_management.py +++ /dev/null @@ -1,41 +0,0 @@ -from discord.ext import commands -from utils.db import SupabaseInterface -import discord -class ServerManagement(commands.Cog): - def __init__(self, bot): - self.bot = bot - def validUser(self, ctx): - authorised_users = [1042682119035568178, 1120262151676895274, 1107555866422562926, 1107555866422562926, 599878601143222282] #bhavya, devaraj, navaneeth, venkatesh, sukhpreet - return ctx.author.id in authorised_users - - - #Channel Notifications Management - @commands.command() - @commands.check(validUser) - # async def assign_channel(self, ctx, product, channel: discord.TExt): - # pass - - async def dissociate_channel(self,ctx, args): - pass - - async def notifs_on(self,ctx,channel: discord.TextChannel): - try: - SupabaseInterface("discord_channels").update({"should_notify": True}, "channel_id", channel.id) - await ctx.send(f"Notifications have been turned on for {channel.name}") - except Exception as e: - print(e) - await ctx.send("An unexpected error occured") - - async def notifs_off(self, ctx, channel: discord.TextChannel): - try: - SupabaseInterface("discord_channels").update({"should_notify": False}, "channel_id", channel.id) - await ctx.send(f"Notifications have been turned on for {channel.name}") - except Exception as e: - print(e) - await ctx.send("An unexpected error occured") - - # async def - - -async def setup(bot): - await bot.add_cog(ServerManagement(bot)) \ No newline at end of file diff --git a/cogs/testing.py b/cogs/testing.py new file mode 100644 index 0000000..54f9057 --- /dev/null +++ b/cogs/testing.py @@ -0,0 +1,149 @@ +from typing import Optional, Union + +import discord +from discord.emoji import Emoji +from discord.enums import ButtonStyle +from discord.ext import commands +from discord.partial_emoji import PartialEmoji +from discord.utils import MISSING +from utils.db import SupabaseInterface + + +async def on_button_click(interaction: discord.Interaction): + # Get the Discord username of the person who pressed the button + username = interaction.user.name + print(username) + + # Create a modal + modal = discord.ui.Modal(title="Testing Panel") + modal.add_item(discord.ui.TextInput(label="Link")) + + # Add a button to the modal that links to Google + modal.add_item( + discord.ui.Button(label="Go to Google", url="https://www.google.com") + ) + + # Add a button to the modal to close it + modal.add_item(discord.ui.Button(label="Close")) + + # Show the modal to the user + await interaction.response.send_modal(modal) + + # After the modal closes, send an ephemeral message to the user with their Discord ID + ephemeral_message = f"Your Discord ID is {interaction.user.id}" + await interaction.channel.send(ephemeral_message, ephemeral=True) + + +class Questionnaire(discord.ui.Modal, title="Questionnaire Response"): + name = discord.ui.TextInput(label="Name") + answer = discord.ui.TextInput(label="Answer", style=discord.TextStyle.paragraph) + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.send_message( + f"Thanks for your response, {self.name}!, Your answer is {self.answer}", + ephemeral=True, + ) + + +async def click(interaction: discord.Interaction): + # modal = InteractionModal() + # await interaction.response.send_message("You Pressed the Button!!!") + modal = Questionnaire() + await interaction.response.send_modal(modal) + + +class TestingPanel(discord.ui.View): + def __init__(self): + super().__init__() + self.timeout = None + + # create a textbox + self.add_item( + discord.ui.Select( + placeholder="CHOOSE", + options=[ + discord.SelectOption(label=f"{x}", value=f"{x}") + for x in [1, 2, 3, 4] + ], + ) + ) + + # Create a button + self.press_me_button = discord.ui.Button(label="Press me") + self.press_me_button.callback = click + + modal = Questionnaire() + + # Add the button to the view + self.add_item(self.press_me_button) + + # Handle the button press event + + +class InteractionModal(discord.ui.Modal): + def __init__( + self, *, title: str = ..., timeout: float | None = None, custom_id: str = ... + ) -> None: + super().__init__(title=title, timeout=timeout, custom_id=custom_id) + + self.add_item(discord.ui.TextInput(label="Enter your Name")) + self.add_item( + discord.ui.TextInput(label="Long Input", style=discord.TextStyle.long) + ) + + +class TestingModule(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def newjoin(self, interaction: discord.Interaction): + button = discord.ui.Button( + style=ButtonStyle.green, label="Authenticate with Github" + ) + view = discord.ui.View() + view.add_item(button) + await interaction.response.send_message( + "Auuthenticate your Github", view=view, ephemeral=True + ) + + @commands.command() + async def arrowrow(self, ctx): + text_input_1 = discord.ui.TextInput( + label="Text 1", placeholder="Text Input 1", min_length=1, max_length=25 + ) + text_input_2 = discord.ui.TextInput( + label="Text 2", placeholder="Text Input 2", min_length=1, max_length=25 + ) + + select_input = discord.ui.Select( + placeholder="Select Input", + options=[ + discord.SelectOption(label="Option 1", value="1"), + discord.SelectOption(label="Option 2", value="2"), + discord.SelectOption(label="Option 3", value="3"), + ], + ) + + user_select_input = discord.ui.UserSelect(placeholder="User Select Input") + + button = discord.ui.Button(style=discord.ButtonStyle.primary, label="Submit") + + row = discord.ActionRow( + [text_input_1, text_input_2, select_input, user_select_input, button] + ) + + await ctx.send("Here's your arrowrow:", components=[row]) + + # Create a Discord command + @commands.command() + async def testing_panel(self, ctx): + # Create a view + view = TestingPanel() + + # Send the view to the user + await ctx.send("Testing Panel", view=view) + + +async def setup(bot): + await bot.add_cog(TestingModule(bot)) diff --git a/cogs/user_interactions.py b/cogs/userInteractions.py similarity index 56% rename from cogs/user_interactions.py rename to cogs/userInteractions.py index d8344d6..45dab26 100644 --- a/cogs/user_interactions.py +++ b/cogs/userInteractions.py @@ -1,12 +1,121 @@ -import discord, asyncio +import csv import os + +import discord from discord.ext import commands, tasks -import time, csv -from utils.db import SupabaseInterface + +from helpers.supabaseClient import SupabaseClient VERIFIED_CONTRIBUTOR_ROLE_ID = 1123967402175119482 NON_CONTRIBUTOR_ROLES = [973852321870118914, 976345770477387788, 973852439054782464] -NON_CONTRIBUTOR_MEMBERS = [704738663883472967, 703157062825410590, 697080616046559352, 694445371816149062, 677090546694619168, 662243718354698240, 637512076084117524, 636277951540887553, 599878601143222282, 476285280811483140, 459239263192612874, 365127154847186945, 314379504157982721, 291548601228722177, 280019116755124226, 262810519184998400, 222905396610859010, 761623930531741788, 760775460178755614, 759107287322329128, 753909213859938385, 749287051035148328, 730733764891770950, 727567753246146633, 722313444325457921, 720297291105304627, 712557512926298153, 788670744120786976, 805863967284920352, 804299931543535626, 902553914916896808, 882206400074358835, 1016564507394457663, 1010140041789571072, 1008331310806335618, 989823092283035678, 987081239892750376, 986500267858083860, 986245349129740338, 973537784537186304, 971297678401089556, 969115972059406376, 967973710617247796, 965956645895147610, 963733651529531444, 961904402459947018, 961529037610713098, 961283715382775818, 960435521786617856, 948478097508954122, 937569989689487420, 936118598102028319, 935911019828641812, 933651897502535690, 1087744252408246373, 1086529617013252167, 1083352549660307466, 1079268207917027348, 1077912548571095092, 1075013295221768213, 1070737923483389972, 1059343450312544266, 1052565902748553236, 1050037458286420099, 1049311176716206164, 1045238281740230656, 1044876122191581194, 1044532981857001502, 1043440759061352588, 1042682119035568178, 1039880103934570517, 1037956974039535636, 1036590822201757696, 1024552723280052294, 1018398460598308915, 1108763601231171594, 1108657717100429312, 1108649642633199636, 1108649434251792514, 1108649344242040883, 1108649032978538497, 1108618174477369426, 1108613175697481749, 1108269782668681276, 1107943353632432208, 1107933295930511431, 1107927044962144286, 1107910689370165248, 1107899551974686831, 1107656943809593395, 1107618486232039454, 1107555866422562926, 1107504062175391815, 1101892125328679024, 1100365706362626098, 1099938102555975690, 1098923182439796818, 1093415042860466268, 1091224095770816552, 1120262010752471112, 1115908663207530577, 1115622606440239184, 1115538129672224880, 1115537984972931173, 1115171977934688296, 1114795277518389391] +NON_CONTRIBUTOR_MEMBERS = [ + 704738663883472967, + 703157062825410590, + 697080616046559352, + 694445371816149062, + 677090546694619168, + 662243718354698240, + 637512076084117524, + 636277951540887553, + 599878601143222282, + 476285280811483140, + 459239263192612874, + 365127154847186945, + 314379504157982721, + 291548601228722177, + 280019116755124226, + 262810519184998400, + 222905396610859010, + 761623930531741788, + 760775460178755614, + 759107287322329128, + 753909213859938385, + 749287051035148328, + 730733764891770950, + 727567753246146633, + 722313444325457921, + 720297291105304627, + 712557512926298153, + 788670744120786976, + 805863967284920352, + 804299931543535626, + 902553914916896808, + 882206400074358835, + 1016564507394457663, + 1010140041789571072, + 1008331310806335618, + 989823092283035678, + 987081239892750376, + 986500267858083860, + 986245349129740338, + 973537784537186304, + 971297678401089556, + 969115972059406376, + 967973710617247796, + 965956645895147610, + 963733651529531444, + 961904402459947018, + 961529037610713098, + 961283715382775818, + 960435521786617856, + 948478097508954122, + 937569989689487420, + 936118598102028319, + 935911019828641812, + 933651897502535690, + 1087744252408246373, + 1086529617013252167, + 1083352549660307466, + 1079268207917027348, + 1077912548571095092, + 1075013295221768213, + 1070737923483389972, + 1059343450312544266, + 1052565902748553236, + 1050037458286420099, + 1049311176716206164, + 1045238281740230656, + 1044876122191581194, + 1044532981857001502, + 1043440759061352588, + 1042682119035568178, + 1039880103934570517, + 1037956974039535636, + 1036590822201757696, + 1024552723280052294, + 1018398460598308915, + 1108763601231171594, + 1108657717100429312, + 1108649642633199636, + 1108649434251792514, + 1108649344242040883, + 1108649032978538497, + 1108618174477369426, + 1108613175697481749, + 1108269782668681276, + 1107943353632432208, + 1107933295930511431, + 1107927044962144286, + 1107910689370165248, + 1107899551974686831, + 1107656943809593395, + 1107618486232039454, + 1107555866422562926, + 1107504062175391815, + 1101892125328679024, + 1100365706362626098, + 1099938102555975690, + 1098923182439796818, + 1093415042860466268, + 1091224095770816552, + 1120262010752471112, + 1115908663207530577, + 1115622606440239184, + 1115538129672224880, + 1115537984972931173, + 1115171977934688296, + 1114795277518389391, +] class Announcement: @@ -16,25 +125,25 @@ def __init__(self, member): async def create_embed(self): embed = discord.Embed( title=f"Hey {self.member.name}!", - description=f''' + description=f""" If you submitted a proposal and did not make it to the C4GT mentoring program or you missed the deadline for applying, worry not! -**We have launched the C4GT Community Program Today!**πŸš€ πŸš€ +**We have launched the C4GT Community Program Today!**πŸš€ πŸš€ -Through this program you can contribute to multiple projects, build your skills & get exclusive rewards & goodies. +Through this program you can contribute to multiple projects, build your skills & get exclusive rewards & goodies. -How will the Community Program work?πŸ€” +How will the Community Program work?πŸ€” - **Explore Projects** πŸ“‹ - Explore [projects](https://www.codeforgovtech.in/community-projects) as per your skills, interest in the domain & more. - **Get Coding** πŸ’» - Interact with mentors for clarity if required & solve the project - **Points & Rewards** 🎁 - On each PR merged, you will get points. These points will give you badges & C4GT goodies. Read more about the point system [here](https://github.com/Code4GovTech/C4GT/wiki/Point-System-for-Contributors) How can you participate? - **Link Discord & GitHub** 🀝 - Use this [link]({os.getenv('FLASK_HOST')}/authenticate/{self.member.id}) to connect these platforms, so we can track your activity & calculate points -- **Explore Issues Listed** πŸ–₯️ - Keep an eye on our project page as more issues will be released every week. +- **Explore Issues Listed** πŸ–₯️ - Keep an eye on our project page as more issues will be released every week. - **Ask Questions** ❓ - Ask away your queries on the #c4gtcommunitychannel -So what are you waiting for? Let's get started!!''', - color=0x00FFFF +So what are you waiting for? Let's get started!!""", + color=0x00FFFF, ) # embed.add_field(name="How will the Community Program work?πŸ€”", @@ -45,32 +154,32 @@ async def create_embed(self): # embed.add_field(name="So what are you waiting for? Let's get started!!", value='') return embed - - - +# 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") -#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}') + 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 UserHandler(commands.Cog): def __init__(self, bot) -> None: self.bot = bot 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 + # 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 @@ -89,72 +198,73 @@ def __init__(self, bot) -> None: @commands.command(aliases=["badges"]) async def list_badges(self, ctx): + converseDesc = f"""Well done *{ctx.author.name}*! πŸ‘ - converseDesc = f'''Well done *{ctx.author.name}*! πŸ‘ - - You have engaged on the C4GT discord community with 10 or more messages and earned the **Converser Badge!** πŸ’¬ This badge shows that you are a friendly and helpful member of our community! 😊 ''' + You have engaged on the C4GT discord community with 10 or more messages and earned the **Converser Badge!** πŸ’¬ This badge shows that you are a friendly and helpful member of our community! 😊 """ converseEmbed = discord.Embed(title="Converse Badge", description=converseDesc) - converseEmbed.set_image(url="https://raw.githubusercontent.com/KDwevedi/testing_for_github_app/main/WhatsApp%20Image%202023-06-20%20at%202.57.12%20PM.jpeg") + converseEmbed.set_image( + url="https://raw.githubusercontent.com/KDwevedi/testing_for_github_app/main/WhatsApp%20Image%202023-06-20%20at%202.57.12%20PM.jpeg" + ) - rockstarDesc = f'''Amazing *{ctx.author.name}*! πŸ™Œ - You have received 5 upvotes on your message and earned the **Rockstar Badge!** 🌟 You add so much value to our community and we are grateful for your contribution! πŸ’– + rockstarDesc = f"""Amazing *{ctx.author.name}*! πŸ™Œ + You have received 5 upvotes on your message and earned the **Rockstar Badge!** 🌟 You add so much value to our community and we are grateful for your contribution! πŸ’– Please keep up the good work and share your expertise with us! πŸ™Œ - ''' + """ reactionsEmbed = discord.Embed(title="Rockstar Badge", description=rockstarDesc) - reactionsEmbed.set_image(url="https://raw.githubusercontent.com/KDwevedi/testing_for_github_app/main/WhatsApp%20Image%202023-06-20%20at%202.57.12%20PM.jpeg") - + reactionsEmbed.set_image( + url="https://raw.githubusercontent.com/KDwevedi/testing_for_github_app/main/WhatsApp%20Image%202023-06-20%20at%202.57.12%20PM.jpeg" + ) await ctx.channel.send(embed=converseEmbed) await ctx.channel.send(embed=reactionsEmbed) - return - + # @commands.command() # async def give_badges(self, ctx): # self.give_discord_badges.start() - - @tasks.loop(minutes=10) async def update_contributors(self): - contributors = SupabaseInterface("contributors_registration").read_all() - guild = await self.bot.fetch_guild(973851473131761674) - contributor_role:discord.Role = guild.get_role(VERIFIED_CONTRIBUTOR_ROLE_ID) + contributors = SupabaseClient().read_all("contributors_registration") + guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) + contributor_role = guild.get_role(VERIFIED_CONTRIBUTOR_ROLE_ID) + count = 1 for contributor in contributors: - try: - member = await guild.fetch_member(contributor["discord_id"]) - except Exception: - continue - if contributor_role not in member.roles: - #Give Contributor Role + print(count) + count += 1 + member = guild.get_member(contributor["discord_id"]) + if member and contributor_role not in member.roles: + # Give Contributor Role + print(member.name) await member.add_roles(contributor_role) - #add to discord engagement - # SupabaseInterface("discord_engagement").insert({"contributor": member.id}) - - #update engagement + print("Given Roles") + # add to discord engagement + # SupabaseClient("discord_engagement").insert({"contributor": member.id}) + + # update engagement # for contributor in contributors: - # contributorData = SupabaseInterface("discord_engagement").read("contributor", contributor["discord_id"])[0] + # contributorData = SupabaseClient("discord_engagement").read("contributor", contributor["discord_id"])[0] # member = await guild.fetch_member(contributorData["contributor"]) # print(f"-----Contributor-----{member.name}-------") # badges = Badges(member.name) # if contributorData: # if contributorData["total_message_count"]>10 and not contributorData["converserBadge"]: - # SupabaseInterface("discord_engagement").update({"converserBadge":True},"contributor", contributorData["contributor"]) + # SupabaseClient("discord_engagement").update({"converserBadge":True},"contributor", contributorData["contributor"]) # dmchannel = member.dm_channel if member.dm_channel else await member.create_dm() # await dmchannel.send(embed=badges.converseBadge) # if contributorData["total_reaction_count"]>5 and not contributorData["rockstarBadge"]: - # SupabaseInterface("discord_engagement").update({"rockstarBadge":True},"contributor", contributorData["contributor"]) + # SupabaseClient("discord_engagement").update({"rockstarBadge":True},"contributor", contributorData["contributor"]) # dmchannel = member.dm_channel if member.dm_channel else await member.create_dm() # await dmchannel.send(embed=badges.rockstarBadge) # if contributorData["has_introduced"] and not contributorData["apprenticeBadge"]: - # SupabaseInterface("discord_engagement").update({"apprenticeBadge":True},"contributor", contributorData["contributor"]) + # SupabaseClient("discord_engagement").update({"apprenticeBadge":True},"contributor", contributorData["contributor"]) # dmchannel = member.dm_channel if member.dm_channel else await member.create_dm() # await dmchannel.send(embed=badges.apprenticeBadge) # github_id = contributor["github_id"] # prData = { - # "raised": SupabaseInterface(table="pull_requests").read(query_key="raised_by", query_value=github_id), - # "merged":SupabaseInterface(table="pull_requests").read(query_key="merged_by", query_value=github_id) + # "raised": SupabaseClient(table="pull_requests").read(query_key="raised_by", query_value=github_id), + # "merged":SupabaseClient(table="pull_requests").read(query_key="merged_by", query_value=github_id) # } # points = 0 # for action in prData.keys(): @@ -162,27 +272,20 @@ async def update_contributors(self): # for pr in prs: # points+=pr["points"] # if len(prData["raised"])+len(prData["merged"])>0and not contributorData["enthusiastBadge"]: - # SupabaseInterface("discord_engagement").update({"enthusiastBadge":True},"contributor", contributorData["contributor"]) + # SupabaseClient("discord_engagement").update({"enthusiastBadge":True},"contributor", contributorData["contributor"]) # await dmchannel.send(embed=Badges(member.name, points=points).enthusiastBadge) # if points>=30 and not contributorData["risingStarBadge"]: - # SupabaseInterface("discord_engagement").update({"risingStarBadge":True},"contributor", contributorData["contributor"]) + # SupabaseClient("discord_engagement").update({"risingStarBadge":True},"contributor", contributorData["contributor"]) # await dmchannel.send(embed=badges.risingStarBadge) - - - - - - - return - - + @commands.command() async def github_profile(self, ctx): if isinstance(ctx.channel, discord.DMChannel): - githubProfileInfoEmbed = discord.Embed(title=f"Show off your contributions on your github profile!", - description='''Hey ContributorπŸ”₯ + githubProfileInfoEmbed = discord.Embed( + title="Show off your contributions on your github profile!", + description="""Hey ContributorπŸ”₯ *Great work on contributing to Digital Public Goods* @@ -197,20 +300,25 @@ async def github_profile(self, ctx): 3️⃣ Then, open your profile README file on Github and edit it by adding the copied section from the bot response, wherever you want.πŸ’» -4️⃣ Commit the changes to your README on github. +4️⃣ Commit the changes to your README on github. *Congratulations on your hard work & achievement!!*πŸ₯³ -Your profile page will now show your achievements from the C4GT community.πŸ†''') +Your profile page will now show your achievements from the C4GT community.πŸ†""", + ) + + desc = f"""Hey {ctx.author.name} - noPointsGithubProfileEmbed = discord.Embed(title="", description=f'''Hey {ctx.author.name} +You have currently not earned any C4GT points or badges yet! +But worry not, you can do so by solving issue tickets & earning more points✨ -You have currently not earned any C4GT points yet! -But don’t worry, all you need to do is collect 50 DPG points and get a Rising Star :stars: badge by solving issue tickets to become eligible for your first certificate. **Get coding now!!**:computer: +**Discover issue tickets [here](https://www.codeforgovtech.in/community-program-projects).**🎟️🌟 +**Know more about [badges & points](https://github.com/Code4GovTech/C4GT/wiki/Point-System-for-Contributors)**πŸ§—""" -**Discover issue tickets [here](https://www.codeforgovtech.in/community-program-projects).** -''') - user = SupabaseInterface("github_profile_data").read("discord_id", ctx.author.id) + noPointsGithubProfileEmbed = discord.Embed(title="", description=desc) + user = SupabaseClient().read( + "github_profile_data", "discord_id", ctx.author.id + ) if len(user) == 0: await ctx.send("Oops! It seems you aren't currently registered") elif len(user) == 1: @@ -219,9 +327,11 @@ async def github_profile(self, ctx): await ctx.send(embed=noPointsGithubProfileEmbed) else: await ctx.send(embed=githubProfileInfoEmbed) - await ctx.send(f'''Snippet for your Github Profile README: + await ctx.send( + f"""Snippet for your Github Profile README: ```[![C4GTGithubDisplay](https://kcavhjwafgtoqkqbbqrd.supabase.co/storage/v1/object/public/c4gt-github-profile/{ctx.author.id}githubdisplay.jpg)](https://github.com/Code4GovTech) -Know more about: Code For GovTech ([Website](https://www.codeforgovtech.in) | [GitHub](https://github.com/Code4GovTech/C4GT/wiki)) | [Digital Public Goods (DPGs)](https://digitalpublicgoods.net/digital-public-goods/) | [India & DPGs](https://government.economictimes.indiatimes.com/blog/digital-public-goods-digital-public-infrastructure-an-evolving-india-story/99532036)```''') +Know more about: Code For GovTech ([Website](https://www.codeforgovtech.in) | [GitHub](https://github.com/Code4GovTech/C4GT/wiki)) | [Digital Public Goods (DPGs)](https://digitalpublicgoods.net/digital-public-goods/) | [India & DPGs](https://government.economictimes.indiatimes.com/blog/digital-public-goods-digital-public-infrastructure-an-evolving-india-story/99532036)```""" + ) # githubProfileInfoEmbed.set_footer(text="Respond with πŸ† to get the link") # message = await ctx.send(embed=githubProfileInfoEmbed) # await message.add_reaction("πŸ†") @@ -234,8 +344,7 @@ async def github_profile(self, ctx): # else: # if str(reaction.emoji) == 'πŸ†': # await ctx.send(f'[![C4GTGithubDisplay](https://kcavhjwafgtoqkqbbqrd.supabase.co/storage/v1/object/public/c4gt-github-profile/{ctx.author.id}githubdisplay.jpg?maxAge=10)](https://github.com/Code4GovTech)') - - + @update_contributors.before_loop async def before_update_loop(self): print("starting auto-badge") @@ -244,7 +353,7 @@ async def before_update_loop(self): async def read_members_csv(self, file_path): with open(file_path, mode="r", encoding="utf-8") as file: reader = csv.reader(file) - header = next(reader) # Skip the header row + next(reader) # Skip the header row rows = [] count = 0 @@ -258,138 +367,94 @@ async def read_members_csv(self, file_path): if rows: yield rows - - # @commands.command() - # async def announce(self, ctx): - # guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) #SERVER_ID Should be C4GT Server ID - # file_path = "members.csv" - # async for batch in self.read_members_csv(file_path): - # for row in batch: - # member = await guild.fetch_member(row[0]) - # print(member.name) - # dmchannel = await member.create_dm() - # time.sleep(10) - # print("---") - - # count = 0 - # non_con_members = [] - # async for member in guild.fetch_members(limit=None): - # # dmchannel = member.dm_channel if member.dm_channel else await member.create_dm() - # announcement = await Announcement(member).create_embed() - # # await dmchannel.send(embed=announcement) - # count +=1 - # print(member.name) - # if any(role.id in NON_CONTRIBUTOR_ROLES for role in member.roles): - # non_con_members.append(member.id) - # print(non_con_members) - # print(count) - - - # @commands.command() - # async def announce(self, ctx): - # guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) - # members = [] - # async for member in guild.fetch_members(limit=None): - # members.append(member.id) - - # # Save members to a CSV file - # file_path = "members.csv" - # with open(file_path, mode="w", newline="\n", encoding="utf-8") as file: - # writer = csv.writer(file) - # writer.writerow(["Member id"]) # Write header row - # writer.writerows(zip(members)) # Write member names - - # print(f"Members saved to {file_path}") - @commands.command(aliases=["point_system_breakdown", "point_system"]) async def point_breakdown(self, ctx): - message =f'''Hey **{ctx.author.name}** + message = f"""Hey **{ctx.author.name}** Points are allocated on the following basis:bar_chart: : -:arrow_forward: **Number of PRs accepted** +:arrow_forward: **Number of PRs accepted** -:rocket: **10 points per ticket are given** +:rocket: **10 points per ticket are given** :rocket: **Get more points for complex tickets** -- 1x for Low Complexity +- 1x for Low Complexity - 2x for Medium Complexity - 3x for High Complexity -:arrow_forward: **Number of PRs reviewed** +:arrow_forward: **Number of PRs reviewed** -:rocket: **10 points per ticket for those who have been made a maintainer to review PRs** +:rocket: **10 points per ticket for those who have been made a maintainer to review PRs** :rocket: **Get more points for complex tickets** -- 1x for Low Complexity +- 1x for Low Complexity - 2x for Medium Complexity - 3x for High Complexity -''' +""" await ctx.channel.send(message) - @commands.command(aliases=["my_points"]) async def get_points(self, ctx): if isinstance(ctx.channel, discord.DMChannel): discord_id = ctx.author.id - contributor = SupabaseInterface(table="contributors_registration").read(query_key="discord_id", query_value=discord_id) + contributor = SupabaseClient().read( + table="contributors_registration", + query_key="discord_id", + query_value=discord_id, + ) print(contributor) github_id = contributor[0]["github_id"] - prs_raised = SupabaseInterface(table="pull_requests").read(query_key="raised_by", query_value=github_id) - prs_merged = SupabaseInterface(table="pull_requests").read(query_key="merged_by", query_value=github_id) + prs_raised = SupabaseClient().read( + table="pull_requests", query_key="raised_by", query_value=github_id + ) + prs_merged = SupabaseClient().read( + table="pull_requests", query_key="merged_by", query_value=github_id + ) raise_points = 0 merge_points = 0 - raiseTicketComplexity = { - "low":0, - "medium": 0, - "high":0 - } - mergeTicketComplexity = { - "low": 0, - "medium": 0, - "high": 0 - } + raiseTicketComplexity = {"low": 0, "medium": 0, "high": 0} + mergeTicketComplexity = {"low": 0, "medium": 0, "high": 0} for pr in prs_raised: if pr["is_merged"]: - raise_points+=pr["points"] + raise_points += pr["points"] if pr["points"] == 10: - raiseTicketComplexity["low"]+=1 + raiseTicketComplexity["low"] += 1 if pr["points"] == 20: - raiseTicketComplexity["medium"]+=1 + raiseTicketComplexity["medium"] += 1 if pr["points"] == 30: - raiseTicketComplexity["high"]+=1 + raiseTicketComplexity["high"] += 1 for pr in prs_merged: if pr["is_merged"]: - merge_points+=pr["points"] + merge_points += pr["points"] if pr["points"] == 10: - mergeTicketComplexity["low"]+=1 + mergeTicketComplexity["low"] += 1 if pr["points"] == 20: - mergeTicketComplexity["medium"]+=1 + mergeTicketComplexity["medium"] += 1 if pr["points"] == 30: - mergeTicketComplexity["high"]+=1 + mergeTicketComplexity["high"] += 1 - text = f'''Hey {ctx.author.name} + text = f"""Hey {ctx.author.name} -**You have a total of {raise_points+merge_points} points**🌟 +**You have a total of {raise_points+merge_points} points**🌟 -▢️ **Points Basis PRs accepted - {raise_points} points**πŸ”₯ +▢️ **Points Basis PRs accepted - {raise_points} points**πŸ”₯ Number of tickets solved - {len(prs_raised)} Points on tickets with low complexity - {raiseTicketComplexity["low"]*10} points Points on tickets with medium complexity - {raiseTicketComplexity["medium"]*20} points Points of tickets with high complexity - {raiseTicketComplexity["high"]*30} points -▢️ **Points as per PRs reviewed - {merge_points} points**πŸ™Œ +▢️ **Points as per PRs reviewed - {merge_points} points**πŸ™Œ Number of tickets reviewed - {len(prs_merged)} Points on tickets with low complexity - {mergeTicketComplexity["low"]*10} points Points on tickets with medium complexity - {mergeTicketComplexity["medium"]*20} points Points of tickets with high complexity - {mergeTicketComplexity["high"]*30} points -Get coding and earn more points to get a spot on the leaderboardπŸ“ˆ''' +Get coding and earn more points to get a spot on the leaderboardπŸ“ˆ""" await ctx.channel.send(text) - - + + async def setup(bot): - await bot.add_cog(UserHandler(bot)) \ No newline at end of file + await bot.add_cog(UserHandler(bot)) diff --git a/config.json b/config.json index 2c69179..079a9b0 100644 --- a/config.json +++ b/config.json @@ -11,4 +11,4 @@ "INTRODUCTIONS_CHANNEL_ID": 1107343423167541328, "ERROR_CHANNEL_ID": 0, "TIME_DURATION": 10 -} \ No newline at end of file +} diff --git a/config/bot.py b/config/bot.py new file mode 100644 index 0000000..7f8474c --- /dev/null +++ b/config/bot.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass +class BotConfig: + taskInterval: int = 10 # seconds diff --git a/config/server.py b/config/server.py new file mode 100644 index 0000000..15017cc --- /dev/null +++ b/config/server.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + + +@dataclass +class ServerConfig: + SERVER: int = 973851473131761674 + + @dataclass + class Channels: + INTRODUCTION_CHANNEL: int = 1107343423167541328 + + @dataclass + class Roles: + CONTRIBUTOR_ROLE: int = 973852365188907048 + + @classmethod + def isCollegeChapter(roleName: str) -> bool: + return True if roleName.startswith("College:") else False diff --git a/helpers/roleHelpers.py b/helpers/roleHelpers.py new file mode 100644 index 0000000..3d42f25 --- /dev/null +++ b/helpers/roleHelpers.py @@ -0,0 +1,18 @@ +from discord import Role + + +def lookForChapterRoles(roles: [Role]): + chapter_roles = [] + for role in roles: + if role.name.startswith("College:"): + chapter_roles.append(role.name[len("College: ") :]) + elif role.name.startswith("Corporate:"): + chapter_roles.append(role).name[len("Corporate: ") :] + return chapter_roles + + +def lookForGenderRoles(roles: [Role]): + for role in roles: + if role.name in ["M", "F", "NB"]: + return role.name + return None diff --git a/helpers/supabaseClient.py b/helpers/supabaseClient.py new file mode 100644 index 0000000..d7c3f39 --- /dev/null +++ b/helpers/supabaseClient.py @@ -0,0 +1,95 @@ +import os + +from discord import Member, User +from supabase import Client, create_client + +from helpers.roleHelpers import lookForChapterRoles, lookForGenderRoles + + +class SupabaseClient: + def __init__(self, url=None, key=None) -> None: + self.supabase_url = url if url else os.getenv("SUPABASE_URL") + self.supabase_key = key if key else os.getenv("SUPABASE_KEY") + self.client: Client = create_client(self.supabase_url, self.supabase_key) + + def read(self, table, query_key, query_value, columns="*"): + data = ( + self.client.table(table) + .select(columns) + .eq(query_key, query_value) + .execute() + ) + # data.data returns a list of dictionaries with keys being column names and values being row values + return data.data + + def read_by_order_limit( + self, + table, + query_key, + query_value, + order_column, + order_by=False, + limit=1, + columns="*", + ): + data = ( + self.client.table(table) + .select(columns) + .eq(query_key, query_value) + .order(order_column) + .limit(limit) + .execute() + ) + return data.data + + def read_all(self, table): + data = self.client.table(table).select("*").execute() + return data.data + + def update(self, table, update, query_key, query_value): + data = ( + self.client.table(table).update(update).eq(query_key, query_value).execute() + ) + return data.data + + def insert(self, table, data): + data = self.client.table(table).insert(data).execute() + return data.data + + def memberIsAuthenticated(self, member: Member | User): + data = ( + self.client.table("contributors_registration") + .select("*") + .eq("discord_id", member.id) + .execute() + .data + ) + if data: + return True + else: + return False + + def addChapter(self, orgName: str, type: str): + data = ( + self.client.table("chapters") + .upsert({"type": type, "org_name": orgName}, on_conflict="org_name") + .execute() + ) + return data.data + + def updateContributor(self, contributor: Member): + table = "contributors_discord" + + chapters = lookForChapterRoles(contributor.roles) + gender = lookForGenderRoles(contributor.roles) + + self.client.table(table).upsert( + { + "discord_id": contributor.id, + "discord_username": contributor.name, + "chapter": chapters[0] if chapters else None, + "gender": gender, + "joined_at": contributor.joined_at.isoformat(), + }, + on_conflict="discord_id", + ).execute() diff --git a/main.py b/main.py index 101dd22..ee925b6 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,23 @@ -from typing import Optional, Union +import asyncio +import json +import os +import sys +from typing import Union + +import aiohttp import discord +import dotenv from discord.ext import commands -import os, sys -import asyncio -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 +from helpers.supabaseClient import SupabaseClient + +# 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") + # 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) @@ -20,10 +25,15 @@ 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}') + 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 [ @@ -51,7 +61,7 @@ def __init__(self, discord_userdata): # 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): @@ -84,84 +94,112 @@ def __init__(self, discord_userdata): # 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)) +# 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: + 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' + 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" + "Prefer": "return=minimal", } async with aiohttp.ClientSession() as session: - async with session.post(url, headers=headers, data=json.dumps(data)) as response: + 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!') + 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 - } + 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_registration").read("discord_id", user.id) + authentication = SupabaseClient().read( + "contributors_registration", "discord_id", user.id + ) while not authentication: await asyncio.sleep(30) print("Found!") - discordEngagement = SupabaseInterface("discord_engagement").read("contributor", user.id)[0] + discordEngagement = SupabaseClient().read( + "discord_engagement", "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") + 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) + super().__init__(timeout=None) - @discord.ui.button(label="Register", style=discord.enums.ButtonStyle.blurple, custom_id='registration_view:blurple') + @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") + 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) - + 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. @@ -170,28 +208,41 @@ async def setup_hook(self) -> None: # we don't have one. self.add_view(RegistrationView()) + client = C4GTBot() -@client.command(aliases=['registration']) + +@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()) - + 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 -#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}') + print(f"We have logged in as {client.user}") -#load cogs + +# load cogs async def load(): for filename in os.listdir("./cogs"): if filename.endswith(".py"): await client.load_extension(f"cogs.{filename[:-3]}") + # ### All listener cogs + for filename in os.listdir("./cogs/listeners"): + if filename.endswith("cog.py"): + await client.load_extension(f"cogs.listeners.{filename[:-3]}") + + async def main(): async with client: await load() @@ -199,8 +250,3 @@ async def main(): asyncio.run(main()) - - - - - diff --git a/models/github_interface.py b/models/github_interface.py deleted file mode 100644 index 9e6c503..0000000 --- a/models/github_interface.py +++ /dev/null @@ -1,14 +0,0 @@ -#This data model is not being used right now - -class Commit: - def __init__(self) -> None: - pass - -class PullRequest: - def __init__(self) -> None: - pass - -class Repository: - def __init__(self) -> None: - pass - \ No newline at end of file diff --git a/models/product.py b/models/product.py index f5b18e8..e010dd8 100644 --- a/models/product.py +++ b/models/product.py @@ -1,50 +1,59 @@ -from utils.db import SupabaseInterface +from helpers.supabaseClient import SupabaseClient + class Product: def __init__(self, data=None, name=None): if name is not None: - data = SupabaseInterface(table="products").read(query_key="name",query_value=name) + data = SupabaseClient().read( + table="products", query_key="name", query_value=name + ) if not data: raise Exception("There is no product with name = ", name) else: - #Assuming is_unique constraint on name in Products table - data=data[0] - #Name of the product + # Assuming is_unique constraint on name in Products table + data = data[0] + # Name of the product self.name = data["name"] - #Description of the product + # Description of the product self.desc = data["description"] - #Organisation associated with the product + # Organisation associated with the product self.org = data["organisation"] - #The wili page url for the product on C4GT github + # The wili page url for the product on C4GT github self.wiki_url = data["wiki_url"] - #Projects under this product + # Projects under this product self.projects = data["projects"] - #Mentors assigned to projects associated with this product + # Mentors assigned to projects associated with this product self.mentors = data["mentors"] if data["mentors"] else [] - #Contributors assigned to projects under this product + # Contributors assigned to projects under this product self.contributers = data["contributors"] if data["contributors"] else [] - #discord channel id of the dedicated discord channel for this Product + # discord channel id of the dedicated discord channel for this Product self.channel = data["channel"] @classmethod def is_product(cls, product_name): - db_client = SupabaseInterface(table="products") - data = db_client.read(query_key="name", query_value=product_name) - if len(data)==1: + db_client = SupabaseClient() + data = db_client.read( + table="products", query_key="name", query_value=product_name + ) + if len(data) == 1: return True - if len(data)>1: - raise Exception("Product name should be unique but recieved multiple items for this name.") + if len(data) > 1: + raise Exception( + "Product name should be unique but recieved multiple items for this name." + ) return False - + @classmethod def get_all_products(cls): - db_client = SupabaseInterface(table="products") - data = db_client.read_all() + db_client = SupabaseClient() + data = db_client.read_all(table="products") return data - + def assign_channel(self, discord_id): - SupabaseInterface(table='products').update(update={ - "channel": discord_id - }, query_key='name', query_value=self.name) - return - \ No newline at end of file + SupabaseClient().update( + table="products", + update={"channel": discord_id}, + query_key="name", + query_value=self.name, + ) + return diff --git a/models/project.py b/models/project.py index be02d35..eb74f6e 100644 --- a/models/project.py +++ b/models/project.py @@ -1,37 +1,39 @@ -import sys, os +from helpers.supabaseClient import SupabaseClient -from utils.db import SupabaseInterface class Project: def __init__(self, data=None, name=None) -> None: if name is not None: - data = SupabaseInterface(table="projects").read(query_key="name",query_value=name)[0] + data = SupabaseClient().read( + table="projects", query_key="name", query_value=name + )[0] self.name = data["name"] self.desc = data["description"] - self.repository = data['repository'] - self.contributor = data['contributor'] - self.mentor = data['mentor'] - self.product = data['product'] - self.issue_page_url = data['issue_page_url'] - + self.repository = data["repository"] + self.contributor = data["contributor"] + self.mentor = data["mentor"] + self.product = data["product"] + self.issue_page_url = data["issue_page_url"] + @classmethod def is_project(project_name): - db_client = SupabaseInterface(table="projects") - data = db_client.read(query_key="name", query_value=project_name) - if len(data)==1: + db_client = SupabaseClient() + data = db_client.read( + table="projects", query_key="name", query_value=project_name + ) + if len(data) == 1: return True - if len(data)>1: - raise Exception("Project name should be unique but recieved multiple items for this name.") + if len(data) > 1: + raise Exception( + "Project name should be unique but recieved multiple items for this name." + ) return False - + @classmethod def get_all_projects(cls): - db_client = SupabaseInterface(table="projects") - data = db_client.read_all() + db_client = SupabaseClient() + data = db_client.read_all(table="projects") return data - - - -# test = Project(name='test') \ No newline at end of file +# test = Project(name='test') diff --git a/models/user.py b/models/user.py index 28a484c..77eaa02 100644 --- a/models/user.py +++ b/models/user.py @@ -1,15 +1,18 @@ -from utils.db import SupabaseInterface +from helpers.supabaseClient import SupabaseClient + class User: - def __init__(self,userData): - #self.name = userData["name"] + def __init__(self, userData): + # self.name = userData["name"] self.discordId = userData["discordId"] self.discordUserName = userData("discordUserName") self.githubId = userData["githubId"] - + def exists(self, table): - data = SupabaseInterface(table).read(query_key='discord_id', query_value=self.discordID) - if len(data.data)>0: + data = SupabaseClient().read( + table, query_key="discord_id", query_value=self.discordID + ) + if len(data.data) > 0: return True else: return False @@ -18,13 +21,13 @@ def exists(self, table): class Contributor(User): def __init__(self, userData): super().__init__(userData) - + class Mentor(User): def __init__(self, userData): super().__init__(userData) + class OrgMember(User): def __init__(self, userData): super().__init__(userData) - diff --git a/requirements.txt b/requirements.txt index 7ede102..5555f28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,4 @@ python-dotenv==1.0.0 requests==2.31.0 urllib3==2.0.2 yarl==1.9.2 -supabase==1.0.3 \ No newline at end of file +supabase==1.0.3 diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/utils/api.py b/utils/api.py deleted file mode 100644 index 75bc01e..0000000 --- a/utils/api.py +++ /dev/null @@ -1,95 +0,0 @@ -import requests -import os, sys, dotenv -import json - -dotenv.load_dotenv() - - -class GithubAPI: - def __init__(self, repo): - url_components = repo.split('/') - self.owner = url_components[3] - self.repo = url_components[4] - self.headers = { - 'Accept': 'application/vnd.github+json', - 'Authorization': f'Bearer {os.getenv("GithubPAT")}' - } - - def get_repo_commits(self): - url = f'https://api.github.com/repos/{self.owner}/{self.repo}/commits' - return requests.get(url, headers=self.headers).json() - - def get_commit_count(self): - contributors = self.get_contributors() - print(contributors, type(contributors)) - commit_count = 0 - for contributor in contributors: - print("""THIS IS CONTRIBUTOR""", contributor) - commit_count+=contributor['contributions'] - return commit_count - - def get_latest_repo_commit(self): - return self.get_commits()[0] - - def get_json(self, dict): - return json.dumps(dict) - - def get_issues(self, status, page): - params={ - "state":status, - "since":"2023-06-08T00:00:00Z", - "per_page":100, - "page":page - } - url = f'https://api.github.com/repos/{self.owner}/{self.repo}/issues' - return requests.get(url, headers=self.headers, params=params).json() - - def get_issue_comments(self, issue_number): - params={ - "since":"2023-05-01T00:00:00Z", #cutoff date for metrics - } - - url=f"https://api.github.com/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments" - - return requests.get(url, headers=self.headers, params=params).json() - - - def get_pull_requests(self, status, page): - params = { - "state":status, - "per_page":100, - "page":page - } - url = f'https://api.github.com/repos/{self.owner}/{self.repo}/pulls' - return requests.get(url, headers=self.headers, params={"state":status}).json() - - def get_issue_count(self): - open_count = len(self.get_issues('open')) - closed_count = len(self.get_issues('closed')) - - return (open_count, closed_count) - - def get_pull_request_count(self): - open_count = len(self.get_pull_requests('open')) - closed_count = len(self.get_pull_requests('closed')) - - return (open_count, closed_count) - - - def get_contributors(self): - url = f'https://api.github.com/repos/{self.owner}/{self.repo}/contributors' - return requests.get(url, headers=self.headers).json() - - - -owner = 'ChakshuGautam' -repo = 'cQube-POCs' -url = f'https://api.github.com/repos/{owner}/{repo}/commits' - -# # r = requests.get(url, headers=headers) -# # # sys.stdout.write(r.text) -# # print(r.json()[0]["commit"]["author"]["date"], r.json()[-1]["commit"]["author"]["date"]) -# tester = GithubAPI('https://github.com/ChakshuGautam/cQube-ingestion') -# print(tester.get_commit_count()) -# print(tester.get_pull_requests('open')) -# print(tester.get_json(tester.get_contributors())) \ No newline at end of file diff --git a/utils/db.py b/utils/db.py deleted file mode 100644 index 782a55e..0000000 --- a/utils/db.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -from supabase import create_client, Client - -class SupabaseInterface: - def __init__(self, table, url=None, key=None) -> None: - - self.supabase_url = url if url else os.getenv("SUPABASE_URL") - self.supabase_key = key if key else os.getenv("SUPABASE_KEY") - self.table = table - self.client: Client = create_client(self.supabase_url, self.supabase_key) - - def read(self, query_key, query_value, columns="*"): - data = self.client.table(self.table).select(columns).eq(query_key, query_value).execute() - #data.data returns a list of dictionaries with keys being column names and values being row values - return data.data - - def read_by_order_limit(self, query_key, query_value, order_column, order_by=False, limit=1, columns="*"): - data = self.client.table(self.table).select(columns).eq(query_key, query_value).order(order_column).limit(limit).execute() - return data.data - - def read_all(self): - data = self.client.table(self.table).select("*").execute() - return data.data - - def update(self, update, query_key, query_value): - data = self.client.table(self.table).update(update).eq(query_key, query_value).execute() - return data.data - - def insert(self, data): - data = self.client.table(self.table).insert(data).execute() - return data.data - def delete(self): - pass - - - - def add_user(self, userdata): - data = self.client.table("users").insert(userdata).execute() - print(data.data) - return data.data - - def user_exists(self, discord_id): - data = self.client.table("users").select("*").eq("discord_id", discord_id).execute() - if len(data.data)>0: - return True - else: - return False \ No newline at end of file diff --git a/utils/github-api/api-response-schemas/contributorsResponseSchema.json b/utils/github-api/api-response-schemas/contributorsResponseSchema.json deleted file mode 100644 index e22992f..0000000 --- a/utils/github-api/api-response-schemas/contributorsResponseSchema.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "type": "array", - "items": { - "title": "Contributor", - "description": "Contributor", - "type": "object", - "properties": { - "login": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "node_id": { - "type": "string" - }, - "avatar_url": { - "type": "string", - "format": "uri" - }, - "gravatar_id": { - "type": [ - "string", - "null" - ] - }, - "url": { - "type": "string", - "format": "uri" - }, - "html_url": { - "type": "string", - "format": "uri" - }, - "followers_url": { - "type": "string", - "format": "uri" - }, - "following_url": { - "type": "string" - }, - "gists_url": { - "type": "string" - }, - "starred_url": { - "type": "string" - }, - "subscriptions_url": { - "type": "string", - "format": "uri" - }, - "organizations_url": { - "type": "string", - "format": "uri" - }, - "repos_url": { - "type": "string", - "format": "uri" - }, - "events_url": { - "type": "string" - }, - "received_events_url": { - "type": "string", - "format": "uri" - }, - "type": { - "type": "string" - }, - "site_admin": { - "type": "boolean" - }, - "contributions": { - "type": "integer" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "contributions", - "type" - ] - } -} \ No newline at end of file diff --git a/utils/github-api/sample-api-responses/sampleCommitRespose.json b/utils/github-api/sample-api-responses/sampleCommitRespose.json deleted file mode 100644 index 2c595f0..0000000 --- a/utils/github-api/sample-api-responses/sampleCommitRespose.json +++ /dev/null @@ -1,80 +0,0 @@ - -{ - "sha": "e044ac41af5dff5f6a38f1694ebb2920c8490682", - "node_id": "C_kwDOJLcmytoAKGUwNDRhYzQxYWY1ZGZmNWY2YTM4ZjE2OTRlYmIyOTIwYzg0OTA2ODI", - "commit": { - "author": { - "name": "KDwevedi", - "email": "74085496+KDwevedi@users.noreply.github.com", - "date": "2023-05-18T14:48:54Z" - }, - "committer": { - "name": "GitHub", - "email": "noreply@github.com", - "date": "2023-05-18T14:48:54Z" - }, - "message": "Update README.md", - "tree": { - "sha": "2821e5ab49172b7d1df7157d7d8ba1ee6f36634d", - "url": "https://api.github.com/repos/KDwevedi/btp/git/trees/2821e5ab49172b7d1df7157d7d8ba1ee6f36634d" - }, - "url": "https://api.github.com/repos/KDwevedi/btp/git/commits/e044ac41af5dff5f6a38f1694ebb2920c8490682", - "comment_count": 1, - "verification": { - "verified": true, - "reason": "valid", - "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJkZjrWCRBK7hj4Ov3rIwAAjKYIAKIZMJePJoZP9Segx44T9G83\nhFfC+Pn2jAWP4NYNbZcrGlXSxNzvGCJld5b5ZBGZhbndF9UAdqPD/hy4pl5CedLh\nFygzbgkEq3iGMMFzeAu5fRZSsJGopejLl5OGfbKMSXoFXYswqCpSJ9C7DQTUdCaW\nmgqJHiMhPGlodxD5iCbC+wLuC4Uf+W5eRftS4w08Aiy8ivcHYv8Ps76xHWDznpRc\nNDwZhTQj16GBy2uge9Wh0ARmMlRvyyit3suRXJ3zcTXXEI8Tf16jOSLVtEanXz1U\nxYM775v7x1BjmCb1ZMFncdpjUDJIWkbMHcoRx8Z/8BOrWUc6FePZqKdf5u46cJM=\n=Iw8+\n-----END PGP SIGNATURE-----\n", - "payload": "tree 2821e5ab49172b7d1df7157d7d8ba1ee6f36634d\nparent 3c28bfcf16b9c7738ddaad2a58c19fc04218fa81\nauthor KDwevedi <74085496+KDwevedi@users.noreply.github.com> 1684421334 +0530\ncommitter GitHub 1684421334 +0530\n\nUpdate README.md" - } - }, - "url": "https://api.github.com/repos/KDwevedi/btp/commits/e044ac41af5dff5f6a38f1694ebb2920c8490682", - "html_url": "https://github.com/KDwevedi/btp/commit/e044ac41af5dff5f6a38f1694ebb2920c8490682", - "comments_url": "https://api.github.com/repos/KDwevedi/btp/commits/e044ac41af5dff5f6a38f1694ebb2920c8490682/comments", - "author": { - "login": "KDwevedi", - "id": 74085496, - "node_id": "MDQ6VXNlcjc0MDg1NDk2", - "avatar_url": "https://avatars.githubusercontent.com/u/74085496?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/KDwevedi", - "html_url": "https://github.com/KDwevedi", - "followers_url": "https://api.github.com/users/KDwevedi/followers", - "following_url": "https://api.github.com/users/KDwevedi/following{/other_user}", - "gists_url": "https://api.github.com/users/KDwevedi/gists{/gist_id}", - "starred_url": "https://api.github.com/users/KDwevedi/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/KDwevedi/subscriptions", - "organizations_url": "https://api.github.com/users/KDwevedi/orgs", - "repos_url": "https://api.github.com/users/KDwevedi/repos", - "events_url": "https://api.github.com/users/KDwevedi/events{/privacy}", - "received_events_url": "https://api.github.com/users/KDwevedi/received_events", - "type": "User", - "site_admin": false - }, - "committer": { - "login": "web-flow", - "id": 19864447, - "node_id": "MDQ6VXNlcjE5ODY0NDQ3", - "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/web-flow", - "html_url": "https://github.com/web-flow", - "followers_url": "https://api.github.com/users/web-flow/followers", - "following_url": "https://api.github.com/users/web-flow/following{/other_user}", - "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", - "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", - "organizations_url": "https://api.github.com/users/web-flow/orgs", - "repos_url": "https://api.github.com/users/web-flow/repos", - "events_url": "https://api.github.com/users/web-flow/events{/privacy}", - "received_events_url": "https://api.github.com/users/web-flow/received_events", - "type": "User", - "site_admin": false - }, - "parents": [ - { - "sha": "3c28bfcf16b9c7738ddaad2a58c19fc04218fa81", - "url": "https://api.github.com/repos/KDwevedi/btp/commits/3c28bfcf16b9c7738ddaad2a58c19fc04218fa81", - "html_url": "https://github.com/KDwevedi/btp/commit/3c28bfcf16b9c7738ddaad2a58c19fc04218fa81" - } - ] -} diff --git a/utils/github-api/sample-api-responses/sampleContributersResponse.json b/utils/github-api/sample-api-responses/sampleContributersResponse.json deleted file mode 100644 index 9cdd9a6..0000000 --- a/utils/github-api/sample-api-responses/sampleContributersResponse.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false, - "contributions": 32 - } - ] \ No newline at end of file