<NME> GitHubAPI.ts
<BEF> import { Octokit as GitHubNodeAPI } from "@octokit/rest"
import { debug } from "../../debug"
import * as node_fetch from "node-fetch"
import * as parse from "parse-link-header"
import * as v from "voca"

import { GitHubPRDSL, GitHubUser } from "../../dsl/GitHubDSL"

import { api as fetch } from "../../api/fetch"
import { RepoMetaData } from "../../dsl/BitBucketServerDSL"
import { CheckOptions } from "./comms/checks/resultsToCheck"

// The Handle the API specific parts of the github


export type APIToken = string

// Note that there are parts of this class which don't seem to be
// used by Danger, they are exposed for Peril support.


export class GitHubAPI {
  fetch: typeof fetch
  additionalHeaders: any
  private readonly d = debug("GitHubAPI")

  private pr: GitHubPRDSL | undefined

  constructor(public readonly repoMetadata: RepoMetaData, public readonly token?: APIToken) {
    // This allows Peril to DI in a new Fetch function
    // which can handle unique API edge-cases around integrations
    this.fetch = fetch
    this.additionalHeaders = {}
  }

  /**
   * Bit weird, yes, but we want something that can be exposed to an end-user.
   * I wouldn't have a problem with moving this to use this API under the hood
   * but for now that's just a refactor someone can try.
   */
  getExternalAPI = (accessTokenForApp?: string): GitHubNodeAPI => {
    // A token should have been set by this point
    const token = accessTokenForApp || this.token!

    const host = process.env["DANGER_GITHUB_API_BASE_URL"] || process.env["GITHUB_URL"] || undefined
    const options: ConstructorParameters<typeof GitHubNodeAPI>[0] & { debug: boolean } = {
      debug: !!process.env.LOG_FETCH_REQUESTS,
      baseUrl: host,
      auth: `token ${token}`,
    }

    if (this.additionalHeaders) {
      options.headers = this.additionalHeaders
    }

    return new GitHubNodeAPI(options)
  }

  /**
   * Grabs the contents of an individual file on GitHub
   *
   * @param {string} path path to the file
   * @param {string} [ref] an optional sha
   * @returns {Promise<string>} text contents
   *
   */
  fileContents = async (path: string, repoSlug?: string, ref?: string): Promise<string> => {
    // Use the current state of PR if no repo/ref is passed
    if (!repoSlug || !ref) {
      const prJSON = await this.getPullRequestInfo()
      repoSlug = prJSON.head.repo.full_name
      ref = prJSON.head.ref
    }

    const data = await this.getFileContents(path, repoSlug, ref)
    const buffer = Buffer.from(data.content, "base64")
    return buffer.toString()
  }

  // The above is the API for Platform

  getDangerCommentIDs = async (dangerID: string): Promise<string[]> => {
    const userID = await this.getUserID()
    const allComments = await this.getPullRequestComments()
    const dangerIDMessage = dangerIDToString(dangerID)
    this.d(`User ID: ${userID}`)
    this.d(`Looking at ${allComments.length} comments for ${dangerIDMessage}`)
    return allComments
      .filter((comment) => comment.body.includes(dangerIDMessage)) // does it contain the right danger ID?
      .filter((comment) => comment.user.id === userID) // Does it have the right user ID?
      .filter((comment) => comment.body.includes("Generated by")) // Does it look like a danger message?
      .map((comment) => comment.id) // only return IDs
  }

  updateCommentWithID = async (id: string, comment: string): Promise<any> => {
    const repo = this.repoMetadata.repoSlug
    const res = await this.patch(
      `repos/${repo}/issues/comments/${id}`,
      {},
      {
        body: comment,
      }
    )

    return res.json()
  }

  deleteCommentWithID = async (id: string): Promise<boolean> => {
    const repo = this.repoMetadata.repoSlug
    const res = await this.api(`repos/${repo}/issues/comments/${id}`, {}, null, "DELETE")

    //https://developer.github.com/v3/issues/comments/#response-5
    return Promise.resolve(res.status === 204)
  }

  deleteInlineCommentWithID = async (id: string): Promise<boolean> => {
    const repo = this.repoMetadata.repoSlug
    const res = await this.api(`repos/${repo}/pulls/comments/${id}`, {}, null, "DELETE", false)

    //https://developer.github.com/v3/pulls/comments/#response-5
    return Promise.resolve(res.status === 204)
  }

  getUserID = async (): Promise<number | undefined> => {
    const perilID = process.env["PERIL_BOT_USER_ID"]
    if (perilID) {
      return parseInt(perilID)
    }

    const info = await this.getUserInfo()
    if (info.id) {
      return info.id
    }

    const useGitHubActionsID = process.env["GITHUB_WORKFLOW"]
    if (useGitHubActionsID) {
      // This is the user.id of the github-actions app (https://github.com/apps/github-actions)
      // that is used to comment when using danger in a GitHub Action
      // with GITHUB_TOKEN (https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token)
      return 41898282
    }

    return undefined
  }

