はじめに
2025年3月にLINE Notify APIのサービスが終了することが発表され、多くの方々が代替手段を探していることと思います。今回は、その代替としてDiscord通知のソースコードをご紹介します。
このコードの特徴は以下の通りです:
- WebRequestの許可リストへの登録が不要
- 画像だけでなく、CSVなど様々なファイル形式の添付が可能
- GET/POSTリクエストに対応した柔軟な実装
Discord Webhookの準備
- Discordサーバーを開き、通知を送信したいチャンネルの設定を開きます
- 「連携サービス」→「ウェブフック」を選択
- 「新しいウェブフック」を作成
- 生成されたWebhook URLをコピーしておきます
コードの実装
まず、以下のコードをMT4/MT5のエディタにコピーしてください。
#property copyright "FX WAN."
#property link "https://twitter.com/wonderfulfxlife"
#property version "1.00"
#property strict
// WebhookのURLを設定する入力パラメータ
input string WebhookURL = "https://discord.com/api/webhooks/abcdefg";
//+------------------------------------------------------------------+
//| スクリプトが開始された時に呼ばれる関数 |
//+------------------------------------------------------------------+
void OnStart()
{
// テストメッセージとファイルをDiscordに送信
SendDiscordNotification("🥺", WebhookURL, "test.png");
}
//+------------------------------------------------------------------+
//| Discordに通知を送る関数
//| @param message 送信するメッセージ
//| @param webhook DiscordのWebhook URL
//| @param fileName 添付するファイル名(省略可能引数。添付はFilesフォルダ内のファイルのみ)
//+------------------------------------------------------------------+
void SendDiscordNotification(string message, string webhook, string fileName = "")
{
// DiscordのWebhook URL
string url = webhook;
// マルチパートフォームデータの境界文字列
string boundary = "---------------------------" + IntegerToString(TimeCurrent());
// リクエストのヘッダー
string headers = "Content-Type: multipart/form-data; boundary=" + boundary + "\r\n";
// POSTデータの準備
string body;
body += "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name=\"content\"\r\n\r\n";
body += message + "\r\n";
int size = 0;
uchar post_data[];
if(fileName != "")
{
// 画像を送るための情報を追加
body += "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name=\"imageFile\"; filename=\"" + fileName + "\"\r\n";
body += "Content-Type: image/png\r\n\r\n";
size = StringToCharArray(body, post_data, 0, WHOLE_ARRAY, CP_UTF8);
// 画像ファイルを開く
uchar img[];
int handle = FileOpen(fileName, FILE_BIN | FILE_READ);
if(handle != INVALID_HANDLE)
{
// 画像データを読み込む
FileReadArray(handle, img);
FileClose(handle);
}
// 画像データを追加
size += ArrayCopy(post_data, img, size - 1);
// データの終わりを示す
StringToCharArray("\r\n" + "--" + boundary + "--\r\n", post_data, size - 1);
}
else
{
// 文字のみを送るための情報を追加
size = StringToCharArray(body, post_data, 0, WHOLE_ARRAY, CP_UTF8);
// データの終わりを示す
StringToCharArray("--" + boundary + "--\r\n", post_data, size - 1);
}
// POSTデータの最終形成
body += "--" + boundary + "--\r\n";
// DiscordにPOSTリクエストを送信
int timeout = 5000;
char result[];
string headers_res;
int res = WebRequest2("POST", url, headers, timeout, post_data, result, headers_res);
// エラーの確認と結果の出力
if(res == -1)
{
Print("HTTPリクエストエラー: ", GetLastError());
}
else
{
string res_str = CharArrayToString(result);
Print("Discord通知が送信されました。レスポンス: ", res_str);
}
}
//+------------------------------------------------------------------+
//| GETまたはPOSTリクエストを実行し、データを取得 ワンダフルFXライフ
//+------------------------------------------------------------------+
#define INTERNET_DEFAULT_HTTPS_PORT 443
#define INTERNET_DEFAULT_HTTP_PORT 80
#define INTERNET_SERVICE_HTTP 3
#define HTTP_QUERY_STATUS_CODE 19
#define HTTP_QUERY_RAW_HEADERS_CRLF 21
#define INTERNET_FLAG_SECURE 0x00800000
#define INTERNET_FLAG_PRAGMA_NOCACHE 0x00000100
#define INTERNET_FLAG_KEEP_CONNECTION 0x00400000
#define INTERNET_FLAG_RELOAD 0x80000000
#define INTERNET_OPTION_CONNECT_TIMEOUT 2
#define INTERNET_OPTION_SEND_TIMEOUT 5
#define INTERNET_OPTION_RECEIVE_TIMEOUT 6
#import "wininet.dll"
int InternetOpenW(string userAgent, int accessType, string proxyServer, string bypassProxyFor, int flags);
int InternetOpenUrlW(int internetSession, string url, string header, int headerLength, int flags, int context);
int InternetConnectW(int sessionHandle, string serverUrl, int serverPort, string userCredentials, string userPassword, int serviceType, int connectionFlags, int context);
int HttpOpenRequestW(int connectionHandle, string httpVerb, string objectUrl, string httpVersion, string referrer, string acceptTypes, uint requestFlags, int context);
bool HttpSendRequestW(int hRequest, string lpszHeaders, int dwHeadersLength, uchar &lpOptional[], int dwOptionalLength);
bool InternetReadFile(int hFile, uchar &lpBuffer[], int dwNumberOfBytesToRead, int &lpdwNumberOfBytesRead);
bool InternetCloseHandle(int hInternet);
bool HttpQueryInfoA(int hRequest, int dwInfoLevel, uchar &lpBuffer[], int &lpdwBufferLength, int &lpdwIndex);
bool InternetSetOptionW(int hInternet, int dwOption, string lpBuffer, int dwBufferLength);
#import
int WebRequest2(const string method, const string url, const string headers, int timeout, char & data[], char & result[], string & result_headers)
{
int secure = 0;
int port = INTERNET_DEFAULT_HTTP_PORT;
if(StringFind(url, "https") >= 0)
{
secure = INTERNET_FLAG_SECURE;
port = INTERNET_DEFAULT_HTTPS_PORT;
}
//URLからホストとエンドポイントの抽出
string host = "", endpoint = "";
int response = -1;
int http_pos = StringFind(url, "//");
if(http_pos >= 0)
{
int next_slash = StringFind(url, "/", http_pos + 2);
if(next_slash < 0)
next_slash = StringLen(url);
host = StringSubstr(url, http_pos + 2, next_slash - http_pos - 2);
endpoint = StringSubstr(url, next_slash);
}
// インターネットセッションを開始
int session = InternetOpenW("Mozilla/5.0", 0, "", "", 0);
if(!session)
{
string error = "エラー:インターネットセッションを開けません";
StringToCharArray(error, result);
return(response);
}
// タイムアウトの設定
string timeoutStr = IntegerToString(timeout);
bool timeout_res = InternetSetOptionW(session, INTERNET_OPTION_CONNECT_TIMEOUT, timeoutStr, sizeof(timeout));
timeout_res = InternetSetOptionW(session, INTERNET_OPTION_SEND_TIMEOUT, timeoutStr, sizeof(timeout));
timeout_res = InternetSetOptionW(session, INTERNET_OPTION_RECEIVE_TIMEOUT, timeoutStr, sizeof(timeout));
if(method == "GET")
{
// URLを開く
int connect = InternetOpenUrlW(session, url, headers, StringLen(headers), secure, 0);
if(!connect)
{
InternetCloseHandle(session);
string error = "エラー:URLを開けません";
StringToCharArray(error, result);
return(response);
}
//レスポンス確認
{
int bufferLength = 1024;
uchar result_array[];
string result_str = "";
ArrayResize(result_array, bufferLength);
int index = 0;
// HTTPステータスコードを取得
if(HttpQueryInfoA(connect, HTTP_QUERY_STATUS_CODE, result_array, bufferLength, index))
{
for(int i = 0; i < bufferLength; i++)
{
uchar ch[1];
ch[0] = result_array[i];
if(ch[0] != NULL)
result_str += CharArrayToString(ch);
else
result_str += "\n";
}
}
response = (int)result_str;
}
char receive[1024];
int byteSize = 0;
int totalBytesRead = 0;
// レスポンスデータを読み取る
while(InternetReadFile(connect, receive, 1024, byteSize))
{
if(byteSize <= 0)
break;
ArrayResize(result, totalBytesRead + byteSize);
for(int i = 0; i < byteSize; ++i)
{
result[totalBytesRead + i] = receive[i];
}
totalBytesRead += byteSize;
}
InternetCloseHandle(connect);
}
else
if(method == "POST")
{
// ホストに接続
int connect = InternetConnectW(session, host, port, "", "", INTERNET_SERVICE_HTTP, 0, 0);
if(!connect)
{
InternetCloseHandle(session);
string error = "エラー:接続できません";
StringToCharArray(error, result);
return(response);
}
// HTTPリクエストを開く
int hRequest = HttpOpenRequestW(connect, method, endpoint, "HTTP/1.1", "", "", secure | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_RELOAD | INTERNET_FLAG_PRAGMA_NOCACHE, 0);
if(!hRequest)
{
InternetCloseHandle(connect);
InternetCloseHandle(session);
string error = "エラー:HTTPリクエストを開けません";
StringToCharArray(error, result);
return(response);
}
if(!HttpSendRequestW(hRequest, headers, StringLen(headers), data, ArraySize(data)))
{
InternetCloseHandle(hRequest);
InternetCloseHandle(connect);
InternetCloseHandle(session);
string error = "エラー:HTTPリクエストの送信に失敗しました。";
StringToCharArray(error, result);
return(response);
}
//レスポンス確認
{
int bufferLength = 1024;
uchar result_array[];
string result_str = "";
int index = 0;
ArrayResize(result_array, bufferLength);
// HTTPステータスコードを取得
if(HttpQueryInfoA(hRequest, HTTP_QUERY_STATUS_CODE, result_array, bufferLength, index))
{
for(int i = 0; i < bufferLength; i++)
{
uchar ch[1];
ch[0] = result_array[i];
if(ch[0] != NULL)
result_str += CharArrayToString(ch);
else
result_str += "\n";
}
}
response = (int)result_str;
}
//リクエストヘッダー取得
{
int bufferLength = 1024;
uchar result_array[];
string result_str = "";
ArrayResize(result_array, bufferLength);
int index = 0;
// HTTPステータスコードを取得
if(HttpQueryInfoA(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, result_array, bufferLength, index))
{
for(int i = 0; i < bufferLength; i++)
{
uchar ch[1];
ch[0] = result_array[i];
if(ch[0] != NULL)
result_str += CharArrayToString(ch);
else
result_str += "\n";
}
}
result_headers = result_str;
}
char receive[1024];
int byteSize = 0;
int totalBytesRead = 0;
// レスポンスデータを読み取る
while(InternetReadFile(hRequest, receive, 1024, byteSize))
{
if(byteSize <= 0)
break;
ArrayResize(result, totalBytesRead + byteSize);
for(int i = 0; i < byteSize; ++i)
{
result[totalBytesRead + i] = receive[i];
}
totalBytesRead += byteSize;
}
InternetCloseHandle(hRequest);
InternetCloseHandle(connect);
}
// インターネットセッションを閉じる
InternetCloseHandle(session);
return(response);
}
//+------------------------------------------------------------------+
使用方法
- 上記のコードをインジケーターやEAに追加します
- WebhookURLパラメータに、先ほどコピーしたDiscord Webhook URLを設定します
- 以下のように関数を呼び出すことで通知が送信できます:
// テキストのみの通知
SendDiscordNotification("トレード開始:USDJPY買い", WebhookURL);
// 画像付きの通知
SendDiscordNotification("トレード結果", WebhookURL, "chart.png");
// CSVファイルの添付
SendDiscordNotification("トレードログ", WebhookURL, "trades.csv");
技術的な特徴
WebRequest2関数について
このコードの特筆すべき点は、WinAPIを使用した独自のWebRequest2関数の実装です。通常のWebRequest関数では必要となるURLの許可リスト登録が不要となり、設定の手間を大幅に削減できます。
【MQL4&MQL5】新WebRequest2関数(DLLを使用した画像付きLINE通知ソースコード)|ワン('ω')FXフリーランス
WebRequestを使用しないでHTTPリクエストがしたいけどWinAPI関数を使用するのはちょっとよくわからない という迷える諸君('ω') お待たせしました、お待たせしすぎたかもしれません WebRequestの引数をそのままに、Wi...
マルチパートフォームデータの対応
LINE通知では画像ファイルのみの対応でしたが、このDiscord実装では様々なファイル形式に対応しています。マルチパートフォームデータの実装により、画像、CSV、その他のファイル形式を柔軟に添付できます。
注意点
- ファイルを添付する場合は、Files フォルダ内のファイルのみが対象となります
- 大容量のファイル転送には適していない場合があります
- HTTPS通信を使用するため、セキュリティは確保されています
ダウンロード
ソースファイルはこちらに置きましたので上記のコードで不安な方は下記からダウンロードしてください。
note ご指定のページが見つかりません