commit 5c0d380740c24e0467cef916fd0560cb26409f9f Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:22:25 2023 +0100 update yarn.lock commit 909ee71ecb8f27e4fba72430aecc92bf527e6cd4 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:22:09 2023 +0100 Squashed commit of the following: commitfc8a9f4506Author: lesion <lesion@autistici.org> Date: Tue Mar 14 16:42:24 2023 +0100 address some issues with recurrent events, fix #247 commitf7357666caAuthor: lesion <lesion@autistici.org> Date: Tue Mar 14 16:16:52 2023 +0100 fix event import from URL commite1bca6f46aAuthor: lesion <lesion@autistici.org> Date: Tue Mar 14 16:15:42 2023 +0100 add Duch (nl) locale (thanks @jeoenepraat) commit5f8afdbc12Merge:57a052a92ca5abAuthor: lesion <lesion@autistici.org> Date: Tue Mar 14 11:39:50 2023 +0100 Merge remote-tracking branch 'weblate/master' commit57a052a7faMerge:63d1d2e55137d2Author: lesion <lesion@autistici.org> Date: Tue Mar 14 11:39:33 2023 +0100 Merge commit '55137d2ac23549e633f36ad10139fd4168c2645f' commit92ca5abf5eAuthor: joenepraat <joenepraat@posteo.org> Date: Fri Mar 10 23:16:32 2023 +0000 Translated using Weblate (Dutch) Currently translated at 68.3% (214 of 313 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/nl/ commit63d1d2ee53Author: lesion <lesion@autistici.org> Date: Thu Mar 9 21:41:06 2023 +0100 minor commitd2759a55a5Author: lesion <lesion@autistici.org> Date: Thu Mar 9 21:38:39 2023 +0100 wrong user / admin merge dark theme settings - fix #244 commitb401d829dbAuthor: lesion <lesion@autistici.org> Date: Thu Mar 9 21:24:45 2023 +0100 remove a small warning commitccffe5f7b0Author: lesion <lesion@autistici.org> Date: Fri Feb 24 11:40:36 2023 +0100 push tags on release commit55137d2ac2Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Feb 23 23:56:16 2023 +0000 Bump sequelize from 6.28.0 to 6.29.0 Bumps [sequelize](https://github.com/sequelize/sequelize) from 6.28.0 to 6.29.0. - [Release notes](https://github.com/sequelize/sequelize/releases) - [Commits](https://github.com/sequelize/sequelize/compare/v6.28.0...v6.29.0) --- updated-dependencies: - dependency-name: sequelize dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> commitb654f29d8bAuthor: lesion <lesion@autistici.org> Date: Wed Feb 22 13:21:17 2023 +0100 update changelog commit0cd1ee9d89Author: lesion <lesion@autistici.org> Date: Wed Feb 22 13:17:29 2023 +0100 increase rate limit max requests per minutes commitb6dafc082eAuthor: lesion <lesion@autistici.org> Date: Wed Feb 22 08:45:39 2023 +0100 minor commit0fa7769844Author: lesion <lesion@autistici.org> Date: Wed Feb 22 08:45:18 2023 +0100 location saving is not working when geocoding is disabled, fix #238 commit07f9e2d9eeAuthor: lesion <lesion@autistici.org> Date: Wed Feb 22 08:33:40 2023 +0100 really fix #232 commitbae930799eAuthor: lesion <lesion@autistici.org> Date: Wed Feb 22 08:33:09 2023 +0100 downgrade mariadb (sequelize is not ready) commitd733d7fef1Author: lesion <lesion@autistici.org> Date: Wed Feb 22 00:16:28 2023 +0100 aargh commit98b22aad70Author: lesion <lesion@autistici.org> Date: Tue Feb 21 00:56:06 2023 +0100 minor commitfc098b603dAuthor: lesion <lesion@autistici.org> Date: Tue Feb 21 00:55:44 2023 +0100 missing i18n in setup, fix #239 commit3eaf72af19Merge:bba196bd6c6034Author: lesion <lesion@autistici.org> Date: Mon Feb 20 21:17:37 2023 +0100 Merge remote-tracking branch 'weblate/master' commitbba196b068Author: lesion <lesion@autistici.org> Date: Sat Feb 18 00:05:52 2023 +0100 update changelog, v1.6.3 commitbb9f7cca47Author: lesion <lesion@autistici.org> Date: Sat Feb 18 00:04:28 2023 +0100 minor commit80d2dbd06bAuthor: lesion <lesion@autistici.org> Date: Fri Feb 17 23:40:28 2023 +0100 minor commitd6c6034630Author: fadelkon <fadelkon@posteo.net> Date: Thu Feb 16 22:09:23 2023 +0000 Translated using Weblate (Catalan) Currently translated at 100.0% (313 of 313 strings) Translation: Gancio/Web Translate-URL: https://hosted.weblate.org/projects/gancio/web/ca/ commitd125cf1506Author: lesion <lesion@autistici.org> Date: Fri Feb 17 21:56:31 2023 +0100 set a default user_locale path commit4367960a62Merge:c8cc5c687dd179Author: lesion <lesion@autistici.org> Date: Tue Feb 7 17:46:58 2023 +0100 Merge branch 'master' into gh commitc8cc5c6c97Merge:88e0c90550e221Author: lesion <lesion@autistici.org> Date: Mon Jan 9 17:15:21 2023 +0100 Merge branch 'master' into gh commit88e0c90a66Merge:421aa12f212ac1Author: lesion <lesion@autistici.org> Date: Thu Dec 15 09:54:41 2022 +0100 Merge branch 'master' into gh commit421aa12781Merge:5f6cc46b3488e7Author: lesion <lesion@autistici.org> Date: Wed Sep 28 12:26:08 2022 +0200 Merge branch 'master' into gh commit5f6cc46cdcMerge:b66feb9171d968Author: lesion <lesion@autistici.org> Date: Mon Aug 8 00:08:12 2022 +0200 Merge branch 'master' into gh commitb66feb92e2Merge:80c55d505d068fAuthor: lesion <lesion@autistici.org> Date: Tue Jun 21 23:48:40 2022 +0200 Merge branch 'master' into gh commit80c55d5601Merge:814090ea154fdfAuthor: lesion <lesion@autistici.org> Date: Mon Jun 6 17:27:00 2022 +0200 Merge branch 'master' into gh commit814090e9b6Merge:616c5422e3aba9Author: lesion <lesion@autistici.org> Date: Mon Jun 6 17:19:31 2022 +0200 Merge branch 'master' into gh commit616c54229aMerge:e4cb22e82dcaf9Author: lesion <lesion@autistici.org> Date: Mon Jun 6 16:57:05 2022 +0200 Merge branch 'master' into gh commite4cb22ee33Merge:5dddfbd8657937Author: lesion <lesion@autistici.org> Date: Fri Mar 11 23:41:22 2022 +0100 Merge branch 'master' into gh commit5dddfbd29eMerge:60e9d9510c6b0dAuthor: lesion <lesion@autistici.org> Date: Fri Mar 11 23:22:12 2022 +0100 Merge branch 'master' into gh commit60e9d95ba8Merge:79445caad93f83Author: lesion <lesion@autistici.org> Date: Tue Dec 7 01:35:18 2021 +0100 Merge branch 'master' into gh commit79445ca8a7Merge:9472d8dcd313efAuthor: les <lesion@autistici.org> Date: Thu Jun 24 21:52:25 2021 +0200 Merge branch 'master' into gh commit9472d8d919Merge:f9601499e9643eAuthor: les <lesion@autistici.org> Date: Fri Mar 26 22:27:41 2021 +0100 Merge branch 'dev' into gh commitf9601492dcAuthor: les <lesion@autistici.org> Date: Fri Dec 6 11:30:41 2019 +0100 update dependencies commitf8c7fa2b45Author: les <lesion@autistici.org> Date: Fri Dec 6 11:41:13 2019 +0100 minor commit33ca266535Author: les <lesion@autistici.org> Date: Fri Dec 6 11:38:15 2019 +0100 prepare gh as a mirror commit 5c8875411631048210eb50030e83cb272a40d54a Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:18:40 2023 +0100 update deps commit 7eac4fce324a6e75cdda296d672317cf2497c005 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:18:25 2023 +0100 refactoring event detail page commit dc9ca88bc62708b869be3f3efe51d9155fe17830 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:17:35 2023 +0100 show hide boosts/bookmarks, fix #241 commit d4a25b1dd0b9404e0de7ca5cf546f0d29bc8943e Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:13:58 2023 +0100 minor with unixFormat commit 239d6bcab19ef3cf53d1b2544a5c9a36ba8dd25b Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:12:25 2023 +0100 minor commit b149f980db8245c12a6940997be6d5657bddf829 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:12:05 2023 +0100 minor commit 6f2955c584ec9da2c10991fb09ab57735a31385d Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:11:49 2023 +0100 minor commit dd586c38c9ef2f0b408ef90eb27dffe53355305a Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:11:31 2023 +0100 minor on style commit 544823717b9801e63bef15394b25bfbcd842c10f Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:11:15 2023 +0100 fix multidate issue, go to event on save commit 9ef0c75d03ee2d69f89034b28d6991f85ffefb06 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 23:09:47 2023 +0100 use v-lazy, improve search, full tag/place events commit ac91072b79960815e0535e63ac45e0b5c6100764 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 22:47:51 2023 +0100 increase DDOS limiter to 250 req/min commit d0ca92efb4afe48d2fd236083d9e290ab8d49704 Author: lesion <lesion@autistici.org> Date: Sun Mar 19 22:47:14 2023 +0100 update changelog commit 2d54f19225acc4118d60ef8c9d12f9495e6776ca Author: lesion <lesion@autistici.org> Date: Sun Mar 19 22:46:51 2023 +0100 use luxon instead of dayjs, new $time plugin
754 lines
23 KiB
JavaScript
754 lines
23 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 dayjs = require('dayjs')
|
|
const helpers = require('../../helpers')
|
|
const Col = helpers.col
|
|
const notifier = require('../../notifier')
|
|
|
|
const { Event, Resource, Tag, Place, Notification, APUser } = require('../models/models')
|
|
|
|
|
|
const exportController = require('./export')
|
|
const tagController = require('./tag')
|
|
|
|
const log = require('../../log')
|
|
|
|
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_address || !place_name) {
|
|
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
|
|
|
|
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'] }
|
|
],
|
|
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)
|
|
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]: dayjs().unix() }
|
|
},
|
|
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`)
|
|
}
|
|
|
|
// 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,
|
|
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 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: body.start_datetime || event.start_datetime,
|
|
end_datetime: body.end_datetime || null,
|
|
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
|
|
}
|
|
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 {
|
|
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())
|
|
}
|
|
}
|
|
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)
|
|
await event.destroy()
|
|
res.sendStatus(200)
|
|
} else {
|
|
res.sendStatus(403)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method to search for events with pagination and filtering
|
|
* @returns
|
|
*/
|
|
async _select({
|
|
start = dayjs().unix(),
|
|
end,
|
|
query,
|
|
tags,
|
|
places,
|
|
show_recurrent,
|
|
show_multidate,
|
|
limit,
|
|
page,
|
|
older }) {
|
|
|
|
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 (!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) {
|
|
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', 'description', 'resources', 'recurrent', 'placeId', 'image_path']
|
|
},
|
|
order: [['start_datetime', older ? '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 || dayjs().unix()
|
|
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 &&
|
|
typeof req.query.show_multidate !== 'undefined' ? req.query.show_multidate !== 'false' : true
|
|
|
|
const show_recurrent = settings.allow_recurrent_event &&
|
|
typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible
|
|
|
|
res.json(await eventController._select({
|
|
start, end, query, places, tags, show_recurrent, show_multidate, limit, page, older
|
|
}))
|
|
},
|
|
|
|
/**
|
|
* Ensure we have the next instance of a recurrent event
|
|
*/
|
|
async _createRecurrentOccurrence(e, startAt) {
|
|
log.debug(`Create recurrent event [${e.id}] ${e.title}"`)
|
|
const event = {
|
|
parentId: e.id,
|
|
title: e.title,
|
|
description: e.description,
|
|
media: e.media,
|
|
is_visible: true,
|
|
userId: e.userId,
|
|
placeId: e.placeId
|
|
}
|
|
|
|
const recurrent = e.recurrent
|
|
const start_date = dayjs.unix(e.start_datetime)
|
|
let cursor = start_date > startAt ? start_date : startAt
|
|
startAt = cursor
|
|
const duration = e.end_datetime ? e.end_datetime-e.start_datetime : 0
|
|
const frequency = recurrent.frequency
|
|
const type = recurrent.type
|
|
|
|
cursor = cursor.hour(start_date.hour()).minute(start_date.minute()).second(0)
|
|
|
|
if (!frequency) { return }
|
|
|
|
// each week or 2
|
|
if (frequency[1] === 'w') {
|
|
cursor = cursor.day(start_date.day())
|
|
if (cursor.isBefore(startAt)) {
|
|
cursor = cursor.add(7, 'day')
|
|
}
|
|
if (frequency[0] === '2') {
|
|
cursor = cursor.add(7, 'day')
|
|
}
|
|
} else if (frequency === '1m') {
|
|
if (type === 'ordinal') {
|
|
cursor = cursor.date(start_date.date())
|
|
|
|
if (cursor.isBefore(startAt)) {
|
|
cursor = cursor.add(1, 'month')
|
|
}
|
|
} else { // weekday
|
|
// get weekday
|
|
// get recurrent freq details
|
|
cursor = helpers.getWeekdayN(cursor, type, start_date.day())
|
|
if (cursor.isBefore(startAt)) {
|
|
cursor = cursor.add(4, 'week')
|
|
cursor = helpers.getWeekdayN(cursor, type, start_date.day())
|
|
}
|
|
}
|
|
}
|
|
log.debug(cursor)
|
|
event.start_datetime = cursor.unix()
|
|
event.end_datetime = e.end_datetime ? event.start_datetime + duration : null
|
|
try {
|
|
const newEvent = await Event.create(event)
|
|
return newEvent.addTags(e.tags)
|
|
} catch (e) {
|
|
console.error(event)
|
|
log.error('[RECURRENT EVENT]', e)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create instances of recurrent events
|
|
*/
|
|
async _createRecurrent(start_datetime = dayjs().unix()) {
|
|
// 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, dayjs.unix(e.child[0].start_datetime + 1))
|
|
}
|
|
return eventController._createRecurrentOccurrence(e, dayjs())
|
|
})
|
|
|
|
return Promise.all(creations)
|
|
}
|
|
}
|
|
|
|
module.exports = eventController
|