Skip to main content

Engagement & Moderation Services

The engagement and moderation services handle user interaction tracking, content popularity scoring, content moderation workflows, and survey management. These services bridge the gap between raw database metrics and meaningful business logic.

Engagement Service

File: lib/services/engagement.service.ts

The engagement service calculates true popularity scores using real engagement metrics with logarithmic scaling designed to handle millions of interactions.

ItemWithEngagement Type

interface ItemWithEngagement extends ItemData {
engagement?: ItemEngagementMetrics;
}

The ItemEngagementMetrics type (from lib/db/queries/engagement.queries.ts) provides:

MetricTypeDescription
viewsnumberTotal page views
votesnumberNet vote count (upvotes minus downvotes)
avgRatingnumberAverage star rating (0-5)
favoritesnumberNumber of times favorited
commentsnumberTotal comments

Logarithmic Scaling

The scoring system uses log10 scaling to handle large metric ranges gracefully:

function logScale(value: number, weight: number = 1000): number {
if (value <= 0) return 0;
return Math.log10(value + 1) * weight;
}

This ensures meaningful differentiation across all scales:

Raw ValueLog Score (weight=1000)
1301
101,041
1002,004
1,0003,000
1,000,0006,000

Popularity Score Calculation

The calculatePopularityScore() function combines multiple engagement signals:

export function calculatePopularityScore(item: ItemWithEngagement): number {
let score = 0;

// Featured items get massive boost
if (item.featured) score += 10000;

if (engagement) {
score += logScale(engagement.views, 1000); // Views
score += logScale(Math.max(engagement.votes, 0), 1200); // Votes
score += engagement.avgRating * 500; // Rating (linear)
score += logScale(engagement.favorites, 1100); // Favorites
score += logScale(engagement.comments, 1000); // Comments
}

// Recency bonus (0-1750 points, decays over time)
score += recencyScore;

return score;
}

Score Component Weights

ComponentScalingWeightMax at 1MDescription
FeaturedFlat10,00010,000Base boost for featured items
ViewsLogarithmic1,000~6,000Page view count
VotesLogarithmic1,200~7,200Net positive votes (higher weight for active engagement)
RatingLinear500/star2,500Average rating (already bounded 0-5)
FavoritesLogarithmic1,100~6,600Strong interest signal
CommentsLogarithmic1,000~6,000Discussion indicator
RecencyLinear decay-1,750Newer items get visibility boost

Recency Scoring

Items receive a time-based bonus that decays linearly over 30 days:

const ageInDays = (now - itemTime) / (1000 * 60 * 60 * 24);
// Items within last 30 days get 0-1000 points (linear decay)

Items older than 30 days receive no recency bonus, while newly created or updated items receive the maximum boost.

Moderation Service

File: lib/services/moderation.service.ts

The moderation service handles content removal, user warnings, suspensions, and bans in response to reported content.

Dependencies

The moderation service integrates with multiple modules:

import { createModerationHistory, incrementWarningCount, suspendUser, banUser } from 'moderation.queries';
import { deleteComment, getCommentById } from 'comment.queries';
import { ItemRepository } from 'item.repository';
import { EmailNotificationService } from 'email-notification.service';

Content Owner Resolution

The getContentOwner() function identifies who authored reported content:

async function getContentOwner(
contentType: ReportContentTypeValues,
contentId: string
): Promise<ContentOwnerResult> {
if (contentType === ReportContentType.COMMENT) {
const comment = await getCommentById(contentId);
return { success: true, userId: comment.userId };
}
if (contentType === ReportContentType.ITEM) {
const item = await itemRepository.findById(contentId);
return { success: true, userId: item.submitted_by };
}
}

Moderation Actions

Content Removal

The removeContent() function handles deletion of reported comments and items:

async function removeContent(
contentType: ReportContentTypeValues,
contentId: string,
reportId: string,
adminId: string
): Promise<ModerationResult>

The removal flow:

1. Get content owner (for audit trail)
2. Delete content:
- Comments: Soft delete via deleteComment()
- Items: Delete from Git repository via ItemRepository.delete()
3. Create moderation history record
4. Send email notification to content owner
5. Return result

Email Notifications

When content is removed, the owner receives an email via EmailNotificationService.sendContentRemovedEmail():

EmailNotificationService.sendContentRemovedEmail(
user.email,
'comment', // or 'item'
reason
).catch((err) => console.error('Failed to send content removed email:', err));

Email sending is fire-and-forget (.catch() prevents unhandled rejections) to avoid blocking the moderation action.

Moderation History

Every moderation action is recorded in the database:

await createModerationHistory({
userId: ownerResult.userId,
action: ModerationAction.CONTENT_REMOVED,
reason: 'Content removed due to report violation',
reportId,
performedBy: adminId,
contentType,
contentId,
details: { itemName: item.name, itemSlug: item.slug }
});

Moderation Actions Enum

ActionDescription
CONTENT_REMOVEDComment or item was deleted
WARNING_ISSUEDWarning sent to user
USER_SUSPENDEDTemporary account suspension
USER_BANNEDPermanent account ban

User Escalation

The moderation service supports escalating actions against repeat offenders:

// Increment warning count
await incrementWarningCount(userId);

// Suspend user (temporary)
await suspendUser(userId, duration);

// Ban user (permanent)
await banUser(userId);

Survey Service

File: lib/services/survey.service.ts

The SurveyService is a server-side only service that handles survey CRUD operations and response collection.

Usage Context

// Server-side only - import guard
import 'server-only'; // (implied by usage pattern)

// API Routes and Server Components only
// Client Components should use surveyApiClient from lib/api/survey-api.client

Standard CRUD Methods

MethodDescription
create(data)Create new survey with auto-generated slug
getOne(id)Get survey by ID
getBySlug(slug)Get survey by URL slug
getMany(filters)Get surveys with filtering and pagination
update(id, data)Update survey by ID
delete(id)Delete survey by ID

Survey-Specific Methods

MethodDescription
submitResponse(surveyId, data)Submit a survey response
getResponses(surveyId, filters)Get responses for a survey
getResponseById(responseId)Get a single response

Slug Generation

Surveys auto-generate URL-friendly slugs from titles, with uniqueness checks:

async create(data: CreateSurveyData): Promise<Survey> {
const slug = this.generateSlug(data.title);
const existingSurvey = await queries.getSurveyBySlug(slug);
const finalSlug = existingSurvey ? await this.ensureUniqueSlug(slug) : slug;
// ...
}

Survey Lifecycle

StatusDescription
draftSurvey is being created, not visible to users
publishedSurvey is live and accepting responses
closedSurvey is no longer accepting responses

The publishedAt and closedAt timestamps are set automatically when the status changes.

Item Audit Service

File: lib/services/item-audit.service.ts

Tracks all changes made to items for accountability and history viewing. The audit trail is accessible via the /api/admin/items/[id]/history endpoint.