DLLなしのMT4およびMT5用ネイティブTwitterクライアント

Soewono Effendi | 11 9月, 2020

はじめに


Tweeterは、誰もが自分のサイトに投稿できる無料のプラットフォームを提供します。投稿には、金融に関するヒントのように価値があるものと、著名人が自分の考えを表現するのと同じくらい価値がないものとがありますが、この記事ではその内容ではなく媒体に主に焦点を当てているので、さっそく始めましょう。

Tweeterにサインアップして、Tweeter APIへのアクセスに必要な多数のトークンをよくご覧ください。

同じような名前のトークンがたくさんあるため、最初はかなりわかりにくいかもしれません。基本的に、Twitter APIを使用するには、以下のトークンが必要です。

注意:

デジタル署名に使用される公開鍵と秘密鍵に精通している方には、わかりやすいかと思います。

customer_tokenとcustomer_token_secretは、「Twitterアプリ」を識別するための公開鍵と秘密鍵です。Twitterアプリは、簡単に言うと、Twitter APIを利用したサービスやアクセスのIDです。 

access_tokenおよびaccess_token_secretは、ユーザを「Twitterユーザ」として識別するための公開鍵と秘密鍵であり、Twitterの用語では「ユーザー認証」または「ユーザーコンテキスト」と呼ばれます。このaccess_tokenに基づいて、Twitter APIは誰がアクセスしているかを識別できます。

Twitter APIを使用して「匿名」アクセスを許可する、別のいわゆるbearer_tokenが存在します。このアクセス方法は、「アプリ認証」または「アプリコンテキスト」と呼ばれます。「ユーザーコンテキスト」がないと、Twitter APIリファレンスに詳しく記載されている一部のTwitter APIにアクセスできません。

他のプログラミング言語でコーディングできる方にとっては、これらのTwitterライブラリが参考として役立つかもしれません。これらは、APIのドキュメントだけを読んだだけでは明らかになるとは限らない実装の詳細への優れた洞察を提供する、優れたリソースです。


Tweeter APIと認証

上記のトークンの使用に焦点を合わせます。

これらのトークンを取得する方法については、YouTubeにたくさんのガイドやチュートリアルがあります。

OAuthはWebベースのAPIの認証および承認のために広く受け入れられ、広く使用されている標準で、Twitter APIも使用しています。
簡単に言うと、OAuthはデジタル署名であり、コンテンツを操作しようとすると無効になるようにデジタルコンテンツに署名する方法です。

コンテンツとその署名を正しく検証するには、そのコンテンツを作成する特定の方法とプロセスを正確に実行する必要があります。

繰り返しますが、Twitter APIドキュメントはこのプロセス全体を非常によくドキュメント化しています。

このプロセスをシンプルに保つために、Twitter APIには、HTTP経由のリクエストをエンコードする特定の方法が必要です。この方法については、次の章で説明します。

URLエンコードとパラメータの並べ替え

デジタル署名されたコンテンツの完全な正確性を保証するために、コンテンツ自体が明確に定義されている必要があります。そのため、Twitter API(正確にはOAuthメソッド)では、デジタル署名される前に、明確に定義された手順を実行するために、HTTP POSTまたはHTTP GETパラメータが必要です。

HTTP POST/HTTP GETパラメータを次のようにエンコードすることが不可欠です。

リファレンスドキュメントおよびこのドキュメントでも、「%」および「+」文字の正しいエンコーディングについてご確認ください。

また、ここにリファレンスドキュメントから引用されているパラメータの並べ替えについてもご注意ください。

この要件の簡単な実装は次のとおりです。

string hex(int i)
  {
   static string h="0123456789ABCDEF";
   string ret="";
   int a = i % 16;
   int b = (i-a)/16;
   if(b>15)
      StringConcatenate(ret,ret,hex(b),StringSubstr(h,a,1));
   else
      StringConcatenate(ret,ret,StringSubstr(h,b,1),StringSubstr(h,a,1));
   return (ret);
  }

string URLEncode(string toCode)
  {
   int max=StringLen(toCode);

   string RetStr="";
   for(int i=0; i<max; i++)
     {
      string c = StringSubstr(toCode,i,1);
      ushort asc = StringGetCharacter(c, 0);

      if((asc >= '0' && asc <= '9')
         || (asc >= 'a' && asc <= 'z')
         || (asc >= 'A' && asc <= 'Z')
         || (asc == '-')
         || (asc == '.')
         || (asc == '_')
         || (asc == '~'))
         StringAdd(RetStr,c);
      else
        {
         StringConcatenate(RetStr,RetStr,"%",hex(asc));
        }
     }
   return (RetStr);
  }