  postPRComment = async (comment: string): Promise<any> => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.post(
      `repos/${repo}/issues/${prID}/comments`,
      {},
      {
        body: comment,
      }
    )

    return res.json()
  }

  postInlinePRComment = async (comment: string, commitId: string, path: string, position: number) => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.post(
      `repos/${repo}/pulls/${prID}/comments`,
      {},
      {
        body: comment,
        commit_id: commitId,
        path: path,
        position: position,
      },
      false
    )
    if (res.ok) {
      return res.json()
    } else {
      throw await res.json()
    }
  }

  postInlinePRReview = async (commitId: string, comments: { comment: string; path: string; position: number }[]) => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.post(
      `repos/${repo}/pulls/${prID}/reviews`,
      {},
      {
        body: "",
        event: "COMMENT",
        commit_id: commitId,
        comments: comments.map(({ comment, path, position }) => ({
          body: comment,
          path,
          position,
        })),
      },
      false
    )
    if (res.ok) {
      return res.json()
    } else {
      throw await res.json()
    }
  }

  updateInlinePRComment = async (comment: string, commentId: string) => {
    const repo = this.repoMetadata.repoSlug
    const res = await this.patch(
      `repos/${repo}/pulls/comments/${commentId}`,
      {},
      {
        body: comment,
      },
      false
    )
    if (res.ok) {
      return res.json()
    } else {
      throw await res.json()
    }
  }

  getPullRequestInfo = async (): Promise<GitHubPRDSL> => {
    if (this.pr) {
      return this.pr
    }
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.get(`repos/${repo}/pulls/${prID}`)
    const prDSL = (await res.json()) as GitHubPRDSL
    this.pr = prDSL

    if (res.ok) {
      return prDSL
    } else {
      throw `Could not get PR Metadata for repos/${repo}/pulls/${prID}`
    }
  }

  getPullRequestCommits = async (): Promise<any[]> => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    return await this.getAllOfResource(`repos/${repo}/pulls/${prID}/commits`)
  }

  /**
   * Get list of commits in pull requests. This'll try to iterate all available pages
   * Until it reaches hard limit of api itself (250 commits).
   * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
   *
   */
  getAllOfResource = async (path: string): Promise<any> => {
    const ret: Array<any> = []

    /**
     * Read response header and locate next page for pagination via link header.
     * If not found, will return -1.
     *
     * @param response Github API response sent via node-fetch
     */
    const getNextPageFromLinkHeader = (response: node_fetch.Response): number => {
      const linkHeader = response.headers.get("link")
      if (!linkHeader) {
        this.d(`getNextPageFromLinkHeader:: Given response does not contain link header for pagination`)
        return -1
      }

      const parsedHeader = parse(linkHeader)
      this.d(`getNextPageFromLinkHeader:: Link header found`, parsedHeader)
      if (!!parsedHeader.next && !!parsedHeader.next.page) {
        return parsedHeader.next.page
      }
      return -1
    }

    //iterates commit request pages until next page's not available, or response failed for some reason.
    let page = 0
    while (page >= 0) {
      const requestUrl = `${path}${page > 0 ? `?page=${page}` : ""}`
      this.d(`getPullRequestCommits:: Sending pull request commit request for ${page === 0 ? "first" : `${page}`} page`)
      this.d(`getPullRequestCommits:: Request url generated "${requestUrl}"`)

      const response = await this.get(requestUrl)
      if (response.ok) {
        ret.push(...(await response.json()))
        page = getNextPageFromLinkHeader(response)
      } else {
        this.d(
          `getPullRequestCommits:: Failed to get response while traverse page ${page} with ${response.status}, bailing rest of pages if exists`
        )
        page = -1
      }
    }

    return ret
  }

  getUserInfo = async (): Promise<GitHubUser> => {
    const response = await this.get("user")
    return response.json()
  }

  getPullRequestComments = async (): Promise<GitHubIssueComment[]> => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    return await this.getAllOfResource(`repos/${repo}/issues/${prID}/comments`)
  }

  getPullRequestInlineComments = async (
    dangerID: string
  ): Promise<(GitHubIssueComment & { ownedByDanger: boolean })[]> => {
    const userID = await this.getUserID()
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const dangerIDMessage = dangerIDToString(dangerID)
    return await this.getAllOfResource(`repos/${repo}/pulls/${prID}/comments`).then((v: GitHubIssueComment[]) => {
      return v
        .filter(Boolean)
        .map((i) => {
          return { ...i, ownedByDanger: i.user.id == userID && i.body.includes(dangerIDMessage) }
        })
        .filter((i) => i.ownedByDanger)
    })
  }

  getPullRequestDiff = async () => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.get(`repos/${repo}/pulls/${prID}`, {
      Accept: "application/vnd.github.v3.diff",
    })
    return res.ok ? res.text() : ""
  }

  getFileContents = async (path: string, repoSlug: string, ref: string): Promise<any> => {
    const res = await this.get(`repos/${repoSlug}/contents/${path}?ref=${ref}`)
    return res.ok ? res.json() : { content: "" }
  }

  getPullRequests = async (): Promise<any> => {
    const repo = this.repoMetadata.repoSlug
    const res = await this.get(`repos/${repo}/pulls`)

    return res.ok ? res.json() : []
  }

  getReviewerRequests = async (): Promise<any> => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.get(`repos/${repo}/pulls/${prID}/requested_reviewers`, {
      Accept: "application/vnd.github.v3+json",
    })

    return res.ok ? res.json() : []
  }

  getReviews = async (): Promise<any> => {
    const repo = this.repoMetadata.repoSlug
    const prID = this.repoMetadata.pullRequestID
    const res = await this.get(`repos/${repo}/pulls/${prID}/reviews`, {
      Accept: "application/vnd.github.v3+json",
    })

    return res.ok ? res.json() : []
  }

  getIssue = async (): Promise<any> => {
    const repo = this.repoMetadata.repoSlug
      // e.g. https://gist.github.com/LTe/5270348
      customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }
    }
    return this.fetch(
      url,
      {
        method: method,
        body: body,
        headers: {
          "Content-Type": "application/json",
          ...headers,
          ...this.additionalHeaders,
          ...customAccept,
        },
      },
      suppressErrors
    )
  }

    }

    // This is the bold bit of the status message, it kinda feels like
    // the name of the service.
    let context = dangerID || "Danger"

    // Suppress errors, because in OSS
    // this failure could be due to access rights.
    //
    // So only error when it's a real message.
    const statusURL = `repos/${repo}/statuses/${ref}`
    try {
      const res = await this.post(
        statusURL,
        {},
        {
          state: state,
          context: context,
          target_url: url || "http://danger.systems/js",
          description: message,
        },
        true
      )
      if (!res.ok) {
        this.d(`Got a non-OK (${res.status} ${res.statusText}) response from ${statusURL}:`)
        this.d(JSON.stringify(res, null, "  "))
      }
      return res.ok
    } catch (error) {
      this.d(`Posting a status to: ${statusURL} failed, this is the response:`)
      this.d((error && (error as Error).message) || error)
    }
  }

  postCheckRun = async (check: CheckOptions, token: string) => {
    const repo = this.repoMetadata.repoSlug
    const res = await this.post(
      `repos/${repo}/check-runs`,
      {
        Accept: "application/vnd.github.antiope-preview+json,application/vnd.github.machine-man-preview+json",
        Authorization: `token ${token}`,
      },
      check
    )
    if (res.ok) {
      return res.json()
    } else {
      throw await res.json()
    }
  }

  // API Helpers

  private api = (path: string, headers: any = {}, body: any = {}, method: string, suppressErrors?: boolean) => {
    if (this.token && !headers["Authorization"]) {
      headers["Authorization"] = `token ${this.token}`
    }

    const containsBase = path.startsWith("http")
    const baseUrl = process.env["DANGER_GITHUB_API_BASE_URL"] || process.env["GITHUB_URL"] || "https://api.github.com"
    const url = containsBase ? path : `${baseUrl}/${path}`

    let customAccept = {}
    if (headers.Accept && this.additionalHeaders.Accept) {
      // We need to merge the accepts which are comma separated according to the HTML spec
      // e.g. https://gist.github.com/LTe/5270348

      // But make sure it doesn't already include it
      if (headers.Accept.includes(this.additionalHeaders.Accept)) {
        // If it's already a subset, ignore
        customAccept = { Accept: headers.Accept }
      } else {
        customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }
      }
    }
    const finalHeaders = {
      "Content-Type": "application/json",
      ...headers,
      ...this.additionalHeaders,
      ...customAccept,
    }

    this.d("Sending: ", url, finalHeaders)
    return limit(() =>
      this.fetch(
        url,
        {
          method,
          body,
          headers: finalHeaders,
        },
        suppressErrors
      )
    )
  }

  get = (path: string, headers: any = {}): Promise<node_fetch.Response> => this.api(path, headers, null, "GET")

  post = (path: string, headers: any = {}, body: any = {}, suppressErrors?: boolean): Promise<node_fetch.Response> =>
    this.api(path, headers, JSON.stringify(body), "POST", suppressErrors)

  patch = (path: string, headers: any = {}, body: any = {}, suppressErrors?: boolean): Promise<node_fetch.Response> =>
    this.api(path, headers, JSON.stringify(body), "PATCH", suppressErrors)
}

