@enfin/chat-server 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PresenceStatus = exports.PresenceSchema = void 0;
4
+ const mongoose_1 = require("mongoose");
5
+ exports.PresenceSchema = new mongoose_1.Schema({
6
+ tenantId: { type: String, required: true, index: true },
7
+ userId: {
8
+ type: String,
9
+ required: true,
10
+ index: true
11
+ },
12
+ status: {
13
+ type: String,
14
+ enum: ['online', 'offline', 'away'],
15
+ default: 'offline'
16
+ },
17
+ lastSeen: { type: Date, default: Date.now },
18
+ typingIn: [{ type: String }]
19
+ }, {
20
+ timestamps: false
21
+ });
22
+ exports.PresenceSchema.index({ tenantId: 1, userId: 1 }, { unique: true });
23
+ exports.PresenceStatus = (0, mongoose_1.model)('Presence', exports.PresenceSchema);
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Room = exports.RoomSchema = void 0;
4
+ const mongoose_1 = require("mongoose");
5
+ exports.RoomSchema = new mongoose_1.Schema({
6
+ tenantId: { type: String, required: true, index: true },
7
+ name: { type: String, required: true },
8
+ type: {
9
+ type: String,
10
+ enum: ['direct', 'group'],
11
+ required: true,
12
+ default: 'group'
13
+ },
14
+ members: [{
15
+ type: String,
16
+ }],
17
+ membersHash: { type: String },
18
+ createdBy: { type: String, required: true },
19
+ lastMessage: { type: mongoose_1.Schema.Types.ObjectId, ref: 'Message' },
20
+ lastActivity: { type: Date, default: Date.now },
21
+ avatar: { type: String }
22
+ }, {
23
+ timestamps: true,
24
+ toJSON: {
25
+ virtuals: true,
26
+ transform: function (doc, ret) {
27
+ ret.id = ret._id;
28
+ delete ret.__v;
29
+ return ret;
30
+ }
31
+ }
32
+ });
33
+ // Indexes
34
+ exports.RoomSchema.index({ tenantId: 1, members: 1 });
35
+ exports.RoomSchema.index({ tenantId: 1, lastActivity: -1 });
36
+ exports.RoomSchema.index({ tenantId: 1, name: 'text', description: 'text' });
37
+ // Prevent duplicate direct rooms within the same tenant.
38
+ // The members array is sorted at write time so the same pair always has the same key.
39
+ exports.RoomSchema.index({ tenantId: 1, type: 1, membersHash: 1 }, { unique: true, partialFilterExpression: { type: 'direct' } });
40
+ exports.Room = (0, mongoose_1.model)('Room', exports.RoomSchema);
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.User = exports.UserSchema = void 0;
4
+ const mongoose_1 = require("mongoose");
5
+ exports.UserSchema = new mongoose_1.Schema({
6
+ userId: { type: String, required: true, index: true },
7
+ tenantId: { type: String, required: true, index: true },
8
+ name: { type: String, required: true },
9
+ avatar: { type: String },
10
+ }, {
11
+ timestamps: { createdAt: true, updatedAt: false }
12
+ });
13
+ exports.UserSchema.index({ tenantId: 1, userId: 1 }, { unique: true });
14
+ exports.User = (0, mongoose_1.model)('User', exports.UserSchema);
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ var ApiKeyService_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.ApiKeyService = void 0;
20
+ const common_1 = require("@nestjs/common");
21
+ const axios_1 = __importDefault(require("axios"));
22
+ const chat_shared_1 = require("../../../shared/dist/index.js");
23
+ const VALIDATION_CACHE_TTL_MS = 5 * 60 * 1000;
24
+ const ADMIN_HTTP_TIMEOUT_MS = 5000;
25
+ // Use string literal directly to avoid circular import with chat.module.ts.
26
+ // The token MUST stay in sync with CHAT_MODULE_OPTIONS in chat.module.ts.
27
+ const CHAT_MODULE_OPTIONS_TOKEN = 'CHAT_MODULE_OPTIONS';
28
+ let ApiKeyService = ApiKeyService_1 = class ApiKeyService {
29
+ constructor(options) {
30
+ this.logger = new common_1.Logger(ApiKeyService_1.name);
31
+ this.cache = new Map();
32
+ this.moduleOptions = options;
33
+ }
34
+ resolveAdminUrl() {
35
+ const fromOptions = this.moduleOptions?.adminValidationUrl?.trim();
36
+ const fromEnv = process.env.ADMIN_API_VALIDATION_URL ||
37
+ process.env.API_VALIDATION_URL ||
38
+ process.env.VALIDATION_URL;
39
+ const raw = fromOptions || fromEnv;
40
+ if (!raw)
41
+ return undefined;
42
+ return raw.replace(/\/+$/, '');
43
+ }
44
+ async validateKey(apiKey, clientVersion) {
45
+ if (!apiKey) {
46
+ return {
47
+ valid: false,
48
+ apiKey: '',
49
+ mode: 'managed',
50
+ version: '1.0.0',
51
+ tenantId: '',
52
+ error: 'apiKey required',
53
+ };
54
+ }
55
+ const cached = this.cache.get(apiKey);
56
+ if (cached && cached.expiresAt > Date.now()) {
57
+ return cached.result;
58
+ }
59
+ const adminUrl = this.resolveAdminUrl();
60
+ if (!adminUrl) {
61
+ return {
62
+ valid: false,
63
+ apiKey,
64
+ mode: 'managed',
65
+ version: '1.0.0',
66
+ tenantId: '',
67
+ error: 'ADMIN_API_VALIDATION_URL not configured',
68
+ };
69
+ }
70
+ const validateUrl = `${adminUrl}/api/validation/validate`;
71
+ try {
72
+ const response = await axios_1.default.post(validateUrl, { apiKey, clientVersion: clientVersion || chat_shared_1.SDK_VERSION }, { timeout: ADMIN_HTTP_TIMEOUT_MS, headers: { 'Content-Type': 'application/json' } });
73
+ const data = response?.data;
74
+ if (!data || typeof data.valid !== 'boolean') {
75
+ this.logger.warn(`Admin validation returned unexpected payload for ${apiKey.slice(0, 12)}...`);
76
+ return {
77
+ valid: false,
78
+ apiKey,
79
+ mode: 'managed',
80
+ version: '1.0.0',
81
+ tenantId: '',
82
+ error: 'Invalid admin response',
83
+ };
84
+ }
85
+ const result = data;
86
+ if (result.valid) {
87
+ this.cache.set(apiKey, { result, expiresAt: Date.now() + VALIDATION_CACHE_TTL_MS });
88
+ this.logger.log(`[VALIDATION] admin -> valid tenantId=${result.tenantId} mode=${result.mode} database=${result.database || '-'}`);
89
+ }
90
+ else {
91
+ this.logger.log(`[VALIDATION] admin -> invalid apiKey=${apiKey.slice(0, 12)}... reason=${result.error || 'unknown'}`);
92
+ }
93
+ return result;
94
+ }
95
+ catch (err) {
96
+ const axiosErr = err;
97
+ const reason = axiosErr?.response?.status
98
+ ? `HTTP ${axiosErr.response.status}`
99
+ : axiosErr?.message || String(err);
100
+ this.logger.error(`Admin validation request failed (${reason})`);
101
+ return {
102
+ valid: false,
103
+ apiKey,
104
+ mode: 'managed',
105
+ version: '1.0.0',
106
+ tenantId: '',
107
+ error: `ApiKey registry unavailable: ${reason}`,
108
+ };
109
+ }
110
+ }
111
+ clearCache() {
112
+ this.cache.clear();
113
+ }
114
+ };
115
+ exports.ApiKeyService = ApiKeyService;
116
+ exports.ApiKeyService = ApiKeyService = ApiKeyService_1 = __decorate([
117
+ (0, common_1.Injectable)(),
118
+ __param(0, (0, common_1.Inject)(CHAT_MODULE_OPTIONS_TOKEN)),
119
+ __metadata("design:paramtypes", [Object])
120
+ ], ApiKeyService);
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.CallService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const mongoose_1 = require("@nestjs/mongoose");
18
+ const mongoose_2 = require("mongoose");
19
+ const audiocall_schema_1 = require("../schemas/audiocall.schema");
20
+ let CallService = class CallService {
21
+ constructor(callModel) {
22
+ this.callModel = callModel;
23
+ }
24
+ async initiateCall(tenantId, roomId, initiatorId, participantId) {
25
+ // Clean up stale "ringing" calls (orphans from prior failed calls).
26
+ // A "ringing" record older than 30s without being accepted is definitely stale.
27
+ const staleThreshold = new Date(Date.now() - 30 * 1000);
28
+ await this.callModel.updateMany({
29
+ tenantId,
30
+ roomId,
31
+ status: 'ringing',
32
+ startedAt: { $lt: staleThreshold },
33
+ }, { $set: { status: 'ended', endedAt: new Date(), endReason: 'stale' } });
34
+ const existingCall = await this.callModel.findOne({
35
+ tenantId,
36
+ roomId,
37
+ status: { $in: ['ringing', 'active'] },
38
+ });
39
+ if (existingCall)
40
+ throw new common_1.BadRequestException('A call is already in progress in this room');
41
+ // Block if either party is already on a call (in any room). We look up the
42
+ // audiocalls collection by matching initiatorId OR participantId against
43
+ // the caller/callee with status "ringing" or "active". If we find any
44
+ // ongoing call involving either user, the line is busy.
45
+ const busyCall = await this.callModel.findOne({
46
+ tenantId,
47
+ status: { $in: ['ringing', 'active'] },
48
+ $or: [
49
+ { initiatorId: participantId },
50
+ { participantId: participantId },
51
+ { initiatorId: initiatorId },
52
+ { participantId: initiatorId },
53
+ ],
54
+ });
55
+ if (busyCall) {
56
+ // If the busy call involves the callee, say the user is busy.
57
+ // If it involves the caller, say the caller is already on a call.
58
+ const isCalleeBusy = busyCall.initiatorId === participantId || busyCall.participantId === participantId;
59
+ if (isCalleeBusy) {
60
+ throw new common_1.BadRequestException('The user you are calling is busy on another call');
61
+ }
62
+ throw new common_1.BadRequestException('You are already on a call');
63
+ }
64
+ const call = new this.callModel({
65
+ tenantId,
66
+ roomId,
67
+ initiatorId,
68
+ participantId,
69
+ status: 'ringing',
70
+ startedAt: new Date(),
71
+ });
72
+ return call.save();
73
+ }
74
+ async acceptCall(tenantId, callId, userId) {
75
+ const call = await this.callModel.findOne({ _id: callId, tenantId });
76
+ if (!call)
77
+ throw new common_1.NotFoundException('Call not found');
78
+ if (call.participantId !== userId && call.initiatorId !== userId) {
79
+ throw new common_1.BadRequestException('You are not authorized to accept this call');
80
+ }
81
+ call.status = 'active';
82
+ return call.save();
83
+ }
84
+ async rejectCall(tenantId, callId, userId) {
85
+ const call = await this.callModel.findOne({ _id: callId, tenantId });
86
+ if (!call)
87
+ throw new common_1.NotFoundException('Call not found');
88
+ if (call.participantId !== userId && call.initiatorId !== userId) {
89
+ throw new common_1.BadRequestException('You are not authorized to reject this call');
90
+ }
91
+ call.status = 'ended';
92
+ call.endedAt = new Date();
93
+ return call.save();
94
+ }
95
+ async endCall(tenantId, callId) {
96
+ const call = await this.callModel.findOne({ _id: callId, tenantId });
97
+ if (!call)
98
+ throw new common_1.NotFoundException('Call not found');
99
+ call.status = 'ended';
100
+ call.endedAt = new Date();
101
+ call.duration = Math.floor((call.endedAt.getTime() - call.startedAt.getTime()) / 1000);
102
+ return call.save();
103
+ }
104
+ async endCallsForUser(tenantId, userId, reason) {
105
+ const calls = await this.callModel.find({
106
+ tenantId,
107
+ status: { $in: ['ringing', 'active'] },
108
+ $or: [{ initiatorId: userId }, { participantId: userId }],
109
+ });
110
+ if (calls.length === 0)
111
+ return [];
112
+ const now = new Date();
113
+ const callIds = calls.map(c => c._id);
114
+ await this.callModel.updateMany({ _id: { $in: callIds } }, {
115
+ $set: {
116
+ status: 'ended',
117
+ endedAt: now,
118
+ endReason: reason,
119
+ },
120
+ });
121
+ return this.callModel.find({ _id: { $in: callIds } });
122
+ }
123
+ async getCall(tenantId, callId) {
124
+ const call = await this.callModel.findOne({ _id: callId, tenantId });
125
+ if (!call)
126
+ throw new common_1.NotFoundException('Call not found');
127
+ return call;
128
+ }
129
+ async getActiveCall(tenantId, roomId) {
130
+ return this.callModel.findOne({ tenantId, roomId, status: { $in: ['ringing', 'active'] } });
131
+ }
132
+ async getCallHistory(tenantId, roomId, limit = 20) {
133
+ return this.callModel
134
+ .find({ tenantId, roomId, status: 'ended' })
135
+ .sort({ startedAt: -1 })
136
+ .limit(limit)
137
+ .exec();
138
+ }
139
+ };
140
+ exports.CallService = CallService;
141
+ exports.CallService = CallService = __decorate([
142
+ (0, common_1.Injectable)(),
143
+ __param(0, (0, mongoose_1.InjectModel)(audiocall_schema_1.AudioCall.modelName)),
144
+ __metadata("design:paramtypes", [mongoose_2.Model])
145
+ ], CallService);
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var MessageService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MessageService = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const mongoose_1 = require("@nestjs/mongoose");
19
+ const mongoose_2 = require("mongoose");
20
+ const message_schema_1 = require("../schemas/message.schema");
21
+ const room_schema_1 = require("../schemas/room.schema");
22
+ let MessageService = MessageService_1 = class MessageService {
23
+ constructor(messageModel, roomModel) {
24
+ this.messageModel = messageModel;
25
+ this.roomModel = roomModel;
26
+ this.logger = new common_1.Logger(MessageService_1.name);
27
+ }
28
+ async createMessage(tenantId, dto, senderId, senderName) {
29
+ console.log('[MESSAGE_SERVICE] createMessage called with dto:', JSON.stringify(dto));
30
+ console.log('[MESSAGE_SERVICE] tenantId=', tenantId, 'senderId=', senderId, 'senderName=', senderName);
31
+ console.log('[MESSAGE_SERVICE] dto.roomId=', dto?.roomId, 'dto.content=', JSON.stringify(dto?.content));
32
+ const message = new this.messageModel({
33
+ ...dto,
34
+ tenantId,
35
+ senderId,
36
+ senderName,
37
+ roomId: dto.roomId,
38
+ });
39
+ console.log('[MESSAGE_SERVICE] mongoose model instance created, calling .save()...');
40
+ let saved;
41
+ try {
42
+ saved = await message.save();
43
+ console.log('[MESSAGE_SERVICE] message.save() succeeded, _id=', saved?._id);
44
+ }
45
+ catch (err) {
46
+ console.log('[MESSAGE_SERVICE] ERROR in message.save():', err?.message || err);
47
+ console.log('[MESSAGE_SERVICE] Validation errors:', JSON.stringify(err?.errors || {}));
48
+ console.log('[MESSAGE_SERVICE] Stack:', err?.stack);
49
+ throw err;
50
+ }
51
+ try {
52
+ await this.roomModel.updateOne({ _id: dto.roomId, tenantId }, { lastActivity: new Date(), lastMessage: saved._id });
53
+ console.log('[MESSAGE_SERVICE] roomModel.updateOne succeeded');
54
+ }
55
+ catch (err) {
56
+ console.log('[MESSAGE_SERVICE] ERROR in roomModel.updateOne (non-fatal):', err?.message || err);
57
+ // Don't fail the whole message send if room update fails
58
+ }
59
+ return saved;
60
+ }
61
+ async getMessages(tenantId, roomId, limit = 50, beforeCursor) {
62
+ const query = { tenantId, roomId };
63
+ if (beforeCursor) {
64
+ const cursorMessage = await this.messageModel.findOne({ _id: beforeCursor, tenantId });
65
+ if (cursorMessage) {
66
+ query.createdAt = { $lt: cursorMessage.createdAt };
67
+ }
68
+ }
69
+ const messages = await this.messageModel
70
+ .find(query)
71
+ .sort({ createdAt: -1 })
72
+ .limit(limit + 1)
73
+ .exec();
74
+ const hasMore = messages.length > limit;
75
+ const result = hasMore ? messages.slice(0, limit) : messages;
76
+ const nextCursor = hasMore ? result[result.length - 1]._id : null;
77
+ return { messages: result.reverse(), nextCursor };
78
+ }
79
+ async getMessageById(tenantId, messageId) {
80
+ const message = await this.messageModel.findOne({ _id: messageId, tenantId });
81
+ if (!message)
82
+ throw new common_1.NotFoundException('Message not found');
83
+ return message;
84
+ }
85
+ async markAsRead(tenantId, messageId, userId) {
86
+ await this.messageModel.updateOne({ _id: messageId, tenantId }, { $addToSet: { readBy: { userId, readAt: new Date() } } });
87
+ }
88
+ async getUnreadCount(tenantId, roomId, userId) {
89
+ return this.messageModel.countDocuments({
90
+ tenantId,
91
+ roomId,
92
+ 'readBy.userId': { $ne: userId },
93
+ });
94
+ }
95
+ async deleteMessage(tenantId, messageId) {
96
+ const result = await this.messageModel.deleteOne({ _id: messageId, tenantId });
97
+ if (result.deletedCount === 0)
98
+ throw new common_1.NotFoundException('Message not found');
99
+ }
100
+ };
101
+ exports.MessageService = MessageService;
102
+ exports.MessageService = MessageService = MessageService_1 = __decorate([
103
+ (0, common_1.Injectable)(),
104
+ __param(0, (0, mongoose_1.InjectModel)(message_schema_1.Message.modelName)),
105
+ __param(1, (0, mongoose_1.InjectModel)(room_schema_1.Room.modelName)),
106
+ __metadata("design:paramtypes", [mongoose_2.Model,
107
+ mongoose_2.Model])
108
+ ], MessageService);
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var PresenceService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PresenceService = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const mongoose_1 = require("@nestjs/mongoose");
19
+ const mongoose_2 = require("mongoose");
20
+ const presence_schema_1 = require("../schemas/presence.schema");
21
+ let PresenceService = PresenceService_1 = class PresenceService {
22
+ constructor(presenceModel) {
23
+ this.presenceModel = presenceModel;
24
+ this.logger = new common_1.Logger(PresenceService_1.name);
25
+ }
26
+ baseFilter(tenantId, userId) {
27
+ return { tenantId, userId };
28
+ }
29
+ async setOnline(tenantId, userId) {
30
+ return this.presenceModel.findOneAndUpdate(this.baseFilter(tenantId, userId), { status: 'online', lastSeen: new Date(), $unset: { typingIn: 1 } }, { upsert: true, new: true });
31
+ }
32
+ async setOffline(tenantId, userId) {
33
+ return this.presenceModel.findOneAndUpdate(this.baseFilter(tenantId, userId), { status: 'offline', lastSeen: new Date(), $unset: { typingIn: 1 } }, { upsert: true, new: true });
34
+ }
35
+ async setAway(tenantId, userId) {
36
+ return this.presenceModel.findOneAndUpdate(this.baseFilter(tenantId, userId), { status: 'away', lastSeen: new Date() }, { upsert: true, new: true });
37
+ }
38
+ async setStatus(tenantId, userId, dto) {
39
+ return this.presenceModel.findOneAndUpdate(this.baseFilter(tenantId, userId), { status: dto.status, lastSeen: new Date() }, { upsert: true, new: true });
40
+ }
41
+ async getPresence(tenantId, userId) {
42
+ return this.presenceModel.findOne(this.baseFilter(tenantId, userId));
43
+ }
44
+ async getPresences(tenantId, userIds) {
45
+ return this.presenceModel.find({ tenantId, userId: { $in: userIds } });
46
+ }
47
+ async getOnlineUsers(tenantId, userIds) {
48
+ return this.presenceModel.find({ tenantId, userId: { $in: userIds }, status: 'online' });
49
+ }
50
+ async setTyping(tenantId, userId, roomId) {
51
+ await this.presenceModel.findOneAndUpdate(this.baseFilter(tenantId, userId), { $addToSet: { typingIn: roomId } }, { upsert: true });
52
+ }
53
+ async stopTyping(tenantId, userId, roomId) {
54
+ await this.presenceModel.findOneAndUpdate(this.baseFilter(tenantId, userId), { $pull: { typingIn: roomId } });
55
+ }
56
+ async getTypingUsers(tenantId, roomId) {
57
+ return this.presenceModel.find({ tenantId, typingIn: roomId, status: 'online' });
58
+ }
59
+ };
60
+ exports.PresenceService = PresenceService;
61
+ exports.PresenceService = PresenceService = PresenceService_1 = __decorate([
62
+ (0, common_1.Injectable)(),
63
+ __param(0, (0, mongoose_1.InjectModel)(presence_schema_1.PresenceStatus.modelName)),
64
+ __metadata("design:paramtypes", [mongoose_2.Model])
65
+ ], PresenceService);
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var RoomService_1;
15
+ var _a;
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.RoomService = void 0;
18
+ const common_1 = require("@nestjs/common");
19
+ const mongoose_1 = require("@nestjs/mongoose");
20
+ const mongoose_2 = require("mongoose");
21
+ const room_schema_1 = require("../schemas/room.schema");
22
+ const chat_shared_1 = require("../../../shared/dist/index.js");
23
+ function computeMembersHash(members) {
24
+ return [...new Set(members)].sort().join('|');
25
+ }
26
+ let RoomService = RoomService_1 = class RoomService {
27
+ constructor(roomModel, hooks) {
28
+ this.roomModel = roomModel;
29
+ this.hooks = hooks;
30
+ this.logger = new common_1.Logger(RoomService_1.name);
31
+ }
32
+ async createRoom(tenantId, dto, authContext) {
33
+ const uniqueMembers = Array.from(new Set([
34
+ ...dto.members,
35
+ authContext.userId,
36
+ ]));
37
+ let roomData = {
38
+ name: dto.name,
39
+ type: dto.type,
40
+ members: uniqueMembers,
41
+ membersHash: dto.type === 'direct' ? computeMembersHash(uniqueMembers) : undefined,
42
+ tenantId,
43
+ createdBy: authContext.userId,
44
+ lastActivity: new Date(),
45
+ avatar: dto.avatar,
46
+ };
47
+ if (this.hooks?.beforeRoomCreate) {
48
+ roomData = this.hooks.beforeRoomCreate(roomData, authContext);
49
+ }
50
+ if (roomData.type === 'direct') {
51
+ const existing = await this.roomModel.findOneAndUpdate({ tenantId, type: 'direct', membersHash: roomData.membersHash }, { $set: { lastActivity: new Date() } }, { new: true });
52
+ if (existing) {
53
+ if (this.hooks?.afterRoomCreate) {
54
+ this.hooks.afterRoomCreate(existing);
55
+ }
56
+ return existing;
57
+ }
58
+ }
59
+ try {
60
+ const room = new this.roomModel(roomData);
61
+ const saved = await room.save();
62
+ if (this.hooks?.afterRoomCreate) {
63
+ this.hooks.afterRoomCreate(saved);
64
+ }
65
+ return saved;
66
+ }
67
+ catch (err) {
68
+ if (err?.code === 11000 && roomData.type === 'direct') {
69
+ const existing = await this.roomModel.findOne({
70
+ tenantId,
71
+ type: 'direct',
72
+ membersHash: roomData.membersHash,
73
+ });
74
+ if (existing)
75
+ return existing;
76
+ }
77
+ throw err;
78
+ }
79
+ }
80
+ async getRoom(tenantId, roomId) {
81
+ const room = await this.roomModel.findOne({ _id: roomId, tenantId });
82
+ if (!room) {
83
+ throw new common_1.NotFoundException('Room not found');
84
+ }
85
+ return room;
86
+ }
87
+ async getRooms(tenantId, userId, limit = 50, skip = 0) {
88
+ const [rooms, total] = await Promise.all([
89
+ this.roomModel
90
+ .find({ tenantId, members: userId })
91
+ .sort({ lastActivity: -1 })
92
+ .skip(skip)
93
+ .limit(limit + 1)
94
+ .exec(),
95
+ this.roomModel.countDocuments({ tenantId, members: userId }),
96
+ ]);
97
+ const hasMore = rooms.length > limit;
98
+ const result = hasMore ? rooms.slice(0, limit) : rooms;
99
+ return { rooms: result, hasMore };
100
+ }
101
+ async getDirectRooms(tenantId, userId) {
102
+ return this.roomModel
103
+ .find({ tenantId, members: userId, type: 'direct' })
104
+ .exec();
105
+ }
106
+ async findDirectRoom(tenantId, members) {
107
+ const uniqueMembers = Array.from(new Set(members));
108
+ return this.roomModel.findOne({
109
+ tenantId,
110
+ type: 'direct',
111
+ membersHash: computeMembersHash(uniqueMembers),
112
+ });
113
+ }
114
+ async findOrCreateDirectRoom(tenantId, members, authContext, name) {
115
+ const uniqueMembers = Array.from(new Set(members));
116
+ const membersHash = computeMembersHash(uniqueMembers);
117
+ const existingRoom = await this.roomModel.findOneAndUpdate({ tenantId, type: 'direct', membersHash }, { $set: { lastActivity: new Date() } }, { new: true });
118
+ if (existingRoom)
119
+ return existingRoom;
120
+ const roomName = name || `Direct: ${uniqueMembers.filter(id => id !== authContext.userId).join(', ')}`;
121
+ return this.createRoom(tenantId, { name: roomName, type: 'direct', members: uniqueMembers }, { ...authContext, tenantId });
122
+ }
123
+ async joinRoom(tenantId, roomId, userId) {
124
+ const room = await this.roomModel.findOneAndUpdate({ _id: roomId, tenantId, type: 'group' }, { $addToSet: { members: userId }, lastActivity: new Date() }, { new: true });
125
+ if (!room)
126
+ throw new common_1.NotFoundException('Room not found or not a group');
127
+ return room;
128
+ }
129
+ async leaveRoom(tenantId, roomId, userId) {
130
+ const room = await this.roomModel.findOneAndUpdate({ _id: roomId, tenantId }, { $pull: { members: userId }, lastActivity: new Date() }, { new: true });
131
+ if (!room)
132
+ throw new common_1.NotFoundException('Room not found');
133
+ return room;
134
+ }
135
+ async deleteRoom(tenantId, roomId, userId) {
136
+ const room = await this.roomModel.findOne({ _id: roomId, tenantId, createdBy: userId });
137
+ if (!room)
138
+ throw new common_1.NotFoundException('Room not found or unauthorized');
139
+ await this.roomModel.deleteOne({ _id: roomId, tenantId });
140
+ }
141
+ async isMember(tenantId, roomId, userId) {
142
+ const room = await this.roomModel.findOne({ _id: roomId, tenantId, members: userId });
143
+ return !!room;
144
+ }
145
+ async getRoomMembers(tenantId, roomId) {
146
+ const room = await this.roomModel.findOne({ _id: roomId, tenantId });
147
+ if (!room)
148
+ throw new common_1.NotFoundException('Room not found');
149
+ return room.members;
150
+ }
151
+ };
152
+ exports.RoomService = RoomService;
153
+ exports.RoomService = RoomService = RoomService_1 = __decorate([
154
+ (0, common_1.Injectable)(),
155
+ __param(0, (0, mongoose_1.InjectModel)(room_schema_1.Room.modelName)),
156
+ __param(1, (0, common_1.Optional)()),
157
+ __metadata("design:paramtypes", [mongoose_2.Model, typeof (_a = typeof chat_shared_1.ChatHooks !== "undefined" && chat_shared_1.ChatHooks) === "function" ? _a : Object])
158
+ ], RoomService);