From d3b8edd5228b24bf2c80c5b2ec5257f83af19e15 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 30 Jun 2025 12:54:50 +0530 Subject: [PATCH] chore: add github basic apis --- .../integrations/github_controller.rb | 145 +++++++++++++++++- .../super_admin/app_configs_controller.rb | 2 +- app/services/github/github_service.rb | 60 ++++++++ config/routes.rb | 3 + 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 app/services/github/github_service.rb diff --git a/app/controllers/api/v1/accounts/integrations/github_controller.rb b/app/controllers/api/v1/accounts/integrations/github_controller.rb index 6f29a4c70..f3d87df0c 100644 --- a/app/controllers/api/v1/accounts/integrations/github_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/github_controller.rb @@ -1,12 +1,155 @@ class Api::V1::Accounts::Integrations::GithubController < Api::V1::Accounts::BaseController - before_action :fetch_hook, only: [:destroy] + before_action :fetch_hook + before_action :ensure_hook_exists, except: [:destroy] def destroy @hook.destroy! head :ok end + def repositories + repositories = github_service.repositories + filtered_repos = repositories.map do |repo| + { + full_name: repo[:full_name] || repo.full_name, + private: repo[:private] || repo.private + } + end + render json: filtered_repos + end + + def search_repositories + repositories = github_service.search_repositories(params[:q]) + filtered_repos = repositories.map do |repo| + { + full_name: repo[:full_name] || repo.full_name, + private: repo[:private] || repo.private + } + end + render json: filtered_repos + end + + def assignees + assignees = github_service.assignees(repo_full_name) + filtered_assignees = assignees.map do |assignee| + { + login: assignee.login, + avatar_url: assignee.avatar_url + } + end + render json: filtered_assignees + end + + def labels + labels = github_service.labels(repo_full_name) + filtered_labels = labels.map do |label| + { + name: label.name, + color: label.color + } + end + render json: filtered_labels + end + + def search_issues + issues = github_service.search_issues(repo_full_name, params[:q]) + filtered_issues = issues.map do |issue| + { + number: issue.number, + title: issue.title, + html_url: issue.html_url + } + end + render json: filtered_issues + end + + def create_issue + issue_data = github_service.create_issue( + repo_full_name, + params[:title], + params[:body], + issue_options + ) + + github_issue = create_linked_issue(issue_data) + render json: issue_response(github_issue), status: :created + end + + def link_issue + issue_data = github_service.issue(repo_full_name, params[:issue_number]) + github_issue = create_linked_issue(issue_data) + render json: issue_response(github_issue), status: :created + end + + def linked_issues + issues = GithubIssue.where(conversation_id: params[:conversation_id]) + + render json: issues.map do |issue| + { + id: issue.id, + issue_number: issue.issue_number, + title: issue.issue_title, + html_url: issue.html_url, + repo_full_name: issue.repo_full_name, + linked_by: { + id: issue.linked_by.id, + name: issue.linked_by.name + }, + created_at: issue.created_at + } + end + end + + def unlink_issue + github_issue = GithubIssue.find(params[:id]) + github_issue.destroy! + head :ok + end + + private + def fetch_hook @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'github') end + + def ensure_hook_exists + render json: { error: 'GitHub integration not configured' }, status: :unauthorized unless @hook + end + + def github_service + @github_service ||= Github::GithubService.new(hook: @hook) + end + + def repo_full_name + params[:repo_full_name] || "#{params[:owner]}/#{params[:repo]}" + end + + def issue_options + options = {} + options[:assignees] = params[:assignees] if params[:assignees].present? + options[:labels] = params[:labels] if params[:labels].present? + options + end + + def create_linked_issue(issue_data) + GithubIssue.create!( + conversation_id: params[:conversation_id], + account: Current.account, + repo_full_name: repo_full_name, + issue_number: issue_data.number, + issue_title: issue_data.title, + html_url: issue_data.html_url, + linked_by: Current.user + ) + end + + def issue_response(github_issue) + { + id: github_issue.id, + issue_number: github_issue.issue_number, + title: github_issue.issue_title, + html_url: github_issue.html_url, + repo_full_name: github_issue.repo_full_name + } + end end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 0601beba7..56b644502 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -39,7 +39,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'], 'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET], 'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET], - 'github' => %w[GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET] + 'github' => %w[GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET], 'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET], 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT] } diff --git a/app/services/github/github_service.rb b/app/services/github/github_service.rb new file mode 100644 index 000000000..52caec4e5 --- /dev/null +++ b/app/services/github/github_service.rb @@ -0,0 +1,60 @@ +class Github::GithubService + attr_reader :hook + + def initialize(hook:) + @hook = hook + end + + def repositories + client.repositories(nil, type: 'all') + rescue Octokit::Error => e + Rails.logger.error("GitHub API error fetching repositories: #{e.message}") + raise_api_error(e) + end + + def assignees(repo_full_name) + client.repository_assignees(repo_full_name) + rescue Octokit::Error => e + Rails.logger.error("GitHub API error fetching assignees for #{repo_full_name}: #{e.message}") + raise_api_error(e) + end + + def search_repositories(query) + return repositories if query.blank? + + # Get all user's repositories first + user_repos = repositories + + # Filter repositories by query string (case-insensitive) + user_repos.select do |repo| + repo.full_name.downcase.include?(query.downcase) || + (repo.description&.downcase&.include?(query.downcase)) + end + rescue Octokit::Error => e + Rails.logger.error("GitHub API error searching repositories with query '#{query}': #{e.message}") + raise_api_error(e) + end + + private + + def client + @client ||= Octokit::Client.new( + access_token: hook.access_token, + auto_paginate: true, + per_page: 100 + ) + end + + def raise_api_error(error) + case error.response_status + when 401 + raise CustomExceptions::Base.new('GitHub authentication failed', 401) + when 403 + raise CustomExceptions::Base.new('GitHub API rate limit exceeded or insufficient permissions', 403) + when 404 + raise CustomExceptions::Base.new('GitHub resource not found', 404) + else + raise CustomExceptions::Base.new("GitHub API error: #{error.message}", error.response_status || 500) + end + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 8c4d44e83..4f90033ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -277,6 +277,9 @@ Rails.application.routes.draw do resource :github, controller: 'github', only: [] do collection do delete :destroy + get :repositories + get :search_repositories + get 'repositories/:owner/:repo/assignees', to: 'github#assignees', as: :repository_assignees end end end