💬

自身のGitHub traffic情報(ビジター・クローン)をDiscordに通知する

に公開

1. 環境

OS: Ubuntu24
プログラム言語: Python 3.13.2
ライブラリ: requests
その他: Discord Webhook URL, GitHubアクセストークン

2. はじめに

GitHubでは、リポジトリごとにトラフィック情報を確認できますが、ブラウザ上では個別リポジトリ単位でしか閲覧できないため、複数のリポジトリを管理している場合には確認作業が非常に煩雑です。
そこで調査したところ、GitHubのWeb APIを利用すれば、リポジトリ単位でトラフィック情報を取得できることが分かりました。これを応用することで、アカウント全体のリポジトリに対して自動的に情報を収集し、まとめて表示することが可能になります。
そのため、このAPIを活用して、すべてのリポジトリのトラフィック情報を一括取得・表示できるプログラムを作成してみました。

3. 前準備

今回、トラフィック情報の通知には Discord を採用しています。
通知を行うには、以下の2つが必要です。
・Discord Webhook URL
・GitHubアクセストークン

3-1. Discord Webhook URLの発行方法

Webhook URLの取得方法については、以下の公式ガイドなどを参照してください。
https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks

3-2. GitHubアクセストークンの発行

GitHub APIを通じてクローン数やビジター数を取得するには、アクセストークンが必要です。
トークンの発行手順は以下に示しますが、現在これらの情報を取得するには強い権限が必要になるため、取り扱いには十分注意してください。
「できるだけ最小限の権限にしたい」と考えていますが、現時点では最適な権限設定が見つかっていません。
もし、より安全で限定的な権限設定方法をご存じの方がいれば、ぜひ教えていただきたいです。

※補足:Metadataパーミッションだけでは取得不可

GitHubの公式リファレンスでは、クローン数やビジター数に関するエンドポイントが「Metadata」セクションに含まれています。
そのため、最初は「Metadata」パーミッションをRead-onlyに設定すればアクセスできると考えました。
しかし実際には、これだけでは権限が不足しており、データを取得することはできませんでした。
GitHub Copilotにも確認しましたが、有効な代替案は得られませんでした。

3-2-1. GitHubのトークン設定

トークンには有効期間を設定する必要がありますが、期間に関してはご自由に設定してください。

  1. GitHubにログイン。

  2. 右上のアイコンから「Settings(設定)」を選択。

  3. 左側メニューの「Developer Settings」を開く。

  4. 「Personal access tokens」の中から「Fine-grained tokens」を選択。

  5. 右上の「Generate new token」をクリック。

  6. 「Repository access」の設定を「Only select repositories」を選択する。

  7. 「Select repositories」からトラフィック情報を取得したいリポジトリを選択してください。

  8. 「Permissions」の「Administration」を「Read-only」に変更します。

  9. アクセストークンを作成し、トークンを任意の場所に保存してください。(一回しか見れないため)

4. プログラムの実装

上記でDiscordにトラフィック情報を通知する準備は整いました。
この章では、python環境のセットアップとプログラムを記載します。

4-1. ライブラリのインストール

requestsライブラリをインストールします。

pip install requests

4-2. トラフィック情報通知プログラム

下記に、GitHubリポジトリのトラフィック情報(ビュー数・クローン数)を取得し、Discordに通知するPythonプログラムを記載します。

import requests
import os
import json

# GitHub Personal Access Token
GITHUB_TOKEN = "YOUR_TOKEN"
# Discord Webhook URL
DISCORD_WEBHOOK = "YOUR_DISCORD_WEBHOOK"
# GitHubのユーザ名
OWNER = "YOUR_GITHUB_USERNAME"
# トラフィック情報を取得したいリポジトリ名のリスト
repos_list_str = []
discord_user = "GitHub Traffic Bot"

