Skip to content

Instantly share code, notes, and snippets.

@saiteja09
Last active May 18, 2024 02:26
Show Gist options
  • Save saiteja09/ef9047d9b5bf63eab55e13d83cd46fb4 to your computer and use it in GitHub Desktop.
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
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();
}
@saiteja09
Copy link
Author

saiteja09 commented Jan 31, 2023

Screenshot

IMG_9914

Instructions

Get Client ID, Client Secret and Refresh Token

  1. The first order of the process here is to be able to Sign into Microsoft account using OAuth 2.0, to get an access token and refresh token. To do that, first you need to create an application in Azure. I am assuming you have an account in Azure, if not you can Sign up for free.
  2. Go to Azure Portal and search for App Registrations.
  3. In App Registrations, Click on New Registration.
  4. Provide a name for your application and choose Personal Microsoft Accounts only.
  5. For Redirect URI, choose platform as Web and provide https://oauth.pstmn.io/v1/callback as the value. This is important, as we will be using Postman to generate Access and Refresh Token.
  6. Click on Register
  7. Once the App is registered, go to the App and under Overview tab, copy and save the Application (client) ID.
  8. Next, go to Certificates & secrets, click on New client secret, choose when you want the secret to expire and click on Add to generate client secret.
  9. Copy the Client Secret value and save it.
  10. With Client ID and Client Secret in our hands, the next step is to Sign In and generate Refresh tokens. We will use Postman to help with this.
  11. Open Postman and create a New Request/ Collection.
  12. Under Authorization, change the type to OAuth 2.0
  13. Change the Grant Type to Authorization Code.
  14. Under 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.
  15. Set Auth URL as https://login.live.com/oauth20_authorize.srf
  16. Set Access Token URL as https://login.live.com/oauth20_token.srf
  17. Set Client ID and Client Secret to the values we obtained from previous section.
  18. Set Scope as Xboxlive.signin Xboxlive.offline_access
  19. Your configuration should like below at this point.
    image
  20. Click on Get New Access Token button, which should launch the browser in postman for you to Sign In with your account.
  21. Complete your Sign In with your credentials and if it’s successful, Postman should now show you the Access Token and Refresh Token that got generated.
  22. Copy and Save the Refresh Token securely.

Save Client ID, Client Secret and Refresh Token to KeyChain

  1. As Client ID, Client Secret and Refresh Token are sensitive information, the script uses Keychain to read the values. You need to save these to Keychain before you can use the script.
  2. Create a new Scriptable file and save the Client ID to xbox_clientid key using the below code
    Keychain.set('xbox_clientid', '<your client id>')
  3. Save Client Secret to xbox_clientsecret to keychain using the below code.
    Keychain.set('xbox_clientsecret', '<your client secret>')
  4. Save Refresh Token to 'xbox_refreshtoken' to keychain using the below code.
    Keychain.set('xbox_refreshtoken', '<your refresh token>')

Run the Script

  1. Copy the Script to a new file and run it.

@jnnsrctr
Copy link

jnnsrctr commented Feb 1, 2023

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

@saiteja09
Copy link
Author

@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