<MSG> Limit concurrent API calls

<DFF> @@ -3,6 +3,7 @@ import * as debug from "debug"
 import * as node_fetch from "node-fetch"
 import * as parse from "parse-link-header"
 import * as v from "voca"
+import * as pLimit from "p-limit"
 
 import { GitHubPRDSL, GitHubUser } from "../../dsl/GitHubDSL"
 
@@ -15,6 +16,8 @@ import { RepoMetaData } from "../../dsl/BitBucketServerDSL"
 
 export type APIToken = string
 
+const limit = pLimit(25)
+
 // Note that there are parts of this class which don't seem to be
 // used by Danger, they are exposed for Peril support.
 
@@ -369,19 +372,21 @@ export class GitHubAPI {
       // e.g. https://gist.github.com/LTe/5270348
       customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }
     }
-    return this.fetch(
-      url,
-      {
-        method: method,
-        body: body,
-        headers: {
-          "Content-Type": "application/json",
-          ...headers,
-          ...this.additionalHeaders,
-          ...customAccept,
+    return limit(() =>
+      this.fetch(
+        url,
+        {
+          method: method,
+          body: body,
+          headers: {
+            "Content-Type": "application/json",
+            ...headers,
+            ...this.additionalHeaders,
+            ...customAccept,
+          },
         },
-      },
-      suppressErrors
+        suppressErrors
+      )
     )
   }
 

{'commit': 'diff --git a/package.json b/package.json\nindex b3ed3eb..5863124 100644\n--- a/package.json\n+++ b/package.json\n@@ -125,6 +125,7 @@\n     "lodash.keys": "^4.0.8",\n     "node-cleanup": "^2.1.2",\n     "node-fetch": "^2.1.2",\n+    "p-limit": "^1.2.0",\n     "parse-diff": "^0.4.2",\n     "parse-git-config": "^2.0.2",\n     "parse-github-url": "^1.0.2",\ndiff --git a/source/platforms/github/GitHubAPI.ts b/source/platforms/github/GitHubAPI.ts\nindex 9fa2894..12fdcf2 100644\n--- a/source/platforms/github/GitHubAPI.ts\n+++ b/source/platforms/github/GitHubAPI.ts\n@@ -3,6 +3,7 @@ import * as debug from "debug"\n import * as node_fetch from "node-fetch"\n import * as parse from "parse-link-header"\n import * as v from "voca"\n+import * as pLimit from "p-limit"\n \n import { GitHubPRDSL, GitHubUser } from "../../dsl/GitHubDSL"\n \n@@ -15,6 +16,8 @@ import { RepoMetaData } from "../../dsl/BitBucketServerDSL"\n \n export type APIToken = string\n \n+const limit = pLimit(25)\n+\n // Note that there are parts of this class which don\'t seem to be\n // used by Danger, they are exposed for Peril support.\n \n@@ -369,19 +372,21 @@ export class GitHubAPI {\n       // e.g. https://gist.github.com/LTe/5270348\n       customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }\n     }\n-    return this.fetch(\n-      url,\n-      {\n-        method: method,\n-        body: body,\n-        headers: {\n-          "Content-Type": "application/json",\n-          ...headers,\n-          ...this.additionalHeaders,\n-          ...customAccept,\n+    return limit(() =>\n+      this.fetch(\n+        url,\n+        {\n+          method: method,\n+          body: body,\n+          headers: {\n+            "Content-Type": "application/json",\n+            ...headers,\n+            ...this.additionalHeaders,\n+            ...customAccept,\n+          },\n         },\n-      },\n-      suppressErrors\n+        suppressErrors\n+      )\n     )\n   }\n \ndiff --git a/yarn.lock b/yarn.lock\nindex b31fcfb..7d5df44 100644\n--- a/yarn.lock\n+++ b/yarn.lock\n@@ -2400,9 +2400,9 @@ find@0.2.6:\n   dependencies:\n     traverse-chain "~0.1.0"\n \n-flow-bin@^0.69.0:\n-  version "0.69.0"\n-  resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.69.0.tgz#053159a684a6051fcbf0b71a2eb19a9679082da6"\n+flow-bin@^0.71.0:\n+  version "0.71.0"\n+  resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.71.0.tgz#fd1b27a6458c3ebaa5cb811853182ed631918b70"\n \n for-in@^1.0.1, for-in@^1.0.2:\n   version "1.0.2"\n@@ -4629,6 +4629,12 @@ p-limit@^1.1.0:\n   version "1.1.0"\n   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"\n \n+p-limit@^1.2.0:\n+  version "1.2.0"\n+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"\n+  dependencies:\n+    p-try "^1.0.0"\n+\n p-locate@^2.0.0:\n   version "2.0.0"\n   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"\n@@ -4639,6 +4645,10 @@ p-map@^1.1.1:\n   version "1.1.1"\n   resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"\n \n+p-try@^1.0.0:\n+  version "1.0.0"\n+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"\n+\n package-json@^4.0.0:\n   version "4.0.1"\n   resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"\n@@ -5952,9 +5962,9 @@ ts-jest@^22.4.2:\n     pkg-dir "^2.0.0"\n     yargs "^11.0.0"\n \n-ts-node@^5.0.1:\n-  version "5.0.1"\n-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-5.0.1.tgz#78e5d1cb3f704de1b641e43b76be2d4094f06f81"\n+ts-node@^6.0.0:\n+  version "6.0.2"\n+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-6.0.2.tgz#e132d530e53173bc6a8c21ea65f64d8b47bc573e"\n   dependencies:\n     arrify "^1.0.0"\n     chalk "^2.3.0"\n', 'commit_subject': 'Limit concurrent API calls', 'repo_name': 'danger/danger-js', 'committer': {'name': 'Maximilian Stoiber', 'email': '1a73af9e7ae00182733b2292511b814be66f065f@mxstbr.com', 'time_sec': '1525189043', 'tz_offset': '120', 'date': {'seconds': '1525189043'}}, 'commit_tree': '2792846f657c42395a9735102797bc117eb80e0d', 'commit_message': 'Limit concurrent API calls\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': '@@ -3,6 +3,7 @@ import * as debug from "debug"\n import * as node_fetch from "node-fetch"\n import * as parse from "parse-link-header"\n import * as v from "voca"\n+import * as pLimit from "p-limit"\n \n import { GitHubPRDSL, GitHubUser } from "../../dsl/GitHubDSL"\n \n@@ -15,6 +16,8 @@ import { RepoMetaData } from "../../dsl/BitBucketServerDSL"\n \n export type APIToken = string\n \n+const limit = pLimit(25)\n+\n // Note that there are parts of this class which don\'t seem to be\n // used by Danger, they are exposed for Peril support.\n \n@@ -369,19 +372,21 @@ export class GitHubAPI {\n       // e.g. https://gist.github.com/LTe/5270348\n       customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }\n     }\n-    return this.fetch(\n-      url,\n-      {\n-        method: method,\n-        body: body,\n-        headers: {\n-          "Content-Type": "application/json",\n-          ...headers,\n-          ...this.additionalHeaders,\n-          ...customAccept,\n+    return limit(() =>\n+      this.fetch(\n+        url,\n+        {\n+          method: method,\n+          body: body,\n+          headers: {\n+            "Content-Type": "application/json",\n+            ...headers,\n+            ...this.additionalHeaders,\n+            ...customAccept,\n+          },\n         },\n-      },\n-      suppressErrors\n+        suppressErrors\n+      )\n     )\n   }\n \n', 'addition_count': 17, 'deletion_count': 12, 'src_file': 'a/source/platforms/github/GitHubAPI.ts', 'tgt_file': 'b/source/platforms/github/GitHubAPI.ts', 'file_extension': '.ts', 'before_file': 'import { Octokit as GitHubNodeAPI } from "@octokit/rest"\nimport { debug } from "../../debug"\nimport * as node_fetch from "node-fetch"\nimport * as parse from "parse-link-header"\nimport * as v from "voca"\n\nimport { GitHubPRDSL, GitHubUser } from "../../dsl/GitHubDSL"\n\nimport { api as fetch } from "../../api/fetch"\nimport { RepoMetaData } from "../../dsl/BitBucketServerDSL"\nimport { CheckOptions } from "./comms/checks/resultsToCheck"\n\n// The Handle the API specific parts of the github\n\n\nexport type APIToken = string\n\n// Note that there are parts of this class which don\'t seem to be\n// used by Danger, they are exposed for Peril support.\n\n\nexport class GitHubAPI {\n  fetch: typeof fetch\n  additionalHeaders: any\n  private readonly d = debug("GitHubAPI")\n\n  private pr: GitHubPRDSL | undefined\n\n  constructor(public readonly repoMetadata: RepoMetaData, public readonly token?: APIToken) {\n    // This allows Peril to DI in a new Fetch function\n    // which can handle unique API edge-cases around integrations\n    this.fetch = fetch\n    this.additionalHeaders = {}\n  }\n\n  /**\n   * Bit weird, yes, but we want something that can be exposed to an end-user.\n   * I wouldn\'t have a problem with moving this to use this API under the hood\n   * but for now that\'s just a refactor someone can try.\n   */\n  getExternalAPI = (accessTokenForApp?: string): GitHubNodeAPI => {\n    // A token should have been set by this point\n    const token = accessTokenForApp || this.token!\n\n    const host = process.env["DANGER_GITHUB_API_BASE_URL"] || process.env["GITHUB_URL"] || undefined\n    const options: ConstructorParameters<typeof GitHubNodeAPI>[0] & { debug: boolean } = {\n      debug: !!process.env.LOG_FETCH_REQUESTS,\n      baseUrl: host,\n      auth: `token ${token}`,\n    }\n\n    if (this.additionalHeaders) {\n      options.headers = this.additionalHeaders\n    }\n\n    return new GitHubNodeAPI(options)\n  }\n\n  /**\n   * Grabs the contents of an individual file on GitHub\n   *\n   * @param {string} path path to the file\n   * @param {string} [ref] an optional sha\n   * @returns {Promise<string>} text contents\n   *\n   */\n  fileContents = async (path: string, repoSlug?: string, ref?: string): Promise<string> => {\n    // Use the current state of PR if no repo/ref is passed\n    if (!repoSlug || !ref) {\n      const prJSON = await this.getPullRequestInfo()\n      repoSlug = prJSON.head.repo.full_name\n      ref = prJSON.head.ref\n    }\n\n    const data = await this.getFileContents(path, repoSlug, ref)\n    const buffer = Buffer.from(data.content, "base64")\n    return buffer.toString()\n  }\n\n  // The above is the API for Platform\n\n  getDangerCommentIDs = async (dangerID: string): Promise<string[]> => {\n    const userID = await this.getUserID()\n    const allComments = await this.getPullRequestComments()\n    const dangerIDMessage = dangerIDToString(dangerID)\n    this.d(`User ID: ${userID}`)\n    this.d(`Looking at ${allComments.length} comments for ${dangerIDMessage}`)\n    return allComments\n      .filter((comment) => comment.body.includes(dangerIDMessage)) // does it contain the right danger ID?\n      .filter((comment) => comment.user.id === userID) // Does it have the right user ID?\n      .filter((comment) => comment.body.includes("Generated by")) // Does it look like a danger message?\n      .map((comment) => comment.id) // only return IDs\n  }\n\n  updateCommentWithID = async (id: string, comment: string): Promise<any> => {\n    const repo = this.repoMetadata.repoSlug\n    const res = await this.patch(\n      `repos/${repo}/issues/comments/${id}`,\n      {},\n      {\n        body: comment,\n      }\n    )\n\n    return res.json()\n  }\n\n  deleteCommentWithID = async (id: string): Promise<boolean> => {\n    const repo = this.repoMetadata.repoSlug\n    const res = await this.api(`repos/${repo}/issues/comments/${id}`, {}, null, "DELETE")\n\n    //https://developer.github.com/v3/issues/comments/#response-5\n    return Promise.resolve(res.status === 204)\n  }\n\n  deleteInlineCommentWithID = async (id: string): Promise<boolean> => {\n    const repo = this.repoMetadata.repoSlug\n    const res = await this.api(`repos/${repo}/pulls/comments/${id}`, {}, null, "DELETE", false)\n\n    //https://developer.github.com/v3/pulls/comments/#response-5\n    return Promise.resolve(res.status === 204)\n  }\n\n  getUserID = async (): Promise<number | undefined> => {\n    const perilID = process.env["PERIL_BOT_USER_ID"]\n    if (perilID) {\n      return parseInt(perilID)\n    }\n\n    const info = await this.getUserInfo()\n    if (info.id) {\n      return info.id\n    }\n\n    const useGitHubActionsID = process.env["GITHUB_WORKFLOW"]\n    if (useGitHubActionsID) {\n      // This is the user.id of the github-actions app (https://github.com/apps/github-actions)\n      // that is used to comment when using danger in a GitHub Action\n      // with GITHUB_TOKEN (https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token)\n      return 41898282\n    }\n\n    return undefined\n  }\n\n  postPRComment = async (comment: string): Promise<any> => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.post(\n      `repos/${repo}/issues/${prID}/comments`,\n      {},\n      {\n        body: comment,\n      }\n    )\n\n    return res.json()\n  }\n\n  postInlinePRComment = async (comment: string, commitId: string, path: string, position: number) => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.post(\n      `repos/${repo}/pulls/${prID}/comments`,\n      {},\n      {\n        body: comment,\n        commit_id: commitId,\n        path: path,\n        position: position,\n      },\n      false\n    )\n    if (res.ok) {\n      return res.json()\n    } else {\n      throw await res.json()\n    }\n  }\n\n  postInlinePRReview = async (commitId: string, comments: { comment: string; path: string; position: number }[]) => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.post(\n      `repos/${repo}/pulls/${prID}/reviews`,\n      {},\n      {\n        body: "",\n        event: "COMMENT",\n        commit_id: commitId,\n        comments: comments.map(({ comment, path, position }) => ({\n          body: comment,\n          path,\n          position,\n        })),\n      },\n      false\n    )\n    if (res.ok) {\n      return res.json()\n    } else {\n      throw await res.json()\n    }\n  }\n\n  updateInlinePRComment = async (comment: string, commentId: string) => {\n    const repo = this.repoMetadata.repoSlug\n    const res = await this.patch(\n      `repos/${repo}/pulls/comments/${commentId}`,\n      {},\n      {\n        body: comment,\n      },\n      false\n    )\n    if (res.ok) {\n      return res.json()\n    } else {\n      throw await res.json()\n    }\n  }\n\n  getPullRequestInfo = async (): Promise<GitHubPRDSL> => {\n    if (this.pr) {\n      return this.pr\n    }\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.get(`repos/${repo}/pulls/${prID}`)\n    const prDSL = (await res.json()) as GitHubPRDSL\n    this.pr = prDSL\n\n    if (res.ok) {\n      return prDSL\n    } else {\n      throw `Could not get PR Metadata for repos/${repo}/pulls/${prID}`\n    }\n  }\n\n  getPullRequestCommits = async (): Promise<any[]> => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    return await this.getAllOfResource(`repos/${repo}/pulls/${prID}/commits`)\n  }\n\n  /**\n   * Get list of commits in pull requests. This\'ll try to iterate all available pages\n   * Until it reaches hard limit of api itself (250 commits).\n   * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request\n   *\n   */\n  getAllOfResource = async (path: string): Promise<any> => {\n    const ret: Array<any> = []\n\n    /**\n     * Read response header and locate next page for pagination via link header.\n     * If not found, will return -1.\n     *\n     * @param response Github API response sent via node-fetch\n     */\n    const getNextPageFromLinkHeader = (response: node_fetch.Response): number => {\n      const linkHeader = response.headers.get("link")\n      if (!linkHeader) {\n        this.d(`getNextPageFromLinkHeader:: Given response does not contain link header for pagination`)\n        return -1\n      }\n\n      const parsedHeader = parse(linkHeader)\n      this.d(`getNextPageFromLinkHeader:: Link header found`, parsedHeader)\n      if (!!parsedHeader.next && !!parsedHeader.next.page) {\n        return parsedHeader.next.page\n      }\n      return -1\n    }\n\n    //iterates commit request pages until next page\'s not available, or response failed for some reason.\n    let page = 0\n    while (page >= 0) {\n      const requestUrl = `${path}${page > 0 ? `?page=${page}` : ""}`\n      this.d(`getPullRequestCommits:: Sending pull request commit request for ${page === 0 ? "first" : `${page}`} page`)\n      this.d(`getPullRequestCommits:: Request url generated "${requestUrl}"`)\n\n      const response = await this.get(requestUrl)\n      if (response.ok) {\n        ret.push(...(await response.json()))\n        page = getNextPageFromLinkHeader(response)\n      } else {\n        this.d(\n          `getPullRequestCommits:: Failed to get response while traverse page ${page} with ${response.status}, bailing rest of pages if exists`\n        )\n        page = -1\n      }\n    }\n\n    return ret\n  }\n\n  getUserInfo = async (): Promise<GitHubUser> => {\n    const response = await this.get("user")\n    return response.json()\n  }\n\n  getPullRequestComments = async (): Promise<GitHubIssueComment[]> => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    return await this.getAllOfResource(`repos/${repo}/issues/${prID}/comments`)\n  }\n\n  getPullRequestInlineComments = async (\n    dangerID: string\n  ): Promise<(GitHubIssueComment & { ownedByDanger: boolean })[]> => {\n    const userID = await this.getUserID()\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const dangerIDMessage = dangerIDToString(dangerID)\n    return await this.getAllOfResource(`repos/${repo}/pulls/${prID}/comments`).then((v: GitHubIssueComment[]) => {\n      return v\n        .filter(Boolean)\n        .map((i) => {\n          return { ...i, ownedByDanger: i.user.id == userID && i.body.includes(dangerIDMessage) }\n        })\n        .filter((i) => i.ownedByDanger)\n    })\n  }\n\n  getPullRequestDiff = async () => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.get(`repos/${repo}/pulls/${prID}`, {\n      Accept: "application/vnd.github.v3.diff",\n    })\n    return res.ok ? res.text() : ""\n  }\n\n  getFileContents = async (path: string, repoSlug: string, ref: string): Promise<any> => {\n    const res = await this.get(`repos/${repoSlug}/contents/${path}?ref=${ref}`)\n    return res.ok ? res.json() : { content: "" }\n  }\n\n  getPullRequests = async (): Promise<any> => {\n    const repo = this.repoMetadata.repoSlug\n    const res = await this.get(`repos/${repo}/pulls`)\n\n    return res.ok ? res.json() : []\n  }\n\n  getReviewerRequests = async (): Promise<any> => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.get(`repos/${repo}/pulls/${prID}/requested_reviewers`, {\n      Accept: "application/vnd.github.v3+json",\n    })\n\n    return res.ok ? res.json() : []\n  }\n\n  getReviews = async (): Promise<any> => {\n    const repo = this.repoMetadata.repoSlug\n    const prID = this.repoMetadata.pullRequestID\n    const res = await this.get(`repos/${repo}/pulls/${prID}/reviews`, {\n      Accept: "application/vnd.github.v3+json",\n    })\n\n    return res.ok ? res.json() : []\n  }\n\n  getIssue = async (): Promise<any> => {\n    const repo = this.repoMetadata.repoSlug\n      // e.g. https://gist.github.com/LTe/5270348\n      customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }\n    }\n    return this.fetch(\n      url,\n      {\n        method: method,\n        body: body,\n        headers: {\n          "Content-Type": "application/json",\n          ...headers,\n          ...this.additionalHeaders,\n          ...customAccept,\n        },\n      },\n      suppressErrors\n    )\n  }\n\n    }\n\n    // This is the bold bit of the status message, it kinda feels like\n    // the name of the service.\n    let context = dangerID || "Danger"\n\n    // Suppress errors, because in OSS\n    // this failure could be due to access rights.\n    //\n    // So only error when it\'s a real message.\n    const statusURL = `repos/${repo}/statuses/${ref}`\n    try {\n      const res = await this.post(\n        statusURL,\n        {},\n        {\n          state: state,\n          context: context,\n          target_url: url || "http://danger.systems/js",\n          description: message,\n        },\n        true\n      )\n      if (!res.ok) {\n        this.d(`Got a non-OK (${res.status} ${res.statusText}) response from ${statusURL}:`)\n        this.d(JSON.stringify(res, null, "  "))\n      }\n      return res.ok\n    } catch (error) {\n      this.d(`Posting a status to: ${statusURL} failed, this is the response:`)\n      this.d((error && (error as Error).message) || error)\n    }\n  }\n\n  postCheckRun = async (check: CheckOptions, token: string) => {\n    const repo = this.repoMetadata.repoSlug\n    const res = await this.post(\n      `repos/${repo}/check-runs`,\n      {\n        Accept: "application/vnd.github.antiope-preview+json,application/vnd.github.machine-man-preview+json",\n        Authorization: `token ${token}`,\n      },\n      check\n    )\n    if (res.ok) {\n      return res.json()\n    } else {\n      throw await res.json()\n    }\n  }\n\n  // API Helpers\n\n  private api = (path: string, headers: any = {}, body: any = {}, method: string, suppressErrors?: boolean) => {\n    if (this.token && !headers["Authorization"]) {\n      headers["Authorization"] = `token ${this.token}`\n    }\n\n    const containsBase = path.startsWith("http")\n    const baseUrl = process.env["DANGER_GITHUB_API_BASE_URL"] || process.env["GITHUB_URL"] || "https://api.github.com"\n    const url = containsBase ? path : `${baseUrl}/${path}`\n\n    let customAccept = {}\n    if (headers.Accept && this.additionalHeaders.Accept) {\n      // We need to merge the accepts which are comma separated according to the HTML spec\n      // e.g. https://gist.github.com/LTe/5270348\n\n      // But make sure it doesn\'t already include it\n      if (headers.Accept.includes(this.additionalHeaders.Accept)) {\n        // If it\'s already a subset, ignore\n        customAccept = { Accept: headers.Accept }\n      } else {\n        customAccept = { Accept: `${this.additionalHeaders.Accept}, ${headers.Accept}` }\n      }\n    }\n    const finalHeaders = {\n      "Content-Type": "application/json",\n      ...headers,\n      ...this.additionalHeaders,\n      ...customAccept,\n    }\n\n    this.d("Sending: ", url, finalHeaders)\n    return limit(() =>\n      this.fetch(\n        url,\n        {\n          method,\n          body,\n          headers: finalHeaders,\n        },\n        suppressErrors\n      )\n    )\n  }\n\n  get = (path: string, headers: any = {}): Promise<node_fetch.Response> => this.api(path, headers, null, "GET")\n\n  post = (path: string, headers: any = {}, body: any = {}, suppressErrors?: boolean): Promise<node_fetch.Response> =>\n    this.api(path, headers, JSON.stringify(body), "POST", suppressErrors)\n\n  patch = (path: string, headers: any = {}, body: any = {}, suppressErrors?: boolean): Promise<node_fetch.Response> =>\n    this.api(path, headers, JSON.stringify(body), "PATCH", suppressErrors)\n}\n', 'message': 'Limit concurrent API calls\n', 'author_name': 'Maximilian Stoiber', 'lang': 'ts'}