def create_url(repository):
    VIEWS_URL = f'https://api.github.com/repos/{OWNER}/{repository}/traffic/views'
    CLONES_URL = f'https://api.github.com/repos/{OWNER}/{repository}/traffic/clones'

    HEADERS = {
        'Authorization': f'token {GITHUB_TOKEN}',
        'Accept': 'application/vnd.github.v3+json',
    }

    return VIEWS_URL, CLONES_URL, HEADERS


def get_traffic_data(_headers, _url):
    try:
        response = requests.get(_url, headers=_headers)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as http_err:
        
        print(f"HTTPエラーが発生しました: {http_err}")
        print(f"レスポンス内容: {response.text}")
        err_message = response.json().get('message', '不明なエラー')
        send_discord_message(discord_user, err_message)
    except requests.exceptions.RequestException as req_err:
        print(f"リクエストエラーが発生しました: {req_err}")
        err_message = response.json().get('message', '不明なエラー')
        send_discord_message(discord_user, err_message)
    except json.JSONDecodeError:
        print("エラー: レスポンスのJSONデコードに失敗しました。")
        print(f"レスポンス内容: {response.text}")
        err_message = response.json().get('message', '不明なエラー')
        send_discord_message(discord_user, err_message)
    return None

def send_discord_message(user, message):
    payload = {
        "username": user,
        "content": message
    }
    try:
        response = requests.post(DISCORD_WEBHOOK, json=payload)
        response.raise_for_status() # HTTPエラーがあれば例外を発生させる
        print("Discordに通知を送信しました。")
    except requests.exceptions.HTTPError as http_err:
            print(f"Discordへの通知送信中にHTTPエラーが発生しました: {http_err}")
            print(f"レスポンス内容: {response.text}")
    except requests.exceptions.RequestException as req_err:
            print(f"Discordへの通知送信中にリクエストエラーが発生しました: {req_err}")
    except Exception as e:
            print(f"Discordへの通知送信中にエラーが発生しました: {e}")
    return None
    
if __name__ == '__main__':
    views_sum = 0
    clones_sum = 0

    # リポジトリ名の最大長を取得
    max_repo_name_length = 0
    if REPOS:
        max_repo_name_length = max(len(repo_name) for repo_name in REPOS)

    static_message = "\n**各リポジトリのトラフィックデータ**\n"
    # ビュー数を取得
    for repo in REPOS:
        VIEWS_URL, CLONES_URL, HEADERS = create_url(repo)
        # ビュー数を取得
        views_data = get_traffic_data(HEADERS, VIEWS_URL)
        views_count = views_data.get('count', 0)
        if views_data:
            views_sum += views_count
        
        clones_data = get_traffic_data(HEADERS, CLONES_URL)
        clones_count = clones_data.get('count', 0)
        if clones_data:
            clones_sum += clones_count            

        # リポジトリ名をフォーマットして長さを揃える
        formatted_repo_name = f"{repo:{max_repo_name_length}}"
        static_message += f"{formatted_repo_name}: ビジター:{views_count:<4} クローン:{clones_count:<4}\n"

    # Discord Webhookに通知
    if DISCORD_WEBHOOK:
        messeage_length = 20
        message = (
            f"**GitHubリポジトリトラフィックレポート(過去14日間)**\n"
            f"```\n"
            f"{"全リポジトリの合計ビュー数:":{messeage_length+1}} {views_sum:<4}\n" #2バイト文字で配置が合わないためmesseage_length+1する
            f"{"全リポジトリの合計クローン数:":{messeage_length}} {clones_sum:<4}\n"
            f"{static_message}"
            f"```"
        )
    send_discord_message(discord_user, message)

上記のプログラムを実行すると下記のようにDiscordに通知されます。

GitHubリポジトリトラフィックレポート(過去14日間)
全リポジトリの合計ビュー数:        0   
全リポジトリの合計クローン数:      3   


**各リポジトリのトラフィックデータ**
xxxxxxxxx        : ビジター:0    クローン:1   
xxxxxxxxxxxxxxx  : ビジター:0    クローン:2   

Discussion