string arrayEncode(string &array[][2])
  {
   string ret="";
   string key,val;
   int l=ArrayRange(array,0);
   for(int i=0; i<l; i++)
     {
      key = URLEncode(array[i,0]);
      val = URLEncode(array[i,1]);
      StringConcatenate(ret,ret,key,"=",val);
      if(i+1<l)
         StringConcatenate(ret,ret,"&");
     }
   return (ret);
  }

void sortParam(string&arr[][2])
  {
   string k1, k2;
   string v1, v2;
   int n = ArrayRange(arr,0);

// bubble sort
   int i, j;
   for(i = 0; i < n-1; i++)
     {
      // Last i elements are already in place
      for(j = 0; j < n-i-1; j++)
        {
         int x = j+1;
         k1 = arr[j][0];
         k2 = arr[x][0];
         if(k1 > k2)
           {
            // swap values
            v1 = arr[j][1];
            v2 = arr[x][1];
            arr[j][1] = v2;
            arr[x][1] = v1;
            // swap keys
            arr[j][0] = k2;
            arr[x][0] = k1;
           }
        }
     }
  }

void addParam(string key,string val,string&array[][2])
  {
   int x=ArrayRange(array,0);
   if(ArrayResize(array,x+1)>-1)
     {
      array[x][0]=key;
      array[x][1]=val;
     }
  }


上記の関数の使用例は次のとおりです。

   string params[][2];

   addParam("oauth_callback", "oob", params);
   addParam("oauth_consumer_key", consumer_key, params);

   sortParam(params);

注意:
簡略化のため、パラメータの並べ替えは不完全であり、複数の同じキーパラメータは考慮されていません。HTMLフォームのラジオボタンやチェックボックスなど、同じキーのパラメータを使用している場合は、この値を改善することをお勧めします。


HMAC-SHA1は可能な限り簡単に

OAuth署名を作成する前のもう1つのハードルは、MQLでのHMAC-SHA1ネイティブサポートの欠如です。MQLのCryptEncode()は、SHA1-HASHの構築のみをサポートしているため、ここではほとんど役に立たないことがわかります(したがって、フラグは CRYPT_HASH_SHA1です)。

それでは、CryptEncode()を使用して独自のHMAC-SHA1をコーディングしてみましょう。


string hmac_sha1(string smsg, string skey, uchar &dstbuf[])
  {
// HMAC as described on:
// https://en.wikipedia.org/wiki/HMAC
//
   uint n;
   uint BLOCKSIZE=64;
   uchar key[];
   uchar msg[];
   uchar i_s[];
   uchar o_s[];
   uchar i_sha1[];
   uchar keybuf[];
   uchar i_key_pad[];
   uchar o_key_pad[];
   string s = "";

   if((uint)StringLen(skey)>BLOCKSIZE)
     {
      uchar tmpkey[];
      StringToCharArray(skey,tmpkey,0,StringLen(skey));
      CryptEncode(CRYPT_HASH_SHA1, tmpkey, keybuf, key);
      n=(uint)ArraySize(key);
     }
   else
      n=(uint)StringToCharArray(skey,key,0,StringLen(skey));

   if(n<BLOCKSIZE)
     {
      ArrayResize(key,BLOCKSIZE);
      ArrayFill(key,n,BLOCKSIZE-n,0);
     }

   ArrayCopy(i_key_pad,key);
   for(uint i=0; i<BLOCKSIZE; i++)
      i_key_pad[i]=key[i]^(uchar)0x36;

   ArrayCopy(o_key_pad,key);
   for(uint i=0; i<BLOCKSIZE; i++)
      o_key_pad[i]=key[i]^(uchar)0x5c;

   n=(uint)StringToCharArray(smsg,msg,0,StringLen(smsg));
   ArrayResize(i_s,BLOCKSIZE+n);
   ArrayCopy(i_s,i_key_pad);
   ArrayCopy(i_s,msg,BLOCKSIZE);

   CryptEncode(CRYPT_HASH_SHA1, i_s, keybuf, i_sha1);
   ArrayResize(o_s,BLOCKSIZE+ArraySize(i_sha1));
   ArrayCopy(o_s,o_key_pad);
   ArrayCopy(o_s,i_sha1,BLOCKSIZE);
   CryptEncode(CRYPT_HASH_SHA1, o_s, keybuf, dstbuf);

   for(int i=0; i < ArraySize(dstbuf); i++)
      StringConcatenate(s, s, StringFormat("%02x",(dstbuf[i])));

   return s;
  }


