Skip to content

Commit 35964aa

Browse files
Cloud function to periodically send push notifications for saved events
1 parent e950901 commit 35964aa

File tree

4 files changed

+288
-0
lines changed

4 files changed

+288
-0
lines changed

.gitignore

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Created by https://www.toptal.com/developers/gitignore/api/node
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=node
3+
4+
### Node ###
5+
# Logs
6+
logs
7+
*.log
8+
npm-debug.log*
9+
yarn-debug.log*
10+
yarn-error.log*
11+
lerna-debug.log*
12+
.pnpm-debug.log*
13+
14+
# Diagnostic reports (https://nodejs.org/api/report.html)
15+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16+
17+
# Runtime data
18+
pids
19+
*.pid
20+
*.seed
21+
*.pid.lock
22+
23+
# Directory for instrumented libs generated by jscoverage/JSCover
24+
lib-cov
25+
26+
# Coverage directory used by tools like istanbul
27+
coverage
28+
*.lcov
29+
30+
# nyc test coverage
31+
.nyc_output
32+
33+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34+
.grunt
35+
36+
# Bower dependency directory (https://bower.io/)
37+
bower_components
38+
39+
# node-waf configuration
40+
.lock-wscript
41+
42+
# Compiled binary addons (https://nodejs.org/api/addons.html)
43+
build/Release
44+
45+
# Dependency directories
46+
node_modules/
47+
jspm_packages/
48+
49+
# Snowpack dependency directory (https://snowpack.dev/)
50+
web_modules/
51+
52+
# TypeScript cache
53+
*.tsbuildinfo
54+
55+
# Optional npm cache directory
56+
.npm
57+
58+
# Optional eslint cache
59+
.eslintcache
60+
61+
# Optional stylelint cache
62+
.stylelintcache
63+
64+
# Microbundle cache
65+
.rpt2_cache/
66+
.rts2_cache_cjs/
67+
.rts2_cache_es/
68+
.rts2_cache_umd/
69+
70+
# Optional REPL history
71+
.node_repl_history
72+
73+
# Output of 'npm pack'
74+
*.tgz
75+
76+
# Yarn Integrity file
77+
.yarn-integrity
78+
79+
# dotenv environment variable files
80+
.env
81+
.env.development.local
82+
.env.test.local
83+
.env.production.local
84+
.env.local
85+
86+
# parcel-bundler cache (https://parceljs.org/)
87+
.cache
88+
.parcel-cache
89+
90+
# Next.js build output
91+
.next
92+
out
93+
94+
# Nuxt.js build / generate output
95+
.nuxt
96+
dist
97+
98+
# Gatsby files
99+
.cache/
100+
# Comment in the public line in if your project uses Gatsby and not Next.js
101+
# https://nextjs.org/blog/next-9-1#public-directory-support
102+
# public
103+
104+
# vuepress build output
105+
.vuepress/dist
106+
107+
# vuepress v2.x temp and cache directory
108+
.temp
109+
110+
# Docusaurus cache and generated files
111+
.docusaurus
112+
113+
# Serverless directories
114+
.serverless/
115+
116+
# FuseBox cache
117+
.fusebox/
118+
119+
# DynamoDB Local files
120+
.dynamodb/
121+
122+
# TernJS port file
123+
.tern-port
124+
125+
# Stores VSCode versions used for testing VSCode extensions
126+
.vscode-test
127+
128+
# yarn v2
129+
.yarn/cache
130+
.yarn/unplugged
131+
.yarn/build-state.yml
132+
.yarn/install-state.gz
133+
.pnp.*
134+
135+
### Node Patch ###
136+
# Serverless Webpack directories
137+
.webpack/
138+
139+
# Optional stylelint cache
140+
141+
# SvelteKit build / generate output
142+
.svelte-kit
143+
144+
# End of https://www.toptal.com/developers/gitignore/api/node
145+
146+
# Firebase service account
147+
service-account.json
148+
149+
# Unnecessary package file
150+
package-lock.json

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Campus App Cloud Functions
2+
3+
This repository contains all cloud functions used by the [Campus App](https://github.com/astarub/campus_app) backend.
4+
5+
---
6+
7+
More about cloud functions: [Appwrite Docs](https://appwrite.io/docs/functions)

savedEvents/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"dependencies": {
3+
"axios": "^1.3.4",
4+
"firebase-admin": "^11.5.0",
5+
"node-appwrite": "^8.2.0"
6+
}
7+
}
+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
const appwrite = require('node-appwrite');
2+
const admin = require('firebase-admin');
3+
const axios = require('axios');
4+
5+
// Firebase service account
6+
const serviceAccount = require("../service-account.json");
7+
8+
// Initialize firebase
9+
admin.initializeApp({
10+
credential: admin.credential.cert(serviceAccount)
11+
});
12+
13+
// Function to split array into chunks
14+
function chunk(array, chunkSize) {
15+
var arr = [];
16+
for (let i = 0; i < array.length; i += chunkSize) {
17+
const chunk = array.slice(i, i + chunkSize);
18+
arr.push(chunk);
19+
}
20+
return arr;
21+
}
22+
23+
module.exports = async (req, res) => {
24+
// Set appwrite endpoint
25+
const client = new appwrite.Client()
26+
.setEndpoint('https://api.app.asta-bochum.de/v1')
27+
.setProject('campus_app')
28+
.setKey(req.variables.API_KEY);
29+
30+
const db = new appwrite.Databases(client);
31+
32+
// Get all events from the asta calendar
33+
let astaEvents;
34+
try {
35+
astaEvents = (await axios('https://asta-bochum.de/wp-json/tribe/events/v1/events/')).data.events;
36+
37+
} catch(e) {
38+
console.log('[ERROR] Could not fetch AStA Events.');
39+
return res.json("Error");
40+
}
41+
42+
// Get all events without duplicates
43+
const events = (await db.listDocuments('push_notifications', 'saved_events')).documents;
44+
const uniqueEvents = [...new Map(events.map(item => [item['eventId'], item])).values()];
45+
46+
const today = new Date(Date.now());
47+
48+
// Get all events that happen today
49+
const eventsToday = uniqueEvents.filter((item) => {
50+
const date = new Date(Date.parse(item.startDate));
51+
52+
return date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() == today.getDate();
53+
});
54+
55+
// Go through all events
56+
for (const eventToday of eventsToday) {
57+
// Find the corresponding event of the AStA calendar
58+
const astaEvent = astaEvents.find(event => event.id == eventToday.eventId);
59+
if(astaEvent == undefined) continue;
60+
61+
// Adjust the notification body based on the existence of an end date
62+
const time = (astaEvent.start_date_details.hour == astaEvent.end_date_details.hour &&
63+
astaEvent.start_date_details.minutes == astaEvent.end_date_details.minutes) ?
64+
`Heute ${astaEvent.start_date_details.hour}:${astaEvent.start_date_details.minutes} Uhr`
65+
: `Heute ${astaEvent.start_date_details.hour}:${astaEvent.start_date_details.minutes} bis ${astaEvent.end_date_details.hour}:${astaEvent.end_date_details.minutes} Uhr`
66+
;
67+
68+
// Send the message
69+
await admin.messaging().send(
70+
{
71+
notification: {
72+
title: `Erinnerung ${astaEvent.title}!`,
73+
body: `${time} ${astaEvent.venue.venue != undefined ? `
74+
Ort: ${astaEvent.venue.venue}` : ''}`,
75+
},
76+
data: {
77+
"interaction": String(JSON.stringify({
78+
"destination": 'calendar',
79+
"data": [
80+
{
81+
"event": {
82+
"id": eventToday.eventId
83+
}
84+
}
85+
]
86+
}))
87+
},
88+
topic: eventToday.eventId.toString()
89+
}
90+
);
91+
92+
console.log('[INFO] Notifications sent.');
93+
94+
// Remove all documents with the corresponding event id
95+
const eventDocuments = events.filter(item => item.eventId == eventToday.eventId);
96+
for (const eventDocument of eventDocuments) {
97+
try {
98+
await db.deleteDocument('push_notifications', 'saved_events', eventDocument.$id);
99+
} catch (e) {
100+
console.log('[ERROR] Error while deleting saved events:' + e);
101+
continue;
102+
}
103+
}
104+
105+
console.log('[INFO] Documents deleted.');
106+
107+
// Get all fcm tokens of the documents with the corresponding event id
108+
const fcmTokens = eventDocuments.map(item => item.fcmToken);
109+
const tokenChunks = chunk(fcmTokens, 1000);
110+
111+
// Unsubscribe all devices from the topic to delete it
112+
for (const chunk of tokenChunks) {
113+
try {
114+
await admin.messaging().unsubscribeFromTopic(chunk, eventToday.eventId.toString());
115+
} catch (e) {
116+
console.log('[ERROR] Error while unsubscribing tokens: ' + e);
117+
}
118+
}
119+
120+
console.log('[INFO] Topic deleted.');
121+
}
122+
console.log('[INFO] All operations concluded. Closing...')
123+
return res.json("Function executed!");
124+
};

0 commit comments

Comments
 (0)