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();
}
@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