Käytän C++Builder XE6 ja sen REST-komponentteja ohjelmankehitykseen pelatakseni erilaisia Veikkauksen "taitopelejä".
Veikkaus on antanut referenssitoteutuksen (https://github.com/VeikkausOy/sport-games-robot) pythonilla, joka on minulle vieras ohjelmointikieli, joten koitan tehdä ohjelman C++Builderilla.
Veikkauksen pelitilin saldon kyselyn pitäisi onnistua alla olevalla koodilla. Tarvitaan 2 Edit-komponenttia (Username ja password), 1 button ja REST-komponenteista: SimpleAuthenticator, RESTClient, RESTRequest ja RESTResponse.
Loggautuminen onnistuu palvelin palauttaa StatusCode-arvon 200. RESTRequest komponentin arvoja muokataan tämänjälkeen saldokyselyä varten, joka kuitenkin epäonnistuu (virheilmoitus: 'HTTP/1.1 400 Bad Request')
Veikkaukselta kertoivat, että
"Näyttäisi siltä että tuo koodi tekee pyynnön, joka on muotoa /api/v1/players/self/account?= . Eli lopussa on ylimääräinen = merkki", mutta koska he eivät tunne C++Builderia eivät he osanneet sanoa mikä koodissani on väärin.
Osaisiko kukaan kertoa missä vika mahtaa olla alla olevassa koodissa ?
Delphi-osaaja voisi ehkä myös tietää. Kiitos etukäteen.
void __fastcall TForm1::Button1Click(TObject *Sender)
{
RESTClient1->Authenticator = SimpleAuthenticator1;
RESTClient1->BaseURL = "https://www.veikkaus.fi";
RESTClient1->AllowCookies = true;
TJSONObject *json = new TJSONObject();
json->AddPair("type", new TJSONString("STANDARD_LOGIN") );
json->AddPair("login", new TJSONString( Edit1->Text ) );
json->AddPair("password", new TJSONString( Edit2->Text ) );
// Login
RESTRequest1->Params->Clear();
RESTRequest1->Params->AddItem("X-ESA-API-Key", "ROBOT", pkHTTPHEADER);
RESTRequest1->Params->AddItem("Accept", "application/json", pkHTTPHEADER);
RESTRequest1->AddParameter("body", json, pkREQUESTBODY);
RESTRequest1->Client = RESTClient1;
RESTRequest1->Method = rmPOST;
RESTRequest1->Resource = "api/v1/sessions";
RESTRequest1->Execute();
if( RESTResponse1->StatusCode == 200) // Login onnistui
{ // Saldokysely
RESTRequest1->Params->Clear();
RESTRequest1->Params->AddItem("X-ESA-API-Key", "ROBOT", pkHTTPHEADER);
RESTRequest1->Params->AddItem("Accept", "application/json", pkHTTPHEADER);
RESTRequest1->Resource = "api/v1/players/self/account";
RESTRequest1->Method = rmGET;
RESTRequest1->Execute(); // virheilmoitus: 'HTTP/1.1 400 Bad Request'.
RESTResponse1->Content;
}
else
{
ShowMessage("Kirjautuminen epäonnistui: " + RESTResponse1->ErrorMessage );
}
}Mod. lisäsi kooditagit!
Voisi kuvitella, että veikkauksen palvelin antaa tuossa kirjautumisen yhteydessä jonkin tunnisteen (eväste, OAuth headeri, tms...), joka pitäisi heittää takaisin palvelimelle seuraavia pyyntöjä tehdessä. Ehkäpä Delphin Restclient ei hoida sitä automaattisesti pyyntöjen välillä.
Veikkauksen APIn mukaan tuo kysely on muotoa GET /api/v1/players/self/account eli tuossa sinun kyselyssä olisi lopussa merkit ?= ylimääräisiä. Jostain tuo koodinpätkä saa päähänsä lisätä nuo vaikka niitä ei tarvita. REST requestissa vaikuttaisi olevan parametrien automaattinen generointi defaulttina päällä, joka voisi nuo merkit lisätä. Kokeiles tuo generointi laittaa pois päältä ja testaa mitä tapahtuu.
Kokeilin seuraavia ehdotettuja vaihtoehtoja alla näkyvässä if-rakenteessa:
a) RESTRequest1->Params->Clear();
b) RESTRequest1->AutoCreateParams = false;
c) RESTRequest1->ResetToDefaults();
if( RESTResponse1->StatusCode == 200) // Login onnistui
{ // Saldokysely
//RESTRequest1->Params->Clear();
//RESTRequest1->AutoCreateParams = false;
RESTRequest1->ResetToDefaults();
RESTRequest1->Params->AddItem("X-ESA-API-Key", "ROBOT", pkHTTPHEADER);
RESTRequest1->Params->AddItem("Accept", "application/json", pkHTTPHEADER);
RESTRequest1->Resource = "api/players/self/account";
RESTRequest1->Method = rmGET;
UnicodeString s = RESTRequest1->GetFullRequestURL();
RESTRequest1->Execute(); // virheilmoitus: 'HTTP/1.1 400 Bad Request'.
RESTResponse1->Content;
}ja debuggerilla katsoin millaisia arvoja funktio antaa muuttujalle s.
1)
RESTRequest1->Params->Clear();
// ...
s == { u"https://www.veikkaus.fi/api/players/self/account?=" }2)
//RESTRequest1->Params->Clear(); // kommentoitu pois
// ...
s == { u"https://www.veikkaus.fi/api/v1/players/self/account?body=%7B%22type%22%3A%22STANDARD_LOGIN%22%2C%22login%22%3A%22TUNNUS%22%2C%22password%22%3A%22SALASANA%22%7D&=" }3)
RESTRequest1->Params->Clear();
RESTRequest1->AutoCreateParams = false;
// ...
s == { u"https://www.veikkaus.fi/api/players/self/account?=" }4)
//RESTRequest1->Params->Clear(); // kommentoitu pois
RESTRequest1->AutoCreateParams = false;
// ...
s == { u"https://www.veikkaus.fi/api/v1/players/self/account?body=%7B%22type%22%3A%22STANDARD_LOGIN%22%2C%22login%22%3A%22TUNNUS%22%2C%22password%22%3A%22SALASANA%22%7D&=" }5)
//RESTRequest1->Params->Clear(); // kommentoitu pois
//RESTRequest1->AutoCreateParams = false; // kommentoitu pois
RESTRequest1->ResetToDefaults();
{ u"https://www.veikkaus.fi/api/players/self/account" }Muuttujan arvo 2) ja 4) tapauksessa
s = { u"https://www.veikkaus.fi/api/v1/players/self/account?body=%7B%22type%22%3A%22STANDARD_LOGIN%22%2C%22login%22%3A%22TUNNUS%22%2C%22password%22%3A%22SALASANA%22%7D&=" }on selkokielisenä
{ u"https://www.veikkaus.fi/api/v1/players/self/account?body={"type":"STANDARD_LOGIN","login":"TUNNUS","password":"SALASANA"}&=" }Mikään näistä ei kuitenkaan toiminut. Ilmeisesti 3) arvo olisi oikein ilman tuota = merkkiä. Jotenkin nuo login vaiheen evästeet pitäisi olla mukana saldokyselyssä, kuten Grez mainitsi. Hoituuko se automaattisesti kun Reguest-komponentti on kytketty aiemmassa pyynnössä käytettyyn RESTClient:tiin ? Embarcaderon dokumentointi on aika olematonta eikä esimerkkejäkään oikein ele saatavilla.
Mod. huom: käytä kooditageja!
Jos oikeat osoitteet ja parametrit eivät selviä dokumentaatiosta, kokeile. Pystytä HTTP-välityspalvelin, joka tallentaa kaiken liikenteen ja välittää pyynnöt edelleen Veikkaukselle. Vaihda Veikkauksen mallikoodiin tuon välityspalvelimen osoite (tyyliin localhost:8080) ja katso, millaisia pyyntöjä tulee. Viilaa sitten omaa koodiasi, kunnes saat samanlaiset pyynnöt.
Ja käytä kooditageja foorumilla, jotta viesteistäsi saa jotain selvää.
Rakentelin taannoin Delphillä Indy-komponentteja käyttäen toimivan ohjelman Veikkauksen pelejä varten. En silloin koodannut saldokyselyä, mutta testasin tänään sen toimivuutta. Onnistuneen kirjautumisen jälkeen saldon saa kysyttyä yksinkertaisesti:
// kirjautuminen
Url := 'https://www.veikkaus.fi/api/v1/sessions';
Teksti := '{"type":"STANDARD_LOGIN","login":"' + EditUsername.Text +
'","password":"' + EditPassword.Text + '"}';
JsonToSend := TStringStream.Create(Utf8Encode(Teksti));
S := IdHTTP1.Post(Url, JsonToSend);
JsonToSend.Free;
// saldon haku
Url := 'https://www.veikkaus.fi/api/v1/players/self/account';
S := IdHTTP1.Get(Url);C++Builderia en tunne, en liioin REST-komponenttejakaan. Niiden suhteen en osaa auttaa. Tältä pohjalta voisi ajatella, että pyynnössä on jotakin liikaa. Voineeko se, että Veikkaus pyrkii palauttamaan datan pakattuna, vaikuttaa asiaan.
^Kiitos vinkintä, luulen pystyväni muokkaamann antamasi mallin C++Builderille. En ole koskaan käyttänyt Indy-komponentteja aiemmin, tarvitaanko antamasi delphi-koodin testaamiseen muita Indy-komponentteja ja tarvitseeko komponentin oletusarvoja muokata ?
Testailin näitä juttuja viime viikonloppuna. Minusta ongelmasi kiteytyy pyyntöön "/api/v1/players/self/account?=". Tällä kertaa käytin rest-komponentteja. En pystynyt edes kirjautumaan Veikkauksen järjestelmään. Virheilmoituksena tuli 415 Unsupported media type. Virheen lähdettä en pystynyt eristämään.
Syystä tai toisesta Indy-komponenteilla kirjautuminen ja tilitietojen hakeminen onnnistuu. Ymmärrät varmaankin asian parhaiten koodista. Tarvitaan siis TIdHTTP-komponentti. OpenSSL:n voit tarvittaessa ladata netistä. Voi olla, että uses-listaan joudut käsin lisäämään joitakin uniteja.
var
SSLIOHandler: TIdSSLIOHandlerSocketOpenSSL;
Viesti: String;
JsonT: TJSONObject;
JsonToSend, Palaute: TStringStream;
Jatka: Boolean;
begin
SSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create;
IdHTTP1.IOHandler := SSLIOHandler;
IdHTTP1.Compressor := TIdCompressorZLib.Create(IdHTTP1);
JsonT := TJSONObject.Create;
JsonT.AddPair('type', 'STANDARD_LOGIN');
JsonT.AddPair('login', '9999999');
JsonT.AddPair('password', '9999999');
Viesti := JsonT.ToString;
JsonToSend := TStringStream.Create(Viesti, TEncoding.UTF8);
Palaute := TStringStream.Create('', TEncoding.UTF8);
with IdHttp1.Request do
begin
CustomHeaders.Clear;
CustomHeaders.AddValue('X-ESA-API-Key', 'ROBOT');
Accept := 'application/json';
ContentType := 'application/json';
Charset := 'UTF-8';
AcceptCharset := 'UTF-8';
end;
try
IdHTTP1.Post('https://www.veikkaus.fi/api/v1/sessions', JsonToSend,
Palaute);
Jatka := True;
Memo1.Lines.Clear;
Memo1.Lines.Add(Palaute.DataString);
except
on E: Exception do
begin
ShowMessage('Lokkautumisvirhe:'#13#10 + e.Message);
Jatka := False;
end;
end;
JsonToSend.Free;
Palaute.Free;
Palaute := TStringStream.Create('', TEncoding.UTF8);
if Jatka then
begin
try
IdHTTP1.Get('https://www.veikkaus.fi/api/v1/players/self/account',
Palaute);
except
on E: Exception do
begin
ShowMessage('Virhe haettaessa saldotietoja:'#13#10 + e.Message);
Jatka := False;
end;
end;
if Jatka then
begin
if Memo1.Lines.Count > 0 then Memo1.Lines.Add('');
Memo1.Lines.Add(Palaute.DataString);
end;
end;
Palaute.Free;
ShowMessage('Tehty');^Kiitoksia vastauksesta, sain saldokyselyn onnistumaan esimerkkisi avulla C++Builderilla :)
Minkäslaisella koodilla lähti toimimaan?
Tuosta näkyvät komponentit ja koodi.
class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *Button1;
TEdit *Edit1;
TEdit *Edit2;
TIdSSLIOHandlerSocketOpenSSL *SSLIOHandler;
TIdHTTP *IdHTTP1;
TIdCompressorZLib *IdCompressorZLib1;
TMemo *Memo1;
void __fastcall Button1Click(TObject *Sender);
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};void __fastcall TForm1::Button1Click(TObject *Sender)
{
IdHTTP1->IOHandler = SSLIOHandler;
IdHTTP1->Compressor = IdCompressorZLib1;
TJSONObject *JsonT = new TJSONObject();
JsonT->AddPair("type", new TJSONString("STANDARD_LOGIN") );
JsonT->AddPair("login", new TJSONString( Edit1->Text ) );
JsonT->AddPair("password", new TJSONString( Edit2->Text ) );
UnicodeString Viesti = JsonT->ToString();
TStringStream* JsonToSend = new TStringStream( Viesti );
TStringStream* Palaute = new TStringStream();
IdHTTP1->Request->CustomHeaders->Clear();
IdHTTP1->Request->CustomHeaders->AddValue("X-ESA-API-Key", "ROBOT");
IdHTTP1->Request->Accept = "application/json";
IdHTTP1->Request->ContentType = "application/json";
IdHTTP1->Request->CharSet = "UTF-8";
IdHTTP1->Request->AcceptCharSet = "UTF-8";
bool Jatka = false;
try
{
IdHTTP1->Post("https://www.veikkaus.fi/api/v1/sessions", JsonToSend, Palaute);
Jatka = true;
Memo1->Lines->Clear();
Memo1->Lines->Add( Palaute->DataString );
}
catch (...)
{
ShowMessage("Lokkautumisvirhe");
Jatka = false;
}
delete JsonToSend;
delete JsonT;
if( Jatka )
{
try
{
Palaute->Clear();
IdHTTP1->Get("https://www.veikkaus.fi/api/v1/players/self/account",Palaute);
}
catch (...)
{
ShowMessage("Virhe haettaessa saldotietoja");
Jatka = false;
}
}
if( Jatka )
{
if( Memo1-> Lines->Count > 0 ) Memo1->Lines->Add("");
Memo1->Lines->Add( Palaute->DataString );
}
}
//---------------------------------------------------------------------------Tuossa näyttää olevan monta new'tä, joilla ei ole vastaavaa delete-riviä. Luulen, että ainakin Palaute pitäisi tuhota. Entä pitäisikö kaikki JSON-oliot tuhota, vai menevätkö ne automaattisesti ulommaisen JsonT:n mukana? Onko mitään syytä ylipäänsä käyttää new'tä, vai voisiko oliot luoda ei-dynaamisesti?
Osa hoituu mielestäni automaattisesti esim. new TJSONString("STANDARD_LOGIN") (TJSONObject hoitaa), Palaute olisi deletetoitava. Muistinhallintaan liittyvät asiat eivät olleet kuitenkaan olennainen asia tässä tapauksessa vaan saada ylipäätään pyynnöt menemään onnistuneesti läpi.
Aihe on jo aika vanha, joten et voi enää vastata siihen.