From ff8a565003c11572677d833e28fca30e74b4d957 Mon Sep 17 00:00:00 2001 From: KDwevedi Date: Tue, 13 Jun 2023 10:04:34 +0530 Subject: [PATCH] initial commit --- .env.sample | 14 ++ .gitignore | 7 + .gitignore copy | 7 + cogs/discord_data_scraper.py | 116 +++++++++++++ cogs/github_data_scraper.py | 26 +++ cogs/metrics_tracker.py | 159 ++++++++++++++++++ cogs/user_interactions.py | 72 ++++++++ main.py | 38 +++++ models/__init__.py | 0 models/github_interface.py | 14 ++ models/product.py | 50 ++++++ models/project.py | 37 ++++ models/user.py | 30 ++++ requirements.txt | 16 ++ utils/__init__.py | 0 utils/api.py | 95 +++++++++++ utils/db.py | 51 ++++++ .../contributorsResponseSchema.json | 88 ++++++++++ .../sampleCommitRespose.json | 80 +++++++++ .../sampleContributersResponse.json | 23 +++ 20 files changed, 923 insertions(+) create mode 100644 .env.sample create mode 100644 .gitignore create mode 100644 .gitignore copy create mode 100644 cogs/discord_data_scraper.py create mode 100644 cogs/github_data_scraper.py create mode 100644 cogs/metrics_tracker.py create mode 100644 cogs/user_interactions.py create mode 100644 main.py create mode 100644 models/__init__.py create mode 100644 models/github_interface.py create mode 100644 models/product.py create mode 100644 models/project.py create mode 100644 models/user.py create mode 100644 requirements.txt create mode 100644 utils/__init__.py create mode 100644 utils/api.py create mode 100644 utils/db.py create mode 100644 utils/github-api/api-response-schemas/contributorsResponseSchema.json create mode 100644 utils/github-api/sample-api-responses/sampleCommitRespose.json create mode 100644 utils/github-api/sample-api-responses/sampleContributersResponse.json diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..dc1fa81 --- /dev/null +++ b/.env.sample @@ -0,0 +1,14 @@ +#token to connect to the bot +TOKEN="" +#discord server id +SERVER_ID="" +INTRODUCTIONS_CHANNEL="" +NON_CONTRIBUTOR_ROLES="" + +FLASK_HOST="" + +SUPABASE_URL="" +SUPABASE_KEY="" + +GithubPAT="" + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dfbc50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +#all files relating to python vitual env +/.venv +#environment variables and cache +*.env +*/__pycache__/* +*.csv +/local_only diff --git a/.gitignore copy b/.gitignore copy new file mode 100644 index 0000000..5dfbc50 --- /dev/null +++ b/.gitignore copy @@ -0,0 +1,7 @@ +#all files relating to python vitual env +/.venv +#environment variables and cache +*.env +*/__pycache__/* +*.csv +/local_only diff --git a/cogs/discord_data_scraper.py b/cogs/discord_data_scraper.py new file mode 100644 index 0000000..f4a9dd9 --- /dev/null +++ b/cogs/discord_data_scraper.py @@ -0,0 +1,116 @@ +from discord.ext import commands, tasks +from discord.channel import TextChannel +from discord import Member +import os, dateutil, json +from datetime import datetime + +from utils.db import SupabaseInterface +from utils.api import GithubAPI +import csv + +#CONSTANTS +RUNTIME_DATA_DIRECTORY = 'scraping-runtime-data' +RUNTIME_DATA_FILE = 'discordScraperRuntimeData.json' + +#check id directory exists for scraping runtime data and create one if it doesn't +def createRuntimeDataDirectory(): + cwd = os.getcwd() + path = f'{cwd}/{RUNTIME_DATA_DIRECTORY}' + if not os.path.isdir(path): + os.mkdir(path) + + return path + + + + +class DiscordDataScaper(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + self.runtimeDataDirectory = createRuntimeDataDirectory() + + @commands.command() + async def introductions(self, ctx): + guild = ctx.guild if ctx.guild else await self.bot.fetch_guild(os.getenv("SERVER_ID")) + intro_channel = await guild.fetch_channel(os.getenv("INTRODUCTIONS_CHANNEL")) + with open('introduced.csv', 'w') as file: + writer = csv.writer(file) + data = [] + async for message in intro_channel.history(limit=None): + row = [message.author.id] + if row not in data: + count+=1 + data.append(row) + writer.writerows(data) + + @commands.command() + async def not_contributors(self, ctx): + guild = ctx.guild if ctx.guild else await self.bot.fetch_guild(os.getenv("SERVER_ID")) + orgAndMentors = [role for role in os.getenv("NON_CONTRIBUTOR_ROLES").split(',')] + with open("not_contributors.csv", "w") as file: + writer = csv.writer(file) + data = [] + async for member in guild.fetch_members(limit=None): + for role in member.roles: + if role.id in orgAndMentors: + user = [member.name, member.id, member.roles] + if user not in data: + data.append(user) + writer.writerows(data) + + #Store all messages on Text Channels in the Discord Server to SupaBase + @commands.command() + async def add_messages(self,ctx): + + def addMessageData(data): + client = SupabaseInterface("unstructured discord data") + client.insert(data) + return + + def recordLastRunTime(data, directory): + with open(f'{directory}/{RUNTIME_DATA_FILE}', 'w+') as file: + json.dump(data, file) + + def getLastRunTime(channelId): + with open(f'{self.runtimeDataDirectory}/{RUNTIME_DATA_FILE}', 'r') as file: + data = json.load(file) + lastRuntime = data.get(str(channelId)) + if lastRuntime is None: + #all messages will be read + return None + else: + return dateutil.parser.parse(lastRuntime) + + + + guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) #SERVER_ID Should be C4GT Server ID + channels = await guild.fetch_channels() + runtimeData = {} + + 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 + messages = [] + last_run = getLastRunTime(channel.id) + print(last_run) + async for message in channel.history(limit=None, after =last_run ): + 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) + } + messages.append(msg_data) + print(len(messages)) + addMessageData(messages) + runtimeData[channel.id] = datetime.now().isoformat() + recordLastRunTime(runtimeData, self.runtimeDataDirectory) + print("Complete!") + +async def setup(bot): + await bot.add_cog(DiscordDataScaper(bot)) \ No newline at end of file diff --git a/cogs/github_data_scraper.py b/cogs/github_data_scraper.py new file mode 100644 index 0000000..2348fe7 --- /dev/null +++ b/cogs/github_data_scraper.py @@ -0,0 +1,26 @@ +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/metrics_tracker.py b/cogs/metrics_tracker.py new file mode 100644 index 0000000..3df682d --- /dev/null +++ b/cogs/metrics_tracker.py @@ -0,0 +1,159 @@ +#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 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 + + + + +class MetricsTracker(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + + + + #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/user_interactions.py b/cogs/user_interactions.py new file mode 100644 index 0000000..70fbb4c --- /dev/null +++ b/cogs/user_interactions.py @@ -0,0 +1,72 @@ +import discord +import os +from discord.ext import commands, tasks +import time, csv +from utils.db import SupabaseInterface +from utils.api import GithubAPI + + + +#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 AuthenticationView(discord.ui.View): + def __init__(self, discord_userdata): + super().__init__() + button = discord.ui.Button(label='Authenticate Github', style=discord.ButtonStyle.url, url=f'{os.getenv("FLASK_HOST")}/authenticate/{discord_userdata}') + self.add_item(button) + self.message = None + +class UserHandler(commands.Cog): + def __init__(self, bot) -> None: + self.bot = bot + + #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 + dmchannel = ctx.author.dm_channel if ctx.author.dm_channel else await ctx.author.create_dm() + userdata = str(ctx.author.id) + view = AuthenticationView(userdata) + await dmchannel.send("Please authenticate your github account to register for Code for GovTech 2023", view=view) + + + + + + + + @commands.command() + async def announce(self, ctx): + guild = ctx.guild if ctx.guild else await self.bot.fetch_guild(os.getenv("SERVER_ID")) + count = 0 + with open('introduced.csv', 'w') as file: + writer = csv.writer(file) + data = [] + + print(count) + + # async for member in guild.fetch_members(limit=None): + # print(member.id) + # count+=1 + # print(count) + # members = [476285280811483140] + # for member_id in members: + # member = await guild.fetch_member(member_id) + # dmchannel = member.dm_channel if member.dm_channel else await member.create_dm() + # await dmchannel.send("Test Announcement") + + + + @commands.command() + async def test(self,ctx): + # print(os.getenv("SERVER_ID")) + guild = await self.bot.fetch_guild(os.getenv("SERVER_ID")) + channel = await guild.fetch_channel(973851473131761677) + async for message in channel.history(limit=20): + print(message.content, type(message.content)) + if message.content == '': + print(True) + + +async def setup(bot): + await bot.add_cog(UserHandler(bot)) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4df3b53 --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +import discord +from discord.ext import commands +import os, sys +import asyncio +import dotenv + +#Since there are user defined packages, adding current directory to python path +current_directory = os.getcwd() +sys.path.append(current_directory) + +dotenv.load_dotenv(".env") +intents = discord.Intents.all() + +client = commands.Bot(command_prefix='!', intents=intents) + +#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}') + +#load cogs +async def load(): + for filename in os.listdir("./cogs"): + if filename.endswith(".py"): + await client.load_extension(f"cogs.{filename[:-3]}") + +async def main(): + async with client: + await load() + await client.start(os.getenv("TOKEN")) + + +asyncio.run(main()) + + + + + diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/github_interface.py b/models/github_interface.py new file mode 100644 index 0000000..9e6c503 --- /dev/null +++ b/models/github_interface.py @@ -0,0 +1,14 @@ +#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 new file mode 100644 index 0000000..f5b18e8 --- /dev/null +++ b/models/product.py @@ -0,0 +1,50 @@ +from utils.db import SupabaseInterface + +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) + 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 + self.name = data["name"] + #Description of the product + self.desc = data["description"] + #Organisation associated with the product + self.org = data["organisation"] + #The wili page url for the product on C4GT github + self.wiki_url = data["wiki_url"] + #Projects under this product + self.projects = data["projects"] + #Mentors assigned to projects associated with this product + self.mentors = data["mentors"] if data["mentors"] else [] + #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 + 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: + return True + 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() + 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 diff --git a/models/project.py b/models/project.py new file mode 100644 index 0000000..be02d35 --- /dev/null +++ b/models/project.py @@ -0,0 +1,37 @@ +import sys, os + +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] + 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'] + + @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: + return True + 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() + return data + + + + + +# test = Project(name='test') \ No newline at end of file diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..28a484c --- /dev/null +++ b/models/user.py @@ -0,0 +1,30 @@ +from utils.db import SupabaseInterface + +class User: + 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: + return True + else: + return False + + +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 new file mode 100644 index 0000000..03f0a2d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +aiohttp==3.8.4 +aiosignal==1.3.1 +async-timeout==4.0.2 +attrs==23.1.0 +certifi==2023.5.7 +charset-normalizer==3.1.0 +discord==2.2.3 +discord.py==2.2.3 +frozenlist==1.3.3 +idna==3.4 +multidict==6.0.4 +python-dotenv==1.0.0 +requests==2.31.0 +urllib3==2.0.2 +yarl==1.9.2 +supabase==1.0.3 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/api.py b/utils/api.py new file mode 100644 index 0000000..75bc01e --- /dev/null +++ b/utils/api.py @@ -0,0 +1,95 @@ +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 new file mode 100644 index 0000000..5c76be0 --- /dev/null +++ b/utils/db.py @@ -0,0 +1,51 @@ +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_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 + +# tester = SupabaseInterface('users').read_all() +# print(tester) +# tester.add_user({ +# "discord_id": 476285280811483140, +# "github_id": 74085496 +# }) \ 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 new file mode 100644 index 0000000..e22992f --- /dev/null +++ b/utils/github-api/api-response-schemas/contributorsResponseSchema.json @@ -0,0 +1,88 @@ +{ + "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 new file mode 100644 index 0000000..2c595f0 --- /dev/null +++ b/utils/github-api/sample-api-responses/sampleCommitRespose.json @@ -0,0 +1,80 @@ + +{ + "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 new file mode 100644 index 0000000..9cdd9a6 --- /dev/null +++ b/utils/github-api/sample-api-responses/sampleContributersResponse.json @@ -0,0 +1,23 @@ +[ + { + "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