Skip to content

Instantly share code, notes, and snippets.

@sidevesh
Created January 10, 2023 01:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sidevesh/46f0727791b358fcd88992cb21833fc7 to your computer and use it in GitHub Desktop.
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
#!/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