Úvod do Nintendo Switch REST API

Přehled

Díky úsilí komunity můžeme programově přistupovat k API aplikace Nintendo Switch za nulové náklady. To nám umožňuje vytvářet aplikace schopné komunikovat s hrami připojenými k Nintendo Switch Online (NSO) a také získávání uživatelských informací, jako jsou hrané hry a doba hraní.

Zadávejte zprávy nebo používejte reakce v Animal Crossing s požadavky API!

Přístup k rozhraní API

  1. Získání Tokenu Nintendo Session z webu Nintendo
  2. Získání Tokenu webové služby
  3. Použití tokenu webové služby k získání souborů cookie relace specifických pro hru
  4. Přístup k API prostřednictvím souborů cookie relace

1. Nintendo Session Token

Když se někdo přihlásí na speciální autorizační odkaz Nintendo, Nintendo přesměruje prohlížeč na adresu URL obsahující token relace.

Abychom vygenerovali tento odkaz, musíme zahrnout výzvu kódu S256 ve formátu base64url. Nemusíte se bát, pokud nevíte, co to právě teď znamená. Jednoduše řečeno, předáváme hašovanou hodnotu našeho klíče na Nintendo a později použijeme původní klíč jako důkaz, že jsme stejná osoba, která se přihlásila.

$npm install base64url, request-promise-native, uuid
const crypto = require('crypto');
const base64url = require('base64url');

let authParams = {};

function generateRandom(length) {
    return base64url(crypto.randomBytes(length));
  }

function calculateChallenge(codeVerifier) {
    const hash = crypto.createHash('sha256');
    hash.update(codeVerifier);
    const codeChallenge = base64url(hash.digest());
    return codeChallenge;
}

function generateAuthenticationParams() {
    const state = generateRandom(36);
    const codeVerifier = generateRandom(32);
    const codeChallenge = calculateChallenge(codeVerifier);
    return {
        state,
        codeVerifier,
        codeChallenge
    };
}

function getNSOLogin() {
    authParams = generateAuthenticationParams();
    const params = {
      state: authParams.state,
      redirect_uri: 'npf71b963c1b7b6d119://auth&client_id=71b963c1b7b6d119',
      scope: 'openid%20user%20user.birthday%20user.mii%20user.screenName',
      response_type: 'session_token_code',
      session_token_code_challenge: authParams.codeChallenge,
      session_token_code_challenge_method: 'S256',
      theme: 'login_form'
    };
    const arrayParams = [];
    for (var key in params) {
      if (!params.hasOwnProperty(key)) continue;
      arrayParams.push(`${key}=${params[key]}`);
    }
    const stringParams = arrayParams.join('&');
    return `https://accounts.nintendo.com/connect/1.0.0/authorize?${stringParams}`;
}

const loginURL = getNSOLogin();
console.log(loginURL);

Měli byste získat adresu URL podobnou této:
https://accounts.nintendo.com/connect/1.0.0/authorize?state=[SessionStateReturnedHere]&redirect_uri=npf71b963c1b7b6d119://auth...

Navštivte adresu URL ve svém prohlížeči a přihlaste se ke svému účtu Nintendo. Budete přesměrováni na tuto stránku.

Klikněte pravým tlačítkem na Vybrat tento účet a zkopírujte odkaz pro přesměrování. Bude v tomto formátu:

npf71b963c1b7b6d119://auth#session_state=[SessionStateReturned]&session_token_code=[SessionTokenCodeReturned]&state=[StateReturned]

Místo obvyklého HTTP nebo HTTPS protokol, protokol vráceného odkazu je npf71b963c1b7b6d119 , proto nemůžete jednoduše kliknout a nechat prohlížeč přesměrovat.

Z této adresy URL přesměrování pak můžeme extrahovat kód tokenu relace.

const params = {};
redirectURL.split('#')[1]
        .split('&')
        .forEach(str => {
          const splitStr = str.split('=');
          params[splitStr[0]] = splitStr[1];
        });
