Closed7

GithubのContribution数を定時に報告してくれるDiscord botを作りたい

tf63tf63

技術選び

Google App Script (GAS)

  • トリガーを設定して毎日決められた時間にGASを実行できる
  • 無料でデプロイできそう

Github REST API or GraphQL API

  • RESTもあったがとってこれるデータが少なそう
  • GraphQLでもやってみる
tf63tf63

Discord側の設定

Discord側でテキストチャンネルの編集 > 連携サービス > 新しいウェブフックで作成する

謎のWebhookが出来るので適当に編集

ウェブフックURLをコピーからWebhookのURLを取得する.このURLにリクエストボディを{content: "message"}としてPOSTすれば良さそう

usernameとかavatar_urlを設定すればbotの名前とアバターを上書きできるらしい
https://discord.com/developers/docs/resources/webhook#execute-webhook

tf63tf63

GASのコーディング

GoogleドライブでGoogle App Scriptを作成して開く

Github REST APIを叩いてみる


const USERNAME = "<GithubのアカウントID>"
const DISCORD_URL = "<WebhookのURL>"
const GITHUB_URL = "https://api.github.com/users/" + USERNAME + "/events"

// Github APIからContribution数を取得する
function getNumOfContributions (date) {

  // リクエストのオプション
  let options = {
    "method": "GET",
    "muteHttpExceptions": true
  }

  // Github APIからデータを取得する
  let response = UrlFetchApp.fetch(GITHUB_URL, options)

  // Contribution数を取得する
  if (response.getResponseCode() === 200) {
    // 正しくレスポンスが返ってきた場合

    // レスポンスをパース
    let events = JSON.parse(response.getContentText())

    // Contribution数を計算する
    // Contributionとしてカウントするイベントの種類 https://docs.github.com/ja/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28
    let contributions = events.filter(function(event) {
      return (event.type === "PushEvent") && event.created_at.startsWith(date)
    }).length

    return contributions
  } else {
    // レスポンスが返ってこなかった場合,エラーを投げる
    throw new Error("Github APIにアクセスできませんでした.")
  }
}

// DiscordのWebhookにメッセージを登録
function postMessage (message) {
  
  // 登録するメッセージ
  let payload = {
    "content": message
  }

  // リクエストのオプション
  let options = {
    "method": "post",
    "payload": payload
  }

  // Webhookにリクエストを投げる
  UrlFetchApp.fetch(DISCORD_URL, options)
}

// エントリポイント
function main () {

  try {
    // 今日のContribution数を取得してメッセージを送信する
    let today = new Date().toISOString().slice(0, 10) // 2000-01-01のような形式の日付
    let contributions = getNumOfContributions(today) 

    postMessage(`${today}のContribution数】 ${contributions}`)
  } catch (e) {
    // エラーが生じた場合,その内容を送信する
    console.error(e)
    postMessage(`【エラーが発生しました】 ${e}`)
  }
}

main関数を実行

ちゃんと送信できてるっぽい

tf63tf63

上手くいってないっぽい

計算したContribution数と実際のContribution数が一致していない

Contribution数はレスポンスからevent.typePushEventのものを数えていたが,ドキュメントをよく読んでみたらPushEventはブランチに1つ以上のコミットがプッシュされたときに発生するイベントらしい.

これでは3つのコミットを1度にプッシュした場合でも1 Contributionとして扱われてしまう.あと,そもそもPRとかIssueで発生するContributionも数えられていない

tf63tf63

Github GraphQL APIを使ってみる

Github CLIからGraphQL APIを試せるらしいのでやってみる.取得するデータはこれ

    gh api graphql -f query='
                        query contributions {
                          user(login: "tf63") {
                            contributionsCollection(to: "2024-04-10T00:00:00", from: "2024-04-09T00:00:00") {
                              contributionCalendar {
                                weeks {
                                  contributionDays {
                                    date
                                    contributionCount
                                  }
                                }
                              }
                            }
                          }
                        }
                        '

レスポンス

(略)
              "contributionDays": [
                {
                  "date": "2024-04-09",
                  "contributionCount": 2
                },
                {
                  "date": "2024-04-10",
                  "contributionCount": 1
                }
              ]
(略)

ちゃんとContribution数を取得できてそう

tf63tf63

GASからGithub GraphQL APIを叩く

Github GraphQL APIの場合はアクセストークンが必ず必要? みたいなので取得します

Githubの Settings > Developer Settings > Tokens (classic) から Generate new token > Generate new token (classic) を選択します

Expiration (トークンの有効期限) を適当に設定し,user > read:user の項目のみチェックを入れてトークンを生成します

ghp_<...>みたいな文字列がトークンです.絶対に公開しないでください

これを使ってGASのコーディングをします
(一部修正しました)

const USERNAME = "<GithubのアカウントID>"
const GITHUB_TOKEN = "<作成したトークン>"
const DISCORD_URL = "<WebhookのURL>"
const GITHUB_URL = "https://api.github.com/graphql" // 変更

// UTCだったのでJSTに合わせる.良い方法無いですかね
let todayUTC = new Date()
todayUTC.setHours(todayUTC.getHours() + 9)
const TODAY = todayUTC.toISOString() // 2000-01-01T00:00:00.000Zのような形式の日付

// GraphQLのクエリ
// 今日のContribution数を取得する
const query = `query contributions {
                          user(login: "${USERNAME}") {
                            contributionsCollection(to: "${TODAY}", from: "${TODAY}") {
                              contributionCalendar {
                                weeks {
                                  contributionDays {
                                    date
                                    contributionCount
                                  }
                                }
                              }
                            }
                          }
                        }
`

// Github APIからContribution数を取得する
function getNumOfContributions () {

  // リクエストのオプション (GraphQLって常にGETだったっけ?)
  let options = {
    "method": "GET",
    "headers": {
      "Authorization": `Bearer ${GITHUB_TOKEN}`,
      "Content-Type": "application/json"
    },
    "payload": JSON.stringify({ query })
  }

  // Github APIからデータを取得する
  let response = UrlFetchApp.fetch(GITHUB_URL, options)

  // Contribution数を取得する
  if (response.getResponseCode() === 200) {
    // 正しくレスポンスが返ってきた場合

    // レスポンスをパース
    let datas = JSON.parse(response.getContentText())

    // 適当にcontribution数を取り出す
    let contribution = datas.data.user.contributionsCollection.contributionCalendar.weeks[0].contributionDays[0].contributionCount

    return contribution
  } else {
    // レスポンスが返ってこなかった場合,エラーを投げる
    throw new Error("Github APIにアクセスできませんでした.")
  }
}

// DiscordのWebhookにメッセージを登録
function postMessage (message) {
  
  // 登録するメッセージ
  let payload = {
    "content": message
  }

  // リクエストのオプション
  let options = {
    "method": "post",
    "payload": payload
  }

  // Webhookにリクエストを投げる
  UrlFetchApp.fetch(DISCORD_URL, options)
}

// エントリポイント
function main () {

  try {
    // 今日のContribution数を取得してメッセージを送信する
    let contribution = getNumOfContributions() 

    postMessage(`${TODAY.slice(0, 10)}のContribution数】 ${contribution}`)
  } catch (e) {
    // エラーが生じた場合,その内容を送信する
    console.error(e)
    postMessage(`【エラーが発生しました】 ${e}`)
  }
}

動作確認

tf63tf63

トリガーを設定

最後に作成したmain関数を定時実行するように設定します

GASのトリガータブから トリガーを追加 を選択します

こんな感じで出来るのではないでしょうか

今日の22時が楽しみですね

このスクラップは19日前にクローズされました