nso.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import base64
  2. import json
  3. import pickle
  4. import time
  5. import uuid
  6. from enum import Enum
  7. import keyring
  8. import requests
  9. from nso_bridge import __version__
  10. from nso_bridge.metadata import ZNCA_PLATFORM, ZNCA_USER_AGENT, ZNCA_VERSION
  11. from nso_bridge.models import Imink
  12. from nso_bridge.models.accounts import Accounts, Login, ServiceToken
  13. from nso_bridge.models.friends import FriendCode, Friends
  14. from nso_bridge.models.response import ErrorResponse
  15. from nso_bridge.models.user_info import UserInfo
  16. from nso_bridge.models.users import CurrentUser
  17. from nso_bridge.nsa import NintendoSwitchAccount
  18. from nso_bridge.utils import check_friend_code_hash, is_friend_code
  19. class IminkType(Enum):
  20. NSO = 1
  21. APP = 2
  22. class mAPI:
  23. def __init__(self, token: str, step: IminkType):
  24. """Initialize the API object for the given token and step .
  25. Args:
  26. token (str): ID Token or Access Token
  27. step (IminkType): Session type, NSO or APP.
  28. """
  29. self.token = token
  30. self.step = step
  31. self.api_header = {
  32. "User-Agent": f"Nintendo_Switch_Online_Bridge/{__version__}",
  33. "Content-Type": "application/json; charset=utf-8",
  34. }
  35. self.api_body = {"token": token, "hashMethod": step.value}
  36. self.api_url = "https://api.imink.app/f"
  37. def get_response(self) -> Imink:
  38. """Get the response from the API .
  39. Raises:
  40. Exception: Raises an exception if the response is not successful.
  41. Returns:
  42. Imink: Returns the Imink object.
  43. """
  44. api_resp = requests.post(
  45. url=self.api_url, data=json.dumps(self.api_body), headers=self.api_header
  46. )
  47. if api_resp.status_code != 200:
  48. raise Exception(f"{api_resp.json()}")
  49. rs = api_resp.json()
  50. return Imink(**rs)
  51. class NintendoSwitchOnlineLogin:
  52. """Login to Nintendo Switch Online"""
  53. def __init__(
  54. self,
  55. guid: str,
  56. user_info: UserInfo,
  57. user_lang: str,
  58. access_token: str,
  59. id_token: str,
  60. ) -> None:
  61. """
  62. Args:
  63. guid (str): GUID of the user.
  64. user_info (UserInfo): User info.
  65. user_lang (str): User language.
  66. access_token (str): Access token.
  67. """
  68. self.headers = {
  69. "X-Platform": ZNCA_PLATFORM,
  70. "X-ProductVersion": ZNCA_VERSION,
  71. "Accept-Language": user_lang,
  72. "User-Agent": ZNCA_USER_AGENT,
  73. "Authorization": "Bearer",
  74. "Content-Type": "application/json; charset=utf-8",
  75. "Host": "api-lp1.znc.srv.nintendo.net",
  76. }
  77. self.url = "https://api-lp1.znc.srv.nintendo.net/v3/Account/Login"
  78. self.timestamp = int(time.time())
  79. self.guid = guid
  80. self.user_info: UserInfo | None = user_info
  81. self.access_token = access_token
  82. self.id_token = id_token
  83. self._imink_nso = mAPI(token=self.id_token, step=IminkType.NSO).get_response()
  84. self.account: Accounts | None = None
  85. self.body = {
  86. "parameter": {
  87. "f": self._imink_nso.f,
  88. "naIdToken": self.id_token,
  89. "timestamp": self._imink_nso.timestamp,
  90. "requestId": self._imink_nso.request_id,
  91. "naCountry": self.user_info.country,
  92. "naBirthday": self.user_info.birthday,
  93. "language": self.user_info.language,
  94. },
  95. "requestId": str(uuid.uuid4()),
  96. }
  97. def to_account(self) -> Accounts:
  98. """Convert the login response to an account object.
  99. Returns:
  100. Accounts: Returns the account object.
  101. Raises:
  102. Exception: Raises an exception if the response is not successful.
  103. """
  104. response = requests.post(url=self.url, headers=self.headers, json=self.body)
  105. if response.status_code != 200:
  106. raise Exception(f"Error: {response.status_code}")
  107. self.account = Accounts(**response.json())
  108. return self.account
  109. class NintendoSwitchOnlineAPI:
  110. """Nintendo Switch Online API."""
  111. def __init__(
  112. self,
  113. session_token: str | None = None,
  114. user_lang: str = "en-US",
  115. nso_app_version: str | None = None,
  116. ):
  117. """
  118. Args:
  119. session_token: The session token.
  120. user_lang: The user language.
  121. nso_app_version: The Nintendo Switch Online app version.
  122. """
  123. self.nsa = NintendoSwitchAccount()
  124. self.nso_app_version = ZNCA_VERSION or nso_app_version
  125. self.url = "https://api-lp1.znc.srv.nintendo.net"
  126. self.headers = {
  127. "X-Platform": ZNCA_PLATFORM,
  128. "X-ProductVersion": ZNCA_VERSION or self.nso_app_version,
  129. "User-Agent": ZNCA_USER_AGENT,
  130. "Content-Type": "application/json; charset=utf-8",
  131. "Host": "api-lp1.znc.srv.nintendo.net",
  132. }
  133. self.user_lang = user_lang
  134. if session_token is None:
  135. session_token = self.nsa.nso_login(self.nsa.m_input)
  136. self.guid = str(uuid.uuid4())
  137. self.token: ServiceToken = self.nsa.get_service_token(
  138. session_token=session_token
  139. )
  140. self.id_token = self.token.id_token
  141. self.access_token = self.token.access_token
  142. self.user_info = self.nsa.get_user_info(self.access_token)
  143. self.login: Login = Login(**{"login": None, "time": 0})
  144. self.NSOL = NintendoSwitchOnlineLogin(
  145. guid=self.guid,
  146. user_info=self.user_info,
  147. user_lang=self.user_lang,
  148. access_token=self.access_token,
  149. id_token=self.id_token,
  150. )
  151. def imink_app(self):
  152. """Get the Imink APP object.
  153. Returns:
  154. Imink: Returns the Imink object.
  155. """
  156. return mAPI(token=self.access_token, step=IminkType.APP).get_response()
  157. def getAnnouncements(self):
  158. """Get information of announcements."""
  159. resp = requests.post(
  160. url=self.url + "/v1/Announcement/List", headers=self.headers
  161. )
  162. if resp.status_code != 200:
  163. raise Exception(f"Error: {resp.status_code}")
  164. return resp.json()
  165. # Web Service API
  166. def getWebServices(self):
  167. """Get information of web services registered to Nintendo Switch account."""
  168. resp = requests.post(
  169. url=self.url + "/v1/Game/ListWebServices", headers=self.headers
  170. )
  171. if resp.status_code != 200:
  172. raise Exception(f"Error: {resp.status_code}")
  173. return resp.json()
  174. def getGameWebServiceToken(self, game_id: int):
  175. _imink_app = self.imink_app()
  176. resp = requests.post(
  177. url=self.url + "/v2/Game/GetWebServiceToken",
  178. json={
  179. "parameter": {
  180. "id": game_id,
  181. "f": _imink_app.f,
  182. "registrationToken": self.access_token,
  183. "requestId": _imink_app.request_id,
  184. "timestamp": _imink_app.timestamp,
  185. },
  186. "requestId": str(uuid.uuid4()),
  187. },
  188. headers=self.headers,
  189. )
  190. if resp.status_code != 200:
  191. raise Exception(f"Error: {resp.status_code}")
  192. return resp.json()
  193. def getActiveEvent(self):
  194. """Get information of active events."""
  195. resp = requests.post(
  196. url=self.url + "/v1/Event/GetActiveEvent", headers=self.headers
  197. )
  198. if resp.status_code != 200:
  199. raise Exception(f"Error: {resp.status_code}")
  200. return resp.json()
  201. def getEvent(self, user_id: int):
  202. """Get information of events."""
  203. resp = requests.post(
  204. url=self.url + "/v1/Event/Show",
  205. headers=self.headers,
  206. json={
  207. "parameter": {"id": user_id},
  208. "requestId": str(uuid.uuid4()),
  209. },
  210. )
  211. if resp.status_code != 200:
  212. raise Exception(f"Error: {resp.status_code}")
  213. return resp.json()
  214. def getUser(self, user_id: int):
  215. """Get information of user."""
  216. resp = requests.post(
  217. url=self.url + "/v3/User/Show",
  218. headers=self.headers,
  219. json={
  220. "parameter": {"id": user_id},
  221. "requestId": str(uuid.uuid4()),
  222. },
  223. )
  224. if resp.status_code != 200:
  225. raise Exception(f"Error: {resp.status_code}")
  226. return resp.json()
  227. def getCurrentUser(self) -> (CurrentUser | ErrorResponse):
  228. """Get information of My Nintendo Switch Account."""
  229. resp = requests.post(url=self.url + "/v3/User/ShowSelf", headers=self.headers)
  230. if resp.status_code != 200:
  231. raise Exception(f"Error: {resp.status_code}")
  232. resp = resp.json()
  233. if resp["status"] != 0:
  234. return ErrorResponse(**resp)
  235. else:
  236. return CurrentUser(**resp)
  237. def getCurrentUserPermissions(self):
  238. """Get information of current user permissions."""
  239. resp = requests.post(
  240. url=self.url + "/v3/User/Permissions/ShowSelf", headers=self.headers
  241. )
  242. if resp.status_code != 200:
  243. raise Exception(f"Error: {resp.status_code}")
  244. return resp.json()
  245. def getFriends(self) -> (Friends | ErrorResponse):
  246. """Get information of friends registered to Nintendo Switch account."""
  247. resp = requests.post(url=self.url + "/v3/Friend/List", headers=self.headers)
  248. if resp.status_code != 200:
  249. raise Exception(f"Error: {resp.status_code}")
  250. resp = resp.json()
  251. if resp["status"] != 0:
  252. return ErrorResponse(**resp)
  253. else:
  254. return Friends(**resp)
  255. def getFriendCodeUrl(self) -> (FriendCode | ErrorResponse):
  256. """Get information of friend code URL."""
  257. resp = requests.post(
  258. url=self.url + "/v3/Friend/CreateFriendCodeUrl", headers=self.headers
  259. )
  260. if resp.status_code != 200:
  261. raise Exception(f"Error: {resp.status_code}")
  262. resp = resp.json()
  263. if resp["status"] != 0:
  264. return ErrorResponse(**resp)
  265. return FriendCode(**resp)
  266. def getUserByFriendCode(self, friend_code: str, _hash: str | None = None):
  267. if not is_friend_code(friend_code):
  268. raise Exception("Invalid friend code")
  269. if hash is not None:
  270. if not check_friend_code_hash(_hash):
  271. raise Exception("Invalid hash")
  272. else:
  273. resp_hash = requests.post(
  274. url=self.url + "/v3/Friend/GetUserByFriendCodeHash",
  275. headers=self.headers,
  276. json={
  277. "parameter": {
  278. "friendCode": friend_code,
  279. "friendCodeHash": _hash,
  280. },
  281. "requestId": str(uuid.uuid4()),
  282. },
  283. )
  284. if resp_hash.status_code != 200:
  285. raise Exception(f"Error: {resp_hash.status_code}")
  286. else:
  287. resp = requests.post(
  288. url=self.url + "/v3/Friend/GetUserByFriendCode",
  289. headers=self.headers,
  290. json={
  291. "parameter": {
  292. "friendCode": friend_code,
  293. },
  294. "requestId": str(uuid.uuid4()),
  295. },
  296. )
  297. if resp.status_code != 200:
  298. raise Exception(f"Error: {resp.status_code}")
  299. return resp.json()
  300. def sendFriendRequest(self, nsa_id: int):
  301. """Send friend request."""
  302. resp = requests.post(
  303. url=self.url + "/v3/FriendRequest/Create",
  304. headers=self.headers,
  305. json={
  306. "parameter": {"nsaId": nsa_id},
  307. "requestId": str(uuid.uuid4()),
  308. },
  309. )
  310. if resp.status_code != 200:
  311. raise Exception(f"Error: {resp.status_code}")
  312. return resp.json()
  313. def addFavouriteFriend(self, nsa_id: int):
  314. """Add favourite friend."""
  315. resp = requests.post(
  316. url=self.url + "/v3/Friend/Favorite/Create",
  317. headers=self.headers,
  318. json={
  319. "parameter": {"nsaId": nsa_id},
  320. "requestId": str(uuid.uuid4()),
  321. },
  322. )
  323. if resp.status_code != 200:
  324. raise Exception(f"Error: {resp.status_code}")
  325. return resp.json()
  326. def removeFavouriteFriend(self, nsa_id: int):
  327. """Remove favourite friend."""
  328. resp = requests.post(
  329. url=self.url + "/v3/Friend/Favorite/Delete",
  330. headers=self.headers,
  331. json={
  332. "parameter": {"nsaId": nsa_id},
  333. "requestId": str(uuid.uuid4()),
  334. },
  335. )
  336. if resp.status_code != 200:
  337. raise Exception(f"Error: {resp.status_code}")
  338. return resp.json()
  339. def getToken(self):
  340. parameters = {
  341. "parameter": {
  342. "naBirthday": self.user_info.birthday,
  343. "timestamp": self.NSOL._imink_nso.timestamp,
  344. "f": self.NSOL._imink_nso.f,
  345. "requestId": self.NSOL._imink_nso.request_id,
  346. "naIdToken": self.token.id_token,
  347. },
  348. "requestId": str(uuid.uuid4()),
  349. }
  350. resp = requests.post(
  351. url=self.url + "/v3/Account/GetToken",
  352. headers=self.headers,
  353. json=parameters,
  354. )
  355. if resp.status_code != 200:
  356. raise Exception(f"Error: {resp.status_code}")
  357. return resp.json()
  358. def sync_login(self):
  359. wasc_access_token = keyring.get_password("nso-bridge", "login")
  360. wasc_time = keyring.get_password("nso-bridge", "wasc_time")
  361. if wasc_time is None:
  362. wasc_time = 0.0
  363. if wasc_access_token is not None:
  364. self.login = Login(
  365. **dict(
  366. pickle.loads(base64.b64decode(wasc_access_token.encode("utf-8")))
  367. )
  368. )
  369. if time.time() - int(float(wasc_time)) > 7200:
  370. return self.refresh_login()
  371. else:
  372. self.access_token = (
  373. self.login.login.result.webApiServerCredential.accessToken
  374. )
  375. self.headers["Authorization"] = f"Bearer {self.access_token}"
  376. else:
  377. if time.time() - int(float(wasc_time)) > 7200:
  378. return self.refresh_login()
  379. def refresh_login(self):
  380. login = self.NSOL.to_account()
  381. self.login = Login(
  382. **{
  383. "login": login,
  384. "time": time.time(),
  385. }
  386. )
  387. try:
  388. self.access_token = (
  389. self.login.login.result.webApiServerCredential.accessToken
  390. )
  391. except Exception:
  392. time.sleep(60)
  393. return self.refresh_login()
  394. self.headers["Authorization"] = f"Bearer {self.access_token}"
  395. keyring.set_password(
  396. "nso-bridge",
  397. "login",
  398. base64.b64encode(pickle.dumps(self.login)).decode("utf-8"),
  399. )
  400. keyring.set_password(
  401. "nso-bridge",
  402. "wasc_access_token",
  403. self.login.login.result.webApiServerCredential.accessToken,
  404. )
  405. keyring.set_password("nso-bridge", "wasc_time", str(self.login.time))