正確さを確認するために、Twitter APIドキュメントで作成されたハッシュと比較できます。

   uchar hashbuf[];
   string base_string = "POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521";
   string signing_key = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE";  
   string hash = hmac_sha1(base_string, signing_key, hashbuf);

   Print(hash); // 842b5299887e88760212a056ac4ec2ee1626b549

   uchar not_use[];
   uchar base64buf[];
   CryptEncode(CRYPT_BASE64, hashbuf, not_use, base64buf);
   string base64 = CharArrayToString(base64buf);
   
   Print(base64); // hCtSmYh+iHYCEqBWrE7C7hYmtUk=


WebRequestが救済へ


WebRequest()のおかげで、外部DLLを使用せずに、Web経由でREST APIに簡単にアクセスできるようになりました。
次のコードは、 WebRequest()を使用してTwitter APIへのアクセスを簡略化します。

#define WEBREQUEST_TIMEOUT 5000
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
string SendRequest(string method, string url, string headers="", string params="", int timeout=WEBREQUEST_TIMEOUT)
  {
   char data[];
   char result[];
   string resp_headers;
   ResetLastError();
   StringToCharArray(params, data);
   ArrayResize(data, ArraySize(data)-1);
   int res = WebRequest(method, url, headers, timeout, data, result, resp_headers);
   if(res != -1)
     {
      string resp = CharArrayToString(result);
      if(verbose)
        {
         Print("***");
         Print("Data:");
         Print(CharArrayToString(data));
         Print("Resp Headers:");
         Print(resp_headers);
         Print("Resp:");
         Print("***");
         Print(resp);
        }
      return resp;
     }
   else
     {
      int err = GetLastError();
      PrintFormat("* WebRequest error: %d (%d)", res, err);
      if(verbose)
        {
         Print("***");
         Print("Data:");
         Print(CharArrayToString(data));
        }
      if (err == 4014)
      {
         string msg = "* PLEASE allow https://api.tweeter.com in WebRequest listed URL";
         Print(msg);
      }
     }
   return "";
  }

WEBREQUEST()のドキュメントをお読みください。


引用:

WebRequest()関数を使用するには、[オプション]ウィンドウの[エキスパートアドバイザー]タブにある許可されたURLのリストに必要なサーバのアドレスを追加します。サーバポートは、指定されたプロトコルに基づいて「http://」の場合は80、「https://」の場合は443が自動的に選択されます。


Twitter REST APIを構築するためのヘルパー関数

以下は、Twitter API署名の作成に役立ついくつかのヘルパー関数です。

string getNonce()
  {
   const string alnum = "abcdef0123456789";
   char base[];
   StringToCharArray(alnum, base);
   int x, len = StringLen(alnum);
   char res[32];
   for(int i=0; i<32; i++)
     {
      x = MathRand() % len;
      res[i] = base[x];
     }
   return CharArrayToString(res);
  }

string getBase(string&params[][2], string url, string method="POST")
  {
   string s = method;
   StringAdd(s, "&");
   StringAdd(s, URLEncode(url));
   StringAdd(s, "&");
   bool first = true;
   int x=ArrayRange(params,0);
   for(int i=0; i<x; i++)
     {
      if(first)
         first = false;
      else
         StringAdd(s, "%26");  // URLEncode("&")
      StringAdd(s, URLEncode(params[i][0]));
      StringAdd(s, "%3D"); // URLEncode("=")
      StringAdd(s, URLEncode(params[i][1]));
     }
   return s;
  }

string getQuery(string&params[][2], string url = "")
  {
   string key;
   string s = url;
   string sep = "";
   if(StringLen(s) > 0)
     {
      if(StringFind(s, "?") < 0)
        {
         sep = "?";
        }
     }
   bool first = true;
   int x=ArrayRange(params,0);
   for(int i=0; i<x; i++)
     {
      key = params[i][0];
      if(StringFind(key, "oauth_")==0)
         continue;
      if(first)
        {
         first = false;
         StringAdd(s, sep);
        }
      else
         StringAdd(s, "&");
      StringAdd(s, params[i][0]);
      StringAdd(s, "=");
      StringAdd(s, params[i][1]);
     }
   return s;
  }

