using System.Text.RegularExpressions; using JobsMedical.Web.Data; using JobsMedical.Web.Models; using Microsoft.EntityFrameworkCore; namespace JobsMedical.Web.Services; /// /// Anti-garbage gate for user-submitted listings: blocks duplicate positions, screens obvious /// spam/garbage text, and prevents the same applicant applying to a listing twice. /// public class SubmissionGuard { private readonly AppDbContext _db; public SubmissionGuard(AppDbContext db) => _db = db; private static readonly string[] SpamMarkers = { "سرمایه گذاری", "سرمایه‌گذاری", "وام", "ارز دیجیتال", "رمز ارز", "فروش فالوور", "بک لینک", "قرعه کشی", "کازینو", "شرط بندی", "بیت کوین", "http://", "https://", "www." }; /// Spam/scam markers, mashed-keyboard, or stray links in free text (any field). public static bool ContainsSpam(string? text) { var t = (text ?? "").Trim(); if (t.Length == 0) return false; if (SpamMarkers.Any(m => t.Contains(m))) return true; if (Regex.IsMatch(t, @"(.)\1{6,}")) return true; // aaaaaaa / کککککککک return false; } /// For jobs: a real title is required, plus the spam screen on title+description. public static bool LooksLikeGarbage(string? title, string? description) { if (Normalize(title).Length < 3) return true; // no real title return ContainsSpam(title) || ContainsSpam(description); } /// A near-identical OPEN shift already exists at this facility? public Task DuplicateShiftAsync(int facilityId, int roleId, DateOnly date, TimeOnly start, ShiftType type) => _db.Shifts.AnyAsync(s => s.Status == ShiftStatus.Open && s.FacilityId == facilityId && s.RoleId == roleId && s.Date == date && s.StartTime == start && s.ShiftType == type); /// A near-identical OPEN job already exists at this facility (same role + title)? public async Task DuplicateJobAsync(int facilityId, int roleId, string title) { var t = Normalize(title); var candidates = await _db.JobOpenings .Where(j => j.Status == ShiftStatus.Open && j.FacilityId == facilityId && j.RoleId == roleId) .Select(j => j.Title).ToListAsync(); return candidates.Any(c => Normalize(c) == t); } /// Max new listings (shifts + jobs) one account may post per rolling hour. public const int MaxListingsPerHour = 20; /// True if this owner has hit the hourly posting cap (flood protection). public async Task PostingRateExceededAsync(int ownerUserId) { var since = DateTime.UtcNow.AddHours(-1); var shifts = await _db.Shifts.CountAsync(s => s.Facility.OwnerUserId == ownerUserId && s.CreatedAt >= since); var jobs = await _db.JobOpenings.CountAsync(j => j.Facility.OwnerUserId == ownerUserId && j.CreatedAt >= since); return shifts + jobs >= MaxListingsPerHour; } /// Has this visitor already applied (Apply event) to this shift/job? public Task AlreadyAppliedAsync(string visitorId, int? shiftId, int? jobId) => _db.InterestEvents.AnyAsync(e => e.VisitorId == visitorId && e.EventType == InterestEventType.Apply && ((shiftId != null && e.ShiftId == shiftId) || (jobId != null && e.JobOpeningId == jobId))); private static string Normalize(string? s) => Regex.Replace((s ?? "").Trim(), @"\s+", " ") .Replace('ي', 'ی').Replace('ك', 'ک').ToLowerInvariant(); }