mirror of
https://framagit.org/les/gancio.git
synced 2025-02-01 00:52:01 +01:00
854 lines
26 KiB
JavaScript
854 lines
26 KiB
JavaScript
const crypto = require('crypto')
|
|
const path = require('path')
|
|
const config = require('../../config')
|
|
const fs = require('fs/promises')
|
|
const { Op } = require('sequelize')
|
|
const linkifyHtml = require('linkify-html')
|
|
const Sequelize = require('sequelize')
|
|
const { DateTime } = require('luxon')
|
|
const helpers = require('../../helpers')
|
|
const Col = helpers.col
|
|
const notifier = require('../../notifier')
|
|
const { htmlToText } = require('html-to-text')
|
|
|
|
const { Event, Resource, Tag, Place, Notification, APUser, Collection } = require('../models/models')
|
|
|
|
|
|
const exportController = require('./export')
|
|
const tagController = require('./tag')
|
|
|
|
const log = require('../../log')
|
|
const collectionController = require('./collection')
|
|
|
|
const eventController = {
|
|
|
|
async _findOrCreatePlace (body) {
|
|
if (body.place_id) {
|
|
const place = await Place.findByPk(body.place_id)
|
|
if (!place) {
|
|
throw new Error(`Place not found`)
|
|
}
|
|
return place
|
|
}
|
|
|
|
const place_name = body.place_name && body.place_name.trim()
|
|
const place_address = body.place_address && body.place_address.trim()
|
|
if (!place_name || !place_address && place_name !== 'online') {
|
|
throw new Error(`place_id or place_name and place_address are required`)
|
|
}
|
|
let place = await Place.findOne({ where: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), Sequelize.Op.eq, place_name.toLocaleLowerCase()) })
|
|
if (!place) {
|
|
place = await Place.create({
|
|
name: place_name,
|
|
address: place_address || '',
|
|
latitude: Number(body.place_latitude) || null,
|
|
longitude: Number(body.place_longitude) || null
|
|
})
|
|
}
|
|
return place
|
|
},
|
|
|
|
async searchMeta(req, res) {
|
|
const search = req.query.search.toLocaleLowerCase()
|
|
|
|
const places = await Place.findAll({
|
|
order: [[Sequelize.col('w'), 'DESC']],
|
|
where: {
|
|
[Op.or]: [
|
|
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
|
|
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('address')), 'LIKE', '%' + search + '%')
|
|
]
|
|
},
|
|
attributes: [['name', 'label'], 'address', 'latitude', 'longitude', 'id', [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('events.placeId')), 'INTEGER'), 'w']],
|
|
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
|
|
group: ['place.id'],
|
|
raw: true
|
|
})
|
|
|
|
const tags = await Tag.findAll({
|
|
order: [[Sequelize.col('w'), 'DESC']],
|
|
where: {
|
|
tag: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('tag')), 'LIKE', '%' + search + '%'),
|
|
},
|
|
attributes: [['tag', 'label'], [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'INTEGER'), 'w']],
|
|
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
|
|
group: ['tag.tag'],
|
|
raw: true
|
|
})
|
|
|
|
|
|
const ret = places.map(p => {
|
|
p.type = 'place'
|
|
return p
|
|
}).concat(tags.map(t => {
|
|
t.type = 'tag'
|
|
return t
|
|
})).sort((a, b) => b.w - a.w).slice(0, 10)
|
|
|
|
return res.json(ret)
|
|
},
|
|
|
|
async _get(slug) {
|
|
// retrocompatibility, old events URL does not use slug, use id as fallback
|
|
const id = Number(slug) || -1
|
|
return Event.findOne({
|
|
where: {
|
|
[Op.or]: {
|
|
slug,
|
|
id
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
async get(req, res) {
|
|
const format = req.params.format || 'json'
|
|
const is_admin = req.user && req.user.is_admin
|
|
const slug = req.params.event_slug
|
|
|
|
// retrocompatibility, old events URL does not use slug, use id as fallback
|
|
const id = Number(slug) || -1
|
|
let event
|
|
|
|
try {
|
|
event = await Event.findOne({
|
|
where: {
|
|
[Op.or]: {
|
|
slug,
|
|
id
|
|
}
|
|
},
|
|
attributes: {
|
|
exclude: ['createdAt', 'updatedAt', 'placeId']
|
|
},
|
|
include: [
|
|
{ model: Tag, required: false, attributes: ['tag'], through: { attributes: [] } },
|
|
{ model: Place, attributes: ['name', 'address', 'latitude', 'longitude', 'id'] },
|
|
{
|
|
model: Resource,
|
|
where: !is_admin && { hidden: false },
|
|
include: [{ model: APUser, required: false, attributes: ['object', 'ap_id'] }],
|
|
required: false,
|
|
attributes: ['id', 'activitypub_id', 'data', 'hidden']
|
|
},
|
|
{ model: Event, required: false, as: 'parent', attributes: ['id', 'recurrent', 'is_visible', 'start_datetime'] },
|
|
{ model: APUser, required: false }
|
|
],
|
|
order: [[Resource, 'id', 'DESC']]
|
|
})
|
|
} catch (e) {
|
|
log.error('[EVENT]', e)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
if (!event) {
|
|
return res.sendStatus(404)
|
|
}
|
|
|
|
// get prev and next event
|
|
const next = await Event.findOne({
|
|
attributes: ['id', 'slug'],
|
|
where: {
|
|
id: { [Op.not]: event.id },
|
|
is_visible: true,
|
|
recurrent: null,
|
|
[Op.or]: [
|
|
{ start_datetime: { [Op.gt]: event.start_datetime } },
|
|
{
|
|
start_datetime: event.start_datetime,
|
|
id: { [Op.gt]: event.id }
|
|
}
|
|
]
|
|
},
|
|
order: [['start_datetime', 'ASC'], ['id', 'ASC']]
|
|
})
|
|
|
|
const prev = await Event.findOne({
|
|
attributes: ['id', 'slug'],
|
|
where: {
|
|
is_visible: true,
|
|
id: { [Op.not]: event.id },
|
|
recurrent: null,
|
|
[Op.or]: [
|
|
{ start_datetime: { [Op.lt]: event.start_datetime } },
|
|
{
|
|
start_datetime: event.start_datetime,
|
|
id: { [Op.lt]: event.id }
|
|
}
|
|
]
|
|
},
|
|
order: [['start_datetime', 'DESC'], ['id', 'DESC']]
|
|
})
|
|
|
|
if (event && (event.is_visible || is_admin)) {
|
|
event = event.get()
|
|
event.next = next && (next.slug || next.id)
|
|
event.prev = prev && (prev.slug || prev.id)
|
|
event.tags = event.tags.map(t => t.tag)
|
|
event.plain_description = htmlToText(event.description, event.description.replace('\n', '').slice(0, 1000) )
|
|
|
|
if (format === 'json') {
|
|
res.json(event)
|
|
} else if (format === 'ics') {
|
|
// last arg is alarms/reminder, ref: https://github.com/adamgibbons/ics#attributes (alarms)
|
|
exportController.ics(req, res, [event], [{
|
|
action: 'display',
|
|
description: event.title,
|
|
trigger: { hours: 1, before: true }
|
|
}])
|
|
}
|
|
} else {
|
|
res.sendStatus(404)
|
|
}
|
|
},
|
|
|
|
/** confirm an anonymous event
|
|
* and send related notifications
|
|
*/
|
|
async confirm(req, res) {
|
|
const id = Number(req.params.event_id)
|
|
const event = await Event.findByPk(id, { include: [Place, Tag] })
|
|
if (!event) {
|
|
log.warn(`Trying to confirm a unknown event, id: ${id}`)
|
|
return res.sendStatus(404)
|
|
}
|
|
if (!req.user.is_admin && req.user.id !== event.userId) {
|
|
log.warn(`Someone not allowed is trying to confirm -> "${event.title} `)
|
|
return res.sendStatus(403)
|
|
}
|
|
|
|
log.info(`Event "${event.title}" confirmed`)
|
|
try {
|
|
event.is_visible = true
|
|
|
|
await event.save()
|
|
|
|
res.sendStatus(200)
|
|
|
|
// send notification
|
|
notifier.notifyEvent('Create', event.id)
|
|
} catch (e) {
|
|
log.error('[EVENT]', e)
|
|
res.sendStatus(404)
|
|
}
|
|
},
|
|
|
|
async unconfirm(req, res) {
|
|
const id = Number(req.params.event_id)
|
|
const event = await Event.findByPk(id)
|
|
if (!event) { return req.sendStatus(404) }
|
|
if (!req.user.is_admin && req.user.id !== event.userId) {
|
|
log.warn(`Someone not allowed is trying to unconfirm -> "${event.title} `)
|
|
return res.sendStatus(403)
|
|
}
|
|
|
|
try {
|
|
await event.update({ is_visible: false })
|
|
res.sendStatus(200)
|
|
} catch (e) {
|
|
log.info(e)
|
|
res.sendStatus(404)
|
|
}
|
|
},
|
|
|
|
/** get all unconfirmed events */
|
|
async getUnconfirmed(_req, res) {
|
|
try {
|
|
const events = await Event.findAll({
|
|
where: {
|
|
parentId: null,
|
|
is_visible: false,
|
|
start_datetime: { [Op.gt]: DateTime.local().toUnixInteger() }
|
|
},
|
|
order: [['start_datetime', 'ASC']],
|
|
include: [{ model: Tag, required: false }, Place]
|
|
})
|
|
res.json(events)
|
|
} catch (e) {
|
|
log.info(e)
|
|
res.sendStatus(400)
|
|
}
|
|
},
|
|
|
|
async addNotification(req, res) {
|
|
try {
|
|
const notification = {
|
|
filters: { is_visible: true },
|
|
email: req.body.email,
|
|
type: 'mail',
|
|
remove_code: crypto.randomBytes(16).toString('hex')
|
|
}
|
|
await Notification.create(notification)
|
|
res.sendStatus(200)
|
|
} catch (e) {
|
|
res.sendStatus(404)
|
|
}
|
|
},
|
|
|
|
async delNotification(req, res) {
|
|
const remove_code = req.params.code
|
|
try {
|
|
const notification = await Notification.findOne({ where: { remove_code } })
|
|
await notification.destroy()
|
|
} catch (e) {
|
|
return res.sendStatus(404)
|
|
}
|
|
res.sendStatus(200)
|
|
},
|
|
|
|
async isAnonEventAllowed(req, res, next) {
|
|
if (!res.locals.settings.allow_anon_event && !req.user) {
|
|
return res.sendStatus(403)
|
|
}
|
|
next()
|
|
},
|
|
|
|
async add(req, res) {
|
|
// req.err comes from multer streaming error
|
|
if (req.err) {
|
|
log.warn(req.err)
|
|
return res.status(400).json(req.err.toString())
|
|
}
|
|
|
|
try {
|
|
const body = req.body
|
|
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
|
|
|
|
const required_fields = ['title', 'start_datetime']
|
|
let missing_field = required_fields.find(required_field => !body[required_field])
|
|
if (missing_field) {
|
|
log.warn(`${missing_field} required`)
|
|
return res.status(400).send(`${missing_field} required`)
|
|
}
|
|
|
|
// validate start_datetime and end_datetime
|
|
if (body.end_datetime) {
|
|
if (body.start_datetime > body.end_datetime) {
|
|
return res.status(400).send(`start datetime is greater than end datetime`)
|
|
}
|
|
|
|
if (Number(body.end_datetime) > 1000*24*60*60*365) {
|
|
return res.status(400).send('are you sure?')
|
|
}
|
|
|
|
}
|
|
|
|
if (!Number(body.start_datetime)) {
|
|
return res.status(400).send(`Wrong format for start datetime`)
|
|
}
|
|
|
|
if (Number(body.start_datetime) > 1000*24*60*60*365) {
|
|
return res.status(400).send('are you sure?')
|
|
}
|
|
|
|
// find or create the place
|
|
let place
|
|
try {
|
|
place = await eventController._findOrCreatePlace(body)
|
|
if (!place) {
|
|
return res.status(400).send(`Place not found`)
|
|
}
|
|
} catch (e) {
|
|
log.error(e.message)
|
|
return res.status(400).send(e.message)
|
|
}
|
|
|
|
|
|
const eventDetails = {
|
|
title: body.title.trim(),
|
|
// sanitize and linkify html
|
|
description: helpers.sanitizeHTML(linkifyHtml(body.description || '')),
|
|
multidate: body.multidate,
|
|
start_datetime: body.start_datetime,
|
|
end_datetime: body.end_datetime,
|
|
online_locations: body.online_locations,
|
|
recurrent,
|
|
// publish this event only if authenticated
|
|
is_visible: !!req.user
|
|
}
|
|
|
|
if (req.file || body.image_url) {
|
|
if (!req.file && body.image_url) {
|
|
req.file = await helpers.getImageFromURL(body.image_url)
|
|
}
|
|
|
|
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
|
|
focalpoint = [parseFloat(parseFloat(focalpoint[0]).toFixed(2)), parseFloat(parseFloat(focalpoint[1]).toFixed(2))]
|
|
eventDetails.media = [{
|
|
url: req.file.filename,
|
|
height: req.file.height,
|
|
width: req.file.width,
|
|
name: body.image_name || body.title || '',
|
|
size: req.file.size || 0,
|
|
focalpoint
|
|
}]
|
|
} else {
|
|
eventDetails.media = []
|
|
}
|
|
|
|
let event = await Event.create(eventDetails)
|
|
|
|
await event.setPlace(place)
|
|
|
|
// create/assign tags
|
|
let tags = []
|
|
if (body.tags) {
|
|
tags = await tagController._findOrCreate(body.tags)
|
|
await event.setTags(tags)
|
|
}
|
|
|
|
// associate user to event and reverse
|
|
if (req.user) {
|
|
await req.user.addEvent(event)
|
|
await event.setUser(req.user)
|
|
}
|
|
|
|
event = event.get()
|
|
event.tags = tags.map(t => t.tag)
|
|
event.place = place
|
|
// return created event to the client
|
|
res.json(event)
|
|
|
|
// create recurrent instances of event if needed
|
|
// without waiting for the task manager
|
|
if (event.recurrent) {
|
|
eventController._createRecurrent()
|
|
} else {
|
|
// send notifications
|
|
const notifier = require('../../notifier')
|
|
notifier.notifyEvent('Create', event.id)
|
|
}
|
|
} catch (e) {
|
|
log.error('[EVENT ADD]', e)
|
|
res.sendStatus(400)
|
|
}
|
|
},
|
|
|
|
async update(req, res) {
|
|
if (res.err) {
|
|
log.warn(req.err)
|
|
return res.status(400).json(req.err.toString())
|
|
}
|
|
|
|
try {
|
|
const body = req.body
|
|
const event = await Event.findByPk(body.id)
|
|
if (!event) { return res.sendStatus(404) }
|
|
if (!req.user.is_admin && event.userId !== req.user.id) {
|
|
return res.sendStatus(403)
|
|
}
|
|
|
|
const start_datetime = body.start_datetime || event.start_datetime
|
|
const end_datetime = body.end_datetime || event.end_datetime || null
|
|
|
|
// // validate start_datetime and end_datetime
|
|
if (end_datetime) {
|
|
if (start_datetime > end_datetime) {
|
|
return res.status(400).send(`start datetime is greater than end datetime`)
|
|
}
|
|
|
|
if (Number(end_datetime) > 1000*24*60*60*365) {
|
|
return res.status(400).send('are you sure?')
|
|
}
|
|
}
|
|
|
|
if (!Number(start_datetime)) {
|
|
return res.status(400).send(`Wrong format for start datetime`)
|
|
}
|
|
|
|
if (Number(start_datetime) > 1000*24*60*60*365) {
|
|
return res.status(400).send('are you sure?')
|
|
}
|
|
|
|
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
|
|
const eventDetails = {
|
|
title: body.title || event.title,
|
|
// sanitize and linkify html
|
|
description: helpers.sanitizeHTML(linkifyHtml(body.description || '', { target: '_blank' })) || event.description,
|
|
multidate: body.multidate,
|
|
start_datetime,
|
|
end_datetime,
|
|
online_locations: body.online_locations,
|
|
recurrent
|
|
}
|
|
|
|
// remove old media in case a new one is uploaded
|
|
if ((req.file || /^https?:\/\//.test(body.image_url)) && !event.recurrent && event.media && event.media.length) {
|
|
try {
|
|
const old_path = path.resolve(config.upload_path, event.media[0].url)
|
|
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
|
|
await fs.unlink(old_path)
|
|
await fs.unlink(old_thumb_path)
|
|
} catch (e) {
|
|
log.info(e.toString())
|
|
}
|
|
}
|
|
|
|
// modify associated media only if a new file is uploaded or remote image_url is used
|
|
if (req.file || (body.image_url && /^https?:\/\//.test(body.image_url))) {
|
|
if (!req.file && body.image_url) {
|
|
req.file = await helpers.getImageFromURL(body.image_url)
|
|
}
|
|
|
|
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
|
|
focalpoint = [parseFloat(parseFloat(focalpoint[0]).toFixed(2)), parseFloat(parseFloat(focalpoint[1]).toFixed(2))]
|
|
eventDetails.media = [{
|
|
url: req.file.filename,
|
|
height: req.file.height,
|
|
width: req.file.width,
|
|
name: body.image_name || body.title || '',
|
|
size: req.file.size || 0,
|
|
focalpoint
|
|
}]
|
|
} else if (!body.image) {
|
|
eventDetails.media = []
|
|
} else if (body.image_focalpoint && event.media.length) {
|
|
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
|
|
focalpoint = [parseFloat(parseFloat(focalpoint[0]).toFixed(2)), parseFloat(parseFloat(focalpoint[1]).toFixed(2))]
|
|
eventDetails.media = [{ ...event.media[0], focalpoint }] // [0].focalpoint = focalpoint
|
|
}
|
|
|
|
if (body.image_name && event.media.length && event.media[0].name !== body.image_name) {
|
|
eventDetails.media[0].name = body.image_name || body.title || ''
|
|
}
|
|
|
|
await event.update(eventDetails)
|
|
|
|
// find or create the place
|
|
let place
|
|
try {
|
|
place = await eventController._findOrCreatePlace(body)
|
|
if (!place) {
|
|
return res.status(400).send(`Place not found`)
|
|
}
|
|
} catch (e) {
|
|
return res.status(400).send(e.message)
|
|
}
|
|
await event.setPlace(place)
|
|
|
|
// create/assign tags
|
|
let tags = []
|
|
if (body.tags) {
|
|
tags = await tagController._findOrCreate(body.tags)
|
|
}
|
|
await event.setTags(tags)
|
|
|
|
let newEvent = await Event.findByPk(event.id, { include: [Tag, Place] })
|
|
newEvent = newEvent.get()
|
|
newEvent.tags = tags.map(t => t.tag)
|
|
newEvent.place = place
|
|
res.json(newEvent)
|
|
|
|
// create recurrent instances of event if needed
|
|
// without waiting for the task manager
|
|
if (event.recurrent) {
|
|
eventController._createRecurrent()
|
|
} else {
|
|
if (!event.ap_id) {
|
|
const notifier = require('../../notifier')
|
|
notifier.notifyEvent('Update', event.id)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
log.error('[EVENT UPDATE]', e)
|
|
res.sendStatus(400)
|
|
}
|
|
},
|
|
|
|
async remove(req, res) {
|
|
const event = await Event.findByPk(req.params.id)
|
|
// check if event is mine (or user is admin)
|
|
if (event && (req.user.is_admin || req.user.id === event.userId)) {
|
|
if (event.media && event.media.length && !event.recurrent) {
|
|
try {
|
|
const old_path = path.join(config.upload_path, event.media[0].url)
|
|
const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
|
|
await fs.unlink(old_thumb_path)
|
|
await fs.unlink(old_path)
|
|
} catch (e) {
|
|
log.info(e.toString())
|
|
}
|
|
}
|
|
|
|
// notify local events only
|
|
if (!event.ap_id) {
|
|
const notifier = require('../../notifier')
|
|
await notifier.notifyEvent('Delete', event.id)
|
|
}
|
|
|
|
// unassociate child events
|
|
if (event.recurrent) {
|
|
await Event.update({ parentId: null }, { where: { parentId: event.id } })
|
|
}
|
|
log.debug('[EVENT REMOVED] ' + event.title)
|
|
try {
|
|
// remove related resources
|
|
await Resource.destroy({ where: { eventId: event.id }})
|
|
|
|
// and finally remove the event
|
|
await event.destroy()
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
|
|
res.sendStatus(200)
|
|
} else {
|
|
res.sendStatus(403)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method to search for events with pagination and filtering
|
|
* @returns
|
|
*/
|
|
async _select({
|
|
start = DateTime.local().toUnixInteger(),
|
|
end,
|
|
query,
|
|
tags,
|
|
places,
|
|
show_recurrent,
|
|
show_multidate,
|
|
ap_id=null,
|
|
limit,
|
|
page,
|
|
older,
|
|
reverse,
|
|
include_description=false }) {
|
|
|
|
const where = {
|
|
// do not include _parent_ recurrent event
|
|
recurrent: null,
|
|
|
|
// confirmed event only
|
|
is_visible: true,
|
|
|
|
[Op.or]: {
|
|
start_datetime: { [older ? Op.lte : Op.gte]: start },
|
|
end_datetime: { [older ? Op.lte : Op.gte]: start }
|
|
}
|
|
}
|
|
|
|
// include recurrent events?
|
|
if (!show_recurrent) {
|
|
where.parentId = null
|
|
}
|
|
|
|
if (typeof ap_id !== 'undefined') {
|
|
where.apUserApId = ap_id
|
|
}
|
|
|
|
if (!show_multidate) {
|
|
where.multidate = { [Op.not]: true }
|
|
}
|
|
|
|
if (end) {
|
|
where.start_datetime = { [older ? Op.gte : Op.lte]: end }
|
|
}
|
|
|
|
// normalize tags
|
|
if (tags) {
|
|
tags = tags.split(',').map(t => t.trim().toLocaleLowerCase())
|
|
}
|
|
|
|
const replacements = []
|
|
if (tags && places) {
|
|
where[Op.and] = [
|
|
{ placeId: places ? places.split(',') : [] },
|
|
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) in (?)`))
|
|
]
|
|
replacements.push(tags)
|
|
} else if (tags) {
|
|
where[Op.and] = Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) in (?)`))
|
|
replacements.push(tags)
|
|
} else if (places) {
|
|
where.placeId = places.split(',')
|
|
}
|
|
|
|
if (query) {
|
|
query = query.toLocaleLowerCase()
|
|
replacements.push(query)
|
|
replacements.push(query)
|
|
where[Op.or] =
|
|
[
|
|
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + query + '%') },
|
|
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + query + '%'),
|
|
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
|
|
]
|
|
}
|
|
|
|
let pagination = {}
|
|
if (limit) {
|
|
pagination = {
|
|
limit,
|
|
offset: limit * page,
|
|
}
|
|
}
|
|
|
|
const events = await Event.findAll({
|
|
where,
|
|
attributes: {
|
|
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'resources', 'recurrent', 'placeId', 'image_path', ...(!include_description ? ['description']: [])]
|
|
},
|
|
order: [['start_datetime', reverse ? 'DESC' : 'ASC']],
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
// order: [Sequelize.literal('(SELECT COUNT(tagTag) FROM event_tags WHERE tagTag = tag) DESC')],
|
|
attributes: ['tag'],
|
|
through: { attributes: [] }
|
|
},
|
|
{ model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
|
|
],
|
|
...pagination,
|
|
replacements
|
|
}).catch(e => {
|
|
log.error('[EVENT]' + String(e))
|
|
return []
|
|
})
|
|
|
|
return events.map(e => {
|
|
e = e.get()
|
|
e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
|
|
return e
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Select events based on params
|
|
*/
|
|
async select(req, res) {
|
|
const settings = res.locals.settings
|
|
const start = req.query.start || DateTime.local().toUnixInteger()
|
|
const end = req.query.end
|
|
const query = req.query.query
|
|
const tags = req.query.tags
|
|
const places = req.query.places
|
|
const limit = Number(req.query.max) || 0
|
|
const page = Number(req.query.page) || 0
|
|
const older = req.query.older || false
|
|
|
|
const show_multidate = settings.allow_multidate_event && helpers.queryParamToBool(req.query.show_multidate, true)
|
|
|
|
const show_recurrent = settings.allow_recurrent_event && helpers.queryParamToBool(req.query.show_recurrent, settings.recurrent_event_visible)
|
|
|
|
let events = []
|
|
if (settings.collection_in_home && !(tags || places)) {
|
|
events = await collectionController._getEvents({
|
|
name: settings.collection_in_home,
|
|
start,
|
|
end,
|
|
limit
|
|
})
|
|
} else {
|
|
events = await eventController._select({
|
|
start, end, query, places, tags, show_recurrent, show_multidate, limit, page, older
|
|
})
|
|
}
|
|
|
|
return res.json(events)
|
|
},
|
|
|
|
/**
|
|
* Ensure we have the next occurrence of a recurrent event
|
|
*/
|
|
async _createRecurrentOccurrence(e, startAt = DateTime.local(), firstOccurrence = true) {
|
|
log.debug(`Create recurrent event [${e.id}] ${e.title}"`)
|
|
|
|
// prepare the new event occurrence copying the parent's properties
|
|
const event = {
|
|
parentId: e.id,
|
|
title: e.title,
|
|
description: e.description,
|
|
media: e.media,
|
|
is_visible: true,
|
|
userId: e.userId,
|
|
placeId: e.placeId,
|
|
...(e.online_locations && { online_locations: e.online_locations } )
|
|
}
|
|
|
|
const recurrentDetails = e.recurrent
|
|
const parentStartDatetime = DateTime.fromSeconds(e.start_datetime)
|
|
|
|
// cursor is when start to count
|
|
// in case parent is in past, start to calculate from now
|
|
let cursor = parentStartDatetime > startAt ? parentStartDatetime : startAt
|
|
startAt = cursor
|
|
|
|
const duration = e.end_datetime ? e.end_datetime-e.start_datetime : 0
|
|
const frequency = recurrentDetails.frequency
|
|
const type = recurrentDetails.type
|
|
if (!frequency) {
|
|
log.warn(`Recurrent event ${e.id} - ${e.title} does not have a frequency specified`)
|
|
return
|
|
}
|
|
|
|
cursor = cursor.set({ hour: parentStartDatetime.hour, minute: parentStartDatetime.minute, second: 0 })
|
|
|
|
// each week or 2
|
|
if (frequency[1] === 'w') {
|
|
cursor = cursor.set({ weekday: parentStartDatetime.weekday }) //day(parentStartDatetime.day())
|
|
if (cursor < startAt) {
|
|
cursor = cursor.plus({ days: 7 * Number(frequency[0]) })
|
|
}
|
|
} else if (frequency === '1m') {
|
|
|
|
// day n.X each month
|
|
if (type === 'ordinal') {
|
|
cursor = cursor.set({ day: parentStartDatetime.day })
|
|
|
|
if (cursor< startAt) {
|
|
cursor = cursor.plus({ months: 1 })
|
|
}
|
|
} else { // weekday
|
|
|
|
// get recurrent freq details
|
|
cursor = helpers.getWeekdayN(cursor, type, parentStartDatetime.weekday)
|
|
if (cursor < startAt) {
|
|
cursor = cursor.plus({ months: 1 })
|
|
cursor = helpers.getWeekdayN(cursor, type, parentStartDatetime.weekday)
|
|
}
|
|
}
|
|
}
|
|
log.debug(cursor)
|
|
event.start_datetime = cursor.toUnixInteger()
|
|
event.end_datetime = e.end_datetime ? event.start_datetime + duration : null
|
|
try {
|
|
const newEvent = await Event.create(event)
|
|
if (e.tags) {
|
|
return newEvent.addTags(e.tags)
|
|
} else {
|
|
return newEvent
|
|
}
|
|
} catch (e) {
|
|
console.error(event)
|
|
log.error('[RECURRENT EVENT]', e)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create instances of recurrent events
|
|
*/
|
|
async _createRecurrent(start_datetime = DateTime.local().toUnixInteger()) {
|
|
// select recurrent events and its childs
|
|
const events = await Event.findAll({
|
|
where: { is_visible: true, recurrent: { [Op.ne]: null } },
|
|
include: [{ model: Tag, required: false },
|
|
{ model: Event, as: 'child', required: false, where: { start_datetime: { [Op.gte]: start_datetime } } }],
|
|
order: [['child', 'start_datetime', 'DESC']]
|
|
})
|
|
|
|
// create a new occurrence for each recurring events but the one's that has an already visible occurrence coming
|
|
const creations = events.map(e => {
|
|
if (e.child.length) {
|
|
if (e.child.find(c => c.is_visible)) return
|
|
return eventController._createRecurrentOccurrence(e, DateTime.fromSeconds(e.child[0].start_datetime + 1), false)
|
|
}
|
|
return eventController._createRecurrentOccurrence(e, DateTime.local(), true)
|
|
})
|
|
|
|
return Promise.all(creations)
|
|
}
|
|
}
|
|
|
|
module.exports = eventController
|