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.