[user] mod messages

This commit is contained in:
Andrea Vos 2022-11-21 20:50:55 +01:00
parent 48d45d3dfc
commit e70a635eb9
7 changed files with 157 additions and 0 deletions

View File

@ -88,6 +88,46 @@
<ModerationRules type="rulesUsers" emphasise class="mt-4"/>
<AbuseReports v-if="abuseReports.length" :abuseReports="abuseReports" allowResolving/>
</section>
<section v-if="$isGranted('users')">
<a v-if="!showMessages" href="#" @click.prevent="showMessages = true" class="small">
<Icon v="comment-exclamation"/>
Mod messages
</a>
<div v-else>
<h5>
<Icon v="comment-exclamation"/>
Mod messages
</h5>
<p>
You can use this feature to warn a user without banning them, etc.
</p>
<table class="table table-striped">
<thead>
<tr>
<th>Sent on</th>
<th>Sent by</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="message in messages">
<td>{{$datetime($ulidTime(message.id))}}</td>
<td>
<a :href="`https://pronouns.page/@${message.adminUsername}`" target="_blank" rel="noopener">
@{{message.adminUsername}}
</a>
</td>
<td v-html="nl2br(message.message)"></td>
</tr>
</tbody>
</table>
<textarea v-model="message" class="form-control" rows="5" :disabled="saving" required></textarea>
<button class="btn btn-danger d-block w-100 mt-2" :disabled="saving" @click="sendMessage">
<Icon v="comment-exclamation"/>
Send
</button>
</div>
</section>
</div>
</template>
@ -109,6 +149,10 @@
showBanForm: !!this.user.bannedReason,
isBanned: !!this.user.bannedReason,
showMessages: false,
messages: undefined,
message: '',
saving: false,
forbidden,
@ -124,6 +168,10 @@
if (this.banProposals.length > 0) {
this.showBanForm = true;
}
this.messages = await this.$axios.$get(`/admin/mod-messages/${encodeURIComponent(this.user.username)}`);
if (this.messages.length > 0) {
this.showMessages = true;
}
},
methods: {
async ban() {
@ -166,6 +214,22 @@
this.user.bannedReason = proposal.bannedReason;
this.user.bannedTerms = proposal.bannedTerms.split(',');
},
async sendMessage() {
if (!this.message) { return; }
await this.$confirm(`<strong>Please proof-read and confirm sending:</strong><br/><br/><div class="text-start">${this.nl2br(this.message)}</div>`, 'danger');
this.saving = true;
try {
this.messages = await this.$post(`/admin/mod-message/${encodeURIComponent(this.user.username)}`, {
message: this.message,
});
this.message = '';
} finally {
this.saving = false;
}
},
nl2br(text) {
return text.replace(new RegExp('\\n', 'g'), '<br/>');
}
},
computed: {
canApplyBan() {

View File

@ -583,6 +583,10 @@ user:
qr:
header: 'QR code'
download: 'Download QR code'
modMessage:
subject: 'A message from a moderator'
intro: 'Hi! One of our moderators has sent you the following message:'
respond: 'If you want to respond, you can simply reply to this email.'
profile:
description: 'Description'

View File

@ -693,6 +693,10 @@ user:
qr:
header: 'QR code'
download: 'Download QR code'
modMessage:
subject: 'A message from a moderator'
intro: 'Hi! One of our moderators has sent you the following message:'
respond: 'If you want to respond, you can simply reply to this email.'
profile:
description: 'Description'

View File

@ -1318,6 +1318,10 @@ user:
qr:
header: 'Kod QR'
download: 'Pobierz kod QR'
modMessage:
subject: 'Wiadomość od osoby moderującej'
intro: 'Hej! Jedna z naszych osób moderujących wysłała Ci następującą wiadomość:'
respond: 'Jeśli chcesz odpowiedzieć, możesz po prostu odpisać na tego maila.'
profile:
description: 'Opis'

View File

@ -0,0 +1,11 @@
-- Up
CREATE TABLE user_messages (
id TEXT NOT NULL PRIMARY KEY,
userId TEXT NOT NULL,
adminId TEXT NOT NULL,
message TEXT NOT NULL
);
-- Down

View File

@ -371,6 +371,60 @@ router.post('/admin/reports/handle/:id', handleErrorAsync(async (req, res) => {
return res.json(true);
}));
const fetchModMessages = async (db, user) => {
return db.all(SQL`
SELECT m.id, a.username as adminUsername, m.message
FROM user_messages m
LEFT JOIN users a ON m.adminId = a.id
WHERE m.userId = ${user.id}
`)
}
router.post('/admin/mod-message/:username', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users')) {
return res.status(401).json({error: 'Unauthorised'});
}
if (!req.body.message) {
return res.status(400).json({error: 'Bad request'});
}
const user = await fetchUserByUsername(req.db, req.params.username);
if (!user) {
return res.status(400).json({error: 'No such user'});
}
await req.db.get(SQL`INSERT INTO user_messages (id, userId, adminId, message) VALUES (
${ulid()},
${user.id},
${req.user.id},
${req.body.message}
)`);
mailer(user.email, 'modMessage', {
message: req.body.message,
username: req.params.username,
modUsername: req.user.username,
});
return res.json(await fetchModMessages(req.db, user));
}));
router.get('/admin/mod-messages/:username', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users')) {
return res.status(401).json({error: 'Unauthorised'});
}
const user = await fetchUserByUsername(req.db, req.params.username);
if (!user) {
return res.status(400).json({error: 'No such user'});
}
return res.json(await fetchModMessages(req.db, user));
}));
router.get('/admin/moderation', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users')) {
return res.status(401).json({error: 'Unauthorised'});

View File

@ -89,6 +89,16 @@ const templates = {
text: 'Check them out here: https://[[domain]]/admin',
html: '<p>Check them out here: <a href="https://[[domain]]/admin" target="_blank" rel="noopener">[[domain]]/admin</a></p>',
},
modMessage: {
subject: '[[user.modMessage.subject]]',
text: `[[user.modMessage.intro]]\n\n{{nl2br:message}}\n\n[[user.modMessage.respond]]`,
html: `
<p>[[user.modMessage.intro]]</p>
<p style="color: #222; padding-left: 1em;padding-right: 1em;font-style: italic;">{{nl2br:message}}</p>
<p>[[user.modMessage.respond]]</p>
<p style="color: #999; font-size: 10px;">@{{modUsername}} @{{username}}</p>
`,
},
}
const applyTemplate = (template, context, params) => {
@ -122,6 +132,12 @@ const applyTemplate = (template, context, params) => {
: Object.keys(value).map(s => ` - ${s}: ${value[s]}`).join('\n');
}
}
if (key.startsWith('nl2br:')) {
const value = params[key.substring(6)];
return context === 'html'
? value.replace(new RegExp('\\n', 'g'), '<br/>')
: value;
}
return params[key];
});