Skip to content

Instantly share code, notes, and snippets.

@heptal
Last active December 20, 2024 06:23
Show Gist options
  • Save heptal/40583dafe384d72997f60b8e1c962a90 to your computer and use it in GitHub Desktop.
Save heptal/40583dafe384d72997f60b8e1c962a90 to your computer and use it in GitHub Desktop.
Recent Discord channel/DM messages shown in iOS14 widget - built with Scriptable.app
// set up keychain and token
const namespace = "dscrd.wdgt"
const token = `${namespace}.usertoken`
if (!Keychain.contains(token) || !Keychain.get(token)) {
let alert = new Alert()
alert.title = "Enter Discord User Token"
alert.addTextField("<discord_token>")
await alert.presentAlert()
Keychain.set(token, alert.textFieldValue(0) || "")
}
// discord API fetch
const CDN = "https://cdn.discordapp.com"
const BASE = "https://canary.discordapp.com/api/v6/"
const api = async (path, verb = "GET", opts = {}) => {
let req = new Request(BASE + path);
req.method = verb;
req.headers = {
'authorization': Keychain.get(token)
}
return await req.loadJSON()
}
const me = await api(`users/@me`)
const $G = `${namespace}.${me.id}.guild`
const $C = `${namespace}.${me.id}.channel`
// data resources
const getGuilds = () => api(`users/@me/guilds`)
const getDMs = () => api(`users/@me/channels`)
const getChannel = id => api(`channels/${id}`)
const getGuild = id => api(`guilds/${id}`)
const getChannels = id => api(`guilds/${id}/channels`)
const getMessages = id => api(`channels/${id}/messages`)
// guild and user images
const banner = g => `${CDN}/banners/${g.id}/${g.banner}.png`
const icon = g => `${CDN}/icons/${g.id}/${g.icon}.png`
const avatar = u => u.avatar ?
`${CDN}/avatars/${u.id}/${u.avatar}?size=64` :
`${CDN}/embed/avatars/${u.discriminator % 5}.png`
// pick a DM convo or guild channel
async function picker() {
let table = new UITable()
let makerow = (title, url) => {
let row = new UITableRow()
row.backgroundColor = new Color("#36393f")
row.height = 48
row.cellSpacing = 8
row.addImageAtURL(url).widthWeight = 15
let txt = row.addText(title)
txt.widthWeight = 85
txt.titleColor = Color.white()
txt.titleFont = Font.boldRoundedSystemFont(18)
return row
}
let dms = makerow("DM Convos")
dms.onSelect = () => Keychain.set($G, "")
table.addRow(dms)
for (let guild of (await getGuilds())) {
let row = makerow(guild.name, icon(guild))
row.onSelect = () => Keychain.set($G, guild.id)
table.addRow(row)
}
return table.present(true).then(async function () {
table.removeAllRows()
let channels = (Keychain.get($G) ?
(await getChannels(Keychain.get($G)))
.filter(c => c.type === 0) :
(await getDMs()).filter(dm => dm.type === 1)
.map(dm => ({
...dm,
name: dm.recipients[0].username,
av: avatar(dm.recipients[0])
}))
)
.filter(c => !!c.last_message_id)
.sort((a, b) => b.last_message_id.localeCompare(a.last_message_id))
for (let c of channels) {
let row = makerow(c.name, c.av)
row.onSelect = () => Keychain.set($C, c.id)
table.addRow(row)
}
return table.present(true).then(function () {
console.log("guild id:\t" + Keychain.get($G))
console.log("channel id:\t" + Keychain.get($C))
})
})
}
// build widget for chosen channel
async function createWidget() {
let wdgt = new ListWidget()
let df = new DateFormatter()
df.dateFormat = "hh:mm"
let truncate = false
let shadow = el => {
el.shadowColor = Color.black()
el.shadowRadius = 4
el.shadowOffset = new Point(1, 2)
}
let gradient = new LinearGradient()
gradient.locations = [0, 1]
gradient.colors = [new Color("66666699"), new Color("99336699")]
wdgt.backgroundGradient = gradient
if (!!Keychain.get($G)) {
let gld = await getGuild(Keychain.get($G))
let bgUrl = gld.banner ? banner(gld) : icon(gld)
let bgImage = new Request(bgUrl)
wdgt.backgroundImage = await bgImage.loadImage()
}
let items = await getMessages(Keychain.get($C))
if (!Array.isArray(items)) {
let txt = wdgt.addText(JSON.stringify(items))
txt.textColor = Color.white()
txt.centerAlignText()
wdgt.backgroundColor = Color.red()
return wdgt
}
items = items.reverse().slice(-7)
console.log(JSON.stringify(items, null, 2))
for (let item of items) {
let {
author,
content,
timestamp
} = item
let ts = df.string(new Date(Date.parse(timestamp)))
let stack = wdgt.addStack()
// profile pic
let avRequest = new Request(avatar(author))
let avImage = await avRequest.loadImage()
let pfp = stack.addImage(avImage)
pfp.imageSize = new Size(18, 18)
pfp.cornerRadius = 10
pfp.leftAlignImage()
shadow(pfp)
stack.addSpacer(5)
let ms = stack.addStack()
ms.layoutVertically()
if (truncate) {
let lines = Math.ceil(content.length / 60)
ms.size = new Size(0, 20 * lines)
}
// username
let user = ms.addText(author.username)
user.font = Font.mediumMonospacedSystemFont(8)
user.textColor = Color.cyan()
user.leftAlignText()
shadow(user)
// message
let msg = ms.addText(content)
msg.font = Font.heavyRoundedSystemFont(10)
msg.textColor = Color.white()
msg.lineLimit = truncate ? 3 : 0
msg.leftAlignText()
shadow(msg)
stack.addSpacer()
// timestamp
let d = stack.addText(ts)
d.font = Font.mediumMonospacedSystemFont(9)
d.textColor = Color.white()
d.rightAlignText()
stack.topAlignContent()
}
return wdgt
}
async function refresh() {
await createWidget().then(wdgt => {
config.runsInApp && wdgt.presentMedium()
Script.setWidget(wdgt)
Script.complete()
})
}
if (config.runsInWidget) {
await refresh()
} else {
let alert = new Alert()
alert.addAction("Select Displayed Chat")
alert.addAction("Open Discord.app")
alert.addCancelAction("Cancel")
switch (await alert.presentAlert()) {
case 0:
await picker()
await refresh()
break;
case 1:
Safari.open("https://discord.gg")
break;
}
Script.complete()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment