Files
lampac/mods/Telemetry/Controllers/TelemetryController.cs
T

540 lines
22 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Shared;
using Shared.Attributes;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Telemetry.Models;
using System.Collections.Generic;
namespace Telemetry.Controllers;
[Authorization(redirectUri: "/adminpanel/auth")]
[Route("/telemetry")]
public class TelemetryController : BaseController
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public TelemetryController(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
private class MovieRawDto
{
public string? Title { get; set; }
public string? Tmdb { get; set; }
public string? Kp { get; set; }
public string? Imdb { get; set; }
public bool IsTv { get; set; }
public int Count { get; set; }
}
private static (DateTime start, DateTime end) GetPeriod(int days)
{
var now = DateTime.UtcNow.Date;
var periodStart = days == 0 ? now : (days == -1 ? now.AddDays(-1) : now.AddDays(-days));
var periodEnd = days == -1 ? now : DateTime.UtcNow;
return (periodStart, periodEnd);
}
private static List<object> MapMovies(List<MovieRawDto> raw)
{
return raw.Select(m => new
{
title = m.Title,
id = m.Tmdb ?? m.Kp ?? m.Imdb ?? m.Title,
tmdb = m.Tmdb,
kp = m.Kp,
imdb = m.Imdb,
count = m.Count,
source = m.Tmdb != null ? "tmdb" : m.Kp != null ? "kinopoisk" : m.Imdb != null ? "imdb" : "unknown",
isTv = m.IsTv
}).Cast<object>().ToList();
}
private IQueryable<LogModelSql> ApplySearchFilter(AppDbContext db, IQueryable<LogModelSql> query, string? search)
{
if (string.IsNullOrEmpty(search)) return query;
var rawSearch = search;
var encodedSearch = Uri.EscapeDataString(search);
var encodedSearchPlus = encodedSearch.Replace("%20", "+");
return query.Where(l =>
l.Uid.Contains(rawSearch) ||
l.Uri.Contains(rawSearch) ||
l.Uri.Contains(encodedSearch) ||
l.Uri.Contains(encodedSearchPlus) ||
(l.Balancer != null && l.Balancer.Contains(rawSearch)) ||
db.Users.Any(u => u.Id == l.UnfoId && (u.Ip.Contains(rawSearch) || u.UserAgent.Contains(rawSearch)))
);
}
[HttpGet]
[Route("")]
public ActionResult Index()
{
var path = Path.Combine(ModInit.modpath, "index.html");
if (!System.IO.File.Exists(path)) return Content("UI missing", "text/plain");
var html = System.IO.File.ReadAllText(path, Encoding.UTF8);
return Content(html, "text/html; charset=utf-8");
}
[HttpGet("api/stats")]
public async Task<IActionResult> Stats(string? search, int days = 30)
{
await using var db = await _contextFactory.CreateDbContextAsync();
var query = db.Logs.AsNoTracking();
query = ApplySearchFilter(db, query, search);
var (periodStart, periodEnd) = GetPeriod(days);
var now = DateTime.UtcNow.Date;
int total;
int today;
if (days == 0)
{
today = await query.Where(l => l.Time >= now.Date && l.Time < periodEnd).CountAsync();
total = today;
}
else if (days == -1)
{
total = await query.Where(l => l.Time >= periodStart && l.Time < periodEnd).CountAsync();
today = 0;
}
else
{
total = await query.Where(l => l.Time >= periodStart && l.Time < periodEnd).CountAsync();
today = await query.Where(l => l.Time >= now.Date && l.Time < periodEnd).CountAsync();
}
var uids = await query.Where(l => l.Time >= periodStart && l.Time < periodEnd && l.Uid != "anonymous" && !string.IsNullOrEmpty(l.Uid)).Select(l => l.Uid).Distinct().CountAsync();
var validLogs = query.Where(l => l.Time >= periodStart && l.Time < periodEnd && l.UnfoId != null);
var ips = await validLogs.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.Ip).Where(ip => ip != null && ip != "").Distinct().CountAsync();
var uas = await validLogs.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent).Where(ua => ua != null && ua != "").Distinct().CountAsync();
return new JsonResult(new { success = true, data = new { today, total, uids, ips, uas } });
}
[HttpGet("api/logs")]
public async Task<IActionResult> Logs(string? search, int days = 30, int skip = 0, int take = 100)
{
take = Math.Min(take, 1000);
await using var db = await _contextFactory.CreateDbContextAsync();
var query = db.Logs.AsNoTracking();
var (periodStart, periodEnd) = GetPeriod(days);
query = query.Where(l => l.Time >= periodStart && l.Time < periodEnd);
query = ApplySearchFilter(db, query, search);
var logs = await query.OrderByDescending(l => l.Time).Skip(skip).Take(take)
.GroupJoin(db.Users, l => l.UnfoId, u => u.Id, (l, users) => new { Log = l, Users = users })
.SelectMany(x => x.Users.DefaultIfEmpty(), (x, u) => new
{
id = x.Log.Id,
time = x.Log.Time,
uri = x.Log.Uri,
uid = x.Log.Uid,
ip = u != null ? u.Ip : "unknown",
userAgent = u != null ? u.UserAgent : "unknown",
balancer = x.Log.Balancer,
statusCode = x.Log.StatusCode,
duration = x.Log.DurationMs
})
.ToListAsync();
return new JsonResult(new { success = true, data = logs });
}
[HttpGet("api/chart")]
public async Task<IActionResult> Chart(string? search, int days = 30)
{
var (periodStart, periodEnd) = GetPeriod(days);
await using var db = await _contextFactory.CreateDbContextAsync();
var query = db.Logs.AsNoTracking();
query = ApplySearchFilter(db, query, search);
var data = await query
.Where(l => l.Time >= periodStart && l.Time < periodEnd)
.GroupBy(l => l.Time.Date)
.Select(g => new { date = g.Key, count = g.Count() })
.OrderBy(x => x.date)
.ToListAsync();
return new JsonResult(new { success = true, data = data });
}
[HttpGet("api/tops")]
public async Task<IActionResult> GetTops(string? search, [FromQuery] int days = 7)
{
await using var db = await _contextFactory.CreateDbContextAsync();
var (periodStart, periodEnd) = GetPeriod(days);
var query = db.Logs.AsNoTracking();
query = ApplySearchFilter(db, query, search);
var baseQuery = query.Where(l => l.Time >= periodStart && l.Time < periodEnd);
var topBalancers = await baseQuery
.Where(l => l.Balancer != null && l.Balancer != "" && !l.Balancer.StartsWith("18+ "))
.GroupBy(l => l.Balancer)
.Select(g => new { balancer = g.Key, count = g.Count() })
.OrderByDescending(x => x.count)
.Take(10)
.ToListAsync();
var topAdult = await baseQuery
.Where(l => l.Balancer != null && l.Balancer.StartsWith("18+ "))
.GroupBy(l => l.Balancer)
.Select(g => new { balancer = g.Key!.Substring(4), count = g.Count() })
.OrderByDescending(x => x.count)
.Take(10)
.ToListAsync();
var topUsers = await baseQuery
.Where(l => l.Uid != "anonymous" && l.Uid != null && l.Uid != "")
.GroupBy(l => l.Uid)
.Select(g => new { uid = g.Key, count = g.Count() })
.OrderByDescending(x => x.count)
.Take(10)
.ToListAsync();
var topMoviesRaw = await baseQuery
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
.GroupBy(l => l.MovieTitle)
.Select(g => new MovieRawDto
{
Title = g.Key,
Tmdb = g.Max(l => l.TmdbId),
Kp = g.Max(l => l.KpId),
Imdb = g.Max(l => l.ImdbId),
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.Take(10)
.ToListAsync();
var topMovies = MapMovies(topMoviesRaw);
var userAgents = await baseQuery
.Where(l => l.UnfoId != null)
.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent)
.GroupBy(ua => ua)
.Select(g => new { ua = g.Key, count = g.Count() })
.OrderByDescending(x => x.count)
.Take(1000)
.ToListAsync();
var devices = userAgents
.Select(x => new { name = ParseDevice(x.ua), count = x.count })
.GroupBy(x => x.name)
.Select(g => new { name = g.Key, count = g.Sum(x => x.count) })
.OrderByDescending(x => x.count)
.ToList();
var hours = await baseQuery
.GroupBy(l => l.Time.Hour)
.Select(g => new { hour = g.Key, count = g.Count() })
.OrderBy(x => x.hour)
.ToListAsync();
var seriesCount = await baseQuery.Where(l => l.IsTv).CountAsync();
var moviesCount = await baseQuery.Where(l => l.MovieTitle != null && l.MovieTitle != "" && !l.IsTv).CountAsync();
var animeCount = await baseQuery.Where(l => l.Uri.Contains("anime=1") || l.Uri.Contains("anime=true")).CountAsync();
var contentTypes = new[] {
new { name = "Фильмы", count = moviesCount },
new { name = "Сериалы", count = seriesCount },
new { name = "Аниме", count = animeCount }
}.Where(x => x.count > 0).ToList();
return new JsonResult(new { success = true, data = new { balancers = topBalancers, adult = topAdult, users = topUsers, movies = topMovies, devices, hours, contentTypes } });
}
private string ParseDevice(string? ua)
{
if (ua == null) return "Другие";
ua = ua.ToLower();
if (ua.Contains("tizen") || ua.Contains("samsung")) return "Tizen";
if (ua.Contains("web0s") || ua.Contains("webos")) return "WebOS";
if (ua.Contains("android")) return "Android";
if (ua.Contains("iphone") || ua.Contains("ipad") || ua.Contains("mac os")) return "Apple";
if (ua.Contains("windows")) return "Windows";
if (ua.Contains("linux")) return "Linux";
return "Другие";
}
[HttpGet("api/user")]
public async Task<IActionResult> UserInfo(string uid, int days = 30)
{
if (string.IsNullOrEmpty(uid)) return BadRequest();
await using var db = await _contextFactory.CreateDbContextAsync();
var (periodStart, periodEnd) = GetPeriod(days);
var baseQuery = db.Logs.AsNoTracking().Where(l => l.Uid == uid && l.Time >= periodStart && l.Time < periodEnd);
var count = await baseQuery.CountAsync();
if (count == 0) return new JsonResult(new { success = false });
var totalRequests = count;
var firstSeen = await baseQuery.MinAsync(l => l.Time);
var lastSeen = await baseQuery.MaxAsync(l => l.Time);
var ips = await baseQuery.Where(l => l.UnfoId != null).Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.Ip).Where(ip => ip != null && ip != "").Distinct().ToListAsync();
var userAgents = await baseQuery.Where(l => l.UnfoId != null).Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent).Where(ua => ua != null && ua != "").Distinct().ToListAsync();
var balancers = await baseQuery.Where(l => l.Balancer != null && l.Balancer != "")
.GroupBy(l => l.Balancer)
.Select(g => new { name = g.Key, count = g.Count() })
.OrderByDescending(x => x.count)
.ToListAsync();
var isHourly = days <= 1;
var timeline = await (isHourly
? baseQuery.GroupBy(l => new DateTime(l.Time.Year, l.Time.Month, l.Time.Day, l.Time.Hour, 0, 0)).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync()
: baseQuery.GroupBy(l => l.Time.Date).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync());
var moviesRaw = await baseQuery
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
.GroupBy(l => l.MovieTitle)
.Select(g => new MovieRawDto
{
Title = g.Key,
Tmdb = g.Max(l => l.TmdbId),
Kp = g.Max(l => l.KpId),
Imdb = g.Max(l => l.ImdbId),
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.Take(10)
.ToListAsync();
var movies = MapMovies(moviesRaw);
return new JsonResult(new
{
success = true,
data = new
{
totalRequests,
firstSeen,
lastSeen,
ips,
userAgents,
balancers,
timeline,
movies
}
});
}
[HttpGet("api/ip")]
public async Task<IActionResult> IpInfo(string ip, int days = 30)
{
if (string.IsNullOrEmpty(ip)) return BadRequest();
await using var db = await _contextFactory.CreateDbContextAsync();
var (periodStart, periodEnd) = GetPeriod(days);
var baseQuery = db.Logs.AsNoTracking().Where(l => l.Time >= periodStart && l.Time < periodEnd && l.UnfoId != null && db.Users.Any(u => u.Id == l.UnfoId && u.Ip == ip));
var count = await baseQuery.CountAsync();
if (count == 0) return new JsonResult(new { success = false });
var totalRequests = count;
var firstSeen = await baseQuery.MinAsync(l => l.Time);
var lastSeen = await baseQuery.MaxAsync(l => l.Time);
var uids = await baseQuery.Where(l => l.Uid != "anonymous" && l.Uid != null && l.Uid != "").Select(l => l.Uid).Distinct().ToListAsync();
var userAgents = await baseQuery.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent).Where(ua => ua != null && ua != "").Distinct().ToListAsync();
var balancers = await baseQuery.Where(l => l.Balancer != null && l.Balancer != "")
.GroupBy(l => l.Balancer)
.Select(g => new { name = g.Key, count = g.Count() })
.OrderByDescending(x => x.count)
.ToListAsync();
var isHourly = days <= 1;
var timeline = await (isHourly
? baseQuery.GroupBy(l => new DateTime(l.Time.Year, l.Time.Month, l.Time.Day, l.Time.Hour, 0, 0)).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync()
: baseQuery.GroupBy(l => l.Time.Date).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync());
var moviesRaw = await baseQuery
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
.GroupBy(l => l.MovieTitle)
.Select(g => new MovieRawDto
{
Title = g.Key,
Tmdb = g.Max(l => l.TmdbId),
Kp = g.Max(l => l.KpId),
Imdb = g.Max(l => l.ImdbId),
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.Take(10)
.ToListAsync();
var movies = MapMovies(moviesRaw);
return new JsonResult(new
{
success = true,
data = new
{
totalRequests,
firstSeen,
lastSeen,
uids,
userAgents,
balancers,
timeline,
movies
}
});
}
[HttpGet("api/settings")]
public IActionResult GetSettings()
{
return new JsonResult(new
{
success = true,
data = new
{
prefixes = string.Join("\n", RequestListener.SkipPrefixes),
extensions = string.Join("\n", RequestListener.SkipExtensions),
regexes = string.Join("\n", RequestListener.RegexFilters),
ips = string.Join("\n", RequestListener.BlacklistIps),
rateLimit = RequestListener.RateLimitMs
}
});
}
[HttpPost("api/settings")]
public IActionResult UpdateSettings([FromForm] string? prefixes, [FromForm] string? extensions, [FromForm] string? regexes, [FromForm] string? ips, [FromForm] int? rateLimit)
{
if (prefixes != null) RequestListener.SkipPrefixes = prefixes.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
if (extensions != null) RequestListener.SkipExtensions = extensions.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
if (regexes != null)
{
var patterns = regexes.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
var invalid = new List<string>();
foreach (var p in patterns)
{
try { System.Text.RegularExpressions.Regex.Match("", p, System.Text.RegularExpressions.RegexOptions.None, TimeSpan.FromMilliseconds(50)); }
catch { invalid.Add(p); }
}
if (invalid.Count > 0)
return new JsonResult(new { success = false, error = $"Невалидные regex: {string.Join(", ", invalid)}" });
RequestListener.RegexFilters = patterns;
}
if (ips != null) RequestListener.BlacklistIps = ips.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
if (rateLimit.HasValue) RequestListener.RateLimitMs = rateLimit.Value;
RequestListener.SaveSettings();
return new JsonResult(new { success = true });
}
[HttpGet("api/balancer")]
public async Task<IActionResult> BalancerInfo(string balancer, int days = 30)
{
if (string.IsNullOrEmpty(balancer)) return BadRequest();
await using var db = await _contextFactory.CreateDbContextAsync();
var (periodStart, periodEnd) = GetPeriod(days);
var now = DateTime.UtcNow.Date;
var baseQuery = db.Logs.AsNoTracking().Where(l => l.Balancer == balancer && l.Time >= periodStart && l.Time < periodEnd);
var total = await baseQuery.CountAsync();
var today = await baseQuery.Where(l => l.Time >= now.Date).CountAsync();
var topUids = await baseQuery.Where(l => l.Uid != "anonymous" && l.Uid != null && l.Uid != "")
.GroupBy(l => l.Uid)
.Select(g => new { uid = g.Key, count = g.Count() })
.OrderByDescending(x => x.count).Take(10).ToListAsync();
var topIps = await baseQuery.Where(l => l.UnfoId != null)
.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.Ip)
.Where(ip => ip != null && ip != "")
.GroupBy(ip => ip)
.Select(g => new { ip = g.Key, count = g.Count() })
.OrderByDescending(x => x.count).Take(10).ToListAsync();
var isHourly = days <= 1;
var timeline = await (isHourly
? baseQuery.GroupBy(l => new DateTime(l.Time.Year, l.Time.Month, l.Time.Day, l.Time.Hour, 0, 0)).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync()
: baseQuery.GroupBy(l => l.Time.Date).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync());
var moviesRaw = await baseQuery
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
.GroupBy(l => l.MovieTitle)
.Select(g => new MovieRawDto
{
Title = g.Key,
Tmdb = g.Max(l => l.TmdbId),
Kp = g.Max(l => l.KpId),
Imdb = g.Max(l => l.ImdbId),
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.Take(10)
.ToListAsync();
var movies = MapMovies(moviesRaw);
return new JsonResult(new { success = true, data = new { total, today, uids = topUids, ips = topIps, timeline, movies } });
}
[HttpPost("api/logs/delete")]
public async Task<IActionResult> DeleteLogs([FromForm] string? search, [FromForm] int days = 30)
{
await using var db = await _contextFactory.CreateDbContextAsync();
var now = DateTime.UtcNow.Date;
var periodStart = days == 0 ? now : (days == -1 ? now.AddDays(-1) : now.AddDays(-days));
var periodEnd = days == -1 ? now : DateTime.UtcNow;
var query = db.Logs.Where(l => l.Time >= periodStart && l.Time < periodEnd);
query = ApplySearchFilter(db, query, search);
var deleted = await query.ExecuteDeleteAsync();
return new JsonResult(new { success = true, deleted });
}
[HttpGet("api/export")]
public async Task<IActionResult> ExportData()
{
await using var db = await _contextFactory.CreateDbContextAsync();
var result = await db.Logs.AsNoTracking()
.OrderByDescending(l => l.Id)
.Take(50000)
.GroupJoin(db.Users, l => l.UnfoId, u => u.Id, (l, users) => new { Log = l, Users = users })
.SelectMany(x => x.Users.DefaultIfEmpty(), (x, u) => new
{
time = x.Log.Time,
uri = x.Log.Uri,
uid = x.Log.Uid,
ip = u != null ? u.Ip : "",
userAgent = u != null ? u.UserAgent : "",
balancer = x.Log.Balancer,
statusCode = x.Log.StatusCode
})
.ToListAsync();
return new JsonResult(result);
}
}