diff --git a/.gitignore b/.gitignore index cfb93ff..fb87f67 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ privateRooms.json *.swo *~ .DS_Store +.env diff --git a/chatbot.js b/chatbot.js index c7b95ef..3053b7f 100644 --- a/chatbot.js +++ b/chatbot.js @@ -1,6 +1,8 @@ const ical = require('node-ical') const markdown = require('markdown').markdown var moment = require('moment') +const sentry= require('./sentry.js') + require('moment-timezone') require('moment-recur') let privateRooms = {} @@ -17,150 +19,164 @@ const { } = require('./constants') exports.handleScheduledMessages = function(client) { - let now = moment.utc() - scheduledMessages.forEach(message => { - if (message.when.matches(now)) { - sendMessage(message.message, '', client, message.room) - } - }) + try{ + let now = moment.utc() + scheduledMessages.forEach(message => { + if (message.when.matches(now)) { + sendMessage(message.message, '', client, message.room) + } + }) + } + catch(err){ + sentry.captureException(err); + console.error('Error handling scheduled messages',err); + } + } exports.handleCalendar = function(event, room, toStartOfTimeline, client) { - if (event.getType() === 'm.room.message' && toStartOfTimeline === false) { - let message = event.getContent().body - if (message[1] === ' ') { - message = message.replace(' ', '') - } - message = message.split(' ') - const cmd = message[0] - let localHashtag = '' - if (message.length > 1) { - localHashtag = message[1].toLowerCase() - } - const user = event.getSender() - const roomId = room.roomId - if (cmd === '!calendar' || cmd === '!cal') { - if (localHashtag.length == 0 && hashtagMappings.hasOwnProperty(roomId)) { - localHashtag = hashtagMappings[roomId] + try{ + if (event.getType() === 'm.room.message' && toStartOfTimeline === false) { + let message = event.getContent().body + if (message[1] === ' ') { + message = message.replace(' ', '') } - ical.fromURL(calendarURL, {}, function(err, data) { - if (!err) { - const today = new Date() - const upperLimit = new Date() - upperLimit.setDate(today.getDate() + calendarUpperLimitInMonths * 30) - const globalFormattingOptions = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - } - globals = [] - locals = [] - for (var key in data) { - if (data.hasOwnProperty(key) && data[key].start && data[key].end) { - if (data[key].rrule && data[key].start.tz) { - var nextOccurrences = data[key].rrule.between( - today, - upperLimit, - true - ) - if (nextOccurrences.length > 0) { - var timezone = data[key].start.tz - var diff = data[key].end.getTime() - data[key].start.getTime() - data[key].start = moment - .tz(nextOccurrences[0].getTime(), timezone) - .toDate() - data[key].end = moment - .tz(nextOccurrences[0].getTime() + diff, timezone) - .toDate() + message = message.split(' ') + const cmd = message[0] + let localHashtag = '' + if (message.length > 1) { + localHashtag = message[1].toLowerCase() + } + const user = event.getSender() + const roomId = room.roomId + if (cmd === '!calendar' || cmd === '!cal') { + if (localHashtag.length == 0 && hashtagMappings.hasOwnProperty(roomId)) { + localHashtag = hashtagMappings[roomId] + } + ical.fromURL(calendarURL, {}, function(err, data) { + if (!err) { + const today = new Date() + const upperLimit = new Date() + upperLimit.setDate(today.getDate() + calendarUpperLimitInMonths * 30) + const globalFormattingOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + } + globals = [] + locals = [] + for (var key in data) { + if (data.hasOwnProperty(key) && data[key].start && data[key].end) { + if (data[key].rrule && data[key].start.tz) { + var nextOccurrences = data[key].rrule.between( + today, + upperLimit, + true + ) + if (nextOccurrences.length > 0) { + var timezone = data[key].start.tz + var diff = data[key].end.getTime() - data[key].start.getTime() + data[key].start = moment + .tz(nextOccurrences[0].getTime(), timezone) + .toDate() + data[key].end = moment + .tz(nextOccurrences[0].getTime() + diff, timezone) + .toDate() + } + } + globals.push(data[key]) + if (localHashtag.length > 0) { + locals.push(data[key]) } - } - globals.push(data[key]) - if (localHashtag.length > 0) { - locals.push(data[key]) } } - } - locals = locals.filter( - entry => - entry.start && - entry.end && - entry.end >= today && - entry.end <= upperLimit && - entry.description && - entry.description.includes('#' + localHashtag) - ) - globals = globals.filter( - entry => - !locals.includes(entry) && - entry.start && - entry.end && - entry.end >= today && - entry.end <= upperLimit && - getDayOfYear(entry.start) != getDayOfYear(entry.end) - ) - globals.sort(function(a, b) { - if (getDayOfYear(a.start) == getDayOfYear(b.start)) { - return a.end - b.end - } else { + locals = locals.filter( + entry => + entry.start && + entry.end && + entry.end >= today && + entry.end <= upperLimit && + entry.description && + entry.description.includes('#' + localHashtag) + ) + globals = globals.filter( + entry => + !locals.includes(entry) && + entry.start && + entry.end && + entry.end >= today && + entry.end <= upperLimit && + getDayOfYear(entry.start) != getDayOfYear(entry.end) + ) + globals.sort(function(a, b) { + if (getDayOfYear(a.start) == getDayOfYear(b.start)) { + return a.end - b.end + } else { + return a.start - b.start + } + }) + locals.sort(function(a, b) { return a.start - b.start - } - }) - locals.sort(function(a, b) { - return a.start - b.start - }) - var output = - 'Global events the next ' + - calendarUpperLimitInMonths + - ' months:\n\n' - globals.forEach(entry => { - output += - '- **' + - entry.summary + - '** - ' + - entry.start.toLocaleDateString('en-US', globalFormattingOptions) + - ' - ' + - entry.end.toLocaleDateString('en-US', globalFormattingOptions) + - '\n' - }) - output += '\nFull Calendar: http://calendar.giveth.io' - var localOutput = - 'Local events the next ' + - calendarUpperLimitInMonths + - ' months:\n\n' - if (locals.length > 0) { - locals.forEach(entry => { - localOutput += + }) + var output = + 'Global events the next ' + + calendarUpperLimitInMonths + + ' months:\n\n' + globals.forEach(entry => { + output += '- **' + entry.summary + '** - ' + - entry.start.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - timeZone: 'utc', - }) + + entry.start.toLocaleDateString('en-US', globalFormattingOptions) + ' - ' + - entry.end.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: 'numeric', - timeZone: 'utc', - timeZoneName: 'short', - }) - ;('\n') + entry.end.toLocaleDateString('en-US', globalFormattingOptions) + + '\n' }) - output = localOutput + '\n\n' + output + output += '\nFull Calendar: http://calendar.giveth.io' + var localOutput = + 'Local events the next ' + + calendarUpperLimitInMonths + + ' months:\n\n' + if (locals.length > 0) { + locals.forEach(entry => { + localOutput += + '- **' + + entry.summary + + '** - ' + + entry.start.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + timeZone: 'utc', + }) + + ' - ' + + entry.end.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: 'numeric', + timeZone: 'utc', + timeZoneName: 'short', + }) + ;('\n') + }) + output = localOutput + '\n\n' + output + } + sendMessage(output, user, client, roomId) + } else { + client.sendTextMessage(roomId, 'Something went wrong :(') } - sendMessage(output, user, client, roomId) - } else { - client.sendTextMessage(roomId, 'Something went wrong :(') - } - }) + }) + } } } + catch(err){ + sentry.captureException(err); + console.error('Error handling Calendar',err); + } + } exports.handleNewMember = function( @@ -170,27 +186,32 @@ exports.handleNewMember = function( client, privateRooms ) { - if ( - event.event.membership === 'join' && - (!event.event.unsigned.prev_content || - event.event.unsigned.prev_content.membership === 'invite') - ) { - const user = event.getSender() - const room = event.getRoomId() - - let roomMessages = messages[room] - - if (roomMessages && checkUser(user)) { - handleWelcome( - room, - user, - client, - privateRooms, - roomMessages.externalMsg, - roomMessages.internalMsg - ) + try{ + if ( + event.event.membership === 'join' && + (!event.event.unsigned.prev_content || + event.event.unsigned.prev_content.membership === 'invite') + ) { + const user = event.getSender() + const room = event.getRoomId() + + let roomMessages = messages[room] + + if (roomMessages && checkUser(user)) { + handleWelcome( + room, + user, + client, + privateRooms, + roomMessages.externalMsg, + roomMessages.internalMsg + ) + } } + } catch(err){ + sentry.captureException(err); } + } exports.handleResponse = function( @@ -200,100 +221,105 @@ exports.handleResponse = function( client, privateRooms ) { - if (event.getType() === 'm.room.message' && toStartOfTimeline === false) { - let msg = event.getContent().body - const user = event.getSender() - - if (checkUser(user)) { - if ( - privateRooms[user] && - privateRooms[user].welcoming && - user != client.credentials.userId && - room.roomId == privateRooms[user].room - ) { - let greetingQuestions = - messages[privateRooms[user].welcoming.room].internalMsg - let curQuestion = privateRooms[user].welcoming.curQuestion - - let positive = false - let negative = false - positiveResponses.some(response => { - if (msg.includes(response.toLowerCase())) { - positive = true - return true - } - return false - }) - - negativeResponses.some(response => { - if (msg.includes(response.toLowerCase())) { - negative = true - return true + try{ + if (event.getType() === 'm.room.message' && toStartOfTimeline === false) { + let msg = event.getContent().body + const user = event.getSender() + + if (checkUser(user)) { + if ( + privateRooms[user] && + privateRooms[user].welcoming && + user != client.credentials.userId && + room.roomId == privateRooms[user].room + ) { + let greetingQuestions = + messages[privateRooms[user].welcoming.room].internalMsg + let curQuestion = privateRooms[user].welcoming.curQuestion + + let positive = false + let negative = false + positiveResponses.some(response => { + if (msg.includes(response.toLowerCase())) { + positive = true + return true + } + return false + }) + + negativeResponses.some(response => { + if (msg.includes(response.toLowerCase())) { + negative = true + return true + } + return false + }) + + if (positive) { + sendInternalMessage( + greetingQuestions[curQuestion].positive, + user, + client + ) + } else if (negative) { + sendInternalMessage( + greetingQuestions[curQuestion].negative, + user, + client + ) } - return false - }) - - if (positive) { - sendInternalMessage( - greetingQuestions[curQuestion].positive, - user, - client - ) - } else if (negative) { - sendInternalMessage( - greetingQuestions[curQuestion].negative, - user, - client - ) - } - - if (positive || negative) { - if (greetingQuestions.length > curQuestion + 1) { - sendNextQuestion( - curQuestion, - greetingQuestions, + + if (positive || negative) { + if (greetingQuestions.length > curQuestion + 1) { + sendNextQuestion( + curQuestion, + greetingQuestions, + user, + client, + privateRooms[user].welcoming.room + ) + } else { + privateRooms[user].welcoming = undefined + } + } else { + sendInternalMessage( + "I didn't recognize that response :(", user, - client, - privateRooms[user].welcoming.room + client ) + } + } else if ( + (!privateRooms[user] || !privateRooms[user].welcoming) && + user != client.credentials.userId + ) { + if (privateRooms[user] && privateRooms[user].room == room.roomId) { + for (let key in questions) { + if ( + questions.hasOwnProperty(key) && + checkForRoomQuestions(msg, key, room.roomId, user, client) + ) { + break + } + } } else { - privateRooms[user].welcoming = undefined + checkForRoomQuestions(msg, room.roomId, room.roomId, user, client) } - } else { - sendInternalMessage( - "I didn't recognize that response :(", - user, - client - ) } } else if ( - (!privateRooms[user] || !privateRooms[user].welcoming) && - user != client.credentials.userId + event.getType() === 'm.room.member' && + event.event.membership === 'leave' ) { - if (privateRooms[user] && privateRooms[user].room == room.roomId) { - for (let key in questions) { - if ( - questions.hasOwnProperty(key) && - checkForRoomQuestions(msg, key, room.roomId, user, client) - ) { - break - } - } - } else { - checkForRoomQuestions(msg, room.roomId, room.roomId, user, client) + let privateRoom = privateRooms[event.getSender()] + if (privateRoom && privateRoom.room == event.event.room_id) { + privateRoom.room = undefined + privateRoom.welcoming = undefined } } - } else if ( - event.getType() === 'm.room.member' && - event.event.membership === 'leave' - ) { - let privateRoom = privateRooms[event.getSender()] - if (privateRoom && privateRoom.room == event.event.room_id) { - privateRoom.room = undefined - privateRoom.welcoming = undefined - } } + } catch(err){ + sentry.captureException(err); } + } function getDayOfYear(date) { @@ -318,29 +344,34 @@ function checkForRoomQuestions( user, client ) { - let questionsForRoom = questions[roomForQuestions] - if (questionsForRoom) { - questionsForRoom.forEach(question => { - let shouldAnswerQuestion = false - if (typeof question.trigger === 'string') { - shouldAnswerQuestion = msg - .toLowerCase() - .includes(question.trigger.toLowerCase()) - } else { - question.trigger.some(trigger => { - if (msg.toLowerCase().includes(trigger.toLowerCase())) { - shouldAnswerQuestion = true - return true - } - return false - }) - } - if (shouldAnswerQuestion) { - sendMessage(question.answer, user, client, roomToSendIn) - return true - } - }) + try{ + let questionsForRoom = questions[roomForQuestions] + if (questionsForRoom) { + questionsForRoom.forEach(question => { + let shouldAnswerQuestion = false + if (typeof question.trigger === 'string') { + shouldAnswerQuestion = msg + .toLowerCase() + .includes(question.trigger.toLowerCase()) + } else { + question.trigger.some(trigger => { + if (msg.toLowerCase().includes(trigger.toLowerCase())) { + shouldAnswerQuestion = true + return true + } + return false + }) + } + if (shouldAnswerQuestion) { + sendMessage(question.answer, user, client, roomToSendIn) + return true + } + }) + } + }catch(err){ + sentry.captureException(err); } + return false } @@ -352,19 +383,25 @@ function handleWelcome( externalMsg, internalMsg ) { - if (typeof externalMsg === 'string') { - sendMessage(externalMsg, user, client, room) - } - if (typeof internalMsg === 'string') { - sendInternalMessage(internalMsg, user, client, privateRooms) - } else if (typeof internalMsg === 'object') { - if ( - !privateRooms[user] || - (privateRooms[user] && !privateRooms[user].welcoming) - ) { - sendNextQuestion(-1, internalMsg, user, privateRooms, client, room) + try{ + if (typeof externalMsg === 'string') { + sendMessage(externalMsg, user, client, room) + } + if (typeof internalMsg === 'string') { + sendInternalMessage(internalMsg, user, client, privateRooms) + } else if (typeof internalMsg === 'object') { + if ( + !privateRooms[user] || + (privateRooms[user] && !privateRooms[user].welcoming) + ) { + sendNextQuestion(-1, internalMsg, user, privateRooms, client, room) + } } + }catch(err){ + sentry.captureException(err); } + + } function sendNextQuestion( @@ -375,16 +412,22 @@ function sendNextQuestion( client, room ) { - curQuestion++ - if (privateRooms[user]) { - privateRooms[user].welcoming = { room: room, curQuestion: curQuestion } - } - let question = questions[curQuestion] - sendInternalMessage(question.msg, user, client, () => { - if (!question.positive) { - sendNextQuestion(curQuestion, questions, user, privateRooms, client, room) + try{ + curQuestion++ + if (privateRooms[user]) { + privateRooms[user].welcoming = { room: room, curQuestion: curQuestion } } - }) + let question = questions[curQuestion] + sendInternalMessage(question.msg, user, client, () => { + if (!question.positive) { + sendNextQuestion(curQuestion, questions, user, privateRooms, client, room) + } + }) + } + catch(err){ + sentry.captureException(err); + } + } exports.sendInternalMessage = function sendInternalMessage( @@ -394,34 +437,45 @@ exports.sendInternalMessage = function sendInternalMessage( privateRooms, callback ) { - if (privateRooms[user] && privateRooms[user].room) { - sendMessage(msg, user, client, privateRooms[user].room) - if (callback) { - callback() + try{ + if (privateRooms[user] && privateRooms[user].room) { + sendMessage(msg, user, client, privateRooms[user].room) + if (callback) { + callback() + } + } else { + client + .createRoom({ + preset: 'trusted_private_chat', + invite: [user], + is_direct: true, + }) + .then(res => { + privateRooms[user] = { room: res.room_id } + sendMessage(msg, user, client, privateRooms[user].room) + if (callback) { + callback() + } + }) } - } else { - client - .createRoom({ - preset: 'trusted_private_chat', - invite: [user], - is_direct: true, - }) - .then(res => { - privateRooms[user] = { room: res.room_id } - sendMessage(msg, user, client, privateRooms[user].room) - if (callback) { - callback() - } - }) + }catch(err){ + sentry.captureException(err); } + } function sendMessage(msg, user, client, room) { - if (msg.length > 0) { - msg = msg.replace(/^ +| +$/gm, '') - let html = markdown.toHTML(msg) - msg = msg.replace('%USER%', user) - html = html.replace('%USER%', user) - client.sendHtmlMessage(room, msg, html) + try{ + if (msg.length > 0) { + msg = msg.replace(/^ +| +$/gm, '') + let html = markdown.toHTML(msg) + msg = msg.replace('%USER%', user) + html = html.replace('%USER%', user) + client.sendHtmlMessage(room, msg, html) + } + } + catch(err){ + sentry.captureException(err); } + } diff --git a/index.js b/index.js index 4412586..7f2bdb9 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ const sdk = require('matrix-js-sdk') const pointsBot = require('./pointsbot.js') const chatBot = require('./chatbot.js') var cron = require('node-cron') +const sentry= require('./sentry.js') let privateRooms = {} // If modifying these scopes, delete credentials.json. @@ -13,7 +14,10 @@ const TOKEN_PATH = 'credentials.json' // Load client secrets from a local file. fs.readFile('client_secret.json', (err, content) => { - if (err) return console.log('Error loading client secret file:', err) + if (err) { + sentry.captureException(err); + return console.log('Error loading client secret file:', err) + } // Authorize a client with credentials, then call the Google Sheets API. authorize(JSON.parse(content), authenticated) }) @@ -81,8 +85,11 @@ const client = sdk.createClient('https://matrix.org') function authenticated(auth) { fs.readFile('bot_credentials.json', (err, content) => { - if (err) return console.log('Error loading bot credentials', err) - + if (err) { + sentry.captureException(err); + return console.log('Error loading bot credentials', err); + } + content = JSON.parse(content) client.login( @@ -94,6 +101,7 @@ function authenticated(auth) { (err, data) => { if (err) { console.log('Error:', err) + sentry.captureException(err) } console.log(`Logged in ${data.user_id} on device ${data.device_id}`) @@ -136,13 +144,34 @@ function authenticated(auth) { cron.schedule( '1 0 * * *', - () => { - chatBot.handleScheduledMessages(client) + async () => { + const checkInId = sentry.captureCheckIn({ + monitorSlug: process.env.MONITOR_SLUG, + status: 'in_progress', + }); + + try { + chatBot.handleScheduledMessages(client); + sentry.captureCheckIn({ + checkInId, + monitorSlug: process.env.MONITOR_SLUG, + status: 'ok', + + }); + } catch (error) { + sentry.captureCheckIn({ + checkInId, + monitorSlug:process.env.test-cron, + status: 'error', + }); + sentry.captureException(error); + console.error('An error occurred:', error); + } }, { timezone: 'Etc/UTC', } - ) + ); } ) }) @@ -158,6 +187,7 @@ function savePrivateRooms() { ) } + // Zeit NOW workaround const http = require('http') http diff --git a/package.json b/package.json index 555f251..1506c3d 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,20 @@ "start": "node index" }, "dependencies": { + "@sentry/node": "^8.9.1", + "@sentry/profiling-node": "^8.9.1", "bignumber.js": "^7.0.1", "dayjs": "^1.6.0", + "dotenv": "^16.4.5", "googleapis": "^30.0.0", "markdown": "^0.5.0", + "matrix-bot-sdk": "^0.7.1", "matrix-js-sdk": "^0.10.2", "moment": "^2.24.0", "moment-recur": "^1.0.7", "node-cron": "^2.0.3", - "node-ical": "^0.9.2" + "node-ical": "^0.9.2", + "olm": "^0.0.0" }, "devDependencies": { "eslint": "^5.6.1", diff --git a/sentry.js b/sentry.js new file mode 100644 index 0000000..cd820cf --- /dev/null +++ b/sentry.js @@ -0,0 +1,14 @@ +const Sentry = require("@sentry/node"); +const { nodeProfilingIntegration } = require("@sentry/profiling-node"); +require('dotenv').config(); + +Sentry.init({ + dsn: process.env.DSN, + integrations: [ + nodeProfilingIntegration(), + ], + tracesSampleRate: 1.0, + profilesSampleRate: 1.0, + }); + +module.exports = Sentry; \ No newline at end of file