// the sessionTokenCode is params.session_token_code

Pomocí kódu tokenu relace můžeme požádat společnost Nintendo o získání tokenu relace Nintendo.

const request2 = require('request-promise-native');
const jar = request2.jar();
const request = request2.defaults({ jar: jar });

const userAgentVersion = `1.9.0`; // version of Nintendo Switch App, updated once or twice per year

async function getSessionToken(session_token_code, codeVerifier) {
  const resp = await request({
    method: 'POST',
    uri: 'https://accounts.nintendo.com/connect/1.0.0/api/session_token',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'X-Platform': 'Android',
      'X-ProductVersion': userAgentVersion,
      'User-Agent': `OnlineLounge/${userAgentVersion} NASDKAPI Android`
    },
    form: {
      client_id: '71b963c1b7b6d119',
      session_token_code: session_token_code,
      session_token_code_verifier: codeVerifier
    },
    json: true
  });

  return resp.session_token;
}

2. Token webové služby

Zde jsou kroky k získání tokenu webové služby:

I. Získejte Token API s tokenem relace
II. Získejte userInfo s tokenem API
III. Získejte f Flag [NSO]
IV. Získejte API Access Token s f Flag [NSO] a userInfo
V. Získejte f Flag [App] s přístupovým tokenem API
VI. Získejte Token webové služby s přístupovým tokenem API a příznakem f [App]

To může vypadat skličující, ale v implementaci je to jednoduše sekvence asynchronních požadavků serveru.

const { v4: uuidv4 } = require('uuid');

async function getWebServiceTokenWithSessionToken(sessionToken, game) {
    const apiTokens = await getApiToken(sessionToken); // I. Get API Token
    const userInfo = await getUserInfo(apiTokens.access); // II. Get userInfo

    const guid = uuidv4();
    const timestamp = String(Math.floor(Date.now() / 1000));

    const flapg_nso = await callFlapg(apiTokens.id, guid, timestamp, "nso"); // III. Get F flag [NSO] 
    const apiAccessToken = await getApiLogin(userInfo, flapg_nso); // IV. Get API Access Token
    const flapg_app = await callFlapg(apiAccessToken, guid, timestamp, "app"); // V. Get F flag [App]
    const web_service_token =  await getWebServiceToken(apiAccessToken, flapg_app, game); // VI. Get Web Service Token
    return web_service_token;
  }

Nyní tyto požadavky implementujte.

const userAgentString = `com.nintendo.znca/${userAgentVersion} (Android/7.1.2)`;

async function getApiToken(session_token) {
    const resp = await request({
        method: 'POST',
        uri: 'https://accounts.nintendo.com/connect/1.0.0/api/token',
        headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'X-Platform': 'Android',
        'X-ProductVersion': userAgentVersion,
        'User-Agent': userAgentString
        },
        json: {
        client_id: '71b963c1b7b6d119',
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token',
        session_token: session_token
        }
    }); 

    return {
        id: resp.id_token,
        access: resp.access_token
    };
}

async function getHash(idToken, timestamp) {
  const response = await request({
    method: 'POST',
    uri: 'https://elifessler.com/s2s/api/gen2',
    headers: {
      'User-Agent': `yournamehere` // your unique id here
    },
    form: {
      naIdToken: idToken,
      timestamp: timestamp
    }
  });

  const responseObject = JSON.parse(response);
  return responseObject.hash;
}

async function callFlapg(idToken, guid, timestamp, login) {
    const hash = await getHash(idToken, timestamp)
    const response = await request({
        method: 'GET',
        uri: 'https://flapg.com/ika2/api/login?public',
        headers: {
        'x-token': idToken,
        'x-time': timestamp,
        'x-guid': guid,
        'x-hash': hash,
        'x-ver': '3',
        'x-iid': login
        }
    });
    const responseObject = JSON.parse(response);

    return responseObject.result;
}

