Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
329
marketing-agent/services/user-service/src/models/Contact.js
Normal file
329
marketing-agent/services/user-service/src/models/Contact.js
Normal file
@@ -0,0 +1,329 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const contactSchema = new mongoose.Schema({
|
||||
// Multi-tenant support
|
||||
tenantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
telegramId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
index: true
|
||||
},
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
phoneNumber: {
|
||||
type: String,
|
||||
index: true
|
||||
},
|
||||
email: String,
|
||||
bio: String,
|
||||
profilePhoto: String,
|
||||
language: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['active', 'inactive', 'blocked', 'deleted'],
|
||||
default: 'active'
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
enum: ['import', 'chat', 'group', 'channel', 'manual', 'api'],
|
||||
default: 'manual'
|
||||
},
|
||||
tags: [{
|
||||
type: String,
|
||||
trim: true
|
||||
}],
|
||||
groups: [{
|
||||
groupId: String,
|
||||
groupName: String,
|
||||
joinedAt: Date
|
||||
}],
|
||||
customFields: {
|
||||
type: Map,
|
||||
of: mongoose.Schema.Types.Mixed
|
||||
},
|
||||
preferences: {
|
||||
messageFrequency: {
|
||||
type: String,
|
||||
enum: ['high', 'medium', 'low', 'none'],
|
||||
default: 'medium'
|
||||
},
|
||||
preferredTime: {
|
||||
start: String, // HH:mm format
|
||||
end: String // HH:mm format
|
||||
},
|
||||
topics: [String],
|
||||
optOut: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
engagement: {
|
||||
lastSeen: Date,
|
||||
lastMessageSent: Date,
|
||||
lastMessageReceived: Date,
|
||||
totalMessagesSent: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalMessagesReceived: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalInteractions: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
averageResponseTime: {
|
||||
type: Number, // in seconds
|
||||
default: 0
|
||||
},
|
||||
engagementScore: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
demographics: {
|
||||
age: Number,
|
||||
gender: {
|
||||
type: String,
|
||||
enum: ['male', 'female', 'other', 'prefer_not_to_say']
|
||||
},
|
||||
location: {
|
||||
country: String,
|
||||
region: String,
|
||||
city: String,
|
||||
timezone: String
|
||||
},
|
||||
interests: [String]
|
||||
},
|
||||
history: [{
|
||||
event: String,
|
||||
timestamp: Date,
|
||||
details: mongoose.Schema.Types.Mixed
|
||||
}],
|
||||
notes: String,
|
||||
importedAt: Date,
|
||||
lastModified: Date
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes
|
||||
contactSchema.index({ accountId: 1, status: 1 });
|
||||
contactSchema.index({ accountId: 1, tags: 1 });
|
||||
contactSchema.index({ accountId: 1, 'groups.groupId': 1 });
|
||||
contactSchema.index({ accountId: 1, 'engagement.engagementScore': -1 });
|
||||
contactSchema.index({ accountId: 1, createdAt: -1 });
|
||||
|
||||
// Multi-tenant indexes
|
||||
contactSchema.index({ tenantId: 1, accountId: 1, status: 1 });
|
||||
contactSchema.index({ tenantId: 1, accountId: 1, tags: 1 });
|
||||
contactSchema.index({ tenantId: 1, accountId: 1, 'groups.groupId': 1 });
|
||||
contactSchema.index({ tenantId: 1, accountId: 1, 'engagement.engagementScore': -1 });
|
||||
contactSchema.index({ tenantId: 1, accountId: 1, createdAt: -1 });
|
||||
|
||||
// Virtual properties
|
||||
contactSchema.virtual('fullName').get(function() {
|
||||
const parts = [];
|
||||
if (this.firstName) parts.push(this.firstName);
|
||||
if (this.lastName) parts.push(this.lastName);
|
||||
return parts.join(' ') || this.username || this.telegramId;
|
||||
});
|
||||
|
||||
contactSchema.virtual('isActive').get(function() {
|
||||
return this.status === 'active' && !this.preferences.optOut;
|
||||
});
|
||||
|
||||
contactSchema.virtual('daysSinceLastSeen').get(function() {
|
||||
if (!this.engagement.lastSeen) return null;
|
||||
const days = Math.floor((Date.now() - this.engagement.lastSeen) / (1000 * 60 * 60 * 24));
|
||||
return days;
|
||||
});
|
||||
|
||||
// Methods
|
||||
contactSchema.methods.addTag = function(tag) {
|
||||
if (!this.tags.includes(tag)) {
|
||||
this.tags.push(tag);
|
||||
}
|
||||
};
|
||||
|
||||
contactSchema.methods.removeTag = function(tag) {
|
||||
this.tags = this.tags.filter(t => t !== tag);
|
||||
};
|
||||
|
||||
contactSchema.methods.updateEngagement = function(eventType) {
|
||||
this.engagement.lastSeen = new Date();
|
||||
this.engagement.totalInteractions += 1;
|
||||
|
||||
if (eventType === 'message_sent') {
|
||||
this.engagement.lastMessageSent = new Date();
|
||||
this.engagement.totalMessagesSent += 1;
|
||||
} else if (eventType === 'message_received') {
|
||||
this.engagement.lastMessageReceived = new Date();
|
||||
this.engagement.totalMessagesReceived += 1;
|
||||
}
|
||||
|
||||
// Update engagement score based on recent activity
|
||||
this.calculateEngagementScore();
|
||||
};
|
||||
|
||||
contactSchema.methods.calculateEngagementScore = function() {
|
||||
let score = 50; // Base score
|
||||
|
||||
// Recent activity bonus
|
||||
const daysSinceLastSeen = this.daysSinceLastSeen;
|
||||
if (daysSinceLastSeen !== null) {
|
||||
if (daysSinceLastSeen < 7) score += 20;
|
||||
else if (daysSinceLastSeen < 30) score += 10;
|
||||
else if (daysSinceLastSeen > 90) score -= 20;
|
||||
}
|
||||
|
||||
// Message interaction bonus
|
||||
if (this.engagement.totalMessagesReceived > 0) {
|
||||
const responseRate = this.engagement.totalMessagesReceived / this.engagement.totalMessagesSent;
|
||||
score += Math.min(20, responseRate * 20);
|
||||
}
|
||||
|
||||
// Frequency bonus
|
||||
const interactionsPerDay = this.engagement.totalInteractions /
|
||||
Math.max(1, Math.floor((Date.now() - this.createdAt) / (1000 * 60 * 60 * 24)));
|
||||
|
||||
if (interactionsPerDay > 1) score += 10;
|
||||
else if (interactionsPerDay < 0.1) score -= 10;
|
||||
|
||||
// Ensure score is within bounds
|
||||
this.engagement.engagementScore = Math.max(0, Math.min(100, Math.round(score)));
|
||||
};
|
||||
|
||||
contactSchema.methods.logHistory = function(event, details = {}) {
|
||||
this.history.push({
|
||||
event,
|
||||
timestamp: new Date(),
|
||||
details
|
||||
});
|
||||
|
||||
// Keep only last 100 history entries
|
||||
if (this.history.length > 100) {
|
||||
this.history = this.history.slice(-100);
|
||||
}
|
||||
};
|
||||
|
||||
// Static methods
|
||||
contactSchema.statics.findByTags = function(accountId, tags) {
|
||||
return this.find({
|
||||
accountId,
|
||||
tags: { $in: tags },
|
||||
status: 'active'
|
||||
});
|
||||
};
|
||||
|
||||
contactSchema.statics.findByGroup = function(accountId, groupId) {
|
||||
return this.find({
|
||||
accountId,
|
||||
'groups.groupId': groupId,
|
||||
status: 'active'
|
||||
});
|
||||
};
|
||||
|
||||
contactSchema.statics.findEngaged = function(accountId, minScore = 70) {
|
||||
return this.find({
|
||||
accountId,
|
||||
'engagement.engagementScore': { $gte: minScore },
|
||||
status: 'active',
|
||||
'preferences.optOut': false
|
||||
});
|
||||
};
|
||||
|
||||
contactSchema.statics.bulkImport = async function(accountId, contacts) {
|
||||
const operations = contacts.map(contact => ({
|
||||
updateOne: {
|
||||
filter: { telegramId: contact.telegramId, accountId },
|
||||
update: {
|
||||
$set: {
|
||||
...contact,
|
||||
accountId,
|
||||
importedAt: new Date(),
|
||||
source: 'import'
|
||||
}
|
||||
},
|
||||
upsert: true
|
||||
}
|
||||
}));
|
||||
|
||||
return this.bulkWrite(operations);
|
||||
};
|
||||
|
||||
contactSchema.statics.getSegmentStats = async function(accountId) {
|
||||
return this.aggregate([
|
||||
{ $match: { accountId, status: 'active' } },
|
||||
{
|
||||
$facet: {
|
||||
byEngagement: [
|
||||
{
|
||||
$bucket: {
|
||||
groupBy: '$engagement.engagementScore',
|
||||
boundaries: [0, 25, 50, 75, 100],
|
||||
default: 'Unknown',
|
||||
output: { count: { $sum: 1 } }
|
||||
}
|
||||
}
|
||||
],
|
||||
bySource: [
|
||||
{
|
||||
$group: {
|
||||
_id: '$source',
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
byStatus: [
|
||||
{
|
||||
$group: {
|
||||
_id: '$status',
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
topTags: [
|
||||
{ $unwind: '$tags' },
|
||||
{
|
||||
$group: {
|
||||
_id: '$tags',
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 10 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
// Pre-save hook
|
||||
contactSchema.pre('save', function(next) {
|
||||
this.lastModified = new Date();
|
||||
next();
|
||||
});
|
||||
|
||||
export const Contact = mongoose.model('Contact', contactSchema);
|
||||
Reference in New Issue
Block a user