feat: refactoring event moderation, report, report notification

This commit is contained in:
lesion 2024-03-28 13:23:40 +01:00
parent ca3ed0b0d1
commit 22c2735e93
No known key found for this signature in database
GPG key ID: 352918250B012177
13 changed files with 90 additions and 52 deletions

View file

@ -2,21 +2,20 @@
<v-card class="eventModeration"> <v-card class="eventModeration">
<v-card-title>{{$t('common.moderation')}} <v-spacer /><v-btn text label icon @click="$emit('close', false)" size='small'><v-icon v-text="mdiChevronRight"/></v-btn></v-card-title> <v-card-title>{{$t('common.moderation')}} <v-spacer /><v-btn text label icon @click="$emit('close', false)" size='small'><v-icon v-text="mdiChevronRight"/></v-btn></v-card-title>
<v-card-text class="d-flex flex-column flex-grow-1 overflow-auto"> <v-card-text class="d-flex flex-column flex-grow-1 overflow-auto">
<v-textarea :label="$t('event.message')" :hint="$t('event.message_hint')" persistent-hint v-model='message' rows="2" class="mb-2"/> <v-textarea :label="$t('event.message')" :hint="$t(isAdmin ? 'event.message_hint' : 'event.message_author_hint')" persistent-hint v-model='message' rows="2" class="mb-2"/>
<template v-if="$auth.user.is_admin || $auth.user.is_editor"> <template v-if="isAdmin">
<v-btn class='mb-1' small outlined :disabled='!message || loading' :loading='loading' @click="sendMessage(false)" color="primary">{{$t('event.send_to_admins')}}</v-btn> <v-btn class='mb-1' small outlined :disabled='!message || loading' :loading='loading' @click="sendMessage(false)" color="primary">{{$t('event.send_to_admins')}}</v-btn>
<v-btn class='mb-1' small outlined :disabled='!message || loading' :loading='loading' @click="sendMessage(true)" color="primary">{{$t('event.send_to_author_too')}}</v-btn> <v-btn v-if='!event.isAnon' class='mb-1' small outlined :disabled='!message || loading' :loading='loading' @click="sendMessage(true)" color="primary">{{$t('event.send_to_author_too')}}</v-btn>
</template> </template>
<v-btn v-else small outlined :disabled='!message || loading' :loading='loading' @click="sendMessage(true)" color="primary">send</v-btn><br/> <v-btn v-else small outlined :disabled='!message || loading' :loading='loading' @click="sendMessage(true)" color="primary">send</v-btn><br/>
<v-list dense class='messageList'> <v-list dense class='messageList'>
<v-list-item v-for="(item, index) in messages" :key="index" class="px-2"> <v-list-item v-for="(item, index) in messages" :key="index" class="px-2" :class="item.author">
<v-list-item-content> <v-list-item-content>
<span v-if="item?.message">{{ item.message }}</span> <span v-if="item?.message">{{ item.message }}</span>
<v-list-item-subtitle>{{ $time.format(item.createdAt, 'EEEE d MMMM HH:mm') }} / {{ item.author }}</v-list-item-subtitle> <v-list-item-subtitle class="font-weight-light">{{ $time.format(item.createdAt, 'EEEE d MMMM HH:mm') }} / {{ item.author }}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-btn v-if='!event.isAnon' class='mb-1' small outlined :disabled='loading' :loading='loading' @click="disableAuthor" color="primary">{{$t('event.disable_author')}}</v-btn>
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>
@ -44,20 +43,12 @@ export default {
async mounted () { async mounted () {
this.messages = await this.$axios.$get(`/event/messages/${this.event.id}`) this.messages = await this.$axios.$get(`/event/messages/${this.event.id}`)
}, },
methods: { computed: {
async disableAuthor () { isAdmin () {
// ask confirmation only to disable return this.$auth.user.is_admin || this.$auth.user.is_editor
const ret = await this.$root.$confirm('admin.disable_user_confirm')
if (!ret) { return }
this.loading = true
try {
await this.$axios.$put(`/event/disable_author/${this.event.id}`)
this.$root.$message("Author disabled!", { color: 'success' })
} catch (e) {
this.$root.$message(e, { color: 'warning' })
} }
this.loading = false
}, },
methods: {
async sendMessage (is_author_visible) { async sendMessage (is_author_visible) {
try { try {
this.loading = true this.loading = true
@ -96,8 +87,19 @@ export default {
} }
.eventModeration .messageList .v-list-item { .eventModeration .messageList .v-list-item {
border-top: 1px solid rgba(100,100,100,.3); border-top: 1px solid rgba(100,100,100,.07);
word-break: break-word; word-break: break-word;
} }
.eventModeration .messageList .v-list-item.ADMIN {
background-color: rgba(200,100,100,0.1);
border-left: 2px solid rgba(255,69,0, 0.3);
}
.eventModeration .messageList .v-list-item.AUTHOR {
border-left: 2px solid lightblue;
background-color: rgba(35, 193, 255, 0.1);
}
</style> </style>

View file

@ -58,6 +58,8 @@ v-container
v-switch.mt-1(v-model='allow_geolocation' v-switch.mt-1(v-model='allow_geolocation'
inset inset
persistent-hint
:hint="$t('admin.allow_geolocation_hint')"
:label="$t('admin.allow_geolocation')") :label="$t('admin.allow_geolocation')")
v-switch.mt-1(v-model='enable_moderation' v-switch.mt-1(v-model='enable_moderation'
@ -66,6 +68,12 @@ v-container
:hint="$t('admin.enable_moderation_hint')" :hint="$t('admin.enable_moderation_hint')"
:label="$t('admin.enable_moderation')") :label="$t('admin.enable_moderation')")
v-switch.mt-1(v-model='enable_report'
v-if="enable_moderation"
inset
persistent-hint
:hint="$t('admin.enable_report_hint')"
:label="$t('admin.enable_report')")
v-dialog(v-model='showSMTP' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly') v-dialog(v-model='showSMTP' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
SMTP(@close='showSMTP = false') SMTP(@close='showSMTP = false')
@ -145,6 +153,10 @@ export default {
get () { return this.settings.enable_moderation }, get () { return this.settings.enable_moderation },
set (value) { this.setSetting({ key: 'enable_moderation', value }) } set (value) { this.setSetting({ key: 'enable_moderation', value }) }
}, },
enable_report: {
get () { return this.settings.enable_report },
set (value) { this.setSetting({ key: 'enable_report', value }) }
},
filteredTimezones () { filteredTimezones () {
const current_timezone = DateTime.local().zoneName const current_timezone = DateTime.local().zoneName
tzNames.unshift(current_timezone) tzNames.unshift(current_timezone)

View file

@ -27,7 +27,9 @@
"content": "This is a test email, if you are reading this your configuration is working." "content": "This is a test email, if you are reading this your configuration is working."
}, },
"report": { "report": {
"subject": "Report event [{{event.title}}] from {{author}}", "subject": "Event moderation [{{event.title}}]",
"content": "{{author}} reported the event {{event.title}} with the following message:<br/><pre>{{message}}</pre><br/><br/><a href='{{url}}'>Open moderation</a>" "content_ADMIN": "An admin commented about the event <strong>{{event.title}}</strong>:<br/><blockquote>{{message}}</blockquote><br/><br/><a href='{{url}}'>Open moderation</a>",
"content_ANON": "A visitor reported the event <strong>{{event.title}}</strong>:<br/><blockquote>{{message}}</blockquote><br/><br/><a href='{{url}}'>Open moderation</a>",
"content_AUTHOR": "The author of event <strong>{{event.title}}</strong> wrote:<br/><blockquote>{{message}}</blockquote><br/><br/><a href='{{url}}'>Open moderation</a>"
} }
} }

View file

@ -4,6 +4,7 @@
"next": "Next", "next": "Next",
"export": "Export", "export": "Export",
"send": "Send", "send": "Send",
"sent": "Sent",
"where": "Where", "where": "Where",
"address": "Address", "address": "Address",
"when": "When", "when": "When",
@ -204,10 +205,12 @@
"address_geocoded_disclaimer": "If you cannot find the <strong>street address</strong> or the <strong>housenumber</strong> you are looking for in the geocoding results, you can manually insert them in the 'Address' field without loose the coordinates. Consider also that the <a target=\"_blank\" href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> project is open to contributions. If you have Android, we recommend <a target=\"_blank\" href=\"https://f-droid.org/en/packages/de.westnordost.streetcomplete/\">StreetComplete</a> ", "address_geocoded_disclaimer": "If you cannot find the <strong>street address</strong> or the <strong>housenumber</strong> you are looking for in the geocoding results, you can manually insert them in the 'Address' field without loose the coordinates. Consider also that the <a target=\"_blank\" href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> project is open to contributions. If you have Android, we recommend <a target=\"_blank\" href=\"https://f-droid.org/en/packages/de.westnordost.streetcomplete/\">StreetComplete</a> ",
"message": "Message", "message": "Message",
"message_hint": "You could send a message to other admins and optionally to the person who authored the event.", "message_hint": "You could send a message to other admins and optionally to the person who authored the event.",
"report_message_confirmation": "Do you think this event should not be here? Write to us why", "message_author_hint": "Write a message to admins",
"report_message_confirmation": "Do you think this event should not be here? Write us a reason",
"send_to_admins": "Send to admins", "send_to_admins": "Send to admins",
"send_to_author_too": "Send to admins and author", "send_to_author_too": "Send to admins and author",
"disable_author": "Disable the event author" "disable_author": "Disable author",
"disable_author_confirm": "Are you sure you want to disable the author of this event?"
}, },
"admin": { "admin": {
"place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.", "place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.",
@ -225,12 +228,14 @@
"allow_registration_description": "Allow open registrations?", "allow_registration_description": "Allow open registrations?",
"allow_anon_event": "Allow anonymous events (has to be confirmed)?", "allow_anon_event": "Allow anonymous events (has to be confirmed)?",
"enable_moderation": "Enable moderation", "enable_moderation": "Enable moderation",
"enable_moderation_hint": "Anyone could report an event, admins are notified and could talk each other optionally including event's author", "enable_moderation_hint": "Admins could talk each about events, optionally including event's author",
"enable_report": "Enable report event",
"enable_report_hint": "Any visitor could report an event, admins are notified via e-mail",
"allow_multidate_event": "Allow multi-day events", "allow_multidate_event": "Allow multi-day events",
"allow_recurrent_event": "Allow recurring events", "allow_recurrent_event": "Allow recurring events",
"allow_online_event": "Allow online events", "allow_online_event": "Allow online events",
"allow_online_event_hint": "Ask for urls ",
"allow_geolocation": "Allow events geolocation", "allow_geolocation": "Allow events geolocation",
"allow_geolocation_hint": "Optionally set the exact location of an event on a geographic map",
"recurrent_event_visible": "Show recurring events by default", "recurrent_event_visible": "Show recurring events by default",
"federation": "Federation / ActivityPub", "federation": "Federation / ActivityPub",
"enable_federation": "Turn on federation", "enable_federation": "Turn on federation",
@ -238,7 +243,7 @@
"add_instance": "Add instance", "add_instance": "Add instance",
"select_instance_timezone": "Time zone", "select_instance_timezone": "Time zone",
"enable_resources": "Turn on resources", "enable_resources": "Turn on resources",
"enable_resources_help": "Allows adding resources to the event from the fediverse", "enable_resources_help": "Allows adding audio, images and comments to the event from the fediverse",
"hide_boost_bookmark": "Hides boost/bookmarks", "hide_boost_bookmark": "Hides boost/bookmarks",
"hide_boost_bookmark_help": "Hides the small icons showing the number of boosts and bookmarks coming from the fediverse", "hide_boost_bookmark_help": "Hides the small icons showing the number of boosts and bookmarks coming from the fediverse",
"block": "Block", "block": "Block",

View file

@ -69,7 +69,7 @@
v-list-item-title(v-text="$t('common.add_to_calendar')") v-list-item-title(v-text="$t('common.add_to_calendar')")
//- Report //- Report
v-list-item(v-if='settings.enable_moderation && !showModeration' @click='report') v-list-item(v-if='settings.enable_moderation && settings.enable_report && !showModeration' @click='report')
v-list-item-icon v-list-item-icon
v-icon(v-text='mdiMessageTextOutline') v-icon(v-text='mdiMessageTextOutline')
v-list-item-content v-list-item-content
@ -159,7 +159,8 @@ export default {
event: {}, event: {},
showEmbed: false, showEmbed: false,
mapModal: false, mapModal: false,
openModeration: false openModeration: false,
reporting: false
} }
}, },
head () { head () {
@ -277,11 +278,12 @@ export default {
}, },
methods: { methods: {
async report () { async report () {
this.reporting = true
const message = await this.$root.$prompt(this.$t('event.report_message_confirmation'), { title: this.$t('common.report') }) const message = await this.$root.$prompt(this.$t('event.report_message_confirmation'), { title: this.$t('common.report') })
if (!message) { if (!message) {
return return
} }
this.reporting = false
try { try {
await this.$axios.$post(`/event/messages/${this.event.id}`, { message }) await this.$axios.$post(`/event/messages/${this.event.id}`, { message })
this.$root.$message('common.sent', { color: 'success' }) this.$root.$message('common.sent', { color: 'success' })
@ -290,6 +292,9 @@ export default {
} }
}, },
keyDown (ev) { keyDown (ev) {
if (this.openModeration || this.reporting) {
return
}
if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return } if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return }
if (ev.key === 'ArrowRight' && this.event.next) { if (ev.key === 'ArrowRight' && this.event.next) {
this.goNext() this.goNext()

View file

@ -126,6 +126,7 @@ const eventController = {
include: [ include: [
{ model: Tag, required: false, attributes: ['tag'], through: { attributes: [] } }, { model: Tag, required: false, attributes: ['tag'], through: { attributes: [] } },
{ model: Place, attributes: ['name', 'address', 'latitude', 'longitude', 'id'] }, { model: Place, attributes: ['name', 'address', 'latitude', 'longitude', 'id'] },
{ model: User, required: false, attributes: ['is_active'] },
{ {
model: Resource, model: Resource,
where: !is_admin && { hidden: false }, where: !is_admin && { hidden: false },
@ -135,7 +136,7 @@ const eventController = {
}, },
{ model: Event, required: false, as: 'parent', attributes: ['id', 'recurrent', 'is_visible', 'start_datetime'] }, { model: Event, required: false, as: 'parent', attributes: ['id', 'recurrent', 'is_visible', 'start_datetime'] },
], ],
order: [[Resource, 'id', 'DESC']] order: [[Resource, 'id', 'DESC']],
}) })
} catch (e) { } catch (e) {
log.error('[EVENT]', e) log.error('[EVENT]', e)
@ -184,9 +185,10 @@ const eventController = {
if (event && (event.is_visible || is_admin)) { if (event && (event.is_visible || is_admin)) {
event = event.get() event = event.get()
event.isMine = event.userId === req.user?.id event.isMine = event.userId === req.user?.id
event.isAnon = event.userId === null event.isAnon = event.userId === null || !event?.user?.is_active
event.original_url = event?.ap_object?.url || event?.ap_object?.id event.original_url = event?.ap_object?.url || event?.ap_object?.id
delete event.ap_object delete event.ap_object
delete event.user
delete event.userId delete event.userId
event.next = next && (next.slug || next.id) event.next = next && (next.slug || next.id)
event.prev = prev && (prev.slug || prev.id) event.prev = prev && (prev.slug || prev.id)
@ -263,7 +265,6 @@ const eventController = {
return res.json(messages) return res.json(messages)
} }
log.debug('userId: %s event ud %s', event, req.user.id)
return res.sendStatus(400) return res.sendStatus(400)
}, },
@ -275,9 +276,9 @@ const eventController = {
} }
const eventId = Number(req.params.event_id) const eventId = Number(req.params.event_id)
const event = await Event.findByPk(eventId) const event = await Event.findByPk(eventId, { include: [{ model: User, required: false }], raw: true })
if (!event) { if (!event) {
log.warn(`Trying to ... ${eventId}`) log.warn(`[REPORT] Event does not exists: ${eventId}`)
return res.sendStatus(404) return res.sendStatus(404)
} }
@ -285,12 +286,10 @@ const eventController = {
const isMine = req.user?.id === event.userId const isMine = req.user?.id === event.userId
const isAdminOrEditor = req.user?.is_editor || req.user?.is_admin const isAdminOrEditor = req.user?.is_editor || req.user?.is_admin
// if (!isAdminOrEditor && !isMine) { if (!isAdminOrEditor && !isMine && !res.locals.settings.enable_report) {
// log.warn(`Someone not allowed is trying to report on an event -> "${event.title}" isMine: ${isMine} `) log.warn(`[REPORT] Someone not allowed is trying to report an event -> "${event.title}" isMine: ${isMine} `)
// return res.sendStatus(403) return res.sendStatus(403)
// } }
// mail.send(user.email, 'message', { subject, message }, res.locals.settings.locale)
const author = isAdminOrEditor ? 'ADMIN' : isMine ? 'AUTHOR' : 'ANON' const author = isAdminOrEditor ? 'ADMIN' : isMine ? 'AUTHOR' : 'ANON'
try { try {
@ -302,12 +301,18 @@ const eventController = {
}) })
const admins = await User.findAll({ where: { role: ['admin', 'editor'], is_active: true }, attributes: ['email'], raw: true }) const admins = await User.findAll({ where: { role: ['admin', 'editor'], is_active: true }, attributes: ['email'], raw: true })
console.error(admins)
let emails = [res.locals.settings.admin_email] let emails = [res.locals.settings.admin_email]
emails = emails.concat(admins.map(a => a.email)) emails = emails.concat(admins?.map(a => a.email))
log.info('[EVENT] Report event to %s', emails) log.info('[EVENT] Report event to %s', emails)
// notify admins
mail.send(emails, 'report', { event, message: body.message, author }) mail.send(emails, 'report', { event, message: body.message, author })
// notify author
if (event['user.email'] && body.is_author_visible && !isMine) {
mail.send(event['user.email'], 'report', { event, message: body.message, author })
}
return res.json(message) return res.json(message)
} catch (e) { } catch (e) {
log.warn(`[EVENT] ${e}`) log.warn(`[EVENT] ${e}`)

View file

@ -89,7 +89,7 @@ passport.use(new ClientPublicStrategy(verifyPublicClient))
async function verifyToken (req, accessToken, done) { async function verifyToken (req, accessToken, done) {
const token = await OAuthToken.findByPk(accessToken, const token = await OAuthToken.findByPk(accessToken,
{ include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] }) { include: [{ required: true, where: { is_active: true } , model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] })
if (!token) return done(null, false) if (!token) return done(null, false)
if (token.userId) { if (token.userId) {

View file

@ -27,7 +27,8 @@ const defaultSettings = {
allow_multidate_event: true, allow_multidate_event: true,
allow_recurrent_event: false, allow_recurrent_event: false,
allow_online_event: true, allow_online_event: true,
enable_moderation: true, enable_moderation: false,
enable_report: false,
recurrent_event_visible: false, recurrent_event_visible: false,
allow_geolocation: false, allow_geolocation: false,
geocoding_provider_type: 'Nominatim', geocoding_provider_type: 'Nominatim',

View file

@ -120,6 +120,7 @@ const userController = {
async remove (req, res) { async remove (req, res) {
try { try {
let user let user
// TODO: has to unset events first!
if (req.user.is_admin && req.params.id) { if (req.user.is_admin && req.params.id) {
user = await User.findByPk(req.params.id) user = await User.findByPk(req.params.id)
} else { } else {

View file

@ -1,3 +1,3 @@
extends ../layout.pug extends ../layout.pug
block content block content
p !{t('report.content', {config, author, event, message, url: `${config.baseurl}/event/${event.slug || event.id}`})} p !{t(`report.content_${author}`, {config, event, message, url: `${config.baseurl}/event/${event.slug || event.id}`})}

View file

@ -78,6 +78,7 @@ module.exports = {
allow_anon_event: settings.allow_anon_event, allow_anon_event: settings.allow_anon_event,
allow_recurrent_event: settings.allow_recurrent_event, allow_recurrent_event: settings.allow_recurrent_event,
enable_moderation: settings.enable_moderation, enable_moderation: settings.enable_moderation,
enable_report: settings.enable_report,
allow_multidate_event: settings.allow_multidate_event, allow_multidate_event: settings.allow_multidate_event,
allow_online_event: settings.allow_online_event, allow_online_event: settings.allow_online_event,
recurrent_event_visible: settings.recurrent_event_visible, recurrent_event_visible: settings.recurrent_event_visible,

View file

@ -14,7 +14,7 @@ const notifier = {
emitter: new events.EventEmitter(), emitter: new events.EventEmitter(),
sendNotification (notification, event) { async sendNotification (notification, event) {
const promises = [] const promises = []
log.info(`Send ${notification.type} notification ${notification.action}`) log.info(`Send ${notification.type} notification ${notification.action}`)
let p let p
@ -22,7 +22,10 @@ const notifier = {
// case 'mail': TODO: locale? // case 'mail': TODO: locale?
// return mail.send(notification.email, 'event', { event, notification }) // return mail.send(notification.email, 'event', { event, notification })
case 'admin_email': case 'admin_email':
p = mail.send(settingsController.settings.admin_email, 'event', const admins = await User.findAll({ where: { role: ['admin', 'editor'], is_active: true }, attributes: ['email'], raw: true })
let emails = [settingsController.settings.admin_email]
emails = emails.concat(admins?.map(a => a.email))
p = mail.send(emails, 'event',
{ event, to_confirm: !event.is_visible, notification }) { event, to_confirm: !event.is_visible, notification })
promises.push(p) promises.push(p)
break break

View file

@ -8,7 +8,8 @@ export const state = () => ({
instance_name: '', instance_name: '',
allow_registration: true, allow_registration: true,
allow_anon_event: true, allow_anon_event: true,
enable_moderation: true, enable_moderation: false,
enable_report: false,
allow_multidate_event: true, allow_multidate_event: true,
allow_recurrent_event: true, allow_recurrent_event: true,
allow_online_event: true, allow_online_event: true,