models initialization refactored, better dev experience as backend hmr is working
This commit is contained in:
parent
380eaa87ca
commit
cbed0288fe
43 changed files with 624 additions and 707 deletions
|
@ -1,5 +1,9 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
###
|
||||
|
||||
- models initialization refactored, better dev xperience as backend hmr is working
|
||||
|
||||
### 1.6.1 - 15 dec '22
|
||||
- allow edit tags in admin panel, fix #170
|
||||
- fix header / fallback image upload, fix #222
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"passport-oauth2-client-public": "^0.0.1",
|
||||
"pg": "^8.8.0",
|
||||
"sequelize": "^6.27.0",
|
||||
"sequelize": "^6.28.0",
|
||||
"sequelize-slugify": "^1.6.2",
|
||||
"sharp": "^0.27.2",
|
||||
"sqlite3": "^5.1.4",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Announcement = require('../models/announcement')
|
||||
const { Announcement } = require('../models/models')
|
||||
|
||||
const log = require('../../log')
|
||||
|
||||
const announceController = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const APUser = require('../models/ap_user')
|
||||
const { APUser } = require('../models/models')
|
||||
|
||||
const apUserController = {
|
||||
async toggleBlock (req, res) {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
const Collection = require('../models/collection')
|
||||
const Filter = require('../models/filter')
|
||||
const Event = require('../models/event')
|
||||
const Tag = require('../models/tag')
|
||||
const Place = require('../models/place')
|
||||
const { Collection, Filter, Event, Tag, Place } = require('../models/models')
|
||||
|
||||
const log = require('../../log')
|
||||
const dayjs = require('dayjs')
|
||||
const { col: Col } = require('../../helpers')
|
||||
|
@ -114,7 +111,7 @@ const collectionController = {
|
|||
res.json(collection)
|
||||
} catch (e) {
|
||||
log.error(`Create collection failed ${e}`)
|
||||
res.sendStatus(400)
|
||||
res.status(400).send(e)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -138,15 +135,14 @@ const collectionController = {
|
|||
},
|
||||
|
||||
async addFilter (req, res) {
|
||||
const collectionId = req.body.collectionId
|
||||
const tags = req.body.tags
|
||||
const places = req.body.places
|
||||
const { collectionId, tags, places } = req.body
|
||||
|
||||
try {
|
||||
const filter = await Filter.create({ collectionId, tags, places })
|
||||
filter = await Filter.create({ collectionId, tags, places })
|
||||
return res.json(filter)
|
||||
} catch (e) {
|
||||
log.error(String(e))
|
||||
return res.status(500)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -170,6 +166,4 @@ const collectionController = {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = collectionController
|
|
@ -9,12 +9,10 @@ const Sequelize = require('sequelize')
|
|||
const dayjs = require('dayjs')
|
||||
const helpers = require('../../helpers')
|
||||
const Col = helpers.col
|
||||
const Event = require('../models/event')
|
||||
const Resource = require('../models/resource')
|
||||
const Tag = require('../models/tag')
|
||||
const Place = require('../models/place')
|
||||
const Notification = require('../models/notification')
|
||||
const APUser = require('../models/ap_user')
|
||||
const notifier = require('../../notifier')
|
||||
|
||||
const { Event, Resource, Tag, Place, Notification, APUser } = require('../models/models')
|
||||
|
||||
|
||||
const exportController = require('./export')
|
||||
const tagController = require('./tag')
|
||||
|
@ -155,34 +153,6 @@ const eventController = {
|
|||
|
||||
},
|
||||
|
||||
async getNotifications(event, action) {
|
||||
log.debug(`getNotifications ${event.title} ${action}`)
|
||||
function match(event, filters) {
|
||||
// matches if no filter specified
|
||||
if (!filters) { return true }
|
||||
|
||||
// check for visibility
|
||||
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false }
|
||||
|
||||
if (!filters.tags && !filters.places) { return true }
|
||||
if (!filters.tags.length && !filters.places.length) { return true }
|
||||
if (filters.tags.length) {
|
||||
const m = intersection(event.tags.map(t => t.tag), filters.tags)
|
||||
if (m.length > 0) { return true }
|
||||
}
|
||||
if (filters.places.length) {
|
||||
if (filters.places.find(p => p === event.place.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifications = await Notification.findAll({ where: { action }, include: [Event] })
|
||||
|
||||
// get notification that matches with selected event
|
||||
return notifications.filter(notification => match(event, notification.filters))
|
||||
},
|
||||
|
||||
async _get(slug) {
|
||||
// retrocompatibility, old events URL does not use slug, use id as fallback
|
||||
const id = Number(slug) || -1
|
||||
|
@ -317,7 +287,6 @@ const eventController = {
|
|||
res.sendStatus(200)
|
||||
|
||||
// send notification
|
||||
const notifier = require('../../notifier')
|
||||
notifier.notifyEvent('Create', event.id)
|
||||
} catch (e) {
|
||||
log.error('[EVENT]', e)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
const Event = require('../models/event')
|
||||
const Place = require('../models/place')
|
||||
const Tag = require('../models/tag')
|
||||
const { Event, Place, Tag } = require('../models/models')
|
||||
|
||||
const { htmlToText } = require('html-to-text')
|
||||
const { Op, literal } = require('sequelize')
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const APUser = require('../models/ap_user')
|
||||
const Instance = require('../models/instance')
|
||||
const Resource = require('../models/resource')
|
||||
const { APUser, Instance, Resource } = require('../models/models')
|
||||
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
const instancesController = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const User = require('../models/user')
|
||||
const User = require('../models/modles')
|
||||
|
||||
const metrics = {
|
||||
|
||||
|
|
|
@ -2,12 +2,9 @@ const bodyParser = require('body-parser')
|
|||
const cookieParser = require('cookie-parser')
|
||||
const session = require('cookie-session')
|
||||
|
||||
const OAuthClient = require('../models/oauth_client')
|
||||
const OAuthToken = require('../models/oauth_token')
|
||||
const OAuthCode = require('../models/oauth_code')
|
||||
const { OAuthClient, OAuthToken, OAuthCode, User } = require('../models/models')
|
||||
|
||||
const helpers = require('../../helpers.js')
|
||||
const User = require('../models/user')
|
||||
const passport = require('passport')
|
||||
|
||||
const get = require('lodash/get')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const Place = require('../models/place')
|
||||
const Event = require('../models/event')
|
||||
const { Place, Event } = require('../models/models')
|
||||
|
||||
const eventController = require('./event')
|
||||
const exportController = require('./export')
|
||||
|
||||
|
|
|
@ -2,11 +2,12 @@ const path = require('path')
|
|||
const fs = require('fs')
|
||||
const log = require('../../log')
|
||||
const config = require('../../config')
|
||||
const settingsController = require('./settings')
|
||||
const notifier = require('../../notifier')
|
||||
|
||||
const pluginController = {
|
||||
plugins: [],
|
||||
getAll(_req, res) {
|
||||
const settingsController = require('./settings')
|
||||
// return plugins and inner settings
|
||||
const plugins = pluginController.plugins.map( ({ configuration }) => {
|
||||
if (settingsController.settings['plugin_' + configuration.name]) {
|
||||
|
@ -18,7 +19,6 @@ const pluginController = {
|
|||
},
|
||||
|
||||
togglePlugin(req, res) {
|
||||
const settingsController = require('./settings')
|
||||
const pluginName = req.params.plugin
|
||||
const pluginSettings = settingsController.settings['plugin_' + pluginName]
|
||||
if (!pluginSettings) { return res.sendStatus(404) }
|
||||
|
@ -33,7 +33,6 @@ const pluginController = {
|
|||
},
|
||||
|
||||
unloadPlugin(pluginName) {
|
||||
const settingsController = require('./settings')
|
||||
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
|
||||
const settings = settingsController.settings['plugin_' + pluginName]
|
||||
if (!plugin) {
|
||||
|
@ -59,14 +58,12 @@ const pluginController = {
|
|||
},
|
||||
|
||||
loadPlugin(pluginName) {
|
||||
const settingsController = require('./settings')
|
||||
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
|
||||
const settings = settingsController.settings['plugin_' + pluginName]
|
||||
if (!plugin) {
|
||||
log.warn(`Plugin ${pluginName} not found`)
|
||||
return
|
||||
}
|
||||
const notifier = require('../../notifier')
|
||||
log.info('Load plugin ' + pluginName)
|
||||
if (typeof plugin.onEventCreate === 'function') {
|
||||
notifier.emitter.on('Create', plugin.onEventCreate)
|
||||
|
@ -88,7 +85,6 @@ const pluginController = {
|
|||
},
|
||||
|
||||
_load() {
|
||||
const settingsController = require('./settings')
|
||||
// load custom plugins
|
||||
const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'plugins')
|
||||
log.info(`Loading plugin ${plugins_path}`)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
const Resource = require('../models/resource')
|
||||
const APUser = require('../models/ap_user')
|
||||
const Event = require('../models/event')
|
||||
const { Resource, APUser, Event } = require('../models/models')
|
||||
const get = require('lodash/get')
|
||||
|
||||
const resourceController = {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const path = require('path')
|
||||
const URL = require('url')
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const { promisify } = require('util')
|
||||
const sharp = require('sharp')
|
||||
|
@ -9,7 +8,7 @@ const generateKeyPair = promisify(crypto.generateKeyPair)
|
|||
const log = require('../../log')
|
||||
// const locales = require('../../../locales/index')
|
||||
const escape = require('lodash/escape')
|
||||
const pluginController = require('./plugins')
|
||||
const DB = require('../models/models')
|
||||
|
||||
let defaultHostname
|
||||
try {
|
||||
|
@ -30,7 +29,7 @@ const defaultSettings = {
|
|||
allow_multidate_event: true,
|
||||
allow_recurrent_event: false,
|
||||
recurrent_event_visible: false,
|
||||
allow_geolocation: true,
|
||||
allow_geolocation: false,
|
||||
geocoding_provider_type: 'Nominatim',
|
||||
geocoding_provider: 'https://nominatim.openstreetmap.org/search',
|
||||
geocoding_countrycodes: [],
|
||||
|
@ -74,8 +73,7 @@ const settingsController = {
|
|||
// initialize instance settings from db
|
||||
// note that this is done only once when the server starts
|
||||
// and not for each request
|
||||
const Setting = require('../models/setting')
|
||||
const settings = await Setting.findAll()
|
||||
const settings = await DB.Setting.findAll()
|
||||
settingsController.settings = defaultSettings
|
||||
settings.forEach(s => {
|
||||
if (s.is_secret) {
|
||||
|
@ -117,15 +115,14 @@ const settingsController = {
|
|||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
const pluginController = require('./plugins')
|
||||
pluginController._load()
|
||||
},
|
||||
|
||||
async set (key, value, is_secret = false) {
|
||||
const Setting = require('../models/setting')
|
||||
log.info(`SET ${key} ${is_secret ? '*****' : value}`)
|
||||
try {
|
||||
const [setting, created] = await Setting.findOrCreate({
|
||||
const [setting, created] = await DB.Setting.findOrCreate({
|
||||
where: { key },
|
||||
defaults: { value, is_secret }
|
||||
})
|
||||
|
|
|
@ -7,6 +7,8 @@ const settingsController = require('./settings')
|
|||
const path = require('path')
|
||||
const escape = require('lodash/escape')
|
||||
|
||||
const DB = require('../models/models')
|
||||
|
||||
const setupController = {
|
||||
|
||||
async _setupDb (dbConf) {
|
||||
|
@ -23,7 +25,10 @@ const setupController = {
|
|||
|
||||
// try to connect
|
||||
dbConf.logging = false
|
||||
await db.connect(dbConf)
|
||||
db.connect(dbConf)
|
||||
db.loadModels()
|
||||
db.associates()
|
||||
await db.sequelize.authenticate()
|
||||
|
||||
// is empty ?
|
||||
const isEmpty = await db.isEmpty()
|
||||
|
@ -69,8 +74,7 @@ const setupController = {
|
|||
// create admin
|
||||
const password = helpers.randomString()
|
||||
const email = `admin`
|
||||
const User = require('../models/user')
|
||||
await User.create({
|
||||
await DB.User.create({
|
||||
email,
|
||||
password,
|
||||
is_admin: true,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const Tag = require('../models/tag')
|
||||
const Event = require('../models/event')
|
||||
const { Tag, Event } = require('../models/models')
|
||||
const uniq = require('lodash/uniq')
|
||||
const log = require('../../log')
|
||||
|
||||
|
@ -82,20 +81,22 @@ module.exports = {
|
|||
return res.json(tags.map(t => t.tag))
|
||||
},
|
||||
|
||||
async updateTag (req, res) {
|
||||
const tag = await Tag.findByPk(req.body.tag)
|
||||
await tag.update(req.body)
|
||||
res.json(place)
|
||||
},
|
||||
// async updateTag (req, res) {
|
||||
// const tag = await Tag.findByPk(req.body.tag)
|
||||
// await tag.update(req.body)
|
||||
// res.json(place)
|
||||
// },
|
||||
|
||||
async updateTag (req, res) {
|
||||
try {
|
||||
const oldtag = await Tag.findByPk(req.body.tag)
|
||||
const newtag = await Tag.findByPk(req.body.newTag)
|
||||
|
||||
// if the new tag does not exists, just rename the old one
|
||||
if (!newtag) {
|
||||
oldtag.tag = req.body.newTag
|
||||
await oldtag.update({ tag: req.body.newTag })
|
||||
log.info(`Rename tag ${oldtag.tag} to ${req.body.newTag}`)
|
||||
await Tag.update({ tag: req.body.newTag }, { where: { tag: req.body.tag }, raw: true })
|
||||
|
||||
} else {
|
||||
// in case it exists:
|
||||
// - search for events with old tag
|
||||
|
@ -105,6 +106,10 @@ module.exports = {
|
|||
await newtag.addEvents(events)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
res.sendStatus(400)
|
||||
}
|
||||
},
|
||||
|
||||
async remove (req, res) {
|
||||
|
|
|
@ -2,7 +2,7 @@ const crypto = require('crypto')
|
|||
const { Op } = require('sequelize')
|
||||
const config = require('../../config')
|
||||
const mail = require('../mail')
|
||||
const User = require('../models/user')
|
||||
const { User } = require('../models/models')
|
||||
const settingsController = require('./settings')
|
||||
const log = require('../../log')
|
||||
const linkify = require('linkifyjs')
|
||||
|
|
|
@ -5,6 +5,26 @@ const cors = require('cors')()
|
|||
const config = require('../config')
|
||||
const log = require('../log')
|
||||
|
||||
const collectionController = require('./controller/collection')
|
||||
const setupController = require('./controller/setup')
|
||||
const settingsController = require('./controller/settings')
|
||||
const eventController = require('./controller/event')
|
||||
const placeController = require('./controller/place')
|
||||
const tagController = require('./controller/tag')
|
||||
const exportController = require('./controller/export')
|
||||
const userController = require('./controller/user')
|
||||
const instanceController = require('./controller/instance')
|
||||
const apUserController = require('./controller/ap_user')
|
||||
const resourceController = require('./controller/resource')
|
||||
const oauthController = require('./controller/oauth')
|
||||
const announceController = require('./controller/announce')
|
||||
const pluginController = require('./controller/plugins')
|
||||
const helpers = require('../helpers')
|
||||
const storage = require('./storage')
|
||||
|
||||
|
||||
module.exports = () => {
|
||||
|
||||
const api = express.Router()
|
||||
api.use(express.urlencoded({ extended: false }))
|
||||
api.use(express.json())
|
||||
|
@ -12,8 +32,6 @@ api.use(express.json())
|
|||
|
||||
if (config.status !== 'READY') {
|
||||
|
||||
const setupController = require('./controller/setup')
|
||||
const settingsController = require('./controller/settings')
|
||||
api.post('/settings', settingsController.setRequest)
|
||||
api.post('/setup/db', setupController.setupDb)
|
||||
api.post('/setup/restart', setupController.restart)
|
||||
|
@ -22,21 +40,6 @@ if (config.status !== 'READY') {
|
|||
} else {
|
||||
|
||||
const { isAuth, isAdmin } = require('./auth')
|
||||
const eventController = require('./controller/event')
|
||||
const placeController = require('./controller/place')
|
||||
const tagController = require('./controller/tag')
|
||||
const settingsController = require('./controller/settings')
|
||||
const exportController = require('./controller/export')
|
||||
const userController = require('./controller/user')
|
||||
const instanceController = require('./controller/instance')
|
||||
const apUserController = require('./controller/ap_user')
|
||||
const resourceController = require('./controller/resource')
|
||||
const oauthController = require('./controller/oauth')
|
||||
const announceController = require('./controller/announce')
|
||||
const collectionController = require('./controller/collection')
|
||||
const pluginController = require('./controller/plugins')
|
||||
const helpers = require('../helpers')
|
||||
const storage = require('./storage')
|
||||
const upload = multer({ storage })
|
||||
|
||||
/**
|
||||
|
@ -220,4 +223,5 @@ api.use((error, _req, res, _next) => {
|
|||
res.status(500).send('500: Internal Server Error')
|
||||
})
|
||||
|
||||
module.exports = api
|
||||
return api
|
||||
}
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
class Announcement extends Model {}
|
||||
|
||||
Announcement.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('announcement', {
|
||||
title: DataTypes.STRING,
|
||||
announcement: DataTypes.STRING,
|
||||
visible: DataTypes.BOOLEAN
|
||||
}, { sequelize, modelName: 'announcement' })
|
||||
|
||||
module.exports = Announcement
|
||||
})
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
class APUser extends Model {}
|
||||
|
||||
APUser.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('ap_user', {
|
||||
ap_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
|
@ -11,6 +8,4 @@ APUser.init({
|
|||
follower: DataTypes.BOOLEAN,
|
||||
blocked: DataTypes.BOOLEAN,
|
||||
object: DataTypes.JSON
|
||||
}, { sequelize, modelName: 'ap_user' })
|
||||
|
||||
module.exports = APUser
|
||||
})
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
const { Model, DataTypes } = require('sequelize')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
class Collection extends Model {}
|
||||
|
||||
// TODO: slugify!
|
||||
Collection.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('collection', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
|
@ -22,7 +17,4 @@ Collection.init({
|
|||
isTop: {
|
||||
type: DataTypes.BOOLEAN
|
||||
}
|
||||
}, { sequelize, modelName: 'collection', timestamps: false })
|
||||
|
||||
|
||||
module.exports = Collection
|
||||
}, { timestamps: false })
|
||||
|
|
|
@ -1,18 +1,5 @@
|
|||
const config = require('../../config')
|
||||
const { htmlToText } = require('html-to-text')
|
||||
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
const SequelizeSlugify = require('sequelize-slugify')
|
||||
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
const Resource = require('./resource')
|
||||
const Notification = require('./notification')
|
||||
const EventNotification = require('./eventnotification')
|
||||
const Place = require('./place')
|
||||
const User = require('./user')
|
||||
const Tag = require('./tag')
|
||||
|
||||
const dayjs = require('dayjs')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
|
@ -20,9 +7,9 @@ const utc = require('dayjs/plugin/utc')
|
|||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
class Event extends Model {}
|
||||
|
||||
Event.init({
|
||||
// class Event extends Model {}
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Event = sequelize.define('event', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
type: DataTypes.INTEGER,
|
||||
|
@ -51,27 +38,7 @@ Event.init({
|
|||
recurrent: DataTypes.JSON,
|
||||
likes: { type: DataTypes.JSON, defaultValue: [] },
|
||||
boost: { type: DataTypes.JSON, defaultValue: [] }
|
||||
}, { sequelize, modelName: 'event' })
|
||||
|
||||
Event.belongsTo(Place)
|
||||
Place.hasMany(Event)
|
||||
|
||||
Event.belongsTo(User)
|
||||
User.hasMany(Event)
|
||||
|
||||
Event.belongsToMany(Tag, { through: 'event_tags' })
|
||||
Tag.belongsToMany(Event, { through: 'event_tags' })
|
||||
|
||||
Event.belongsToMany(Notification, { through: EventNotification })
|
||||
Notification.belongsToMany(Event, { through: EventNotification })
|
||||
|
||||
Event.hasMany(Resource)
|
||||
Resource.belongsTo(Event)
|
||||
|
||||
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
|
||||
Event.belongsTo(Event, { as: 'parent' })
|
||||
|
||||
SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false })
|
||||
})
|
||||
|
||||
Event.prototype.toAP = function (username, locale, to = []) {
|
||||
const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_'))
|
||||
|
@ -123,5 +90,5 @@ Event.prototype.toAP = function (username, locale, to = []) {
|
|||
summary: content
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Event
|
||||
return Event
|
||||
}
|
|
@ -1,15 +1,9 @@
|
|||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
class EventNotification extends Model {}
|
||||
|
||||
EventNotification.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('event_notification', {
|
||||
status: {
|
||||
type: DataTypes.ENUM,
|
||||
values: ['new', 'sent', 'error', 'sending'],
|
||||
defaultValue: 'new',
|
||||
index: true
|
||||
}
|
||||
}, { sequelize, modelName: 'event_notification' })
|
||||
|
||||
module.exports = EventNotification
|
||||
})
|
|
@ -1,10 +1,6 @@
|
|||
const { Model, DataTypes } = require('sequelize')
|
||||
const Collection = require('./collection')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
class Filter extends Model {}
|
||||
|
||||
Filter.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('filter',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
|
@ -16,9 +12,9 @@ Filter.init({
|
|||
places: {
|
||||
type: DataTypes.JSON,
|
||||
}
|
||||
}, { sequelize, modelName: 'filter', timestamps: false })
|
||||
|
||||
Filter.belongsTo(Collection)
|
||||
Collection.hasMany(Filter)
|
||||
|
||||
module.exports = Filter
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['collectionId', 'tags', 'places'], unique: true }
|
||||
],
|
||||
timestamps: false
|
||||
})
|
|
@ -4,10 +4,78 @@ const Umzug = require('umzug')
|
|||
const path = require('path')
|
||||
const config = require('../../config')
|
||||
const log = require('../../log')
|
||||
const settingsController = require('../controller/settings')
|
||||
const SequelizeSlugify = require('sequelize-slugify')
|
||||
const DB = require('./models')
|
||||
|
||||
const models = {
|
||||
Announcement: require('./announcement'),
|
||||
APUser: require('./ap_user'),
|
||||
Collection: require('./collection'),
|
||||
Event: require('./event'),
|
||||
EventNotification: require('./eventnotification'),
|
||||
Filter: require('./filter'),
|
||||
Instance: require('./instance'),
|
||||
Notification: require('./notification'),
|
||||
OAuthClient: require('./oauth_client'),
|
||||
OAuthCode: require('./oauth_code'),
|
||||
OAuthToken: require('./oauth_token'),
|
||||
Place: require('./place'),
|
||||
Resource: require('./resource'),
|
||||
Setting: require('./setting'),
|
||||
Tag: require('./tag'),
|
||||
User: require('./user'),
|
||||
}
|
||||
|
||||
const db = {
|
||||
sequelize: null,
|
||||
loadModels () {
|
||||
|
||||
for (const modelName in models) {
|
||||
const m = models[modelName](db.sequelize, Sequelize.DataTypes)
|
||||
DB[modelName] = m
|
||||
}
|
||||
|
||||
},
|
||||
associates () {
|
||||
const { Filter, Collection, APUser, Instance, User, Event, EventNotification, Tag,
|
||||
OAuthCode, OAuthClient, OAuthToken, Resource, Place, Notification } = DB
|
||||
|
||||
Filter.belongsTo(Collection)
|
||||
Collection.hasMany(Filter)
|
||||
|
||||
Instance.hasMany(APUser)
|
||||
APUser.belongsTo(Instance)
|
||||
|
||||
OAuthCode.belongsTo(User)
|
||||
OAuthCode.belongsTo(OAuthClient, { as: 'client' })
|
||||
|
||||
OAuthToken.belongsTo(User)
|
||||
OAuthToken.belongsTo(OAuthClient, { as: 'client' })
|
||||
|
||||
APUser.hasMany(Resource)
|
||||
Resource.belongsTo(APUser)
|
||||
|
||||
Event.belongsTo(Place)
|
||||
Place.hasMany(Event)
|
||||
|
||||
Event.belongsTo(User)
|
||||
User.hasMany(Event)
|
||||
|
||||
Event.belongsToMany(Tag, { through: 'event_tags' })
|
||||
Tag.belongsToMany(Event, { through: 'event_tags' })
|
||||
|
||||
Event.belongsToMany(Notification, { through: EventNotification })
|
||||
Notification.belongsToMany(Event, { through: EventNotification })
|
||||
|
||||
Event.hasMany(Resource)
|
||||
Resource.belongsTo(Event)
|
||||
|
||||
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
|
||||
Event.belongsTo(Event, { as: 'parent' })
|
||||
|
||||
SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false })
|
||||
|
||||
},
|
||||
close() {
|
||||
if (db.sequelize) {
|
||||
return db.sequelize.close()
|
||||
|
@ -28,7 +96,6 @@ const db = {
|
|||
}
|
||||
}
|
||||
db.sequelize = new Sequelize(dbConf)
|
||||
return db.sequelize.authenticate()
|
||||
},
|
||||
async isEmpty() {
|
||||
try {
|
||||
|
@ -57,13 +124,12 @@ const db = {
|
|||
})
|
||||
return umzug.up()
|
||||
},
|
||||
async initialize() {
|
||||
initialize() {
|
||||
if (config.status === 'CONFIGURED') {
|
||||
try {
|
||||
await db.connect()
|
||||
log.debug('Running migrations')
|
||||
await db.runMigrations()
|
||||
return settingsController.load()
|
||||
db.connect()
|
||||
db.loadModels()
|
||||
db.associates()
|
||||
} catch (e) {
|
||||
log.warn(` ⚠️ Cannot connect to db, check your configuration => ${e}`)
|
||||
process.exit(1)
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
|
||||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
const APUser = require('./ap_user')
|
||||
|
||||
class Instance extends Model {}
|
||||
|
||||
Instance.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('instance', {
|
||||
domain: {
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
|
@ -14,9 +8,4 @@ Instance.init({
|
|||
name: DataTypes.STRING,
|
||||
blocked: DataTypes.BOOLEAN,
|
||||
data: DataTypes.JSON
|
||||
}, { sequelize, modelName: 'instance' })
|
||||
|
||||
Instance.hasMany(APUser)
|
||||
APUser.belongsTo(Instance)
|
||||
|
||||
module.exports = Instance
|
||||
})
|
2
server/api/models/models.js
Normal file
2
server/api/models/models.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// export default models
|
||||
module.exports = {}
|
|
@ -1,10 +1,5 @@
|
|||
|
||||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
class Notification extends Model {}
|
||||
|
||||
Notification.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('notification', {
|
||||
filters: DataTypes.JSON,
|
||||
email: DataTypes.STRING,
|
||||
remove_code: DataTypes.STRING,
|
||||
|
@ -25,5 +20,3 @@ Notification.init({
|
|||
fields: ['action', 'type']
|
||||
}]
|
||||
})
|
||||
|
||||
module.exports = Notification
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
|
||||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
class OAuthClient extends Model {}
|
||||
|
||||
OAuthClient.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('oauth_client', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
|
@ -15,6 +10,4 @@ OAuthClient.init({
|
|||
scopes: DataTypes.STRING,
|
||||
redirectUris: DataTypes.STRING,
|
||||
website: DataTypes.STRING
|
||||
}, { sequelize, modelName: 'oauth_client' })
|
||||
|
||||
module.exports = OAuthClient
|
||||
})
|
|
@ -1,13 +1,5 @@
|
|||
|
||||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
const User = require('./user')
|
||||
const OAuthClient = require('./oauth_client')
|
||||
|
||||
class OAuthCode extends Model {}
|
||||
|
||||
OAuthCode.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('oauth_code', {
|
||||
authorizationCode: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
|
@ -15,9 +7,4 @@ OAuthCode.init({
|
|||
expiresAt: DataTypes.DATE,
|
||||
scope: DataTypes.STRING,
|
||||
redirect_uri: DataTypes.STRING
|
||||
}, { sequelize, modelName: 'oauth_code' })
|
||||
|
||||
OAuthCode.belongsTo(User)
|
||||
OAuthCode.belongsTo(OAuthClient, { as: 'client' })
|
||||
|
||||
module.exports = OAuthCode
|
||||
})
|
|
@ -1,13 +1,5 @@
|
|||
|
||||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
const User = require('./user')
|
||||
const OAuthClient = require('./oauth_client')
|
||||
|
||||
class OAuthToken extends Model {}
|
||||
|
||||
OAuthToken.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('oauth_token', {
|
||||
accessToken: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
|
@ -27,9 +19,4 @@ OAuthToken.init({
|
|||
}
|
||||
},
|
||||
scope: DataTypes.STRING
|
||||
}, { sequelize, modelName: 'oauth_token' })
|
||||
|
||||
OAuthToken.belongsTo(User)
|
||||
OAuthToken.belongsTo(OAuthClient, { as: 'client' })
|
||||
|
||||
module.exports = OAuthToken
|
||||
})
|
|
@ -1,9 +1,5 @@
|
|||
const { Model, DataTypes } = require('sequelize')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
class Place extends Model {}
|
||||
|
||||
Place.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('place', {
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
|
@ -13,6 +9,4 @@ Place.init({
|
|||
address: DataTypes.STRING,
|
||||
latitude: DataTypes.FLOAT,
|
||||
longitude: DataTypes.FLOAT,
|
||||
}, { sequelize, modelName: 'place' })
|
||||
|
||||
module.exports = Place
|
||||
})
|
|
@ -1,11 +1,5 @@
|
|||
const { Model, DataTypes } = require('sequelize')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
const APUser = require('./ap_user')
|
||||
|
||||
class Resource extends Model {}
|
||||
|
||||
Resource.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('resource', {
|
||||
activitypub_id: {
|
||||
type: DataTypes.STRING,
|
||||
index: true,
|
||||
|
@ -13,9 +7,4 @@ Resource.init({
|
|||
},
|
||||
hidden: DataTypes.BOOLEAN,
|
||||
data: DataTypes.JSON
|
||||
}, { sequelize, modelName: 'resource' })
|
||||
|
||||
APUser.hasMany(Resource)
|
||||
Resource.belongsTo(APUser)
|
||||
|
||||
module.exports = Resource
|
||||
})
|
|
@ -1,9 +1,5 @@
|
|||
const { Model, DataTypes } = require('sequelize')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
class Setting extends Model {}
|
||||
|
||||
Setting.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('setting', {
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
|
@ -12,6 +8,4 @@ Setting.init({
|
|||
},
|
||||
value: DataTypes.JSON,
|
||||
is_secret: DataTypes.BOOLEAN
|
||||
}, { sequelize, modelName: 'setting' })
|
||||
|
||||
module.exports = Setting
|
||||
})
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
const { Model, DataTypes } = require('sequelize')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
class Tag extends Model {}
|
||||
|
||||
Tag.init({
|
||||
module.exports = (sequelize, DataTypes) =>
|
||||
sequelize.define('tag', {
|
||||
tag: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
index: true,
|
||||
primaryKey: true
|
||||
}
|
||||
}, { sequelize, modelName: 'tag' })
|
||||
|
||||
module.exports = Tag
|
||||
})
|
|
@ -1,11 +1,8 @@
|
|||
|
||||
const bcrypt = require('bcryptjs')
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
const sequelize = require('./index').sequelize
|
||||
|
||||
class User extends Model {}
|
||||
|
||||
User.init({
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const User = sequelize.define('user', {
|
||||
settings: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: []
|
||||
|
@ -25,8 +22,6 @@ User.init({
|
|||
is_admin: DataTypes.BOOLEAN,
|
||||
is_active: DataTypes.BOOLEAN
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'user',
|
||||
scopes: {
|
||||
withoutPassword: {
|
||||
attributes: { exclude: ['password', 'recover_code'] }
|
||||
|
@ -50,4 +45,5 @@ User.beforeSave(async (user, _options) => {
|
|||
}
|
||||
})
|
||||
|
||||
module.exports = User
|
||||
return User
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ function _initializeDB () {
|
|||
async function modify (args) {
|
||||
await _initializeDB()
|
||||
const helpers = require('../helpers')
|
||||
const User = require('../api/models/user')
|
||||
const { User } = require('../api/models/models')
|
||||
const user = await User.findOne({ where: { email: args.account } })
|
||||
console.log()
|
||||
if (!user) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
const axios = require('axios')
|
||||
// const request = require('request')
|
||||
const crypto = require('crypto')
|
||||
const config = require('../config')
|
||||
const httpSignature = require('http-signature')
|
||||
const APUser = require('../api/models/ap_user')
|
||||
const Instance = require('../api/models/instance')
|
||||
|
||||
const { APUser, Instance } = require('../api/models/models')
|
||||
|
||||
const url = require('url')
|
||||
const settingsController = require('../api/controller/settings')
|
||||
const log = require('../log')
|
||||
|
|
|
@ -270,9 +270,9 @@ module.exports = {
|
|||
},
|
||||
|
||||
async APRedirect(req, res, next) {
|
||||
const eventController = require('../server/api/controller/event')
|
||||
const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json'
|
||||
if (acceptJson) {
|
||||
const eventController = require('../server/api/controller/event')
|
||||
const event = await eventController._get(req.params.slug)
|
||||
if (event) {
|
||||
return res.redirect(`/federation/m/${event.id}`)
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
const config = require('../server/config')
|
||||
const db = require('./api/models/index')
|
||||
const log = require('../server/log')
|
||||
|
||||
db.initialize()
|
||||
|
||||
const settingsController = require('./api/controller/settings')
|
||||
|
||||
|
||||
const initialize = {
|
||||
// close connections/port/unix socket
|
||||
|
@ -19,14 +26,14 @@ const initialize = {
|
|||
},
|
||||
|
||||
async start () {
|
||||
const log = require('../server/log')
|
||||
const settingsController = require('./api/controller/settings')
|
||||
const db = require('./api/models/index')
|
||||
const dayjs = require('dayjs')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
dayjs.extend(timezone)
|
||||
if (config.status == 'CONFIGURED') {
|
||||
await db.initialize()
|
||||
await db.sequelize.authenticate()
|
||||
log.debug('Running migrations')
|
||||
await db.runMigrations()
|
||||
await settingsController.load()
|
||||
config.status = 'READY'
|
||||
} else {
|
||||
if (process.env.GANCIO_DB_DIALECT) {
|
||||
|
|
|
@ -4,14 +4,10 @@ const mail = require('./api/mail')
|
|||
const log = require('./log')
|
||||
const fediverseHelpers = require('./federation/helpers')
|
||||
|
||||
const Event = require('./api/models/event')
|
||||
const Notification = require('./api/models/notification')
|
||||
const EventNotification = require('./api/models/eventnotification')
|
||||
const User = require('./api/models/user')
|
||||
const Place = require('./api/models/place')
|
||||
const Tag = require('./api/models/tag')
|
||||
|
||||
const eventController = require('./api/controller/event')
|
||||
const { Event, Notification, EventNotification, User, Place, Tag } = require('./api/models/models')
|
||||
|
||||
|
||||
const settingsController = require('./api/controller/settings')
|
||||
|
||||
const notifier = {
|
||||
|
@ -37,7 +33,36 @@ const notifier = {
|
|||
return Promise.all(promises)
|
||||
},
|
||||
|
||||
async getNotifications(event, action) {
|
||||
log.debug(`getNotifications ${event.title} ${action}`)
|
||||
function match(event, filters) {
|
||||
// matches if no filter specified
|
||||
if (!filters) { return true }
|
||||
|
||||
// check for visibility
|
||||
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false }
|
||||
|
||||
if (!filters.tags && !filters.places) { return true }
|
||||
if (!filters.tags.length && !filters.places.length) { return true }
|
||||
if (filters.tags.length) {
|
||||
const m = intersection(event.tags.map(t => t.tag), filters.tags)
|
||||
if (m.length > 0) { return true }
|
||||
}
|
||||
if (filters.places.length) {
|
||||
if (filters.places.find(p => p === event.place.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifications = await Notification.findAll({ where: { action }, include: [Event] })
|
||||
|
||||
// get notification that matches with selected event
|
||||
return notifications.filter(notification => match(event, notification.filters))
|
||||
},
|
||||
|
||||
async notifyEvent (action, eventId) {
|
||||
|
||||
const event = await Event.findByPk(eventId, {
|
||||
include: [Tag, Place, Notification, User]
|
||||
})
|
||||
|
@ -46,7 +71,7 @@ const notifier = {
|
|||
log.debug(action, event.title)
|
||||
|
||||
// insert notifications
|
||||
const notifications = await eventController.getNotifications(event, action)
|
||||
const notifications = await notifier.getNotifications(event, action)
|
||||
await event.addNotifications(notifications)
|
||||
const event_notifications = await event.getNotifications({ through: { where: { status: 'new' } } })
|
||||
|
||||
|
|
|
@ -4,23 +4,22 @@ const initialize = require('./initialize.server')
|
|||
|
||||
const config = require('./config')
|
||||
const helpers = require('./helpers')
|
||||
const api = require('./api')
|
||||
|
||||
async function main () {
|
||||
|
||||
await initialize.start()
|
||||
|
||||
app.use([
|
||||
helpers.initSettings,
|
||||
helpers.logRequest,
|
||||
helpers.serveStatic()
|
||||
])
|
||||
|
||||
async function main () {
|
||||
|
||||
await initialize.start()
|
||||
|
||||
// const metricsController = require('./metrics')
|
||||
// const promBundle = require('express-prom-bundle')
|
||||
// const metricsMiddleware = promBundle({ includeMethod: true })
|
||||
|
||||
const log = require('./log')
|
||||
const api = require('./api')
|
||||
|
||||
app.enable('trust proxy')
|
||||
|
||||
|
@ -60,7 +59,7 @@ async function main () {
|
|||
}
|
||||
|
||||
// api!
|
||||
app.use('/api', api)
|
||||
app.use('/api', api())
|
||||
|
||||
// // Handle 500
|
||||
app.use((error, _req, res, _next) => {
|
||||
|
@ -87,8 +86,6 @@ if (process.env.NODE_ENV !== 'test') {
|
|||
main()
|
||||
}
|
||||
|
||||
// app.listen(13120)
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
handler: app,
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -10634,10 +10634,10 @@ ret@~0.1.10:
|
|||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
||||
|
||||
retry-as-promised@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-6.1.0.tgz#11eca9a0f97804d552ec8e74bc4eb839bd226dc4"
|
||||
integrity sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA==
|
||||
retry-as-promised@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.3.tgz#ca3c13b15525a7bfbf0f56d2996f0e75649d068b"
|
||||
integrity sha512-SEvMa4khHvpU/o6zgh7sK24qm6rxVgKnrSyzb5POeDvZx5N9Bf0s5sQsQ4Fl+HjRp0X+w2UzACGfUnXtx6cJ9Q==
|
||||
|
||||
retry@^0.12.0:
|
||||
version "0.12.0"
|
||||
|
@ -10926,10 +10926,10 @@ sequelize-slugify@^1.6.2:
|
|||
dependencies:
|
||||
sluglife "^0.9.8"
|
||||
|
||||
sequelize@^6.27.0:
|
||||
version "6.27.0"
|
||||
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.27.0.tgz#b267e76997df57842cc1e2c1c1d7e02405bcdb9c"
|
||||
integrity sha512-Rm7BM8HQekeABup0KdtSHriu8ppJuHj2TJWCxvZtzU6j8V1LVnBk2rs38P8r4gMWgdLKs5NYoLC4il95KLsv0w==
|
||||
sequelize@^6.28.0:
|
||||
version "6.28.0"
|
||||
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.28.0.tgz#d6bc4e36647e8501635467c0777c45a33f5d5ba8"
|
||||
integrity sha512-+WHqvUQgTp19GLkt+gyQ+F6qg+FIEO2O5F9C0TOYV/PjZ2a/XwWvVkL1NCkS4VSIjVVvAUutiW6Wv9ofveGaVw==
|
||||
dependencies:
|
||||
"@types/debug" "^4.1.7"
|
||||
"@types/validator" "^13.7.1"
|
||||
|
@ -10940,7 +10940,7 @@ sequelize@^6.27.0:
|
|||
moment "^2.29.1"
|
||||
moment-timezone "^0.5.34"
|
||||
pg-connection-string "^2.5.0"
|
||||
retry-as-promised "^6.1.0"
|
||||
retry-as-promised "^7.0.3"
|
||||
semver "^7.3.5"
|
||||
sequelize-pool "^7.1.0"
|
||||
toposort-class "^1.0.1"
|
||||
|
|
Loading…
Reference in a new issue