Created
January 10, 2023 01:01
-
-
Save sidevesh/46f0727791b358fcd88992cb21833fc7 to your computer and use it in GitHub Desktop.
Replacement for xdg-open copy in "open" node dependency, to help login into Google account for MagicMirror's MMM-GoogleCalendar module over Slack for headless systems
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
const { spawn } = require('child_process'); | |
const fs = require('fs'); | |
const http = require('http'); | |
const https = require('https'); | |
const { basename } = require('path'); | |
function sleep(ms) { | |
return new Promise(resolve => setTimeout(resolve, ms)); | |
} | |
function fetch(method, url, params, headers, body) { | |
return new Promise((resolve, reject) => { | |
let urlWithParams = url; | |
if (params) { | |
urlWithParams += '?' + Object.entries(params) | |
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) | |
.join('&'); | |
} | |
const options = { | |
method: method, | |
headers: { | |
'Content-Type': 'application/x-www-form-urlencoded', | |
}, | |
}; | |
if (headers) { | |
Object.assign(options.headers, headers); | |
} | |
if (body) { | |
if (typeof body === 'object') { | |
options.headers['Content-Type'] = 'application/json'; | |
body = JSON.stringify(body); | |
} | |
options.headers['Content-Length'] = Buffer.byteLength(body); | |
} | |
let protocol; | |
if (url.startsWith('https:')) { | |
protocol = https; | |
} else if (url.startsWith('http:')) { | |
protocol = http; | |
} else { | |
reject(new Error('Invalid protocol')); | |
return; | |
} | |
const req = protocol.request(urlWithParams, options, (res) => { | |
let data = ''; | |
res.on('data', (chunk) => { | |
data += chunk; | |
}); | |
res.on('end', () => { | |
if (res.statusCode >= 200 && res.statusCode < 300) { | |
resolve(data); | |
} else { | |
reject(new Error(`HTTP Error: ${res.statusCode}`, data)); | |
} | |
}); | |
}).on('error', (err) => { | |
reject(err); | |
}); | |
if (body) { | |
req.write(body); | |
} | |
req.end(); | |
}); | |
} | |
async function makeAuthorizationCompleteRequest(url) { | |
const result = await fetch('GET', url); | |
return result; | |
} | |
async function slackPostMessage(token, message) { | |
const result = await fetch('POST', 'https://slack.com/api/chat.postMessage', {}, {'Authorization': `Bearer ${token}`}, message); | |
return JSON.parse(result); | |
} | |
async function slackConversationsReplies(token, conversation) { | |
const result = await fetch('GET', 'https://slack.com/api/conversations.replies', {channel: conversation.channel, ts: conversation.ts}, {'Authorization': `Bearer ${token}`}); | |
return JSON.parse(result); | |
} | |
function removeSlackFormatting(text) { | |
return text.replace(/^<|>$/g, ''); | |
} | |
function isLocalhostUrl(url) { | |
return url.startsWith('http://localhost') || url.startsWith('https://localhost'); | |
} | |
function isValidUrl(url) { | |
try { | |
new URL(url); | |
return true; | |
} catch (error) { | |
return false; | |
} | |
} | |
function getUrlFromSlackMessage(text) { | |
const url = removeSlackFormatting(text); | |
if (isValidUrl(url) && isLocalhostUrl(url)) { | |
return url; | |
} | |
return null; | |
} | |
async function main(url) { | |
// Get the Slack token and channel from the environment variables | |
const SLACK_TOKEN = process.env.SLACK_TOKEN; | |
const SLACK_CHANNEL = process.env.SLACK_CHANNEL; | |
console.log('Starting Google account login over slack...'); | |
console.log(`SLACK_TOKEN: ${SLACK_TOKEN}`); | |
console.log(`SLACK_CHANNEL: ${SLACK_CHANNEL}`); | |
let existingSeenThreadMessages = 0; | |
// Send the URL to the Slack channel | |
const message = `Your Google account login to access Google Calendar events has expired, please re-login into your Google account at: ${url}, once the page redirects to a localhost page, share the complete URL from the address bar as a reply to this thread.`; | |
const result = await slackPostMessage(SLACK_TOKEN, { | |
channel: SLACK_CHANNEL, | |
text: message | |
}); | |
console.log('Login request message sent...'); | |
existingSeenThreadMessages += 1; | |
// Poll for replies to the message | |
const { channel: conversationId, ts: messageTs } = result; | |
let timeout = false; | |
const timeoutPromise = new Promise(resolve => setTimeout(() => { | |
timeout = true; | |
resolve(); | |
}, 60000)); | |
console.log('Waiting for reply...'); | |
while (!timeout) { | |
// Use the conversations.replies method to get replies to the message | |
const { messages: replies } = await slackConversationsReplies(SLACK_TOKEN, { | |
channel: conversationId, | |
ts: messageTs | |
}); | |
// Check if the latest reply is a valid URL | |
if (replies.length > existingSeenThreadMessages) { | |
existingSeenThreadMessages = replies.length; | |
const latestReply = replies[replies.length - 1]; | |
const validUrlIfPresentInMessage = getUrlFromSlackMessage(latestReply.text); | |
if (latestReply && validUrlIfPresentInMessage !== null) { | |
console.log(`Received message with valid URL: ${latestReply.text}`); | |
console.log(`Received URL: ${validUrlIfPresentInMessage}`); | |
// Make an authorization complete request to the URL | |
const authorizationCompleteRequestResponse = await makeAuthorizationCompleteRequest(validUrlIfPresentInMessage); | |
console.log(`Called the authorization complete URL, Response: ${authorizationCompleteRequestResponse}`); | |
await slackPostMessage(SLACK_TOKEN, { | |
channel: conversationId, | |
text: `Authorization URL accepted, response: ${authorizationCompleteRequestResponse}`, | |
thread_ts: messageTs | |
}); | |
process.exit(); | |
} else if (latestReply && validUrlIfPresentInMessage === null) { | |
console.error(`Received message with no valid URL: ${latestReply.text}`); | |
// Send a message if the URL is not correct | |
await slackPostMessage(SLACK_TOKEN, { | |
channel: conversationId, | |
text: 'The shared URL is not correct, make sure you copy the complete URL from the address bar, the url should start with localhost.', | |
thread_ts: messageTs | |
}); | |
existingSeenThreadMessages += 1; | |
} | |
} | |
// Sleep for 1 second before polling again | |
await sleep(1000); | |
} | |
console.error('Timed out waiting for reply.'); | |
// If the timeout is reached, send an error message | |
await slackPostMessage(SLACK_TOKEN, { | |
channel: conversationId, | |
text: 'Timed out waiting for authorization complete URL.', | |
thread_ts: messageTs | |
}); | |
} | |
const processNowFlags = ['--process-now', '-p']; | |
if ( | |
process.argv.length > 4 || | |
(process.argv.length === 4 && !processNowFlags.includes(process.argv[2])) || | |
(process.argv.length === 3 && processNowFlags.includes(process.argv[2])) || | |
process.argv.length < 3 | |
) { | |
console.log('Invalid arguments, usage:'); | |
console.log(`${basename(process.argv[1])} [options] [ url ]`); | |
console.log('Options:'); | |
console.log('-p, --process-now Specify whether the slack communication process should processed now or be scheduled in a separate process in the background, schedules in background by default'); | |
process.exit(1); | |
} | |
if (processNowFlags.includes(process.argv[2])) { | |
main(process.argv[3]); | |
} else { | |
const logFile = '/tmp/magicmirror-mmm-google-calendar-login-over-slack-' + new Date().toISOString() + '.log'; | |
if (fs.existsSync(logFile)) { | |
fs.unlinkSync(logFile); | |
} | |
fs.closeSync(fs.openSync(logFile, 'w')); | |
// Create a writable stream to the log file | |
const logStream = fs.createWriteStream(logFile, { flags: 'a' }); | |
// Start the new process once the writeStream is open | |
logStream.on('open', () => { | |
const child = spawn(process.argv[0], [process.argv[1], '-p', process.argv[2]], { detached: true, stdio: ['ignore', logStream, logStream] }); | |
// Detach the child process from the parent process | |
child.unref(); | |
// Exit the parent process | |
process.exit(); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment