Created
September 21, 2020 21:02
-
-
Save thesved/354af985c681f31b2914a1201e629caa to your computer and use it in GitHub Desktop.
Send notifications from Roam to Slack
This file contains hidden or 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
// PoC by @filipematossilv and @ViktorTabori | |
// Token for a Slack App. | |
var oauthToken = "TOKEN_HERE"; | |
// Slack app must already have been invited to the channel. | |
// See https://api.slack.com/methods/channels.list on how to get the ID. | |
var notificationChannel = "CHANNEL_HERE"; | |
// Email must be the one used in the Slack that contains the channel. | |
var tagToEmail = { | |
"#[[!name]]": "[email protected]", | |
} | |
function jsonEncode(baseUrl, json) { | |
const url = new URL(baseUrl); | |
Object.entries(json).map(([k, v]) => url.searchParams.set(k, v)) | |
return url.href; | |
} | |
async function callSlackAPI(url, data) { | |
// Can only submit GET requests to the Slack API via web since they have CORS | |
// setup to deny Authentication and Content-Type headers. | |
const response = await fetch(jsonEncode(url, { token: oauthToken, ...data })); | |
return response.json(); | |
} | |
async function getUserId(email) { | |
// Needs users:read.email permissions. | |
const lookupByEmailURL = "https://slack.com/api/users.lookupByEmail"; | |
const response = await callSlackAPI(lookupByEmailURL, { email }); | |
return response.ok ? response.user.id : false; | |
} | |
async function postNotification(msg) { | |
// Needs chat:write permissions. | |
const postMessageURL = "https://slack.com/api/chat.postMessage"; | |
const response = await callSlackAPI(postMessageURL, { | |
channel: notificationChannel, | |
unfurl_links: false, | |
link_names: true, | |
text: msg | |
}); | |
return response.ok; | |
} | |
async function notifyUserOfLink(email, link, msg) { | |
const uid = await getUserId(email); | |
if (!uid) { | |
return false; | |
} | |
return await postNotification(`Ping <@${uid}> at ${link}\n${msg}`); | |
} | |
function blockLink(blockId) { | |
const graph = document.location.href.match(/^.*?\/app\/([^\/]+)\/?/)[1]; | |
return `https://roamresearch.com/#/app/${graph}/page/${blockId}`; | |
} | |
function handleTag(tag, blockId, blockStr) { | |
const email = tagToEmail[tag]; | |
if (email) { | |
// console.log("sending notification"); | |
return notifyUserOfLink(email, blockLink(blockId), blockStr); | |
} | |
} | |
function newTags(before, after) { | |
const tagRegExp = /#(?:\[\[)?([^\s])*(?:\]\])?/g; | |
const tagsBefore = before.match(tagRegExp) || []; | |
const tagsAfter = after.match(tagRegExp) || []; | |
return tagsAfter.filter(t => !tagsBefore.includes(t)); | |
} | |
function watchBlocksForNewTags() { | |
let lastBlockId = null; | |
let lastBlockStr = null; | |
const callback = function (mutationsList, _) { | |
// Events aren't necessarily ordered. | |
// First check all the blocks left from and handle new tags. | |
for (var mutation of mutationsList) { | |
if (mutation.type === 'childList' && mutation.removedNodes.length && mutation.removedNodes[0].childElementCount && mutation.removedNodes[0].children[0].classList.contains('rm-block-input')) { | |
const blockId = mutation.target.children[1].id.match(/.{9}$/)[0]; | |
const str = mutation.removedNodes[0].children[0].textContent; | |
// console.log("exit block", blockId, str) | |
if (lastBlockId == blockId) { | |
const tags = newTags(lastBlockStr, str); | |
tags.forEach(t => handleTag(t, blockId, str)); | |
} | |
} | |
} | |
// Then check what the new block is, if any. | |
for (var mutation of mutationsList) { | |
// Enter existing block. | |
if (mutation.type === 'childList' && mutation.addedNodes.length && mutation.addedNodes[0].childElementCount && mutation.addedNodes[0].children[0].classList.contains('rm-block-input')) { | |
const blockId = mutation.target.children[1].children[0].id.match(/.{9}$/)[0]; | |
const str = mutation.addedNodes[0].children[0].textContent; | |
lastBlockId = blockId; | |
lastBlockStr = str; | |
} | |
// Enter new block. | |
if (mutation.type === 'childList' && mutation.addedNodes.length && mutation.addedNodes[0].childElementCount && | |
mutation.addedNodes[0].children[0] && | |
mutation.addedNodes[0].children[0].children[0] && | |
mutation.addedNodes[0].children[0].children[0].children[1] && | |
mutation.addedNodes[0].children[0].children[0].children[1].children[0] && | |
mutation.addedNodes[0].children[0].children[0].children[1].children[0].classList.contains('rm-block-input') | |
) { | |
const el = mutation.addedNodes[0].children[0].children[0].children[1].children[0]; | |
const blockId = el.id.match(/.{9}$/)[0]; | |
const str = el.textContent; | |
lastBlockId = blockId; | |
lastBlockStr = str; | |
// console.log("enter new block", blockId, str) | |
} | |
} | |
}; | |
// Create an observer instance linked to the callback function | |
const observer = new MutationObserver(callback); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
} | |
watchBlocksForNewTags(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment