Skip to content

Commit

Permalink
Add push notification support
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperManifolds committed Feb 11, 2023
1 parent 6a096c0 commit 7d806fa
Show file tree
Hide file tree
Showing 12 changed files with 590 additions and 7 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@log4js-node/gelf": "^1.0.2",
"@log4js-node/slack": "^1.0.0",
"@log4js-node/smtp": "^1.1.0",
"apn": "^2.2.0",
"axios": "^0.21.1",
"bcrypt": "^3.0.7",
"date-fns": "^2.8.1",
Expand Down Expand Up @@ -52,6 +53,7 @@
"sinon": "^7.5.0",
"twitter": "^1.7.1",
"ua-parser-js": "^0.7.20",
"web-push": "^3.5.0",
"workerpool": "^5.0.2",
"ws": "^7.2.0",
"yayson": "^2.0.8"
Expand Down
4 changes: 2 additions & 2 deletions src/classes/Anope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ class Anope {

account.user = await User.findOne({
where: {
email: { iLike: account.email },
email: { iLike: account.email, status: 'active' },
},
})
return account
Expand Down Expand Up @@ -265,7 +265,7 @@ class Anope {

account.user = await User.findOne({
where: {
email: { iLike: account.email },
email: { iLike: account.email, status: 'active' },
},
})
return account
Expand Down
24 changes: 21 additions & 3 deletions src/classes/Authentication.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ class Authentication {

// const user = await User.findByEmail(email)

const user = await User.findOne({ where: { email: { ilike: email } } })
const user = await User.findOne({
where: {
email: { ilike: email },
suspended: null,
status: 'active',
},
})
if (!user) {
return undefined
}
Expand Down Expand Up @@ -70,7 +76,13 @@ class Authentication {
where: { id: user.id },
})
}
return User.findOne({ where: { email: { ilike: email } } })
return User.findOne({
where: {
email: { ilike: email },
suspended: null,
status: 'active',
},
})
}

/**
Expand All @@ -92,7 +104,13 @@ class Authentication {
throw new GoneAPIError({})
}

const user = await User.findOne({ where: { id: token.userId } })
const user = await User.findOne({
where: {
id: token.userId,
suspended: null,
status: 'active',
},
})
return {
user,
scope: token.scope,
Expand Down
11 changes: 11 additions & 0 deletions src/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ const config = {
token: optional('FRAPI_TWITTER_TOKEN', [], undefined),
tokenSecret: optional('FRAPI_TWITTER_TOKEN_SECRET', [], undefined),
},
webpush: {
privateKey: recommended('FRAPI_WEBPUSH_PRIVATE_KEY', [], undefined),
publicKey: recommended('FRAPI_WEBPUSH_PUBLIC_KEY', [], undefined),
},
apn: {
token: {
key: recommended('FRAPI_APN_PATH', [], undefined),
keyId: recommended('FRAPI_APN_ID', [], undefined),
teamId: recommended('FRAPI_APN_TEAM', [], undefined),
},
},
}

ensureValidConfig()
Expand Down
49 changes: 49 additions & 0 deletions src/db/ApplePushSubscription.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Model, { column, table, validate, type } from './Model'

/**
* Model class for user sessions
*/
@table({
indexes: [{
fields: ['deviceToken', 'userId'],
}],
})
export default class ApplePushSubscription extends Model {
@validate({ isUUID: 4 })
@column(type.UUID, { primaryKey: true })
static id = type.UUIDV4

@column(type.STRING, { unique: true })
static deviceToken = undefined

@validate({ isUUID: 4 })
@column(type.UUID)
static userId = undefined

/**
* @inheritdoc
*/
static getScopes (models) {
return {
defaultScope: [{
include: [
{
model: models.User,
as: 'user',
required: true,
},
],
}, {
override: true,
}],
}
}

/**
* @inheritdoc
*/
static associate (models) {
super.associate(models)
models.ApplePushSubscription.belongsTo(models.User, { as: 'user' })
}
}
70 changes: 70 additions & 0 deletions src/db/WebPushSubscription.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Model, { column, table, validate, type } from './Model'

