1
import { Action } from '@directus/constants';
2
import { useEnv } from '@directus/env';
3
import { ErrorCode, isDirectusError } from '@directus/errors';
4
import type { Accountability, Item, PrimaryKey } from '@directus/types';
5
import { uniq } from 'lodash-es';
6
import { useLogger } from '../logger.js';
7
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
8
import { getPermissions } from '../utils/get-permissions.js';
9
import { isValidUuid } from '../utils/is-valid-uuid.js';
10
import { Url } from '../utils/url.js';
11
import { userName } from '../utils/user-name.js';
12
import { AuthorizationService } from './authorization.js';
13
import { ItemsService } from './items.js';
14
import { NotificationsService } from './notifications.js';
15
import { UsersService } from './users.js';
18
const logger = useLogger();
20
export class ActivityService extends ItemsService {
21
notificationsService: NotificationsService;
22
usersService: UsersService;
24
constructor(options: AbstractServiceOptions) {
25
super('directus_activity', options);
26
this.notificationsService = new NotificationsService({ schema: this.schema });
27
this.usersService = new UsersService({ schema: this.schema });
30
override async createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
31
if (data['action'] === Action.COMMENT && typeof data['comment'] === 'string') {
32
const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
34
const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
36
const sender = await this.usersService.readOne(this.accountability!.user!, {
37
fields: ['id', 'first_name', 'last_name', 'email'],
40
for (const mention of mentions) {
41
const userID = mention.substring(1);
43
const user = await this.usersService.readOne(userID, {
44
fields: ['id', 'first_name', 'last_name', 'email', 'role.id', 'role.admin_access', 'role.app_access'],
47
const accountability: Accountability = {
49
role: user['role']?.id ?? null,
50
admin: user['role']?.admin_access ?? null,
51
app: user['role']?.app_access ?? null,
54
accountability.permissions = await getPermissions(accountability, this.schema);
56
const authorizationService = new AuthorizationService({ schema: this.schema, accountability });
57
const usersService = new UsersService({ schema: this.schema, accountability });
60
await authorizationService.checkAccess('read', data['collection'], data['item']);
62
const templateData = await usersService.readByQuery({
63
fields: ['id', 'first_name', 'last_name', 'email'],
64
filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
67
const userPreviews = templateData.reduce(
69
acc[user['id']] = `<em>${userName(user)}</em>`;
72
{} as Record<string, string>,
75
let comment = data['comment'];
77
for (const mention of mentions) {
78
const uuid = mention.substring(1);
79
// We only match on UUIDs in the first place. This is just an extra sanity check.
80
if (isValidUuid(uuid) === false) continue;
81
comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
84
comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
86
const href = new Url(env['PUBLIC_URL'] as string)
87
.addPath('admin', 'content', data['collection'], data['item'])
91
Hello ${userName(user)},
93
${userName(sender)} has mentioned you in a comment:
97
<a href="${href}">Click here to view.</a>
100
await this.notificationsService.createOne({
102
sender: sender['id'],
103
subject: `You were mentioned in ${data['collection']}`,
105
collection: data['collection'],
109
if (isDirectusError(err, ErrorCode.Forbidden)) {
110
logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
118
return super.createOne(data, opts);