string getOauth(string&params[][2])
  {
   string key;
   string s = "OAuth ";
   bool first = true;
   int x=ArrayRange(params,0);
   for(int i=0; i<x; i++)
     {
      key = params[i][0];
      if(StringFind(key, "oauth_")!=0)
         continue;
      if(first)
         first = false;
      else
         StringAdd(s, ", ");
      StringAdd(s, URLEncode(key));
      StringAdd(s, "=\"");
      StringAdd(s, URLEncode(params[i][1]));
      StringAdd(s, "\"");
     }
   return s;
  }


スクリプト例

これで、最初のTwitter API要求を送信する準備ができました。

void verifyCredentials()
  {

   string _api_key = consumer_key;
   string _api_secret = consumer_secret;
   string _token = access_token;
   string _secret = access_secret;

   string url = "https://api.twitter.com/1.1/account/verify_credentials.json";
   
   string params[][2];
   addParam("oauth_consumer_key", _api_key, params);
   string oauth_nonce = getNonce();
   addParam("oauth_nonce", oauth_nonce, params);
   addParam("oauth_signature_method", "HMAC-SHA1", params);
   
   string oauth_timestamp = IntegerToString(TimeGMT());
   addParam("oauth_timestamp", oauth_timestamp, params);
   
   addParam("oauth_token", _token, params);
   addParam("oauth_version", "1.0", params);
   
   sortParam(params);
   
   string query = getQuery(params, url);
   string base = getBase(params, url, "GET");
   uchar buf[];
   string key = URLEncode(_api_secret);
   StringAdd(key, "&");
   StringAdd(key, URLEncode(_secret));
   
   uchar hashbuf[], base64buf[], nokey[];
   string hash = hmac_sha1(base, key, hashbuf);
   CryptEncode(CRYPT_BASE64, hashbuf, nokey, base64buf);
   string base64 = CharArrayToString(base64buf);

   addParam("oauth_signature", base64, params);
   sortParam(params);
   string o = getOauth(params);

   string headers = "Host:api.twitter.com\r\nContent-Encoding: identity\r\nConnection: close\r\n";
   StringAdd(headers, "Authorization: ");
   StringAdd(headers, o);
   StringAdd(headers, "\r\n\r\n");
   
   string resp = SendRequest("GET", query, headers);
   Print(resp);

   // if everything works well, we shall receive JSON-response similar to the following
   // NOTE: Identity has been altered to protect the innocent.
   // {"id":122,"id_str":"122","name":"xxx","screen_name":"xxx123","location":"","description":"", ... 
  }


MT5チャートのTwitterクライアント例


次の画像は、インドネシアのニュースチャネルのツイートを表示しているTwitterクライアントを示しています。Twitter APIを実装したフォローアップ記事を準備しており、できるだけ早く公開したいと思っています。



MT5チャート上のTwitterクライアント

図1: チャートに表示されたツイート


次は、MT5端末から投稿されたツイートを示す別のスクリーンショットです。



MT5端末からのツイート
図2:. MT5端末からのツイート 


将来の機能強化


上記のメソッドは完成には程遠く、すべてのTwitter APIを網羅するものではありませんが、問題なく機能します。Tweeter APIをさらに詳しく知りたい方にとっては、これはよい演習です。チャートのスクリーンショットを含むメディア投稿の詳細については、後続の記事で説明します。

TwitterAPIクラスまたは一般的なOAuthクライアントクラスを構築することもできます。

3-Legged認証とPINベースの認証に関する注意


いわゆる 3-legged承認 PINベースの承認についてさらに詳しく知りたい場合があるかもしれません。

それらについて説明するのはこの記事の範囲外なので、さらにガイダンスが必要な場合は、お気軽にご連絡ください。

終わりに

Twitterは、誰もがほぼ何でも公開できる、広く受け入れられているプラットフォームです。
この記事と上記で説明したコードで、Twitter APIへのアクセスにおけるOAuthの理解についての私のこのささやかな冒険の結果をMQLコミュニティに貢献できたらと思っています。

フィードバックでの励みとなる改善のアイディアを楽しみにしています。提供されているコードはすべて、無料有料にかかわらずあらゆるプロジェクトでご自由にご利用ください。

このライブラリコード(MetaTrader 4用のSHA256、SHA384、SHA512 + HMACライブラリ)を提供してくださったGrzegorz Koryckiさんに感謝します)。これは、私のHMAC-SHA1関数の作成に影響を与えました。

楽しみと利益のためにツイートしましょう。

では、お楽しみください。