Files
Reddit-Widget/redditModule.js
T
2024-11-15 12:25:37 +01:00

613 lines
24 KiB
JavaScript

//=======================================//
//=========== START OF MODULE ===========//
//============= Version 1.1 =============//
// sends request to reddit-api
async function getKarmaFromAPI(numberFormatting){
let df = new DateFormatter()
df.dateFormat = 'MMMM dd, yyyy'
let fm = FileManager.iCloud()
let dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
let jsonPath = fm.joinPath(dir, 'UserCredentials.json')
let user, userData, postData;
try {
await fm.downloadFileFromiCloud(jsonPath)
user = await JSON.parse(fm.readString(jsonPath))
try {
// get access-token for api request
let reqToken = new Request('https://www.reddit.com/api/v1/access_token')
reqToken.method = 'POST'
reqToken.headers = {
'Authorization': 'Basic ' + btoa(user.CLIENT_ID + ":" + user.CLIENT_SECRET)
}
reqToken.body = `grant_type=password&username=${user.USERNAME}&password=${user.PASSWORD}`
token = await reqToken.loadJSON()
//console.warn(JSON.stringify(token, null, 1))
// get user datas with token
let reqUserData = new Request('https://oauth.reddit.com/api/v1/me')
reqUserData.headers = {
'user-Agent': 'getKarmaFromRedditBy-iamrbn/v1.4',
'Authorization': `${token.token_type} ${token.access_token}`
}
userData = await reqUserData.loadJSON()
//console.log(JSON.stringify(userData, null, 2))
let reqPostData = new Request(`https://oauth.reddit.com/user/${ user.USERNAME }/overview/new/?limit=7`)
reqPostData.headers = {
'user-Agent': 'getKarmaFromRedditBy-iamrbn/v1.4',
'Authorization': `${token.token_type} ${token.access_token}`
}
postData = await reqPostData.loadJSON()
//console.log(JSON.stringify(postData, null, 2))
let arr = []
for (i = 0; i < postData.data.children.length; i++){
arr.push(postData.data.children[i].data.created * 1000)
//log(arr)// always the 3th number is the biggest?
max = arr.reduce((a, b) => Math.max(a, b), -Infinity)
idx = arr.reduce((idxMax, element, index) => element > arr[idxMax] ? index : idxMax, 0)
}
//console.warn(idx + ': ' + max)
//idx += 1
//console.log(postData.data.children[idx])
//Declare account variables
totalKarma = Intl.NumberFormat('en-EN', {notation:'compact', maximumSignificantDigits: 2}).format(userData.total_karma)
postKarma = Intl.NumberFormat(numberFormatting).format(userData.link_karma)
commentKarma = Intl.NumberFormat(numberFormatting).format(userData.comment_karma)
awarderKarma = Intl.NumberFormat(numberFormatting).format(userData.awarder_karma)
awardeeKarma = Intl.NumberFormat(numberFormatting).format(userData.awardee_karma)
snoovatarImg = userData.snoovatar_img
usertitle = userData.subreddit.title
username = userData.subreddit.display_name_prefixed
puplicDescription = userData.subreddit.public_description
inboxCount = userData.inbox_count
profileURL = "https://reddit.com"+userData.subreddit.url.slice(0, -1)
profileCreated = df.string(new Date(userData.created*1000))
minutesDiff = Math.floor((new Date(Date.now()).getTime() - new Date(userData.created * 1000).getTime()) / 1000 / 60)
accountAge = (minutesDiff < 525600) ? Math.abs(minutesDiff/60/24).toFixed(0)+" d" : Math.abs(minutesDiff/60/24/365).toFixed(1)+" y"
//console.log({snoovatarImg})
//Declare post variables
sub = postData.data.children[idx].data.subreddit_name_prefixed
subURL = 'https://reddit.com/' + sub
upvotes = Intl.NumberFormat(numberFormatting).format(postData.data.children[idx].data.score).toString()
downvotes = Intl.NumberFormat(numberFormatting).format(postData.data.children[idx].data.downs)
numComments = Intl.NumberFormat(numberFormatting).format(postData.data.children[idx].data.num_comments).toString()
postCreated = df.string(new Date(postData.data.children[idx].data.created * 1000))
kind = postData.data.children[idx].kind // 't1_' = comment / 't2_' = account / 't3_' = link / 't4_' = message / 't5_' = subreddit / 't6_' = award
author = 'u/' + postData.data.children[idx].data.author
if (kind.includes('t1')){ // comment
title = postData.data.children[idx].data.link_title
body = postData.data.children[idx].data.body.replace(/[\[\]]/g, '').replace(/\(.+?\)/g, '↗')
link = postData.data.children[idx].data.link_permalink
contentLink = postData.data.children[idx].data.link_url
link_author = '‣ u/' + postData.data.children[idx].data.link_author //of link post
numCrossposts = ''
url = ''
} else if (kind.includes('t3')){ // link
title = postData.data.children[idx].data.title
body = postData.data.children[idx].data.selftext
link = 'https://reddit.com' + postData.data.children[idx].data.permalink
contentLink = postData.data.children[idx].data.preview.images[0].resolutions[5].url.split('?')[0]
link_author = ''
url = postData.data.children[idx].data.url + '↗'
numCrossposts = Intl.NumberFormat(numberFormatting).format(postData.data.children[idx].data.num_crossposts)
}
if (contentLink.includes('/gallery/')){
let fullPostCommentData = await new Request(`https://www.reddit.com/r/${ postData.data.children[idx].data.subreddit }/comments/${ postData.data.children[idx].data.parent_id.split('_')[1] }/.json`).loadJSON()
mediaID = fullPostCommentData[0].data.children[0].data.gallery_data.items[0].media_id
mediaURL = fullPostCommentData[0].data.children[0].data.media_metadata[mediaID].p[5].u
contentLink = mediaURL.replaceAll('&amp;', '&')
}
timeDiff = Math.floor((new Date(Date.now()).getTime() - new Date(postData.data.children[idx].data.created * 1000).getTime()) / 1000 / 60)
postAge = "--"
if (timeDiff > 525600) postAge = Math.abs(timeDiff/60/24/365).toFixed(1)+"y"
else if (timeDiff > 1440) postAge = Math.abs(timeDiff/60/24).toFixed(0)+"d"
else if (timeDiff > 60) postAge = Math.abs(timeDiff/60).toFixed(0)+"h"
else if (timeDiff >= 1) postAge= Math.abs(timeDiff).toFixed(0)+"m"
return {
totalKarma,
postKarma,
commentKarma,
awarderKarma,
awardeeKarma,
snoovatarImg,
usertitle,
username,
puplicDescription,
inboxCount,
profileURL,
profileCreated,
minutesDiff,
accountAge,
sub,
subURL,
upvotes,
downvotes,
numComments,
postCreated,
kind,
author,
title,
body,
link,
link_author,
contentLink,
numCrossposts,
url,
timeDiff,
postAge
}
} catch(err){
console.error('ERROR 404: No response from reddit API')
return 404
}
} catch(err){
console. error('ERROR 410: Failed to load user credentials from iCloud')
return 410
}
};
// Save images from github and appstore
async function saveAllImages(imgKey, snoovatarImg){
//log({profileImg})
let fm = FileManager.iCloud()
let dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
let imgURL = 'https://raw.githubusercontent.com/iamrbn/Reddit-Widget/main/Images/'
let imgs = ["karma.png", "cakedayConfetti.png", "cakedayApollo.png", "cakedayReddit.png", "alienblue.png", "black.png", "classic.png", "orange.png", "oldReddit.png", "arrowsLS.png", "redditLS.png"]
let appStoreIcons = {
apollo: 'https://is1-ssl.mzstatic.com/image/thumb/Purple126/v4/a1/d8/a6/a1d8a63c-1534-2a04-b0fe-3de6e9c800b9/AppIcon-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-85-220.png/512x512bb.png'
}
for (img of imgs){
imgPath = fm.joinPath(dir, img)
if (!fm.fileExists(imgPath)){
console.warn("Loading image: " + img)
request = new Request(imgURL + img)
image = await request.loadImage()
fm.writeImage(imgPath, image)
}
}
for (appIcon in appStoreIcons){
iconName = appIcon + ".png"
imgPath = fm.joinPath(dir, iconName)
//console.log(iconName + " already exists in iCloud")
if (!fm.fileExists(imgPath)){
console.warn("loading image: " + iconName)
req = new Request(appStoreIcons[appIcon])
img = await req.loadImage()
m.writeImage(imgPath, img)
}
}
imgName = snoovatarImg.match(/snoovatar.*/)[0].replace('/avatars/', '_').replace('.png', '')
//log({imgName})
if (!imgKey.contains("nameProfileImage")){
imgKey.set("nameProfileImage", imgName)
}
if (imgKey.get("nameProfileImage") != imgName){
imgPathOld = fm.joinPath(dir, imgKey.get("nameProfileImage") + '.png')
try {
fm.remove(imgPathOld)
await popUp("DELETED SUCCESSFULLY!" ,fm.fileName(imgPathOld, true))
imgKey.set("nameProfileImage", imgName)
await presentMenu()
} catch(err){
console.error(err.message)
}
}
imgPath = fm.joinPath(dir, imgName + '.png')
imgFileName = fm.fileName(imgPath, true)
if (!fm.fileExists(imgPath)){
console.warn("Loading & Saving Current Profile Image: " + imgName + '.png')
img = await new Request(snoovatarImg).loadImage()
fm.writeImage(imgPath, img)
}
};
function addString(stack, image, radius, size, text, txtSize, txtOpacity){
let txtColor = Color.dynamic(Color.black(), Color.white())
let wImg = stack.addImage(image)
wImg.cornerRadius = radius
wImg.imageSize = new Size(size, size)
stack.addSpacer(0.5)
let wTxt = stack.addText(text)
wTxt.font = Font.lightRoundedSystemFont(txtSize)
wTxt.textColor = txtColor
wTxt.textOpacity = txtOpacity
};
function addText(stack, text, size, opacity){
let txtColor = Color.dynamic(Color.black(), Color.white())
let wTxt = stack.addText(text)
wTxt.font = Font.lightRoundedSystemFont(size)
wTxt.textColor = txtColor
wTxt.textOpacity = opacity
};
// creating menu for start script
async function presentMenu(api, username, totalKarma, accountAge, inboxCount, imgKey){
let alrt = new Alert()
alrt.title = username
alrt.message = `${totalKarma} Total Karma\nRedditor Since ${accountAge}\nUnread Inbox: ${inboxCount}`
alrt.addDestructiveAction("⌦ Delete Menu")
alrt.addAction('⚙︎ Report Bug')
alrt.addCancelAction("Cancel")
idx = await alrt.presentSheet()
if (idx === 0) await deleteUserDatas(imgKey)
else if (idx === 1) await reportBug(api)
};
//Asks for user login datas to save in iCloud ~ iCloud/Scriptable/Reddit-Widget
async function askForLoginDatas(){
let fm = FileManager.iCloud()
let dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
let jsonPath = fm.joinPath(dir, 'UserCredentials.json')
let alert = new Alert()
alert.title = "No Data Found in iCloud!"
alert.message = "Please Enter Your Credentials"
alert.addTextField('Username')
alert.addTextField("Password")
alert.addTextField("Client ID")
alert.addTextField("Client Secret")
alert.addAction("Save")
alert.addDestructiveAction("Cancel")
alert.addAction("Documentation ↗")
idx = await alert.present()
if (idx === 0){
userDatas = {
USERNAME: alert.textFieldValue(0).replace('u/', ''),
PASSWORD: alert.textFieldValue(1),
CLIENT_ID: alert.textFieldValue(2),
CLIENT_SECRET: alert.textFieldValue(3)
}
checkObj = Object.values(userDatas).every(value => value !== "" && value.length > 3)
if (checkObj){
FileManager.iCloud().writeString(jsonPath, JSON.stringify(userDatas, null, 1))
await popUp('SUCCESSFULLY SAVED!', '~ iCloud/Scriptable/Reddit-Widget/UserCredentials.json')
} else {
await popUp('ERROR: Input Too Short!', 'Please Try Again')
await askForLoginDatas()
}
} else if (idx === 1){
throw new Error('User clicked Cancel')
} else if (idx === 2){
Safari.openInApp('https://github.com/iamrbn/Reddit-Widget/#create-personal-reddit-appscript', false)
}
};
//Delete Menu
async function deleteUserDatas(imgKey){
let fm = FileManager.iCloud()
let dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
let jsonPath = fm.joinPath(dir, 'UserCredentials.json')
let alert = new Alert()
alert.title = "Are You Sure To Delete?"
alert.message = "Removed files can NOT be restored"
alert. addDestructiveAction("Profile Image")
alert. addDestructiveAction("User Credentials")
alert.addCancelAction("Cancel")
let idx = await alert.present()
if (idx === 0){
try {
// Delete Profile Image
fm.remove(dir + '/' + imgKey.get("nameProfileImage") + '.png')
await popUp("DELETED SUCCESSFULLY!", fm.fileName(dir + '/' + imgKey.get("nameProfileImage") + '.png', true))
imgKey.remove("nameProfileImage")
console.warn('User Deleted Profile Image')
} catch(err){
if (err.message.includes('snoovatar_')){
imgKey.remove("nameProfileImage")
}
throw new Error(err.message)
}
} else if (idx === 1){
try {
// Delete Login Datas
fm.remove(jsonPath)
await popUp("DELETED SUCCESSFULLY!", fm.fileName(jsonPath, true))
console.warn('User Deleted Login Datas')
} catch(err){
throw new Error(err.message)
}
}
};
async function scndAlert(){
filesList = fm.listContents(dir).toString().replaceAll(",", "\n").replaceAll(".icloud", "").replaceAll(/^[.]/gm, '');
logWarning(filesList)
scndAlrt = new Alert()
scndAlrt.title = "Are You Really Sure?"
scndAlrt.message = "Removed files can NOT be restored\n\n" + filesList
scndAlrt.addDestructiveAction("Yes, Delete Everything!");
scndAlrt.addCancelAction("Nope, I'm Out")
let idx = await scndAlrt.present()
if (idx === 0){
fm.remove(dir+'/'+imgKey.get("nameProfileImage")+'.png')
await popUp("Deleted Sucessfully", fm.fileName(dir+'/'+imgKey.get("nameProfileImage")+'.png', true))
} else if (idx === -1){
await presentMenu()
}
};
async function reportBug(api){
fm = FileManager.iCloud()
dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
bugPath = fm.joinPath(dir, 'apiResponse_'+Date.now()+'.json')
info = new Alert()
info.title = "This function saves your API response in the Reddit-Widget folder"
info.message = "Please contact the script owner via GitHub issue"
info.addAction("Continue")
info.addCancelAction("Cancel")
idx = await info.presentSheet()
if (idx === 0){
FileManager.iCloud().writeString(bugPath, JSON.stringify(api, null, 2))
await popUp('Successfully Saved File', '~ '+bugPath)
Safari.open('https://github.com/iamrbn/Reddit-Widget/issues/')
} else {
console.warn('>> USER CLICKED CANCEL')
}
};
async function popUp(title ,message){
alrt = new Alert()
alrt.title = title
alrt.message = message
alrt.addAction("OK")
await alrt.presentAlert()
};
//creates background of widget
async function createBG(base, dock, url){
try {
base.backgroundImage = await new Request(url).loadImage()
} catch(err){
//console.warn('Error creating widget background:\nNo image from the specified URL could be loaded. Maybe there is no usable image from the last post!')
} finally {
bgGradient = new LinearGradient()
top = Color.dynamic(new Color('#FFFFFF'), new Color('#FF8420'))
top2 = Color.dynamic(new Color('#FFFFFF'), new Color('#FF5304'))
middle = Color.dynamic(new Color('#EDEDED'), new Color('#FD3F12'))
bottom = Color.dynamic(new Color('#D4D4D4'), new Color('#EA2128'))
location = [0, 0.8, 1]
color = [top, middle, bottom]
if (dock){
location = [0, 0.33, 0.63, 1]
color = [bottom, middle, middle, top2]
}
bgGradient.locations = location
bgGradient.colors = color
base.backgroundGradient = bgGradient
}
};
// get image from icloud ~iCloud/Scriptable/Reddit-Widget
async function getImageFor(name){
fm = FileManager.iCloud()
dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
imgPath = fm.joinPath(dir, name + '.png')
await fm.downloadFileFromiCloud(imgPath)
return await fm.readImage(imgPath)
};
// creating error homescreen widget (first run or no datas saved)
async function errorWidget(type, code){
//if (code != 404 || code != 410){throw new Error(`Unknown Error code => ${code}`)}
//if (typeof code != 'number'){throw new Error(`Unknown Error code => ${code}`)}
title = (code === 410) ? "No Credentials Found" : "No API Response"
body = (code === 410) ? "Please tap widget to run script and enter your user credentials or visit the Repo for more information" : "Maybe there is no internet connection"
symbol = (code === 410) ? 'questionmark.folder' : 'network.slash' // questionmark.folder - exclamationmark.magnifyingglass
button = (code === 410) ? "GitHub Repo ↗" : "Check Connection ↗"
url = (code === 410)? "https://github.com/iamrbn/Reddit-Widget?tab=readme-ov-file#%EF%B8%8F-setup" : "https://github.com/iamrbn/"
if (type === 'small'){
padding = 12
radius = 13
size = 14
symbolSize = 30
} else if (type === 'medium'){
padding = 14
radius = 17
size = 16
symbolSize = 35
} else if (type === 'large'){
padding = 25
radius = 19
size = 22
symbolSize = 80
}
errWidget = new ListWidget()
errWidget.url = "scriptable:///run/Reddit%20Widget"
errWidget.refreshAfterDate = new Date(Date.now() + 1000 * 60 * 1)
errWidget.setPadding(padding, padding, padding, padding)
await createBG(errWidget, false, null)
wTitle = errWidget.addText(title)
wTitle.font = Font.boldMonospacedSystemFont(size)
wTitle.minimumScaleFactor = 0.7
wTitle.lineLimit = 1
errWidget.addSpacer(3)
wBody = errWidget.addText(body)
wBody.font = Font.regularMonospacedSystemFont(size)
wBody.minimumScaleFactor = 0.6
errWidget.addSpacer(5)
sf = SFSymbol.named(symbol)
sf.applyFont(Font.regularRoundedSystemFont(150))
wIMG = errWidget.addImage(sf.image)
wIMG.imageSize = new Size(symbolSize, symbolSize)
wIMG.tintColor = Color.dynamic(Color.black(), Color.white())
wIMG.imageOpacity = 0.4
wIMG.centerAlignImage()
errWidget.addSpacer()
linkButton = errWidget.addStack()
linkButton.setPadding(5, 0, 5, 0)
linkButton.spacing = 3
await createBG(linkButton, true, null)
linkButton.cornerRadius = radius
linkButton.centerAlignContent()
linkButton.borderColor = Color.dynamic(new Color('#EDEDED'), new Color('#FD3F12'))
linkButton.borderWidth = 0.5
linkButton.url = url
linkButton.addSpacer()
wURL = linkButton.addText(button)
wURL.font = Font.regularMonospacedSystemFont(size-2)
wURL.minimumScaleFactor = 0.7
wURL.textColor = Color.blue()//Color.dynamic(Color.blue(), new Color('#1F8FFF'))
wURL.shadowColor = Color.dynamic(new Color('#D4D4D4'), new Color('#EA2128'))
wURL.shadowOffset = new Point(0, 2)
wURL.shadowRadius = 1
wURL.centerAlignText()
linkButton.addSpacer()
return errWidget
};
// creating error lockscreen widget (first run or no datas saved)
async function errorWidgetLS(type, code){
errWidget = new ListWidget()
errWidget.addAccessoryWidgetBackground = true
errWidget.url = "scriptable:///run/Reddit%20Widget"
errWidget.refreshAfterDate = new Date(Date.now() + 1000 * 60 * 1)
if (type === 'accessoryRectangular'){
errWidget.setPadding(0, 0, 0, 0)
txt = (code === 404) ? 'No API Response' : 'No User Credentials Found'
scale = (code === 404) ? 1.0 : 0.8
title = errWidget.addText(txt)
title.font = Font.boldMonospacedSystemFont(11)
title.minimumScaleFactor = scale
title.centerAlignText()
errWidget.addSpacer(2)
bdy = (code === 404) ? 'Maybe there is no internet connection' : 'Tap widget to start the setup'
body = errWidget.addText(bdy)
body.font = Font.regularMonospacedSystemFont(10)
body.minimumScaleFactor = scale
body.centerAlignText()
symbol = (code === 404) ? 'network.slash' : 'questionmark.folder'
sf = SFSymbol.named(symbol)
sf.applyFont(Font.regularRoundedSystemFont(150))
img = errWidget.addImage(sf.image)
img.imageSize = new Size(16, 16)
img.tintColor = Color.white()
img.imageOpacity = 0.5
img.centerAlignImage()
} else if (type === 'accessoryCircular'){
txt = (code === 404) ? 'No API\nResponse' : 'Tap\nto\nSetup\n☞'
errWidget.setPadding(2, 0, 0, 0)
title = errWidget.addText(txt)
size = (code === 404) ? 8 : 9
title.font = Font.boldMonospacedSystemFont(size)
title.centerAlignText()
}
return errWidget
};
//Checks if's there an server update on GitHub available
async function updateCheck(version){
fm = fm = FileManager.iCloud()
dir = fm.joinPath(fm.documentsDirectory(), 'Reddit-Widget')
modulePath = fm.joinPath(dir, 'redditModule.js')
url = 'https://raw.githubusercontent.com/iamrbn/Reddit-Widget/main/'
endpoints = ['Reddit-Widget.js', 'redditModule.js']
let uC;
try {
updateCheck = new Request(url + endpoints[0] + 'on')
uC = await updateCheck.loadJSON()
} catch (err){
return log(err.message)
}
needUpdate = false
if (uC.version > version){
needUpdate = true
if (config.runsInApp){
//console.error(`New Server Version ${uC.version} Available`)
let newAlert = new Alert()
newAlert.title = `New Server Version ${uC.version} Available!`
newAlert.addAction("OK")
newAlert.addDestructiveAction("Later")
newAlert.message="Changes:\n" + uC.notes + "\n\nOK starts the download from GitHub\n More informations about the update changes go to the GitHub Repo"
if (await newAlert.present() === 0){
reqCode = new Request(url + endpoints[0])
updatedCode = await reqCode.loadString()
pathCode = fm.joinPath(fm.documentsDirectory(), `${Script.name()}.js`)
fm.writeString(pathCode, updatedCode)
reqModule = new Request(url + endpoints[1])
moduleFile = await reqModule.loadString()
fm.writeString(modulePath, moduleFile)
throw new Error("Update Complete!")
}
}
} else {
console.log(">> SCRIPT IS UP TO DATE!")
}
return {uC, needUpdate}
};
module.exports = {
getKarmaFromAPI,
updateCheck,
getImageFor,
presentMenu,
askForLoginDatas,
deleteUserDatas,
reportBug,
scndAlert,
popUp,
createBG,
saveAllImages,
addString,
errorWidget,
errorWidgetLS,
addText
};
//=========================================//
//============== END OF MODULE ============//
//=========================================//