using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Core.Exporting; internal class ExportContext { private readonly MediaDownloader _mediaDownloader; public ExportRequest Request { get; } public IReadOnlyCollection Members { get; } public IReadOnlyCollection Channels { get; } public IReadOnlyCollection Roles { get; } public ExportContext( ExportRequest request, IReadOnlyCollection members, IReadOnlyCollection channels, IReadOnlyCollection roles) { Request = request; Members = members; Channels = channels; Roles = roles; _mediaDownloader = new MediaDownloader(request.OutputMediaDirPath, request.ShouldReuseMedia); } public string FormatDate(DateTimeOffset date) => Request.DateFormat switch { "unix" => date.ToUnixTimeSeconds().ToString(), "unixms" => date.ToUnixTimeMilliseconds().ToString(), var dateFormat => date.ToLocalString(dateFormat) }; public Member? TryGetMember(Snowflake id) => Members.FirstOrDefault(m => m.Id == id); public Channel? TryGetChannel(Snowflake id) => Channels.FirstOrDefault(c => c.Id == id); public Role? TryGetRole(Snowflake id) => Roles.FirstOrDefault(r => r.Id == id); public Color? TryGetUserColor(Snowflake id) { var member = TryGetMember(id); var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role); return roles? .Where(r => r.Color is not null) .OrderByDescending(r => r.Position) .Select(r => r.Color) .FirstOrDefault(); } public async ValueTask ResolveMediaUrlAsync(string url, CancellationToken cancellationToken = default) { if (!Request.ShouldDownloadMedia) return url; try { var filePath = await _mediaDownloader.DownloadAsync(url, cancellationToken); // We want relative path so that the output files can be copied around without breaking. // Base directory path may be null if the file is stored at the root or relative to working directory. var relativeFilePath = !string.IsNullOrWhiteSpace(Request.OutputBaseDirPath) ? Path.GetRelativePath(Request.OutputBaseDirPath, filePath) : filePath; // HACK: for HTML, we need to format the URL properly if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight) { // Need to escape each path segment while keeping the directory separators intact return string.Join( Path.AltDirectorySeparatorChar, relativeFilePath .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) .Select(Uri.EscapeDataString) ); } return relativeFilePath; } // Try to catch only exceptions related to failed HTTP requests // https://github.com/Tyrrrz/DiscordChatExporter/issues/332 // https://github.com/Tyrrrz/DiscordChatExporter/issues/372 catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException) { // TODO: add logging so we can be more liberal with catching exceptions // We don't want this to crash the exporting process in case of failure return url; } } }