TTL's Blog

日々の備忘録てきなblogです。徒然なるままに...

【AssettoCorsa】 AssettoServer ログイン状況をDiscordへ通知する

はじめに

AsettoServer にてオンラインサーバーを運用している時、 誰がコネクトしているのか知りたい。誰かが走っているなら、追走したい。 という欲求から AssettoServerへコネクト(Connect) / ディスコネクト(Disconnect)している情報を Discordへ通知出来れば便利だよねって事で実現を目指す。

オンライン参加者が Chat を行うと、内容を Discord へ通知する仕立ては 導入済みだけど、一人で参加している時にわざわざchatは打たない。

自動で Connect / Disconnect が分かると皆うれしい(はず

※個人練習している時はバレたくないが....

Discord Webhook

AssettoServerのプラグインにて"DiscordAuditPlugin"がある。 オンラインにてChatした内容がDiscordへ送られるプラグイン。 これがあるなら...Connect/DisconetもDiscordへ送ってくれればよいのに...

Discord側にて Webhook を事前に準備する必要あり。

Webhookの準備が出来れば、あとは送るだけ。 AssettoServerに限らず、Webhookのurlが分かっていれば どこからでもDiscordに対してメッセージ送信が可能となる。

AssettoServerが出力するログをトリガーにてwebhookする方法も考えたが、 AssettoServerが吐くログファイルはタイムスタンプが付加されているので 最新のファイルを検索処理も作る必要がある。

だが、そこまでして通知を実装する気なし!!

どうする?

git hub を漁ったところ... iiferedon さんが作ってた!!

github.com

一先ず、ありがたくダウンロードさせて頂き導入してみた。

Assetto Corsa Server Listener - Discord Webhook -

設定は簡単に終了。以下の2点だけで完了 * Discord webhook URL 設定 * AssettoServerのURL、port番号設定

Pythonで書かれているので、Pythonで実行するだけ...

AssettoServer Onlineへconnectしてみると...

おぉぉ。connect情報がDiscordへ送信されている!!!

ぢゃぁ、Disconnect!!

ん?? Errorが出て止まってる??

ちょいと調査が必要

仕立て

そもそもAssettoServerを動作させると、Content Managerなどオンライン情報表示を行うため、 jsonにて現在のサーバー接続状況を取得可能となっている。 ※ウェブブラウザを使用すると簡単に見る事が可能。

  • " Assetto Corsa Server Listener - Discord Webhook -"では最初の起動時にサーバーからjsonデータを取得
  • 取得したjsonデータをローカルのファイルへ書き出す。
  • 一定間隔にてサーバーからjsonデータを取得
  • ローカル側のファイルと比較し、差異があった場合に Connect / Disconnect を判断
  • 結果をDiscordへwebhook

という流れぽい。

"Disconnect"の情報までは検知しているが、Webhookするためのデータ作成処理においてエラーが発生。

エラーの状況から"Null"参照しているっぽいけど... どのタイミングで発生しているのかわかりづらい... "Python"ってデバッグしづらい... (Pythonに限ったことではないが...)

※"pdb"というpythonデバッガが存在している事は後で知る事になる。

※"pdb"は便利!! これが無いと python デバッグ無理!!

デバッグすれば済む事なのだが、以下の点が気になる。 * jsonデータをローカルファイルへ保存 * AssettoServerに誰かがログインしている状態にて python を動かすとエラー出る。

だったら勉強がてら自分で組むことにする

ChatGPT

Python , jsonともに素人の私がググって調べると時間がかかるので、 "ChatGPT"へお伺いする。

※ChatGPTの回答内容は自粛

Pythonを使用しWebページからjsonデータを取得しjsonデータの内容に変化があるか確認を行うためのサンプルコードを教えてほしい
ChatGPT : 再帰的なアプローチを使用してJSONデータを探索し、差分を検出する方法を採用しています。

で、サンプルコードが吐かれる... ふむふむ。 さすがPython!! コード量が少なくて見やすいwww

追加質問

get_json_dataにて取得したjsonデータにてどの部分が変更になったか確認したい

あぁ、なるほどねぇwww

雰囲気はわかったのであとは実装!! (ほんとうはわかってない

ぷろぐらみんぐ 方向性

  • Python起動時、AsettoServerから接続情報(json)取得
  • 一定時間にて最新jsonを取得
  • 前回 / 最新 json にて差異があるか確認
  • Connect / Disconnect 情報のみが必要なので、json内の"DriverName"に変化があるか確認
  • 変化があった場合、以下の条件を確認しDiscordへwebhookする
    • None -> user名 : Connect
    • user名 -> None : Disconet

※ iiferedonさんのPythonではユーザー名、使用車両、スキン名などもWebhookされるが Connect情報が重要なので、他の項目は捨てる。 webhookしている部分は参考にする。(ありがたい)

ついでにwebhook関係をググってみる www

※最重要な事は 動けば良い をコンセプトとする。

AssettoServerからjson取得

get_jsonする事で簡単にjsonデータ取得が行える。便利だねぇ。

current_data= get_json_data(url)

新旧データ

新旧jsonデータを保持するために2つバッファを用意 * current_data * previous_data

起動時に"previous_data"へ一発jsonを読ませる。 最新データを"current_data"へ読み込み 二つのデータに差異があるか確認すれば全てOK

比較

ChatGPTさんに教えてもらったサンプルコードをcopy して2つのjsonデータ比較を行う。

def get_json_data(url):
    try:
        response = requests.get(url)
        if response.status_code == 200:
            return response.json()
        else:
            print(f"Failed to fetch data from {url}. Status code: {response.status_code}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"An error occurred while fetching data: {e}")
        return None

def compare_json_data(data1, data2, path=""):
    if isinstance(data1, dict) and isinstance(data2, dict):
        for key in data2.keys():
            if key not in data1:
                print(f"Key '{key}' added at path: {path}/{key}")
            else:
                compare_json_data(data1[key], data2[key], f"{path}/{key}")

        for key in data1.keys():
            if key not in data2:
                print(f"Key '{key}' removed from path: {path}/{key}")

    elif isinstance(data1, list) and isinstance(data2, list):
        for i in range(min(len(data1), len(data2))):
            compare_json_data(data1[i], data2[i], f"{path}/{i}")

        if len(data1) > len(data2):
            for i in range(len(data2), len(data1)):
                print(f"Item '{data1[i]}' removed from path: {path}/{i}")

        elif len(data1) < len(data2):
            for i in range(len(data1), len(data2)):
                print(f"Item '{data2[i]}' added at path: {path}/{i}")

(続くけど...)

上記の場合 "data1"と"data2"を使用しているので

if data1 != data2:

とするだけで比較完了!! なんて簡単な!!

Webhook

後は条件をダラダラを書いて、Webhookを書けば終了。

ログインしているユーザーは"DriverName"というキー名にて管理されている。 結果が保持されている"path"中に "DriverName"が含まれているか確認

  • "path"中に"DriverName"が含まれているか確認
  • 新旧 "DriverName"の状態確認
    • None -> user名 : Connect
    • user名 --> None : Disconnect
  • Webhook データ生成
    • Connectしたよん / Disconnect したよん
  • Webhook 実行
  • Discordに表示される

Code

出来上がったコードは以下の通り。

import requests
import json
from urllib.request import urlopen, Request
import time
import os
import sys
import colorama
from colorama import Fore
from discord_webhook import DiscordWebhook, DiscordEmbed


#Change these values
webhook = DiscordWebhook(url="DiscordWebHookURL")
server_address = "localhost:8081"

def get_json_data(url):
    try:
        response = requests.get(url)
        if response.status_code == 200:
            return response.json()
        else:
            print(f"Failed to fetch data from {url}. Status code: {response.status_code}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"An error occurred while fetching data: {e}")
        return None


def send_webhook(title, player, colour):
    embed = DiscordEmbed(title=title, color=colour)
    embed.set_timestamp()
    embed.add_embed_field(name='User Name :', value=player)
    webhook.add_embed(embed)
    response = webhook.execute()
    return None


def compare_json_data(data1, data2, path=""):
    if isinstance(data1, dict) and isinstance(data2, dict):
        for key in data2.keys():
            if key not in data1:
                print(f"Key '{key}' added at path: {path}/{key}")
            else:
                compare_json_data(data1[key], data2[key], f"{path}/{key}")

        for key in data1.keys():
            if key not in data2:
                print(f"Key '{key}' removed from path: {path}/{key}")

    elif isinstance(data1, list) and isinstance(data2, list):
        for i in range(min(len(data1), len(data2))):
            compare_json_data(data1[i], data2[i], f"{path}/{i}")

        if len(data1) > len(data2):
            for i in range(len(data2), len(data1)):
                print(f"Item '{data1[i]}' removed from path: {path}/{i}")

        elif len(data1) < len(data2):
            for i in range(len(data1), len(data2)):
                print(f"Item '{data2[i]}' added at path: {path}/{i}")

    else:
        if data1 != data2:
            if path.find('DriverName') > 0:
                #Send Discord Messages
                if data1 == None:
                    title = "SRP Connected"
                    color=5763719
                    usr = data2
                else:
                    title = "SRP DisConnected"
                    color=15548997
                    usr = data1

                print(f"Path: {title}")
                print(f"{usr}")
                send_webhook(title,usr,color)


if __name__ == "__main__":
    # Specify the URL of the web page you want to check for changes
    url = "http://"+server_address+"/JSON%7C"
    
    # first fetch
    previous_data = get_json_data(url)
    if not previous_data:
        exit()

    while True:
        # Get data every 10 seconds and check the change
        time.sleep(10)
        current_data = get_json_data(url)
        if current_data:
            compare_json_data(previous_data, current_data)
            previous_data = current_data
        else:
            print("Failed to Get Data")

準備

  • 自分のDiscord Webhook アドレスへ書き換える
  • AssettoServerのIPアドレスおよびポート番号へ書き換える

実行

webからjson情報を取得し実行するため、AssettoServer動作中のPC以外からも実行可能である。 まずはAssettoServerが動作している端末にてpythonを実行し通知を行う事とする。

  • 新規コンソールを開く
    • "tmux"などを使用し別スレッドでもOK
  • AssettoServerが実行中である事を確認
    • サーバーが停止状態の場合、jsonデータ取得が行えないのでエラーとなる。
  • python実行
    • python3 xxxx.py <リターン>
  • 何事もなければ、画面はウンスン状態 (変化なし)
    • Error発生時はエラー内容を表示し停止するので、エラー内容に応じて対処する。

Connect / Disconnectを行い、以下のような通知が来ればOK.