async function getUserInfo(token) {
const response = await request({
    method: 'GET',
    uri: 'https://api.accounts.nintendo.com/2.0.0/users/me',
    headers: {
    'Content-Type': 'application/json; charset=utf-8',
    'X-Platform': 'Android',
    'X-ProductVersion': userAgentVersion,
    'User-Agent': userAgentString,
    Authorization: `Bearer ${token}`
    },
    json: true
});

return {
    nickname: response.nickname,
    language: response.language,
    birthday: response.birthday,
    country: response.country
};
}

async function getApiLogin(userinfo, flapg_nso) {
    const resp = await request({
        method: 'POST',
        uri: 'https://api-lp1.znc.srv.nintendo.net/v1/Account/Login',
        headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'X-Platform': 'Android',
        'X-ProductVersion': userAgentVersion,
        'User-Agent': userAgentString,
        Authorization: 'Bearer'
        },
        body: {
        parameter: {
            language: userinfo.language,
            naCountry: userinfo.country,
            naBirthday: userinfo.birthday,
            f: flapg_nso.f,
            naIdToken: flapg_nso.p1,
            timestamp: flapg_nso.p2,
            requestId: flapg_nso.p3
        }
        },
        json: true,
        gzip: true
    });
    return resp.result.webApiServerCredential.accessToken;
}


async function getWebServiceToken(token, flapg_app, game) {
  let parameterId;
    if (game == 'S2') {
      parameterId = 5741031244955648; // SplatNet 2 ID
    } else if (game == 'AC') {
      parameterId = 4953919198265344; // Animal Crossing ID
    }
  const resp = await request({
    method: 'POST',
    uri: 'https://api-lp1.znc.srv.nintendo.net/v2/Game/GetWebServiceToken',
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
      'X-Platform': 'Android',
      'X-ProductVersion': userAgentVersion,
      'User-Agent': userAgentString,
      Authorization: `Bearer ${token}`
    },
    json: {
      parameter: {
        id: parameterId,
        f: flapg_app.f,
        registrationToken: flapg_app.p1,
        timestamp: flapg_app.p2,
        requestId: flapg_app.p3
      }
    }
  });

  return {
    accessToken: resp.result.accessToken,
    expiresAt: Math.round(new Date().getTime()) + resp.result.expiresIn
  };
}

Nyní zavolejte funkce a získejte náš token webové služby.

(async () => {
    const sessionToken = await getSessionToken(params.session_token_code, authParams.codeVerifier);
    const webServiceToken = await getWebServiceTokenWithSessionToken(sessionToken, game='S2');
    console.log('Web Service Token', webServiceToken);
})()

Takto vypadá vrácený token webové služby.

Gratulujeme, že jste to dotáhli tak daleko! Nyní začíná zábava s Nintendo API :)

Přístup ke SplatNet pro Splatoon 2

Pro přístup ke službě SplatNet (Splatoon 2) použijeme token webové služby k získání souboru cookie s názvem iksm_session .

(async () => {
    const sessionToken = await getSessionToken(params.session_token_code, authParams.codeVerifier);
    const webServiceToken = await getWebServiceTokenWithSessionToken(sessionToken, game='S2');
    await getSessionCookieForSplatNet(webServiceToken.accessToken);
    const iksmToken = getIksmToken();
    console.log('iksm_token', iksmToken);
})()

const splatNetUrl = 'https://app.splatoon2.nintendo.net';

async function getSessionCookieForSplatNet(accessToken) {
  const resp = await request({
    method: 'GET',
    uri: splatNetUrl,
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
      'X-Platform': 'Android',
      'X-ProductVersion': userAgentVersion,
      'User-Agent': userAgentString,
      'x-gamewebtoken': accessToken,
      'x-isappanalyticsoptedin': false,
      'X-Requested-With': 'com.nintendo.znca',
      Connection: 'keep-alive'
    }
  });

  const iksmToken = getIksmToken();
}

