Skip to content

Instantly share code, notes, and snippets.

@simonbs
Created January 31, 2022 15:19
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simonbs/4de456dc344748d8048b923baf0edf3f to your computer and use it in GitHub Desktop.
Save simonbs/4de456dc344748d8048b923baf0edf3f to your computer and use it in GitHub Desktop.
Shows the total number of GitHub sponsors and the latest sponsors in a widget using the Scriptable app
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: brown; icon-glyph: hand-holding-usd;
// To use this script, you must create a personal access token on GitHub with the `read:org// scope.
// Follow the instructions on the link below to create your personal access token.
//
// https://github.com/settings/tokens
//
// Run the script when you ahve created your personal access token. It'll prompt you to enter the token and store it securely in the keychain on the device.
// Keychain key for the access token
let ACCESS_TOKEN_KEY = "github.sponsors.accessToken"
if (config.runsInWidget) {
if (Keychain.contains(ACCESS_TOKEN_KEY)) {
let widget = await createWidget()
Script.setWidget(widget)
Script.complete()
} else {
let widget = await createNotConfiguredWidget()
Script.setWidget(widget)
Script.complete()
}
} else if (Keychain.contains(ACCESS_TOKEN_KEY)) {
// Present the main menu.
await presentMenu()
} else {
// Prompt to enter the client ID and client secret and then start the authorization flow.
await promptForPersonalAccessToken()
await presentMenu()
}
// Presents the main menu.
async function presentMenu() {
let alert = new Alert()
alert.addAction("Preview Widget")
alert.addDestructiveAction("Remove Personal Access Token")
alert.addCancelAction("Cancel")
let idx = await alert.presentAlert()
if (idx == 0) {
let widget = await createWidget()
await widget.presentMedium()
} else if (idx == 1) {
await confirmRemovePersonalAccessToken()
}
}
// Creates and returns the widget.
async function createWidget() {
let isSmallWidget = config.widgetFamily == "small"
let latestSponsorCount = isSmallWidget ? 3 : 8
let totalCountData = await loadTotalSponsorCount()
let latestSponsorsData = await loadLatestSponsors(latestSponsorCount)
let latestSponsors = latestSponsorsData.latestSponsors
let widget = new ListWidget()
let wTitle = widget.addText("GitHub Sponsors")
wTitle.font = Font.mediumSystemFont(isSmallWidget ? 12 : 16)
widget.addSpacer()
if (isSmallWidget) {
let wTotalCount = widget.addText(totalCountData.totalCount + "")
wTotalCount.font = Font.semiboldSystemFont(32)
} else {
let wTotalCount = widget.addText(totalCountData.totalCount + " sponsors")
wTotalCount.font = Font.semiboldSystemFont(32)
}
if (latestSponsors != null && latestSponsors.length > 0) {
widget.addSpacer()
let avatarStack = widget.addStack()
avatarStack.spacing = -6
for (let latestSponsor of latestSponsors) {
let avatarImage = await loadImage(latestSponsor.avatarUrl)
let wAvatarImage = avatarStack.addImage(avatarImage)
wAvatarImage.imageSize = new Size(40, 40)
wAvatarImage.cornerRadius = 20
wAvatarImage.url = latestSponsor.url
}
}
return widget
}
// Creates widget showing that the script is not configured.
function createNotConfiguredWidget() {
let scriptName = Script.name()
let widget = new ListWidget()
let wText = widget.addText("The script haven't been configured. Please run \"" + scriptName + "\" in Scriptable to configure the script.")
wText.minimumScaleFactor = 0.2
widget.url = "scriptable:///run?scriptName=" + encodeURIComponent(scriptName)
return widget
}
// Loads latest new sponsors from GraphQL.
async function loadLatestSponsors(count) {
let query = `query {
viewer {
sponsorsActivities(last: 25, orderBy: {field: TIMESTAMP, direction: DESC}) {
edges {
node {
action
sponsor {
... on User {
name
avatarUrl(size:400)
url
}
... on Organization {
name
avatarUrl(size:400)
url
}
}
}
}
}
}
}`
let obj = await loadDataForQuery(query)
let edges = obj.data.viewer.sponsorsActivities.edges
let latestSponsors = edges
.filter(edge => {
return edge.node.action == "NEW_SPONSORSHIP"
})
.slice(0, count)
.map(edge => {
return edge.node.sponsor
})
return {
"latestSponsors": latestSponsors
}
}
// Loads total sponsor count from GraphQL.
async function loadTotalSponsorCount() {
let query = `query {
viewer {
sponsors {
totalCount
}
}
}`
let obj = await loadDataForQuery(query)
return {
"totalCount": obj.data.viewer.sponsors.totalCount
}
}
// Sends GraphQL query to GitHub's API and returns data.
async function loadDataForQuery(query) {
let rawQuery = query.replace(/\n/g, "")
let accessToken = Keychain.get(ACCESS_TOKEN_KEY)
let url = "https://api.github.com/graphql"
let request = new Request(url)
request.method = "POST"
request.body = JSON.stringify({"query": rawQuery})
request.headers = {
"Authorization": "Bearer " + accessToken
}
return await request.loadJSON()
}
// Load image at given URL
async function loadImage(url) {
let request = new Request(url)
return await request.loadImage()
}
// Asks the user if they really want to remove the stored personal access token.
async function confirmRemovePersonalAccessToken() {
let alert = new Alert()
alert.title = "Remove Personal Access Token"
alert.message = "Are you sure you want to remove the personal access token from your keychain? If you remove the personal access token, you'll need to enter it again the next time you run the script."
alert.addDestructiveAction("Yes, remove token")
alert.addCancelAction("Cancel")
let idx = await alert.presentAlert()
if (idx == 0) {
removeCredentials()
}
}
// Prompts the user to enter their personal access token.
async function promptForPersonalAccessToken() {
let accessToken = await promptForValue(
"Personal Access Token",
"Paste the personal access token you created on GitHub.",
"Personal Access Token",
null)
Keychain.set(ACCESS_TOKEN_KEY, accessToken)
}
// Removes all stored credentials.
function removeCredentials() {
Keychain.remove(ACCESS_TOKEN_KEY)
}
// Presents an alert where the user can enter a value in a text field. Returns the entered value.
async function promptForValue(title, message, placeholder, value) {
let alert = new Alert()
alert.title = title
alert.message = message
alert.addTextField(placeholder, value)
alert.addAction("OK")
alert.addCancelAction("Cancel")
let idx = await alert.present()
if (idx != -1) {
return alert.textFieldValue(0)
} else {
throw new Error("Cancelled entering value")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment