Skip to content

Commit

Permalink
add push notifications for reminders
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcel Kalveram committed May 5, 2019
1 parent fc19b74 commit de1b670
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 190 deletions.
26 changes: 26 additions & 0 deletions bin/send-reminders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node

const mongoose = require("mongoose")
const dotenv = require("dotenv")
const remindersController = require("../controllers/reminders")

/**
* Load environment variables from .env file, where API keys and passwords are configured.
*/
dotenv.load({ path: ".env" })

/**
* Connect to MongoDB.
*/
mongoose.Promise = global.Promise
mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/vlctechhub", {
user: process.env.MONGODB_USER,
pass: process.env.MONGODB_PASS,
})
mongoose.connection.on("error", err => {
console.error(err)
console.log("%s MongoDB connection error. Please make sure MongoDB is running.", chalk.red("✗"))
process.exit()
})

remindersController.sendRemindersForEventsOnDate(new Date())
16 changes: 8 additions & 8 deletions controllers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ exports.removeUser = (req, res) => {

exports.updateUser = (req, res) => {
const { response, type, token } = validateInput(req, res)
remoteApi.getLatestItemForType(type).then(latestItem => {
remoteApi.fetchLatestItemForType(type).then(latestItem => {
User.setLatestItemIdForUser([token], type, latestItem.id).then(status => {
if (status.ok) {
response.status = "success"
Expand All @@ -79,18 +79,18 @@ exports.updateUser = (req, res) => {

exports.postUser = (req, res) => {
const { response, type, token, eventId } = validateInput(req, res)
const newUserCb = err => newUserCallback(err, res, response)
if (type === "reminders") {
const newUser = new User({ type, token, eventId })
newUser.save(newUserCb)
return
}
User.getUserByTypeAndToken(type, token).then(user => {
if (user) {
response.message = "user exists"
return res.json(response)
}
const newUserCb = err => newUserCallback(err, res, response)
if (type === "reminders") {
const newUser = new User({ type, token, eventId })
newUser.save(newUserCb)
return
}
remoteApi.getLatestItemForType(type).then(latestItem => {
remoteApi.fetchLatestItemForType(type).then(latestItem => {
const newUser = new User({ type, token, latestItemId: latestItem.id })
newUser.save(newUserCb)
})
Expand Down
76 changes: 22 additions & 54 deletions controllers/notifications.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,28 @@
const User = require("../models/User");
const remoteApi = require("../services/remoteApi");
const Expo = require("expo-server-sdk");
const User = require("../models/User")
const remoteApi = require("../services/remoteApi")
const pushNotificationsHelper = require("../services/pushNotifications")

const notificationTitles = {
events: "Hay un nuevo evento",
jobs: "Hay una nueva oferta de trabajo"
};

// Create a new Expo SDK client
const expo = new Expo();

const sendNotifications = (tokens, { title, message }) => {
let messages = [];
for (let token of tokens) {
if (!Expo.isExpoPushToken(token)) {
console.error(`Push token ${token} is not a valid Expo push token`);
continue;
}
messages.push({
to: token,
sound: "default",
title: title,
body: message
});
}

let chunks = expo.chunkPushNotifications(messages);
let tickets = [];
(async () => {
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
} catch (error) {
console.error(error);
}
}
})();
};
events: "Hay un nuevo evento",
jobs: "Hay una nueva oferta de trabajo",
}

const sendPushNotificationsForType = type => {
remoteApi.getLatestItemForType(type).then(latestItem => {
User.getUsersByType(type, latestItem.id).then(users => {
const tokens = users.map(user => user.token);
sendNotifications(tokens, {
title: notificationTitles[type],
message:
type === "events"
? latestItem.title
: `${latestItem.title} en ${latestItem.company.name}`
});
User.setLatestItemIdForUser(tokens, type, latestItem.id).then(res => res);
});
});
};
remoteApi.fetchLatestItemForType(type).then(latestItem => {
User.getUsersByType(type, latestItem.id).then(users => {
const tokens = users.map(user => user.token)
pushNotificationsHelper.sendNotifications(tokens, {
title: notificationTitles[type],
message:
type === "events"
? latestItem.title
: `${latestItem.title} en ${latestItem.company.name}`,
})
User.setLatestItemIdForUser(tokens, type, latestItem.id).then(res => res)
})
})
}

module.exports = {
sendPushNotificationsForType
};
sendPushNotificationsForType,
}
27 changes: 27 additions & 0 deletions controllers/reminders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const User = require("../models/User")
const remoteApi = require("../services/remoteApi")
const pushNotificationsHelper = require("../services/pushNotifications")

const sendRemindersForEventsOnDate = date => {
remoteApi.fetchEventsByDate(date).then(sendRemindersForEvents)
}

const sendRemindersForEvents = events => {
for (event of events) {
sendRemindersForEvent(event)
}
}

const sendRemindersForEvent = event => {
User.getUsersByEventId(event.id).then(users => {
const tokens = users.map(user => user.token)
pushNotificationsHelper.sendNotifications(tokens, {
title: "Recordatorio para el evento",
message: event.title,
})
})
}

module.exports = {
sendRemindersForEventsOnDate,
}
4 changes: 4 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ userSchema.static("setLatestItemIdForUser", function(tokens, type, itemId) {
return this.update(criteria, { $set: { latestItemId: itemId } }, { multi: true })
})

userSchema.static("getUsersByEventId", function(eventId) {
return this.find({ eventId })
})

const User = mongoose.model("User", userSchema)

module.exports = User
37 changes: 37 additions & 0 deletions services/pushNotifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const Expo = require("expo-server-sdk")

// Create a new Expo SDK client
const expo = new Expo()

const sendNotifications = (tokens, { title, message }) => {
let messages = []
for (let token of tokens) {
if (!Expo.isExpoPushToken(token)) {
console.error(`Push token ${token} is not a valid Expo push token`)
continue
}
messages.push({
to: token,
sound: "default",
title: title,
body: message,
})
}

let chunks = expo.chunkPushNotifications(messages)
let tickets = []
;(async () => {
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk)
tickets.push(...ticketChunk)
} catch (error) {
console.error(error)
}
}
})()
}

module.exports = {
sendNotifications,
}
112 changes: 67 additions & 45 deletions services/remoteApi.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,76 @@
const fetch = require("node-fetch");
const API_URI = "https://api.vlctechhub.org/v1";
const fetch = require("node-fetch")
const API_URI = "https://api.vlctechhub.org/v1"

const remoteApiCalls = {
events: {
uri: "/events?category=next",
responseObject: "events"
},
jobs: {
uri: "/jobs",
responseObject: "jobs"
}
};
events: {
uri: "/events?category=next",
responseObject: "events",
},
jobs: {
uri: "/jobs",
responseObject: "jobs",
},
}

const fetchData = function(uri) {
return new Promise((resolve, reject) => {
fetch(`${API_URI}/${uri}`, {
method: "GET",
headers: {
Accept: "application/json"
}
}).then(response => resolve(response.json()), err => reject(err));
});
};
return new Promise((resolve, reject) => {
fetch(`${API_URI}/${uri}`, {
method: "GET",
headers: {
Accept: "application/json",
},
}).then(response => resolve(response.json()), err => reject(err))
})
}

const getLatestItem = (field, items) => {
items.sort((a, b) => {
const dateA = new Date(a[field]);
const dateB = new Date(b[field]);
if (dateA < dateB) return 1;
if (dateA > dateB) return -1;
return 0;
});
return items[0];
};

const getLatestItemForType = (type, fetch = fetchData) => {
return new Promise((res, rej) => {
fetch(remoteApiCalls[type].uri).then(json =>
res(
getLatestItem(
type === "jobs" ? "published_at" : "date",
json[remoteApiCalls[type].responseObject]
items.sort((a, b) => {
const dateA = new Date(a[field])
const dateB = new Date(b[field])
if (dateA < dateB) return 1
if (dateA > dateB) return -1
return 0
})
return items[0]
}

const fetchLatestItemForType = (type, fetch = fetchData) => {
return new Promise((res, rej) => {
fetch(remoteApiCalls[type].uri).then(json =>
res(
getLatestItem(
type === "jobs" ? "published_at" : "date",
json[remoteApiCalls[type].responseObject],
),
),
)
)
);
});
};
})
}

const compareDate = (a, b) => {
const dateA = new Date(a)
const dateB = new Date(b)
return (
dateA.getDate() === dateB.getDate() &&
dateA.getMonth() == dateB.getMonth() &&
dateA.getFullYear() == dateB.getFullYear()
)
}

const getEventsByDate = (events, date) => {
return events.filter(e => compareDate(e.date, date))
}

const fetchEventsByDate = (date, fetch = fetchData) => {
return new Promise((res, rej) => {
fetch(remoteApiCalls["events"].uri).then(json => res(getEventsByDate(json["events"], date)))
})
}

module.exports = {
fetchData,
getLatestItem,
getLatestItemForType
};
fetchData,
fetchEventsByDate,
getLatestItem,
fetchLatestItemForType,
compareDate,
}
30 changes: 30 additions & 0 deletions test/controllers/reminders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { expect } = require("chai")
const sinon = require("sinon")

const remindersController = require("../../controllers/reminders")
const remoteApi = require("../../services/remoteApi")
const pushNotificationsHelper = require("../../services/pushNotifications")
const User = require("../../models/User")

sinon
.stub(remoteApi, "fetchEventsByDate")
.resolves([
{ date: "2019-05-03T16:00:00Z", title: "test-1" },
{ date: "2019-05-03T16:00:00Z", title: "test-2" },
{ date: "2019-06-03T16:00:00Z", title: "test-3" },
])

sinon.stub(User, "getUsersByEventId").resolves([{ token: "abc" }, { token: "def" }])
const pushSpy = sinon.spy(pushNotificationsHelper, "sendNotifications")

describe("Test send reminders functions", () => {
it("should test the reminders for events date function", done => {
remindersController.sendRemindersForEventsOnDate("2019-05-03")
setTimeout(function() {
expect(pushSpy.called).to.eq(true)
expect(pushSpy.firstCall.args[0]).to.eql(["abc", "def"])
expect(pushSpy.secondCall.args[1].message).to.eql("test-2")
done()
}, 100)
})
})
Loading

0 comments on commit de1b670

Please sign in to comment.