/**
* Model class for user sessions
*/
@table({
indexes: [{
fields: ['endpoint', 'userId'],
}],
})
export default class WebPushSubscription extends Model {
@validate({ isUUID: 4 })
@column(type.UUID, { primaryKey: true })
static id = type.UUIDV4

@column(type.STRING, { unique: true })
static endpoint = undefined

@column(type.INTEGER, { allowNull: true })
static expirationTime = undefined

@column(type.STRING)
static auth = undefined

@column(type.STRING)
static p256dh = undefined

@column(type.BOOLEAN)
static alertsOnly = true

@column(type.BOOLEAN)
static pc = true

@column(type.BOOLEAN)
static xb = true

@column(type.BOOLEAN)
static ps = true

@validate({ isUUID: 4 })
@column(type.UUID)
static userId = undefined

/**
* @inheritdoc
*/
static getScopes (models) {
return {
defaultScope: [{
include: [
{
model: models.User,
as: 'user',
required: true,
},
],
}, {
override: true,
}],
}
}

/**
* @inheritdoc
*/
static associate (models) {
super.associate(models)
models.WebPushSubscription.belongsTo(models.User, { as: 'user' })
}
}
6 changes: 6 additions & 0 deletions src/db/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Sequelize from 'sequelize'
import config from '../config'
import logger from '../logging'

import ApplePushSubscription from './ApplePushSubscription'
import Authenticator from './Authenticator'
import Avatar from './Avatar'
import Client from './Client'
Expand All @@ -22,8 +23,10 @@ import Token from './Token'
import User from './User'
import UserGroups from './UserGroups'
import VerificationToken from './VerificationToken'
import WebPushSubscription from './WebPushSubscription'

const models = {
ApplePushSubscription,
User,
Authenticator,
Avatar,
Expand All @@ -42,6 +45,7 @@ const models = {
UserGroups,
VerificationToken,
Session,
WebPushSubscription,
}

const {
Expand Down Expand Up @@ -137,6 +141,7 @@ export {
db as sequelize,
Sequelize,
Op,
ApplePushSubscription,
Authenticator,
Avatar,
Client,
Expand All @@ -157,4 +162,5 @@ export {
User,
UserGroups,
VerificationToken,
WebPushSubscription,
}
56 changes: 55 additions & 1 deletion src/routes/Rescues.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import apn from 'apn'
import Announcer from '../classes/Announcer'
import {
NotFoundAPIError, UnprocessableEntityAPIError,
Expand All @@ -7,7 +8,7 @@ import Event from '../classes/Event'
import Permission from '../classes/Permission'
import StatusCode from '../classes/StatusCode'
import { websocket } from '../classes/WebSocket'
import { Rescue, db } from '../db'
import { Rescue, db, ApplePushSubscription, WebPushSubscription } from '../db'
import DatabaseDocument from '../Documents/DatabaseDocument'
import { DocumentViewType } from '../Documents/Document'
import DatabaseQuery from '../query/DatabaseQuery'
Expand All @@ -25,6 +26,7 @@ import {
WritePermission,
} from './API'
import APIResource from './APIResource'
import { apnProvider, webPushPool } from './WebPushSubscriptions'

const rescueAccessHours = 3
const rescueAccessTime = rescueAccessHours * 60 * 60 * 1000
Expand Down Expand Up @@ -96,8 +98,22 @@ export default class Rescues extends APIResource {

const query = new DatabaseQuery({ connection: ctx })
const document = new DatabaseDocument({ query, result, type: RescueView })

Event.broadcast('fuelrats.rescuecreate', ctx.state.user, result.id, document)
ctx.response.status = StatusCode.created
if (apnProvider) {
const apnSubscriptions = await ApplePushSubscription.findAll({})
const deviceTokens = apnSubscriptions.map((sub) => {
return sub.deviceToken
})
const notification = new apn.Notification({
'content-available': 1,
sound: 'Ping.aiff',
category: 'rescue',
payload: result,
})
await apnProvider.send(notification, deviceTokens)
}
return document
}

Expand Down Expand Up @@ -320,6 +336,44 @@ export default class Rescues extends APIResource {
return true
}

/**
* @endpoint
*/
@POST('/rescues/:id/alert')
@authenticated
@parameters('id')
@permissions('rescues.write')
async postRescueAlert (ctx) {
const rescue = await Rescue.findOne({ where: { id: ctx.params.id } })
if (!rescue) {
throw new NotFoundAPIError({ parameter: 'id' })
}


const subscriptions = WebPushSubscription.findAll({
where: {
pc: rescue.platform === 'pc',
xb: rescue.platform === 'xb',
ps: rescue.platform === 'ps',
},
})
webPushPool.exec('webPushBroadcast', [subscriptions, rescue])
if (apnProvider) {
const apnSubscriptions = await ApplePushSubscription.findAll({})
const deviceTokens = apnSubscriptions.map((sub) => {
return sub.deviceToken
})
const notification = new apn.Notification({
'content-available': 1,
sound: 'Ping.aiff',
category: 'alert',
payload: rescue,
})
await apnProvider.send(notification, deviceTokens)
}
return true
}

/**
* @inheritdoc
*/
Expand Down
Loading

0 comments on commit 7d806fa

Please sign in to comment.