Last active
May 18, 2024 02:26
-
-
Save saiteja09/ef9047d9b5bf63eab55e13d83cd46fb4 to your computer and use it in GitHub Desktop.
Widget for Yearly Xbox GamerScore Tracking for use with Scriptable app in iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let xbox_refreshtoken = null | |
let xbox_clientid = null | |
let xbox_clientsecret = null | |
let xbox_credential_base64 = null | |
let xbox_authorization = null | |
let xbox_id = null | |
let xbox_profileurl = 'https://peoplehub.xboxlive.com/users/me/people/xuids(<xid>)/decoration/detail,preferredColor,presenceDetail,multiplayerSummary' | |
let xbox_titleHistoryurl = 'https://titlehub.xboxlive.com/users/xuid(<xid>)/titles/titleHistory/decoration/GamePass,TitleHistory,Achievement,Stats' | |
let xbox_achievementsurl = 'https://achievements.xboxlive.com/users/xuid(<xid>)/achievements?orderBy=UnlockTime&unlockedOnly=true' | |
const xbox_tokenurl = 'https://login.live.com/oauth20_token.srf' | |
const xbox_live_authurl = 'https://user.auth.xboxlive.com/user/authenticate' | |
const xbox_live_xstsurl = 'https://xsts.auth.xboxlive.com/xsts/authorize' | |
const xbox_logourl = 'https://user-images.githubusercontent.com/8601809/202868884-b3b47156-8314-4022-ab96-aa1168437464.png' | |
const xbox_gs_logourl = 'https://user-images.githubusercontent.com/8601809/202863495-ae6c706b-66d9-47a8-b035-46c70dffec74.png' | |
const xbox_ach_logourl = 'https://user-images.githubusercontent.com/8601809/202863432-84bae84a-7025-4705-b2d9-8171f13ffb8b.png' | |
const quick_chart_url = 'https://quickchart.io/chart' | |
let numOfAchByMnth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | |
let sumofGscByMnth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | |
let sumGamerScore = 0 | |
let sumOfAch = 0 | |
// Read Refresh Token from Keychain | |
if (Keychain.contains('xbox_refreshtoken')) { | |
xbox_refreshtoken = Keychain.get('xbox_refreshtoken') | |
} else { | |
console.error('Refresh Token not found in Keychain. Please store Refresh Token in the key \'xbox_refreshtoken\'') | |
Script.complete() | |
} | |
// Read Client ID from Keychain | |
if (Keychain.contains('xbox_clientid')) { | |
xbox_clientid = Keychain.get('xbox_clientid') | |
} else { | |
console.error('Client ID not found in Keychain. Please store Client ID in the key \'xbox_clientid\'') | |
Script.complete() | |
} | |
// Read Client Secret from Keychain | |
if (Keychain.contains('xbox_clientsecret')) { | |
xbox_clientsecret = Keychain.get('xbox_clientsecret') | |
} else { | |
console.error('Client Secret not found in Keychain. Please store Client Secret in the key \'xbox_clientid\'') | |
Script.complete() | |
} | |
//Base 64 for ClientID and Client Secret | |
xbox_credential_base64 = 'Basic ' + btoa(xbox_clientid + ':' + xbox_clientsecret) | |
//Start Authentication | |
await authenticateWithXbox() | |
uPResp = await getUserProfile() | |
//Widget Rendering | |
xboxWidget = await renderWidget() | |
if (config.runsInWidget) { | |
Script.setWidget(xboxWidget) | |
} else { | |
xboxWidget.presentLarge() | |
} | |
Script.complete() | |
// Main function for Rendering widget | |
async function renderWidget() { | |
widget = new ListWidget() | |
widget.backgroundColor = new Color('#107C10') | |
firstStack = widget.addStack() | |
firstStack.centerAlignContent() | |
xboxlogo = firstStack.addImage(await getImageFromURL(xbox_logourl)) | |
xboxlogo.tintColor = Color.white() | |
xboxlogo.imageSize = new Size(100, 40) | |
firstStack.addSpacer() | |
uPImage = firstStack.addImage(await getImageFromURL(uPResp.people[0].displayPicRaw)) | |
uPImage.imageSize = new Size(30, 30) | |
uPImage.cornerRadius = 100 | |
firstStack.addSpacer(5) | |
uPGamerTag = firstStack.addText(uPResp.people[0].gamertag) | |
uPGamerTag.leftAlignText() | |
uPGamerTag.font = Font.boldRoundedSystemFont(15) | |
uPGamerTag.textColor = Color.white() | |
secondStack = widget.addStack(10) | |
secondStack.centerAlignContent() | |
secondStack.addText(' ') | |
secondStack.addSpacer() | |
tgsImage = secondStack.addImage(await getImageFromURL(xbox_gs_logourl)) | |
tgsImage.tintColor = Color.white() | |
tgsImage.imageSize = new Size(15, 15) | |
secondStack.addSpacer(5) | |
tgsTxt = secondStack.addText(uPResp.people[0].gamerScore) | |
tgsTxt.font = Font.boldRoundedSystemFont(14) | |
tgsTxt.textColor = Color.white() | |
secondStack.setPadding(0, 0, 10, 0) | |
thirdStack = widget.addStack() | |
thirdStack.centerAlignContent() | |
thirdStack.addSpacer() | |
gsBMnthTxt = thirdStack.addText('Yearly GamerScore Tracker') | |
gsBMnthTxt.font = Font.boldRoundedSystemFont(12) | |
gsBMnthTxt.textColor = Color.white() | |
thirdStack.addSpacer() | |
thirdStack.setPadding(0, 0, 10, 0) | |
await getAchievementsByMonth() | |
fourthStack = widget.addStack() | |
fourthStack.addImage(await getGamerScoreChart()) | |
fifthStack = widget.addStack() | |
fifthStack.setPadding(10, 0, 0, 0) | |
tgwTxt = fifthStack.addText("Total GamerScore Won \n" + sumGamerScore.toString()) | |
tgwTxt.font = Font.boldMonospacedSystemFont(12) | |
tgwTxt.textColor = Color.white() | |
fifthStack.addSpacer() | |
ngsTxt = fifthStack.addText("Num. Of Achievements Won \n" + sumOfAch.toString()) | |
ngsTxt.font = Font.boldMonospacedSystemFont(12) | |
ngsTxt.textColor = Color.white() | |
return widget | |
} | |
// Get GamerScore Chart from QuickChart | |
async function getGamerScoreChart() { | |
body = { | |
"version": "2", | |
"backgroundColor": "transparent", | |
"width": 500, | |
"height": 300, | |
"devicePixelRatio": 2.0, | |
"format": "png", | |
"chart": { | |
"type": "line", | |
"data": { | |
"labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |
"datasets": [{ | |
"data": sumofGscByMnth, | |
"fill": false, | |
"borderColor": "#fff", | |
"borderWidth": 5, | |
"pointRadius": 0, | |
"lineTension": 0.4 | |
}] | |
}, | |
"options": { | |
"legend": { | |
"display": false | |
}, | |
"scales": { | |
"xAxes": [{ | |
"display": true, | |
"gridLines": { | |
"display": false | |
}, | |
"ticks": { | |
"fontColor": "#fff", | |
"fontStyle": "bold" | |
} | |
}], | |
"yAxes": [{ | |
"display": true, | |
"gridLines": { | |
"display": false | |
}, | |
"ticks": { | |
"fontColor": "#fff", | |
"fontStyle": "bold" | |
} | |
}] | |
} | |
} | |
} | |
} | |
let req = new Request(quick_chart_url) | |
req.method = 'post' | |
req.headers = { | |
'Content-Type': 'application/json' | |
} | |
req.body = JSON.stringify(body) | |
return req.loadImage() | |
} | |
// Calculate Sum of GamerScore and Number of Achievements each month | |
async function getAchievementsByMonth() { | |
let breakwhile = false | |
let skip = 0 | |
while (1) { | |
achResp = await getUserAchievements(skip) | |
achievements = achResp.achievements | |
if (achievements.length == 0) { | |
break | |
} | |
for (let i = 0; i < achievements.length; i++) { | |
rewards = achievements[i].rewards | |
const d = new Date(achievements[i].progression.timeUnlocked) | |
if (d.getFullYear() == getCurrentYear()) { | |
for (let j = 0; j < rewards.length; j++) { | |
if (rewards[j].type == 'Gamerscore') { | |
numOfAchByMnth[d.getMonth()] = numOfAchByMnth[d.getMonth()] + 1 | |
sumofGscByMnth[d.getMonth()] = sumofGscByMnth[d.getMonth()] + parseInt(rewards[j].value) | |
sumGamerScore = sumGamerScore + parseInt(rewards[j].value) | |
sumOfAch++ | |
} | |
} | |
} | |
} | |
skip = skip + 1000; | |
} | |
} | |
// Call User Achievements Endpoint | |
async function getUserAchievements(skip) { | |
let url = xbox_achievementsurl.replace('<xid>', xbox_id) | |
url = url + '&maxItems=1000&skipItems=' + skip | |
let req = new Request(url) | |
req.headers = { | |
'Authorization': xbox_authorization, | |
'x-xbl-contract-version': '2', | |
'Content-Type': 'application/json' | |
} | |
return await req.loadJSON() | |
} | |
// GET GAME TITLES PLAYED BY USER | |
async function getUserTitleHistory() { | |
let url = xbox_titleHistoryurl.replace('<xid>', xbox_id) | |
let req = new Request(url) | |
req.headers = { | |
'Authorization': xbox_authorization, | |
'x-xbl-contract-version': '2', | |
'Content-Type': 'application/json' | |
} | |
return await req.loadJSON() | |
} | |
// READ XBOX PROFILE INFO | |
async function getUserProfile() { | |
let url = xbox_profileurl.replace('<xid>', xbox_id) | |
let req = new Request(url) | |
req.headers = { | |
'Authorization': xbox_authorization, | |
'x-xbl-contract-version': '3', | |
'Content-Type': 'application/json' | |
} | |
return await req.loadJSON() | |
} | |
// GET XSTS TOKEN, USER HASH AND XBOX ID | |
async function getXSTSAndUHS(xblt) { | |
let body = { | |
'Properties': { | |
'SandboxId': 'RETAIL', | |
'UserTokens': [xblt] | |
}, | |
'RelyingParty': 'http://xboxlive.com', | |
'TokenType': 'JWT' | |
} | |
let req = new Request(xbox_live_xstsurl) | |
req.method = 'post' | |
req.headers = { | |
'Content-Type': 'application/json' | |
} | |
req.body = JSON.stringify(body) | |
return req.loadJSON() | |
} | |
// GET XBOX LIVE TOKEN | |
async function getXBLToken(msat) { | |
let body = { | |
'Properties': { | |
'AuthMethod': 'RPS', | |
'RpsTicket': 'd=' + msat, | |
'SiteName': 'user.auth.xboxlive.com' | |
}, | |
'RelyingParty': 'http://auth.xboxlive.com', | |
'TokenType': 'JWT' | |
} | |
// console.log(JSON.stringify(body)) | |
let req = new Request(xbox_live_authurl) | |
req.method = 'post' | |
req.headers = { | |
'Content-Type': 'application/json' | |
} | |
req.body = JSON.stringify(body) | |
return await req.loadJSON() | |
} | |
// GET ACCESS TOKEN FROM MICROSOFT OAUTH2.0 | |
async function getMSAccessToken() { | |
let req = new Request(xbox_tokenurl) | |
req.method = 'POST' | |
req.headers = { | |
'Authorization': xbox_credential_base64, | |
'Content-Type': 'application/x-www-form-urlencoded' | |
} | |
req.body = 'grant_type=' + encodeURIComponent('refresh_token') + '&refresh_token=' + encodeURIComponent(xbox_refreshtoken) | |
return await req.loadJSON(); | |
} | |
//START AUTHENTICATION AND COLLECT INFO | |
async function authenticateWithXbox() { | |
msatr = await getMSAccessToken() | |
Keychain.set('xbox_refreshtoken', msatr.refresh_token) | |
xlatr = await getXBLToken(msatr.access_token) | |
xstsr = await getXSTSAndUHS(xlatr.Token) | |
xsts = xstsr.Token | |
uhs = xstsr.DisplayClaims.xui[0].uhs | |
xbox_id = xstsr.DisplayClaims.xui[0].xid | |
xbox_authorization = 'XBL3.0 x=' + uhs + ';' + xsts | |
} | |
// GET IMAGE FROM URL | |
async function getImageFromURL(url) { | |
let req = new Request(url) | |
return await req.loadImage() | |
} | |
//Get Current Year | |
function getCurrentYear() { | |
const d = new Date(); | |
return d.getFullYear(); | |
} |
Really cool widget, thank you for sharing and the detailed documentation!!
One hint, when you're using Postman Web, the Redirect URI needs to be https://oauth.pstmn.io/v1/browser-callback
@jnnsrctr Good callout for anyone using Postman Web.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screenshot
Instructions
Get Client ID, Client Secret and Refresh Token
App Registrations
.New Registration
.https://oauth.pstmn.io/v1/callback
as the value. This is important, as we will be using Postman to generate Access andRefresh Token
.Application (client) ID
.Certificates & secrets
, click onNew client secret
, choose when you want the secret to expire and click on Add to generate client secret.Client Secret
value and save it.Refresh tokens
. We will use Postman to help with this.OAuth 2.0
Authorization Code
.Callback URL
, check Authorize using browser. This will disable editing the Callback URL and it should be defaulted to the value we set when creating an Application in Azure.Auth URL
ashttps://login.live.com/oauth20_authorize.srf
Access Token URL
ashttps://login.live.com/oauth20_token.srf
Client ID
andClient Secret
to the values we obtained from previous section.Xboxlive.signin Xboxlive.offline_access
Access Token
andRefresh Token
that got generated.Refresh Token
securely.Save Client ID, Client Secret and Refresh Token to KeyChain
xbox_clientid
key using the below codeKeychain.set('xbox_clientid', '<your client id>')
xbox_clientsecret
to keychain using the below code.Keychain.set('xbox_clientsecret', '<your client secret>')
Keychain.set('xbox_refreshtoken', '<your refresh token>')
Run the Script