diff --git a/.eslintrc.js b/.eslintrc.js index b6351b05..6962b689 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,6 @@ module.exports = { - "extends": "standard" + "extends": "standard", + "rules": { + "camelcase": 0 + } }; diff --git a/app/api.js b/app/api.js index dd316d79..261f7052 100644 --- a/app/api.js +++ b/app/api.js @@ -3,6 +3,8 @@ const { fillUser, isAuth, isAdmin } = require('./auth') const eventController = require('./controller/event') const exportController = require('./controller/export') const userController = require('./controller/user') +const settingsController = require('./controller/settings') + // const botController = require('./controller/bot') const multer = require('multer') @@ -51,6 +53,9 @@ api.get('/event/unconfirmed', isAuth, isAdmin, eventController.getUnconfirmed) api.post('/event/notification', eventController.addNotification) api.delete('/event/del_notification/:code', eventController.delNotification) +api.get('/settings', settingsController.getAdminSettings) +api.post('/settings', settingsController.setAdminSetting) + // get event api.get('/event/:event_id', eventController.get) diff --git a/app/controller/bot.js b/app/controller/bot.js index d2c0104e..38851e55 100644 --- a/app/controller/bot.js +++ b/app/controller/bot.js @@ -1,10 +1,13 @@ -const { User, Event, Comment, Tag } = require('../model') +// const { User, Event, Comment, Tag } = require('../model') const config = require('../config') const Mastodon = require('mastodon-api') -const Sequelize = require('sequelize') -const Op = Sequelize.Op +// const Sequelize = require('sequelize') +// const Op = Sequelize.Op +const fs = require('fs') +const path = require('path') const moment = require('moment') moment.locale('it') + const botController = { bots: [], // async initialize () { @@ -30,12 +33,20 @@ const botController = { // listener.on('error', botController.error) // botController.bots.push({ email: user.email, bot }) // }, - post (user, event) { - const { client_id, client_secret, access_token } = user.mastodon_auth - const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` }) + async post (mastodon_auth, event) { + const { access_token, instance } = mastodon_auth + const bot = new Mastodon({ access_token, api_url: `https://${instance}/api/v1/` }) const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} - ${event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}` - return bot.post('/statuses', { status, visibility: 'private' }) + + let media + if (event.image_path) { + const file = path.join(__dirname, '..', '..', event.image_path) + if (fs.statSync(file)) { + media = await bot.post('media', { file: fs.createReadStream(file) }) + } + } + return bot.post('statuses', { status, visibility: 'direct', media_ids: media ? [media.data.id] : [] }) } // async message (msg) { // console.log(msg) diff --git a/app/controller/event.js b/app/controller/event.js index d542eb09..bb9f8d57 100644 --- a/app/controller/event.js +++ b/app/controller/event.js @@ -19,7 +19,6 @@ const eventController = { }, async getMeta (req, res) { - console.log('GET META') const places = await Place.findAll() const tags = await Tag.findAll() res.json({ tags, places }) @@ -28,6 +27,7 @@ const eventController = { async getNotifications (event) { function match (event, filters) { // matches if no filter specified + if (!filters) return true if (!filters.tags.length && !filters.places.length) return true if (filters.tags.length) { const m = lodash.intersection(event.tags.map(t => t.tag), filters.tags) @@ -47,7 +47,6 @@ const eventController = { async updateTag (req, res) { const tag = await Tag.findByPk(req.body.tag) - console.log(tag) if (tag) { res.json(await tag.update(req.body)) } else { diff --git a/app/controller/export.js b/app/controller/export.js index dba94cf7..407b8b4e 100644 --- a/app/controller/export.js +++ b/app/controller/export.js @@ -5,6 +5,7 @@ const moment = require('moment') const ics = require('ics') const exportController = { + async export (req, res) { console.log('type ', req.params.type) const type = req.params.type @@ -34,6 +35,7 @@ const exportController = { return exportController.ics(res, events) } }, + async feed (res, events) { res.type('application/rss+xml; charset=UTF-8') res.render('feed/rss.pug', { events, config, moment }) diff --git a/app/controller/settings.js b/app/controller/settings.js new file mode 100644 index 00000000..85f32819 --- /dev/null +++ b/app/controller/settings.js @@ -0,0 +1,27 @@ +const { Settings } = require('../model') + +const settingsController = { + async setAdminSetting (key, value) { + await Settings.findOrCreate({ where: { key }, + defaults: { value } }) + .spread((settings, created) => { + if (!created) return settings.update({ value }) + }) + }, + + async getAdminSettings (req, res) { + const settings = await settingsController.settings() + res.json(settings) + }, + + async settings () { + const settings = await Settings.findAll() + const map = {} + settings.forEach(setting => { + map[setting.key] = setting.value + }) + return map + } +} + +module.exports = settingsController diff --git a/app/controller/user.js b/app/controller/user.js index a54e0b02..e4b0f2af 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -2,7 +2,8 @@ const jwt = require('jsonwebtoken') const Mastodon = require('mastodon-api') const User = require('../models/user') -const { Event, Tag, Place, Notification } = require('../models/event') +const { Event, Tag, Place } = require('../models/event') +const settingsController = require('./settings') const eventController = require('./event') const config = require('../config') const mail = require('../mail') @@ -57,7 +58,7 @@ const userController = { async addEvent (req, res) { const body = req.body - // remove description tag and create anchor tag + // remove description tag and create anchor tags const description = body.description .replace(/(<([^>]+)>)/ig, '') .replace(/(https?:\/\/[^\s]+)/g, '$1') @@ -68,28 +69,27 @@ const userController = { multidate: body.multidate, start_datetime: body.start_datetime, end_datetime: body.end_datetime, - is_visible: req.user ? true : false + is_visible: !!req.user } if (req.file) { eventDetails.image_path = req.file.path } + let event = await Event.create(eventDetails) + // create place let place try { place = await Place.findOrCreate({ where: { name: body.place_name }, defaults: { address: body.place_address } }) .spread((place, created) => place) + await event.setPlace(place) } catch (e) { - console.log(e) + console.error(e) } - let event = await Event.create(eventDetails) - await event.setPlace(place) - // create/assign tags - console.log(body.tags) if (body.tags) { await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } }) @@ -98,22 +98,11 @@ const userController = { if (req.user) await req.user.addEvent(event) event = await Event.findByPk(event.id, { include: [User, Tag, Place] }) - // check if bot exists - if (req.user && req.user.mastodon_auth) { - const post = await bot.post(req.user, event) - event.activitypub_id = post.id - event.save() - } - if (req.user) { // insert notifications const notifications = await eventController.getNotifications(event) await event.setNotifications(notifications) - } else { - const notification = await Notification.create({ type: 'admin_email' }) - await event.setNotification(notification) } - return res.json(event) }, @@ -139,7 +128,6 @@ const userController = { } await event.setPlace(place) await event.setTags([]) - console.log(body.tags) if (body.tags) { await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) const tags = await Tag.findAll({ where: { tag: { [Op.eq]: body.tags } } }) @@ -157,26 +145,47 @@ const userController = { async getAuthURL (req, res) { const instance = req.body.instance - const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, 'eventi', 'read write', `${config.baseurl}/settings`) - const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, `https://${instance}`, 'read write', `${config.baseurl}/settings`) - console.log(req.user) - req.user.mastodon_instance = instance - req.user.mastodon_auth = { client_id, client_secret } - await req.user.save() + const is_admin = req.body.admin && req.user.is_admin + const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}` + const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, + config.title, 'read write', callback) + const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, + `https://${instance}`, 'read write', callback) + + if (is_admin) { + await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance }) + } else { + req.user.mastodon_auth = { client_id, client_secret, instance } + await req.user.save() + } res.json(url) }, async code (req, res) { - const code = req.body.code - const { client_id, client_secret } = req.user.mastodon_auth - const instance = req.user.mastodon_instance + const { code, is_admin } = req.body + let client_id, client_secret, instance + const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}` + + if (is_admin) { + const settings = await settingsController.settings(); + ({ client_id, client_secret, instance } = settings.mastodon_auth) + } else { + ({ client_id, client_secret, instance } = req.user.mastodon_auth) + } + try { - const token = await Mastodon.getAccessToken(client_id, client_secret, code, `https://${instance}`, `${config.baseurl}/settings`) - const mastodon_auth = { client_id, client_secret, access_token: token } - req.user.mastodon_auth = mastodon_auth - await req.user.save() - await bot.add(req.user, token) - res.json(req.user) + const token = await Mastodon.getAccessToken(client_id, client_secret, code, + `https://${instance}`, callback) + const mastodon_auth = { client_id, client_secret, access_token: token, instance } + if (is_admin) { + await settingsController.setAdminSetting('mastodon_auth', mastodon_auth) + res.json(instance) + } else { + req.user.mastodon_auth = mastodon_auth + await req.user.save() + // await bot.add(req.user, token) + res.json(req.user) + } } catch (e) { res.json(e) } @@ -204,13 +213,18 @@ const userController = { }, async register (req, res) { + const n_users = await User.count() try { - req.body.is_active = false + if (n_users === 0) { + // admin will be the first registered user + req.body.is_active = req.body.is_admin = true + } else { + req.body.is_active = false + } const user = await User.create(req.body) try { mail.send(user.email, 'register', { user }) } catch (e) { - console.log(e) return res.status(400).json(e) } const payload = { email: user.email } diff --git a/app/cron.js b/app/cron.js index 7a31eff1..8826817a 100644 --- a/app/cron.js +++ b/app/cron.js @@ -1,26 +1,57 @@ const mail = require('./mail') -const { Event, Notification, EventNotification, User, Place, Tag } = require('./model') +const bot = require('./controller/bot') +const settingsController = require('./controller/settings') + +const { Event, Notification, EventNotification, + User, Place, Tag } = require('./model') +let settings + +async function sendNotification (notification, event, eventNotification) { + const promises = [] + try { + switch (notification.type) { + case 'mail': + const p = mail.send(notification.email, 'event', { event }) + promises.push(p) + break + case 'mail_admin': + const admins = await User.findAll({ where: { is_admin: true } }) + promises.push(admins.map(admin => + mail.send(admin.email, 'event', { event, to_confirm: true, notification }))) + break + case 'mastodon': + // instance publish + if (settings.mastodon_auth.instance) { + const b = bot.post(settings.mastodon_auth, event) + promises.push(b) + } + // user publish + if (event.user && event.user.mastodon_auth) { + const b = bot.post(event.user.mastodon_auth, event).then(ret => { + event.activitypub_id = ret.id + return event.save() + }) + promises.push(b) + } + break + } + } catch (e) { + console.log('CATCH!', e) + return false + } + return Promise.all(promises) +} async function loop () { + settings = await settingsController.settings() // get all event notification in queue const eventNotifications = await EventNotification.findAll() const promises = eventNotifications.map(async e => { const event = await Event.findByPk(e.eventId, { include: [User, Place, Tag] }) if (!event.place) return const notification = await Notification.findByPk(e.notificationId) - try { - if (notification.type === 'mail') { - await mail.send(notification.email, 'event', { event }) - } else if (notification.type === 'mail_admin') { - const admins = await User.findAll({ where: { is_admin: true } }) - await Promise.all(admins.map(admin => - mail.send(admin.email, 'event', { event, to_confirm: true, notification }))) - } - } catch (e) { - console.log('CATCH!', e) - return false - } - return e.destroy() + await sendNotification(notification, event, e) + e.destroy() }) return Promise.all(promises) diff --git a/app/db.js b/app/db.js index 98f95a23..3d526686 100644 --- a/app/db.js +++ b/app/db.js @@ -1,6 +1,5 @@ const Sequelize = require('sequelize') const conf = require('./config.js') -console.error(conf.db) const db = new Sequelize(conf.db) // db.sync({ force: true }) diff --git a/app/locales/es.json b/app/locales/es.json index f01ae3b9..9cd985b3 100644 --- a/app/locales/es.json +++ b/app/locales/es.json @@ -1,3 +1,3 @@ { "registration_email": "registration_email" -} \ No newline at end of file +} diff --git a/app/model.js b/app/model.js index 81e07db1..303fa1dd 100644 --- a/app/model.js +++ b/app/model.js @@ -1,4 +1,14 @@ const User = require('./models/user') const { Event, Comment, Tag, Place, Notification, EventNotification } = require('./models/event') +const Settings = require('./models/settings') -module.exports = { User, Event, Comment, Tag, Place, Notification, EventNotification } +module.exports = { + User, + Event, + Comment, + Tag, + Place, + Notification, + EventNotification, + Settings +} diff --git a/app/models/event.js b/app/models/event.js index 334b8ef1..27436ac5 100644 --- a/app/models/event.js +++ b/app/models/event.js @@ -30,10 +30,13 @@ const Notification = db.define('notification', { remove_code: Sequelize.STRING, type: { type: Sequelize.ENUM, - values: ['mail', 'admin_mail', 'activity_pub'] + values: ['mail', 'admin_mail', 'mastodon'] } }) +Notification.findOrCreate({ where: { type: 'mastodon' } }) +Notification.findOrCreate({ where: { type: 'admin_email' } }) + const Place = db.define('place', { name: { type: Sequelize.STRING, unique: true, index: true }, address: { type: Sequelize.STRING } diff --git a/app/models/settings.js b/app/models/settings.js new file mode 100644 index 00000000..148b7bdb --- /dev/null +++ b/app/models/settings.js @@ -0,0 +1,9 @@ +const db = require('../db') +const Sequelize = require('sequelize') + +const Settings = db.define('settings', { + key: { type: Sequelize.STRING, primaryKey: true, index: true }, + value: Sequelize.JSON +}) + +module.exports = Settings diff --git a/app/models/user.js b/app/models/user.js index 45eb963f..3af9a95a 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -13,7 +13,6 @@ const User = db.define('user', { password: Sequelize.STRING, is_admin: Sequelize.BOOLEAN, is_active: Sequelize.BOOLEAN, - mastodon_instance: Sequelize.STRING, mastodon_auth: Sequelize.JSON }) diff --git a/app/server.js b/app/server.js index 8b8b7be2..7d75173b 100644 --- a/app/server.js +++ b/app/server.js @@ -11,8 +11,8 @@ app.set('views', path.join(__dirname, 'views')) app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()) app.use(cors()) -app.use('/static', express.static(path.join(__dirname, 'uploads'))) -app.use('/uploads', express.static('uploads')) +app.use('/static', express.static(path.join(__dirname, '..', 'uploads'))) +app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'))) app.use('/api', api) app.use('/', express.static(path.join(__dirname, '..', 'client', 'dist'))) app.use('/css', express.static(path.join(__dirname, '..', 'client', 'dist', 'css'))) diff --git a/app/storage.js b/app/storage.js index 4c81ce1d..7e117daa 100644 --- a/app/storage.js +++ b/app/storage.js @@ -23,10 +23,10 @@ DiskStorage.prototype._handleFile = function _handleFile (req, file, cb) { that.getDestination(req, file, function (err, destination) { if (err) return cb(err) - const filename = crypto.randomBytes(16).toString('hex') + '.webp' + const filename = crypto.randomBytes(16).toString('hex') + '.jpg' const finalPath = path.join(destination, filename) const outStream = fs.createWriteStream(finalPath) - const resizer = sharp().resize(800).webp() + const resizer = sharp().resize(800).jpeg({ quality: 80 }) file.stream.pipe(resizer).pipe(outStream) outStream.on('error', cb) diff --git a/client/src/api.js b/client/src/api.js index d293039e..d2908963 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -60,5 +60,7 @@ export default { getAuthURL: mastodonInstance => post('/user/getauthurl', mastodonInstance), setCode: code => post('/user/code', code), getKnowLocations: () => get('/locations'), - getKnowTags: () => get('/tags') + getKnowTags: () => get('/tags'), + getAdminSettings: () => get('/settings') + // setAdminSetting: (key, value) => post('/settings', { key, value }) } diff --git a/client/src/components/Admin.vue b/client/src/components/Admin.vue index 87317905..75c7596b 100644 --- a/client/src/components/Admin.vue +++ b/client/src/components/Admin.vue @@ -1,6 +1,6 @@