function getCookie(key, url) {
    const cookies = jar.getCookies(url);
    let value;
    cookies.find(cookie => {
        if (cookie.key === key) {
            value = cookie.value;
        }
        return cookie.key === key;
    });
    return value;
}

function getIksmToken() {
    iksm_session = getCookie('iksm_session', splatNetUrl);
    if (iksm_session == null) {
        throw new Error('Could not get iksm_session cookie');
    }
    return iksm_session
}

S tímto souborem cookie můžeme přímo navštívit SplatNet v prohlížeči úpravou iksm_session cookie.

Můžeme sledovat kartu sítě v nástrojích pro vývojáře při procházení SplatNet a vidět volaná rozhraní API.

Tato rozhraní API pak můžeme použít pro naši aplikaci. Jakmile provedeme požadavek pomocí webového tokenu, bude soubor cookie nastaven na požadavek objekt.

const userLanguage = 'en-US';
(async () => {
  ..
  const iksmToken = getIksmToken();
  const records = await getSplatnetApi('records');
  console.log('records', records);

async function getSplatnetApi(url) {
    const resp = await request({
      method: 'GET',
      uri: `${splatNetUrl}/api/${url}`,
      headers: {
        Accept: '*/*',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': userLanguage,
        'User-Agent': userAgentString,
        Connection: 'keep-alive'
      },
      json: true,
      gzip: true
    });

    return resp;
  }

Zde je výsledek pro spuštění záznamů Koncový bod API.

Běžné koncové body SplatNet

  • /results zobrazuje posledních 50 zápasů.
  • /coop_results zobrazuje posledních 50 zápasů Salmon Run.
  • /plány ukazuje nadcházející rotace.
  • /coop_schedules ukazuje nadcházející rotace Salmon Run.
  • /x_power_ranking/201101T00_201201T00/summary zobrazuje aktuální nejvyšší výkon X ve výsledkové tabulce a také aktuální výkon X.

Přístup ke službě Animal Crossing

Abychom měli přístup k Animal Crossing, musíme nejprve získat token webové služby.

(async () => {
    const sessionToken = await getSessionToken(params.session_token_code, authParams.codeVerifier);
    const webServiceToken = await getWebServiceTokenWithSessionToken(sessionToken, game='AC');
    const acTokens = await getCookiesForAnimalCrossing(webServiceToken.accessToken);

Jakmile přistoupíme ke koncovému bodu Animal Crossing, token webové služby bude uložen jako _gtoken . Tento soubor cookie potřebujeme k přístupu k uživatelskému rozhraní API pro jiný soubor cookie s názvem _park_session stejně jako ověřovací token.

const ACUrl = 'https://web.sd.lp1.acbaa.srv.nintendo.net';
let ACBearerToken;
let ACHeaders = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Encoding': 'gzip,deflate',
    'Content-Type': 'application/json; charset=utf-8',
    'User-Agent': userAgentString,
    'x-isappanalyticsoptedin': false,
    'X-Requested-With': 'com.nintendo.znca',
    'DNT': '0',
    Connection: 'keep-alive'
}

async function getCookiesForAnimalCrossing(accessToken) {
    const resp = await request({
        method: 'GET',
        uri: ACUrl,
        headers: Object.assign(ACHeaders, {'X-GameWebToken': accessToken}),
    });
    const animalCrossingTokens = await getAnimalCrossingTokens();
    return animalCrossingTokens;
}

async function getAnimalCrossingTokens() {
    const gToken = getCookie('_gtoken', ACUrl)
    if (gToken == null) {
        throw new Error('Could not get _gtoken for Animal Crossing');
    }
    jar.setCookie(request2.cookie(`_gtoken=${gToken}`), ACUrl);
    const userResp = await request({
        method: 'GET',
        uri: `${ACUrl}/api/sd/v1/users`,
        headers: ACHeaders,
        json: true
      });
      if (userResp !== null) {
        const userResp2 = await request({
            method: 'POST',
            uri: `${ACUrl}/api/sd/v1/auth_token`,
            headers: ACHeaders,
            form: {
                userId: userResp['users'][0]['id']
            },
            json: true
          });
          const bearer = userResp2;
          const parkSession = getCookie('_park_session', ACUrl);
          if (parkSession == null) {
              throw new Error('Could not get _park_session for Animal Crossing');
          }
          if (bearer == null || !bearer['token']) {
            throw new Error('Could not get bearer for Animal Crossing');
          }
         ACBearerToken = bearer['token']; // Used for Authorization Bearer in Header
         return {
             ac_g: gToken,
             ac_p: parkSession
         }
      }
}

Nyní můžeme zavolat API Animal Crossing!

Zde je výsledek /sd/v1/friends koncový bod, který obsahuje seznam všech vašich nejlepších přátel.

(async () => {
    ..
    const acTokens = await getCookiesForAnimalCrossing(webServiceToken.accessToken);
    const bestFriends = await getAnimalCrossingApi('sd/v1/friends');
    console.log('Best Friends', bestFriends);
})()

async function getAnimalCrossingApi(url) {
    const resp = await request({
      method: 'GET',
      uri: `${ACUrl}/api/${url}`,
      headers: Object.assign(ACHeaders, { Authorization: `Bearer ${ACBearerToken}`}),
      json: true,
      gzip: true
    });
    return resp;
}

Běžné koncové body překračování zvířat

  • /sd/v1/users zobrazuje jméno uživatele, ostrov, pasovou fotografii.
  • /sd/v1/users/:user_id/profile?language=cs-CZ zobrazuje pas jednoho uživatele.
  • /sd/v1/lands/:land_id/profile zobrazuje ostrovní data.
  • /sd/v1/friends seznam nejlepších přátel a jejich informací.
  • /sd/v1/messages odešle zprávu nebo reakci ve hře s dotazem POST.

Tělo požadavku POST pro odesílání zpráv :

{
  "body": "Sweet",
  "type": "keyboard"
}

Tělo požadavku POST pro odeslání reakcí :

{
  "body": "Aha",
  "type": "emoticon"
}

Seznam reakčních hodnot

Obnovení tokenů a souborů cookie

Jakmile vyprší platnost tokenu webové služby, můžeme získat nový s naším počátečním Tokenem Nintendo Session . Obvykle není potřeba se znovu přihlašovat.

Souhrn

  • Nintendo Switch API umožňuje aplikacím komunikovat s informacemi o hře a uživateli.
  • K získání přístupového tokenu, který lze použít k získání tokenu webové služby, je vyžadováno ověření uživatele.
  • Pomocí tokenu webové služby můžeme generovat soubory cookie specifické pro hru pro přístup k rozhraní API hry.

Příklady projektů

Splatnet/Music Bot:Robot Discord, který uživatelům umožňuje ukázat svůj Animal Crossing Passport a své pozice Splatoon 2.

Squid Tracks:Plně funkční desktopový klient pro Splatoon 2. Nedávno jsem pomohl aktualizovat logiku ověřování pro tuto aplikaci, aby byla znovu spuštěna.

Splatnet Desktop:Jednoduchá elektronová aplikace, kterou jsem napsal pro přístup ke SplatNet na ploše s přímou autentizací.

Splatoon2.Ink:Webová stránka, která zobrazuje aktuální fáze Splatoon 2.

Widget pro streamování:Widget, který zobrazuje výsledky zápasů Splatoon 2.

Poznámky

  1. Aktuální metoda zahrnuje odeslání požadavku na jiný server než Nintendo (pro příznaky f)
  2. Herní soubory cookie můžete získat ručně pomocí mitmproxy

Reference

  • Nintendo Switch REST API
  • splatnet2statink