mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-31 23:08:23 -04:00
Improve performance (#162)
This commit is contained in:
parent
359278afec
commit
4bfb2ec7fd
86 changed files with 1242 additions and 900 deletions
|
@ -1,31 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\PlainText\Template.txt" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlDark\Template.html" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlLight\Template.html" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlShared\Main.html" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlShared\Main.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlDark\Theme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlLight\Theme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\Csv\Template.csv" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Failsafe" Version="1.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||
<PackageReference Include="Onova" Version="2.4.2" />
|
||||
<PackageReference Include="Scriban" Version="2.0.0" />
|
||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.1" />
|
||||
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordChatExporter.Core.Markdown\DiscordChatExporter.Core.Markdown.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,19 +0,0 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exceptions
|
||||
{
|
||||
public class HttpErrorStatusCodeException : Exception
|
||||
{
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
|
||||
public string ReasonPhrase { get; }
|
||||
|
||||
public HttpErrorStatusCodeException(HttpStatusCode statusCode, string reasonPhrase)
|
||||
: base($"Error HTTP status code: {statusCode} - {reasonPhrase}")
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
ReasonPhrase = reasonPhrase;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Helpers
|
||||
{
|
||||
public static class ExportHelper
|
||||
{
|
||||
public static bool IsDirectoryPath(string path)
|
||||
=> path.Last() == Path.DirectorySeparatorChar ||
|
||||
path.Last() == Path.AltDirectorySeparatorChar ||
|
||||
Path.GetExtension(path).IsBlank();
|
||||
|
||||
public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel,
|
||||
DateTime? from = null, DateTime? to = null)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
|
||||
// Append guild and channel names
|
||||
result.Append($"{guild.Name} - {channel.Name} [{channel.Id}]");
|
||||
|
||||
// Append date range
|
||||
if (from != null || to != null)
|
||||
{
|
||||
result.Append(" (");
|
||||
|
||||
// Both 'from' and 'to' are set
|
||||
if (from != null && to != null)
|
||||
{
|
||||
result.Append($"{from:yyyy-MM-dd} to {to:yyyy-MM-dd}");
|
||||
}
|
||||
// Only 'from' is set
|
||||
else if (from != null)
|
||||
{
|
||||
result.Append($"after {from:yyyy-MM-dd}");
|
||||
}
|
||||
// Only 'to' is set
|
||||
else
|
||||
{
|
||||
result.Append($"before {to:yyyy-MM-dd}");
|
||||
}
|
||||
|
||||
result.Append(")");
|
||||
}
|
||||
|
||||
// Append extension
|
||||
result.Append($".{format.GetFileExtension()}");
|
||||
|
||||
// Replace invalid chars
|
||||
foreach (var invalidChar in Path.GetInvalidFileNameChars())
|
||||
result.Replace(invalidChar, '_');
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
namespace DiscordChatExporter.Core.Internal
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
public static string ToSnowflake(this DateTime dateTime)
|
||||
{
|
||||
const long epoch = 62135596800000;
|
||||
var unixTime = dateTime.ToUniversalTime().Ticks / TimeSpan.TicksPerMillisecond - epoch;
|
||||
var value = ((ulong) unixTime - 1420070400000UL) << 22;
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
public static Color ResetAlpha(this Color color) => Color.FromArgb(1, color);
|
||||
|
||||
public static string HtmlEncode(this string value) => WebUtility.HtmlEncode(value);
|
||||
|
||||
public static IEnumerable<IReadOnlyList<T>> GroupAdjacentWhile<T>(this IEnumerable<T> source,
|
||||
Func<IReadOnlyList<T>, T, bool> groupPredicate)
|
||||
{
|
||||
// Create buffer
|
||||
var buffer = new List<T>();
|
||||
|
||||
// Enumerate source
|
||||
foreach (var element in source)
|
||||
{
|
||||
// If buffer is not empty and group predicate failed - yield and flush buffer
|
||||
if (buffer.Any() && !groupPredicate(buffer, element))
|
||||
{
|
||||
yield return buffer;
|
||||
buffer = new List<T>(); // new instance to reset reference
|
||||
}
|
||||
|
||||
// Add element to buffer
|
||||
buffer.Add(element);
|
||||
}
|
||||
|
||||
// If buffer still has something after the source has been enumerated - yield
|
||||
if (buffer.Any())
|
||||
yield return buffer;
|
||||
}
|
||||
|
||||
public static IEnumerable<IReadOnlyList<T>> GroupAdjacentWhile<T>(this IEnumerable<T> source,
|
||||
Func<IReadOnlyList<T>, bool> groupPredicate)
|
||||
=> source.GroupAdjacentWhile((buffer, _) => groupPredicate(buffer));
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#attachment-object
|
||||
|
||||
public class Attachment
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public int? Width { get; }
|
||||
|
||||
public int? Height { get; }
|
||||
|
||||
public string FileName { get; }
|
||||
|
||||
public bool IsImage => FileName.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) ||
|
||||
FileName.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) ||
|
||||
FileName.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ||
|
||||
FileName.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) ||
|
||||
FileName.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public FileSize FileSize { get; }
|
||||
|
||||
public Attachment(string id, int? width, int? height, string url, string fileName, FileSize fileSize)
|
||||
{
|
||||
Id = id;
|
||||
Url = url;
|
||||
Width = width;
|
||||
Height = height;
|
||||
FileName = fileName;
|
||||
FileSize = fileSize;
|
||||
}
|
||||
|
||||
public override string ToString() => FileName;
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class AuthToken
|
||||
{
|
||||
public AuthTokenType Type { get; }
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public AuthToken(AuthTokenType type, string value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public enum AuthTokenType
|
||||
{
|
||||
User,
|
||||
Bot
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#channel-object
|
||||
|
||||
public partial class Channel
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public string ParentId { get; }
|
||||
|
||||
public string GuildId { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string Topic { get; }
|
||||
|
||||
public ChannelType Type { get; }
|
||||
|
||||
public Channel(string id, string parentId, string guildId, string name, string topic, ChannelType type)
|
||||
{
|
||||
Id = id;
|
||||
ParentId = parentId;
|
||||
GuildId = guildId;
|
||||
Name = name;
|
||||
Topic = topic;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public partial class Channel
|
||||
{
|
||||
public static Channel CreateDeletedChannel(string id) =>
|
||||
new Channel(id, null, null, "deleted-channel", null, ChannelType.GuildTextChat);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
|
||||
public enum ChannelType
|
||||
{
|
||||
GuildTextChat,
|
||||
DirectTextChat,
|
||||
GuildVoiceChat,
|
||||
DirectGroupTextChat,
|
||||
Category
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class ChatLog
|
||||
{
|
||||
public Guild Guild { get; }
|
||||
|
||||
public Channel Channel { get; }
|
||||
|
||||
public DateTime? From { get; }
|
||||
|
||||
public DateTime? To { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public Mentionables Mentionables { get; }
|
||||
|
||||
public ChatLog(Guild guild, Channel channel, DateTime? from, DateTime? to,
|
||||
IReadOnlyList<Message> messages, Mentionables mentionables)
|
||||
{
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
From = from;
|
||||
To = to;
|
||||
Messages = messages;
|
||||
Mentionables = mentionables;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Guild.Name} | {Channel.Name}";
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object
|
||||
|
||||
public class Embed
|
||||
{
|
||||
public string Title { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public DateTime? Timestamp { get; }
|
||||
|
||||
public Color Color { get; }
|
||||
|
||||
public EmbedAuthor Author { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public IReadOnlyList<EmbedField> Fields { get; }
|
||||
|
||||
public EmbedImage Thumbnail { get; }
|
||||
|
||||
public EmbedImage Image { get; }
|
||||
|
||||
public EmbedFooter Footer { get; }
|
||||
|
||||
public Embed(string title, string url, DateTime? timestamp, Color color, EmbedAuthor author, string description,
|
||||
IReadOnlyList<EmbedField> fields, EmbedImage thumbnail, EmbedImage image, EmbedFooter footer)
|
||||
{
|
||||
Title = title;
|
||||
Url = url;
|
||||
Timestamp = timestamp;
|
||||
Color = color;
|
||||
Author = author;
|
||||
Description = description;
|
||||
Fields = fields;
|
||||
Thumbnail = thumbnail;
|
||||
Image = image;
|
||||
Footer = footer;
|
||||
}
|
||||
|
||||
public override string ToString() => Title;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
||||
|
||||
public class EmbedAuthor
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public string IconUrl { get; }
|
||||
|
||||
public EmbedAuthor(string name, string url, string iconUrl)
|
||||
{
|
||||
Name = name;
|
||||
Url = url;
|
||||
IconUrl = iconUrl;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
||||
|
||||
public class EmbedField
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public bool IsInline { get; }
|
||||
|
||||
public EmbedField(string name, string value, bool isInline)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
IsInline = isInline;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Name} | {Value}";
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
||||
|
||||
public class EmbedFooter
|
||||
{
|
||||
public string Text { get; }
|
||||
|
||||
public string IconUrl { get; }
|
||||
|
||||
public EmbedFooter(string text, string iconUrl)
|
||||
{
|
||||
Text = text;
|
||||
IconUrl = iconUrl;
|
||||
}
|
||||
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
||||
|
||||
public class EmbedImage
|
||||
{
|
||||
public string Url { get; }
|
||||
|
||||
public int? Width { get; }
|
||||
|
||||
public int? Height { get; }
|
||||
|
||||
public EmbedImage(string url, int? width, int? height)
|
||||
{
|
||||
Url = url;
|
||||
Height = height;
|
||||
Width = width;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/emoji#emoji-object
|
||||
|
||||
public partial class Emoji
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool IsAnimated { get; }
|
||||
|
||||
public string ImageUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
// Custom emoji
|
||||
if (Id.IsNotBlank())
|
||||
{
|
||||
// Animated
|
||||
if (IsAnimated)
|
||||
return $"https://cdn.discordapp.com/emojis/{Id}.gif";
|
||||
|
||||
// Non-animated
|
||||
return $"https://cdn.discordapp.com/emojis/{Id}.png";
|
||||
}
|
||||
|
||||
// Standard unicode emoji (via twemoji)
|
||||
return $"https://twemoji.maxcdn.com/2/72x72/{GetTwemojiName(Name)}.png";
|
||||
}
|
||||
}
|
||||
|
||||
public Emoji(string id, string name, bool isAnimated)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
IsAnimated = isAnimated;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Emoji
|
||||
{
|
||||
private static IEnumerable<int> GetCodePoints(string emoji)
|
||||
{
|
||||
for (var i = 0; i < emoji.Length; i += char.IsHighSurrogate(emoji[i]) ? 2 : 1)
|
||||
yield return char.ConvertToUtf32(emoji, i);
|
||||
}
|
||||
|
||||
private static string GetTwemojiName(string emoji)
|
||||
=> GetCodePoints(emoji).Select(i => i.ToString("x")).JoinToString("-");
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public enum ExportFormat
|
||||
{
|
||||
PlainText,
|
||||
HtmlDark,
|
||||
HtmlLight,
|
||||
Csv
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetFileExtension(this ExportFormat format)
|
||||
{
|
||||
if (format == ExportFormat.PlainText)
|
||||
return "txt";
|
||||
if (format == ExportFormat.HtmlDark)
|
||||
return "html";
|
||||
if (format == ExportFormat.HtmlLight)
|
||||
return "html";
|
||||
if (format == ExportFormat.Csv)
|
||||
return "csv";
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(format));
|
||||
}
|
||||
|
||||
public static string GetDisplayName(this ExportFormat format)
|
||||
{
|
||||
if (format == ExportFormat.PlainText)
|
||||
return "Plain Text";
|
||||
if (format == ExportFormat.HtmlDark)
|
||||
return "HTML (Dark)";
|
||||
if (format == ExportFormat.HtmlLight)
|
||||
return "HTML (Light)";
|
||||
if (format == ExportFormat.Csv)
|
||||
return "Comma Seperated Values (CSV)";
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(format));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// Loosely based on https://github.com/omar/ByteSize (MIT license)
|
||||
|
||||
public struct FileSize
|
||||
{
|
||||
public const long BytesInKiloByte = 1024;
|
||||
public const long BytesInMegaByte = 1024 * BytesInKiloByte;
|
||||
public const long BytesInGigaByte = 1024 * BytesInMegaByte;
|
||||
public const long BytesInTeraByte = 1024 * BytesInGigaByte;
|
||||
public const long BytesInPetaByte = 1024 * BytesInTeraByte;
|
||||
|
||||
public const string ByteSymbol = "B";
|
||||
public const string KiloByteSymbol = "KB";
|
||||
public const string MegaByteSymbol = "MB";
|
||||
public const string GigaByteSymbol = "GB";
|
||||
public const string TeraByteSymbol = "TB";
|
||||
public const string PetaByteSymbol = "PB";
|
||||
|
||||
public double Bytes { get; }
|
||||
public double KiloBytes => Bytes / BytesInKiloByte;
|
||||
public double MegaBytes => Bytes / BytesInMegaByte;
|
||||
public double GigaBytes => Bytes / BytesInGigaByte;
|
||||
public double TeraBytes => Bytes / BytesInTeraByte;
|
||||
public double PetaBytes => Bytes / BytesInPetaByte;
|
||||
|
||||
public string LargestWholeNumberSymbol
|
||||
{
|
||||
get
|
||||
{
|
||||
// Absolute value is used to deal with negative values
|
||||
if (Math.Abs(PetaBytes) >= 1)
|
||||
return PetaByteSymbol;
|
||||
|
||||
if (Math.Abs(TeraBytes) >= 1)
|
||||
return TeraByteSymbol;
|
||||
|
||||
if (Math.Abs(GigaBytes) >= 1)
|
||||
return GigaByteSymbol;
|
||||
|
||||
if (Math.Abs(MegaBytes) >= 1)
|
||||
return MegaByteSymbol;
|
||||
|
||||
if (Math.Abs(KiloBytes) >= 1)
|
||||
return KiloByteSymbol;
|
||||
|
||||
return ByteSymbol;
|
||||
}
|
||||
}
|
||||
|
||||
public double LargestWholeNumberValue
|
||||
{
|
||||
get
|
||||
{
|
||||
// Absolute value is used to deal with negative values
|
||||
if (Math.Abs(PetaBytes) >= 1)
|
||||
return PetaBytes;
|
||||
|
||||
if (Math.Abs(TeraBytes) >= 1)
|
||||
return TeraBytes;
|
||||
|
||||
if (Math.Abs(GigaBytes) >= 1)
|
||||
return GigaBytes;
|
||||
|
||||
if (Math.Abs(MegaBytes) >= 1)
|
||||
return MegaBytes;
|
||||
|
||||
if (Math.Abs(KiloBytes) >= 1)
|
||||
return KiloBytes;
|
||||
|
||||
return Bytes;
|
||||
}
|
||||
}
|
||||
|
||||
public FileSize(double bytes)
|
||||
{
|
||||
Bytes = bytes;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{LargestWholeNumberValue:0.##} {LargestWholeNumberSymbol}";
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/guild#guild-object
|
||||
|
||||
public partial class Guild
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string IconHash { get; }
|
||||
|
||||
public string IconUrl => IconHash.IsNotBlank()
|
||||
? $"https://cdn.discordapp.com/icons/{Id}/{IconHash}.png"
|
||||
: "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
|
||||
public Guild(string id, string name, string iconHash)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
IconHash = iconHash;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public partial class Guild
|
||||
{
|
||||
public static Guild DirectMessages { get; } = new Guild("@me", "Direct Messages", null);
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class Mentionables
|
||||
{
|
||||
public IReadOnlyList<User> Users { get; }
|
||||
|
||||
public IReadOnlyList<Channel> Channels { get; }
|
||||
|
||||
public IReadOnlyList<Role> Roles { get; }
|
||||
|
||||
public Mentionables(IReadOnlyList<User> users, IReadOnlyList<Channel> channels, IReadOnlyList<Role> roles)
|
||||
{
|
||||
Users = users;
|
||||
Channels = channels;
|
||||
Roles = roles;
|
||||
}
|
||||
|
||||
public User GetUser(string id) =>
|
||||
Users.FirstOrDefault(u => u.Id == id) ?? User.CreateUnknownUser(id);
|
||||
|
||||
public Channel GetChannel(string id) =>
|
||||
Channels.FirstOrDefault(c => c.Id == id) ?? Channel.CreateDeletedChannel(id);
|
||||
|
||||
public Role GetRole(string id) =>
|
||||
Roles.FirstOrDefault(r => r.Id == id) ?? Role.CreateDeletedRole(id);
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#message-object
|
||||
|
||||
public class Message
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public string ChannelId { get; }
|
||||
|
||||
public MessageType Type { get; }
|
||||
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public DateTime? EditedTimestamp { get; }
|
||||
|
||||
public string Content { get; }
|
||||
|
||||
public IReadOnlyList<Attachment> Attachments { get; }
|
||||
|
||||
public IReadOnlyList<Embed> Embeds { get; }
|
||||
|
||||
public IReadOnlyList<Reaction> Reactions { get; }
|
||||
|
||||
public IReadOnlyList<User> MentionedUsers { get; }
|
||||
|
||||
public Message(string id, string channelId, MessageType type, User author, DateTime timestamp,
|
||||
DateTime? editedTimestamp, string content, IReadOnlyList<Attachment> attachments,
|
||||
IReadOnlyList<Embed> embeds, IReadOnlyList<Reaction> reactions, IReadOnlyList<User> mentionedUsers)
|
||||
{
|
||||
Id = id;
|
||||
ChannelId = channelId;
|
||||
Type = type;
|
||||
Author = author;
|
||||
Timestamp = timestamp;
|
||||
EditedTimestamp = editedTimestamp;
|
||||
Content = content;
|
||||
Attachments = attachments;
|
||||
Embeds = embeds;
|
||||
Reactions = reactions;
|
||||
MentionedUsers = mentionedUsers;
|
||||
}
|
||||
|
||||
public override string ToString() => Content;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#message-object-message-types
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
Default,
|
||||
RecipientAdd,
|
||||
RecipientRemove,
|
||||
Call,
|
||||
ChannelNameChange,
|
||||
ChannelIconChange,
|
||||
ChannelPinnedMessage,
|
||||
GuildMemberJoin
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#reaction-object
|
||||
|
||||
public class Reaction
|
||||
{
|
||||
public int Count { get; }
|
||||
|
||||
public Emoji Emoji { get; }
|
||||
|
||||
public Reaction(int count, Emoji emoji)
|
||||
{
|
||||
Count = count;
|
||||
Emoji = emoji;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/topics/permissions#role-object
|
||||
|
||||
public partial class Role
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Role(string id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public partial class Role
|
||||
{
|
||||
public static Role CreateDeletedRole(string id) =>
|
||||
new Role(id, "deleted-role");
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
using System;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/topics/permissions#role-object
|
||||
|
||||
public partial class User
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public int Discriminator { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string FullName => $"{Name}#{Discriminator:0000}";
|
||||
|
||||
public string DefaultAvatarHash => $"{Discriminator % 5}";
|
||||
|
||||
public string AvatarHash { get; }
|
||||
|
||||
public bool IsAvatarAnimated =>
|
||||
AvatarHash.IsNotBlank() && AvatarHash.StartsWith("a_", StringComparison.Ordinal);
|
||||
|
||||
public string AvatarUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
// Custom avatar
|
||||
if (AvatarHash.IsNotBlank())
|
||||
{
|
||||
// Animated
|
||||
if (IsAvatarAnimated)
|
||||
return $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.gif";
|
||||
|
||||
// Non-animated
|
||||
return $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png";
|
||||
}
|
||||
|
||||
// Default avatar
|
||||
return $"https://cdn.discordapp.com/embed/avatars/{DefaultAvatarHash}.png";
|
||||
}
|
||||
}
|
||||
|
||||
public User(string id, int discriminator, string name, string avatarHash)
|
||||
{
|
||||
Id = id;
|
||||
Discriminator = discriminator;
|
||||
Name = name;
|
||||
AvatarHash = avatarHash;
|
||||
}
|
||||
|
||||
public override string ToString() => FullName;
|
||||
}
|
||||
|
||||
public partial class User
|
||||
{
|
||||
public static User CreateUnknownUser(string id) =>
|
||||
new User(id, 0, "Unknown", null);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
Author;Date;Content;Attachments;
|
||||
{{~ for message in Model.Messages -}}
|
||||
{{- }}"{{ message.Author.FullName }}";
|
||||
|
||||
{{- }}"{{ message.Timestamp | FormatDate }}";
|
||||
|
||||
{{- }}"{{ message.Content | FormatMarkdown | string.replace "\"" "\"\"" }}";
|
||||
|
||||
{{- }}"{{ message.Attachments | array.map "Url" | array.join "," }}";
|
||||
{{~ end -}}
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
@ -1,3 +0,0 @@
|
|||
{{~ ThemeStyleSheet = include "HtmlDark.Theme.css" ~}}
|
||||
{{~ HighlightJsStyleName = "solarized-dark" ~}}
|
||||
{{~ include "HtmlShared.Main.html" ~}}
|
|
@ -1,100 +0,0 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
background-color: #36393e;
|
||||
color: #dcddde;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096cf;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.pre {
|
||||
background-color: #2f3136 !important;
|
||||
}
|
||||
|
||||
.pre--multiline {
|
||||
border-color: #282b30 !important;
|
||||
color: #839496 !important;
|
||||
}
|
||||
|
||||
.mention {
|
||||
color: #7289da;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info__guild-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog__message-group {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
background-color: rgba(46, 48, 54, 0.3);
|
||||
border-color: rgba(46, 48, 54, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name-link {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__reaction {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.chatlog__reaction-count {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{{~ ThemeStyleSheet = include "HtmlLight.Theme.css" ~}}
|
||||
{{~ HighlightJsStyleName = "solarized-light" ~}}
|
||||
{{~ include "HtmlShared.Main.html" ~}}
|
|
@ -1,101 +0,0 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
color: #747f8d;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00b0f4;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pre {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.pre--multiline {
|
||||
border-color: #f3f3f3 !important;
|
||||
color: #657b83 !important;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: #f1f3fb;
|
||||
color: #7289da;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info__guild-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog__message-group {
|
||||
border-color: #eceeef;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
color: #99aab5;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
color: #99aab5;
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
background-color: rgba(249, 249, 249, 0.3);
|
||||
border-color: rgba(204, 204, 204, 0.3);
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name-link {
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
color: #737f8d;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
color: #36393e;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
color: #737f8d;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
color: rgba(79, 83, 91, 0.4);
|
||||
}
|
||||
|
||||
.chatlog__reaction {
|
||||
background-color: rgba(79, 84, 92, 0.06);
|
||||
}
|
||||
|
||||
.chatlog__reaction-count {
|
||||
color: #99aab5;
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
font-family: "Whitney", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.3;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pre {
|
||||
font-family: "Consolas", "Courier New", Courier, Monospace;
|
||||
}
|
||||
|
||||
.pre--multiline {
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
border: 2px solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.pre--inline {
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mention {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1.45em;
|
||||
height: 1.45em;
|
||||
margin: 0 1px;
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
|
||||
.emoji--small {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.emoji--large {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
margin: 0 5px 10px 5px;
|
||||
}
|
||||
|
||||
.info__guild-icon-container {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.info__guild-icon {
|
||||
max-width: 88px;
|
||||
max-height: 88px;
|
||||
}
|
||||
|
||||
.info__metadata {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.info__guild-name {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info__channel-message-count {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info__channel-date-range {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog {
|
||||
max-width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chatlog__message-group {
|
||||
display: flex;
|
||||
margin: 0 10px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.chatlog__author-avatar-container {
|
||||
flex: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.chatlog__author-avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.chatlog__messages {
|
||||
flex: 1;
|
||||
min-width: 50%;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
margin-left: 5px;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
.chatlog__content {
|
||||
padding-top: 5px;
|
||||
font-size: .9375em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
margin-left: 3px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.chatlog__attachment {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.chatlog__attachment-thumbnail {
|
||||
max-width: 50%;
|
||||
max-height: 500px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed {
|
||||
display: flex;
|
||||
max-width: 520px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chatlog__embed-color-pill {
|
||||
flex-shrink: 0;
|
||||
width: 4px;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatlog__embed-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chatlog__embed-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 9px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chatlog__embed-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chatlog__embed-field {
|
||||
flex: 0;
|
||||
min-width: 100%;
|
||||
max-width: 506px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-field--inline {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
margin-bottom: 4px;
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
font-size: .875em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chatlog__embed-thumbnail {
|
||||
flex: 0;
|
||||
margin-left: 20px;
|
||||
max-width: 80px;
|
||||
max-height: 80px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-image-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-image {
|
||||
max-width: 500px;
|
||||
max-height: 400px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer-icon {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer-text {
|
||||
font-weight: 600;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
.chatlog__reactions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatlog__reaction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 6px 2px 2px 2px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__reaction-count {
|
||||
min-width: 9px;
|
||||
margin-left: 6px;
|
||||
font-size: .875em;
|
||||
}
|
|
@ -1,225 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
{{~ # Metadata ~}}
|
||||
<title>{{ Model.Guild.Name | html.escape }} - {{ Model.Channel.Name | html.escape }}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
{{~ # Styles ~}}
|
||||
<style>
|
||||
{{ include "HtmlShared.Main.css" }}
|
||||
</style>
|
||||
<style>
|
||||
{{ ThemeStyleSheet }}
|
||||
</style>
|
||||
|
||||
{{~ # Syntax highlighting ~}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/{{HighlightJsStyleName}}.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.pre--multiline').forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{~ # Info ~}}
|
||||
<div class="info">
|
||||
<div class="info__guild-icon-container">
|
||||
<img class="info__guild-icon" src="{{ Model.Guild.IconUrl }}" />
|
||||
</div>
|
||||
<div class="info__metadata">
|
||||
<div class="info__guild-name">{{ Model.Guild.Name | html.escape }}</div>
|
||||
<div class="info__channel-name">{{ Model.Channel.Name | html.escape }}</div>
|
||||
|
||||
{{~ if Model.Channel.Topic ~}}
|
||||
<div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
|
||||
{{~ end ~}}
|
||||
|
||||
<div class="info__channel-message-count">{{ Model.Messages | array.size | Format "N0" }} messages</div>
|
||||
|
||||
{{~ if Model.From || Model.To ~}}
|
||||
<div class="info__channel-date-range">
|
||||
{{~ if Model.From && Model.To ~}}
|
||||
Between {{ Model.From | FormatDate | html.escape }} and {{ Model.To | FormatDate | html.escape }}
|
||||
{{~ else if Model.From ~}}
|
||||
After {{ Model.From | FormatDate | html.escape }}
|
||||
{{~ else if Model.To ~}}
|
||||
Before {{ Model.To | FormatDate | html.escape }}
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{~ # Log ~}}
|
||||
<div class="chatlog">
|
||||
{{~ for group in Model.Messages | GroupMessages ~}}
|
||||
<div class="chatlog__message-group">
|
||||
{{~ # Avatar ~}}
|
||||
<div class="chatlog__author-avatar-container">
|
||||
<img class="chatlog__author-avatar" src="{{ group.Author.AvatarUrl }}" />
|
||||
</div>
|
||||
<div class="chatlog__messages">
|
||||
{{~ # Author name and timestamp ~}}
|
||||
<span class="chatlog__author-name" title="{{ group.Author.FullName | html.escape }}">{{ group.Author.Name | html.escape }}</span>
|
||||
<span class="chatlog__timestamp">{{ group.Timestamp | FormatDate | html.escape }}</span>
|
||||
|
||||
{{~ # Messages ~}}
|
||||
{{~ for message in group.Messages ~}}
|
||||
{{~ # Content ~}}
|
||||
{{~ if message.Content ~}}
|
||||
<div class="chatlog__content">
|
||||
<span class="markdown">{{ message.Content | FormatMarkdown }}</span>
|
||||
|
||||
{{~ # Edited timestamp ~}}
|
||||
{{~ if message.EditedTimestamp ~}}
|
||||
<span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | html.escape }}">(edited)</span>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Attachments ~}}
|
||||
{{~ for attachment in message.Attachments ~}}
|
||||
<div class="chatlog__attachment">
|
||||
<a href="{{ attachment.Url }}">
|
||||
{{ # Image }}
|
||||
{{~ if attachment.IsImage ~}}
|
||||
<img class="chatlog__attachment-thumbnail" src="{{ attachment.Url }}" />
|
||||
{{~ # Non-image ~}}
|
||||
{{~ else ~}}
|
||||
Attachment: {{ attachment.FileName }} ({{ attachment.FileSize }})
|
||||
{{~ end ~}}
|
||||
</a>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Embeds ~}}
|
||||
{{~ for embed in message.Embeds ~}}
|
||||
<div class="chatlog__embed">
|
||||
<div class="chatlog__embed-color-pill" style="background-color: rgba({{ embed.Color.R }},{{ embed.Color.G }},{{ embed.Color.B }},{{ embed.Color.A }})"></div>
|
||||
<div class="chatlog__embed-content-container">
|
||||
<div class="chatlog__embed-content">
|
||||
<div class="chatlog__embed-text">
|
||||
{{~ # Author ~}}
|
||||
{{~ if embed.Author ~}}
|
||||
<div class="chatlog__embed-author">
|
||||
{{~ if embed.Author.IconUrl ~}}
|
||||
<img class="chatlog__embed-author-icon" src="{{ embed.Author.IconUrl }}" />
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ if embed.Author.Name ~}}
|
||||
<span class="chatlog__embed-author-name">
|
||||
{{~ if embed.Author.Url ~}}
|
||||
<a class="chatlog__embed-author-name-link" href="{{ embed.Author.Url }}">{{ embed.Author.Name | html.escape }}</a>
|
||||
{{~ else ~}}
|
||||
{{ embed.Author.Name | html.escape }}
|
||||
{{~ end ~}}
|
||||
</span>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Title ~}}
|
||||
{{~ if embed.Title ~}}
|
||||
<div class="chatlog__embed-title">
|
||||
{{~ if embed.Url ~}}
|
||||
<a class="chatlog__embed-title-link" href="{{ embed.Url }}"><span class="markdown">{{ embed.Title | FormatMarkdown }}</span></a>
|
||||
{{~ else ~}}
|
||||
<span class="markdown">{{ embed.Title | FormatMarkdown }}</span>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Description ~}}
|
||||
{{~ if embed.Description ~}}
|
||||
<div class="chatlog__embed-description"><span class="markdown">{{ embed.Description | FormatMarkdown }}</span></div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Fields ~}}
|
||||
{{~ if embed.Fields | array.size > 0 ~}}
|
||||
<div class="chatlog__embed-fields">
|
||||
{{~ for field in embed.Fields ~}}
|
||||
<div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}">
|
||||
{{~ if field.Name ~}}
|
||||
<div class="chatlog__embed-field-name"><span class="markdown">{{ field.Name | FormatMarkdown }}</span></div>
|
||||
{{~ end ~}}
|
||||
{{~ if field.Value ~}}
|
||||
<div class="chatlog__embed-field-value"><span class="markdown">{{ field.Value | FormatMarkdown }}</span></div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
|
||||
{{~ # Thumbnail ~}}
|
||||
{{~ if embed.Thumbnail ~}}
|
||||
<div class="chatlog__embed-thumbnail-container">
|
||||
<a class="chatlog__embed-thumbnail-link" href="{{ embed.Thumbnail.Url }}">
|
||||
<img class="chatlog__embed-thumbnail" src="{{ embed.Thumbnail.Url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
|
||||
{{~ # Image ~}}
|
||||
{{~ if embed.Image ~}}
|
||||
<div class="chatlog__embed-image-container">
|
||||
<a class="chatlog__embed-image-link" href="{{ embed.Image.Url }}">
|
||||
<img class="chatlog__embed-image" src="{{ embed.Image.Url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Footer ~}}
|
||||
{{~ if embed.Footer || embed.Timestamp ~}}
|
||||
<div class="chatlog__embed-footer">
|
||||
{{~ if embed.Footer ~}}
|
||||
{{~ if embed.Footer.Text && embed.Footer.IconUrl ~}}
|
||||
<img class="chatlog__embed-footer-icon" src="{{ embed.Footer.IconUrl }}" />
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
|
||||
<span class="chatlog__embed-footer-text">
|
||||
{{~ if embed.Footer ~}}
|
||||
{{~ if embed.Footer.Text ~}}
|
||||
{{ embed.Footer.Text | html.escape }}
|
||||
{{ if embed.Timestamp }} • {{ end }}
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ if embed.Timestamp ~}}
|
||||
{{ embed.Timestamp | FormatDate | html.escape }}
|
||||
{{~ end ~}}
|
||||
</span>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Reactions ~}}
|
||||
{{~ if message.Reactions | array.size > 0 ~}}
|
||||
<div class="chatlog__reactions">
|
||||
{{~ for reaction in message.Reactions ~}}
|
||||
<div class="chatlog__reaction">
|
||||
<img class="emoji emoji--small" alt="{{ reaction.Emoji.Name }}" title="{{ reaction.Emoji.Name }}" src="{{ reaction.Emoji.ImageUrl }}" />
|
||||
<span class="chatlog__reaction-count">{{ reaction.Count }}</span>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,21 +0,0 @@
|
|||
{{~ # Info ~}}
|
||||
==============================================================
|
||||
Guild: {{ Model.Guild.Name }}
|
||||
Channel: {{ Model.Channel.Name }}
|
||||
Topic: {{ Model.Channel.Topic }}
|
||||
Messages: {{ Model.Messages | array.size | Format "N0" }}
|
||||
Range: {{ if Model.From }}{{ Model.From | FormatDate }} {{ end }}{{ if Model.From || Model.To }}->{{ end }}{{ if Model.To }} {{ Model.To | FormatDate }}{{ end }}
|
||||
==============================================================
|
||||
|
||||
{{~ # Log ~}}
|
||||
{{~ for message in Model.Messages ~}}
|
||||
{{~ # Author name and timestamp ~}}
|
||||
{{~ }}[{{ message.Timestamp | FormatDate }}] {{ message.Author.FullName }}
|
||||
{{~ # Content ~}}
|
||||
{{~ message.Content | FormatMarkdown }}
|
||||
{{~ # Attachments ~}}
|
||||
{{~ for attachment in message.Attachments ~}}
|
||||
{{~ attachment.Url }}
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ end ~}}
|
|
@ -1,207 +0,0 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class DataService
|
||||
{
|
||||
private User ParseUser(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var discriminator = json["discriminator"].Value<int>();
|
||||
var name = json["username"].Value<string>();
|
||||
var avatarHash = json["avatar"].Value<string>();
|
||||
|
||||
return new User(id, discriminator, name, avatarHash);
|
||||
}
|
||||
|
||||
private Guild ParseGuild(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var name = json["name"].Value<string>();
|
||||
var iconHash = json["icon"].Value<string>();
|
||||
|
||||
return new Guild(id, name, iconHash);
|
||||
}
|
||||
|
||||
private Channel ParseChannel(JToken json)
|
||||
{
|
||||
// Get basic data
|
||||
var id = json["id"].Value<string>();
|
||||
var parentId = json["parent_id"]?.Value<string>();
|
||||
var type = (ChannelType) json["type"].Value<int>();
|
||||
var topic = json["topic"]?.Value<string>();
|
||||
|
||||
// Try to extract guild ID
|
||||
var guildId = json["guild_id"]?.Value<string>();
|
||||
|
||||
// If the guild ID is blank, it's direct messages
|
||||
if (guildId.IsBlank())
|
||||
guildId = Guild.DirectMessages.Id;
|
||||
|
||||
// Try to extract name
|
||||
var name = json["name"]?.Value<string>();
|
||||
|
||||
// If the name is blank, it's direct messages
|
||||
if (name.IsBlank())
|
||||
name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
|
||||
|
||||
return new Channel(id, parentId, guildId, name, topic, type);
|
||||
}
|
||||
|
||||
private Role ParseRole(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var name = json["name"].Value<string>();
|
||||
|
||||
return new Role(id, name);
|
||||
}
|
||||
|
||||
private Attachment ParseAttachment(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var url = json["url"].Value<string>();
|
||||
var width = json["width"]?.Value<int>();
|
||||
var height = json["height"]?.Value<int>();
|
||||
var fileName = json["filename"].Value<string>();
|
||||
var fileSizeBytes = json["size"].Value<long>();
|
||||
|
||||
var fileSize = new FileSize(fileSizeBytes);
|
||||
|
||||
return new Attachment(id, width, height, url, fileName, fileSize);
|
||||
}
|
||||
|
||||
private EmbedAuthor ParseEmbedAuthor(JToken json)
|
||||
{
|
||||
var name = json["name"]?.Value<string>();
|
||||
var url = json["url"]?.Value<string>();
|
||||
var iconUrl = json["icon_url"]?.Value<string>();
|
||||
|
||||
return new EmbedAuthor(name, url, iconUrl);
|
||||
}
|
||||
|
||||
private EmbedField ParseEmbedField(JToken json)
|
||||
{
|
||||
var name = json["name"].Value<string>();
|
||||
var value = json["value"].Value<string>();
|
||||
var isInline = json["inline"]?.Value<bool>() ?? false;
|
||||
|
||||
return new EmbedField(name, value, isInline);
|
||||
}
|
||||
|
||||
private EmbedImage ParseEmbedImage(JToken json)
|
||||
{
|
||||
var url = json["url"]?.Value<string>();
|
||||
var width = json["width"]?.Value<int>();
|
||||
var height = json["height"]?.Value<int>();
|
||||
|
||||
return new EmbedImage(url, width, height);
|
||||
}
|
||||
|
||||
private EmbedFooter ParseEmbedFooter(JToken json)
|
||||
{
|
||||
var text = json["text"].Value<string>();
|
||||
var iconUrl = json["icon_url"]?.Value<string>();
|
||||
|
||||
return new EmbedFooter(text, iconUrl);
|
||||
}
|
||||
|
||||
private Embed ParseEmbed(JToken json)
|
||||
{
|
||||
// Get basic data
|
||||
var title = json["title"]?.Value<string>();
|
||||
var description = json["description"]?.Value<string>();
|
||||
var url = json["url"]?.Value<string>();
|
||||
var timestamp = json["timestamp"]?.Value<DateTime>();
|
||||
|
||||
// Get color
|
||||
var color = json["color"] != null
|
||||
? Color.FromArgb(json["color"].Value<int>()).ResetAlpha()
|
||||
: Color.FromArgb(79, 84, 92); // default color
|
||||
|
||||
// Get author
|
||||
var author = json["author"] != null ? ParseEmbedAuthor(json["author"]) : null;
|
||||
|
||||
// Get fields
|
||||
var fields = json["fields"].EmptyIfNull().Select(ParseEmbedField).ToArray();
|
||||
|
||||
// Get thumbnail
|
||||
var thumbnail = json["thumbnail"] != null ? ParseEmbedImage(json["thumbnail"]) : null;
|
||||
|
||||
// Get image
|
||||
var image = json["image"] != null ? ParseEmbedImage(json["image"]) : null;
|
||||
|
||||
// Get footer
|
||||
var footer = json["footer"] != null ? ParseEmbedFooter(json["footer"]) : null;
|
||||
|
||||
return new Embed(title, url, timestamp, color, author, description, fields, thumbnail, image, footer);
|
||||
}
|
||||
|
||||
private Emoji ParseEmoji(JToken json)
|
||||
{
|
||||
var id = json["id"]?.Value<string>();
|
||||
var name = json["name"]?.Value<string>();
|
||||
var isAnimated = json["animated"]?.Value<bool>() ?? false;
|
||||
|
||||
return new Emoji(id, name, isAnimated);
|
||||
}
|
||||
|
||||
private Reaction ParseReaction(JToken json)
|
||||
{
|
||||
var count = json["count"].Value<int>();
|
||||
var emoji = ParseEmoji(json["emoji"]);
|
||||
|
||||
return new Reaction(count, emoji);
|
||||
}
|
||||
|
||||
private Message ParseMessage(JToken json)
|
||||
{
|
||||
// Get basic data
|
||||
var id = json["id"].Value<string>();
|
||||
var channelId = json["channel_id"].Value<string>();
|
||||
var timestamp = json["timestamp"].Value<DateTime>();
|
||||
var editedTimestamp = json["edited_timestamp"]?.Value<DateTime?>();
|
||||
var content = json["content"].Value<string>();
|
||||
var type = (MessageType) json["type"].Value<int>();
|
||||
|
||||
// Workarounds for non-default types
|
||||
if (type == MessageType.RecipientAdd)
|
||||
content = "Added a recipient.";
|
||||
else if (type == MessageType.RecipientRemove)
|
||||
content = "Removed a recipient.";
|
||||
else if (type == MessageType.Call)
|
||||
content = "Started a call.";
|
||||
else if (type == MessageType.ChannelNameChange)
|
||||
content = "Changed the channel name.";
|
||||
else if (type == MessageType.ChannelIconChange)
|
||||
content = "Changed the channel icon.";
|
||||
else if (type == MessageType.ChannelPinnedMessage)
|
||||
content = "Pinned a message.";
|
||||
else if (type == MessageType.GuildMemberJoin)
|
||||
content = "Joined the server.";
|
||||
|
||||
// Get author
|
||||
var author = ParseUser(json["author"]);
|
||||
|
||||
// Get attachments
|
||||
var attachments = json["attachments"].EmptyIfNull().Select(ParseAttachment).ToArray();
|
||||
|
||||
// Get embeds
|
||||
var embeds = json["embeds"].EmptyIfNull().Select(ParseEmbed).ToArray();
|
||||
|
||||
// Get reactions
|
||||
var reactions = json["reactions"].EmptyIfNull().Select(ParseReaction).ToArray();
|
||||
|
||||
// Get mentioned users
|
||||
var mentionedUsers = json["mentions"].EmptyIfNull().Select(ParseUser).ToArray();
|
||||
|
||||
return new Message(id, channelId, type, author, timestamp, editedTimestamp, content, attachments, embeds,
|
||||
reactions, mentionedUsers);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Exceptions;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using Failsafe;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class DataService : IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient = new HttpClient();
|
||||
|
||||
private async Task<JToken> GetApiResponseAsync(AuthToken token, string resource, string endpoint,
|
||||
params string[] parameters)
|
||||
{
|
||||
// Create retry policy
|
||||
var retry = Retry.Create()
|
||||
.Catch<HttpErrorStatusCodeException>(false, e => (int) e.StatusCode >= 500)
|
||||
.Catch<HttpErrorStatusCodeException>(false, e => (int) e.StatusCode == 429)
|
||||
.WithMaxTryCount(10)
|
||||
.WithDelay(TimeSpan.FromSeconds(0.4));
|
||||
|
||||
// Send request
|
||||
return await retry.ExecuteAsync(async () =>
|
||||
{
|
||||
// Create request
|
||||
const string apiRoot = "https://discordapp.com/api/v6";
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/{resource}/{endpoint}"))
|
||||
{
|
||||
// Set authorization header
|
||||
request.Headers.Authorization = token.Type == AuthTokenType.Bot
|
||||
? new AuthenticationHeaderValue("Bot", token.Value)
|
||||
: new AuthenticationHeaderValue(token.Value);
|
||||
|
||||
// Add parameters
|
||||
foreach (var parameter in parameters.ExceptBlank())
|
||||
{
|
||||
var key = parameter.SubstringUntil("=");
|
||||
var value = parameter.SubstringAfter("=");
|
||||
|
||||
// Skip empty values
|
||||
if (value.IsBlank())
|
||||
continue;
|
||||
|
||||
request.RequestUri = request.RequestUri.SetQueryParameter(key, value);
|
||||
}
|
||||
|
||||
// Get response
|
||||
using (var response = await _httpClient.SendAsync(request))
|
||||
{
|
||||
// Check status code
|
||||
// We throw our own exception here because default one doesn't have status code
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpErrorStatusCodeException(response.StatusCode, response.ReasonPhrase);
|
||||
|
||||
// Get content
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Parse
|
||||
return JToken.Parse(raw);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Guild> GetGuildAsync(AuthToken token, string guildId)
|
||||
{
|
||||
// Special case for direct messages pseudo-guild
|
||||
if (guildId == Guild.DirectMessages.Id)
|
||||
return Guild.DirectMessages;
|
||||
|
||||
var response = await GetApiResponseAsync(token, "guilds", guildId);
|
||||
var guild = ParseGuild(response);
|
||||
|
||||
return guild;
|
||||
}
|
||||
|
||||
public async Task<Channel> GetChannelAsync(AuthToken token, string channelId)
|
||||
{
|
||||
var response = await GetApiResponseAsync(token, "channels", channelId);
|
||||
var channel = ParseChannel(response);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Guild>> GetUserGuildsAsync(AuthToken token)
|
||||
{
|
||||
var response = await GetApiResponseAsync(token, "users", "@me/guilds", "limit=100");
|
||||
var guilds = response.Select(ParseGuild).ToArray();
|
||||
|
||||
return guilds;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(AuthToken token)
|
||||
{
|
||||
var response = await GetApiResponseAsync(token, "users", "@me/channels");
|
||||
var channels = response.Select(ParseChannel).ToArray();
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(AuthToken token, string guildId)
|
||||
{
|
||||
var response = await GetApiResponseAsync(token, "guilds", $"{guildId}/channels");
|
||||
var channels = response.Select(ParseChannel).ToArray();
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Role>> GetGuildRolesAsync(AuthToken token, string guildId)
|
||||
{
|
||||
var response = await GetApiResponseAsync(token, "guilds", $"{guildId}/roles");
|
||||
var roles = response.Select(ParseRole).ToArray();
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(AuthToken token, string channelId,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||
{
|
||||
var result = new List<Message>();
|
||||
|
||||
// Get the last message
|
||||
var response = await GetApiResponseAsync(token, "channels", $"{channelId}/messages",
|
||||
"limit=1", $"before={to?.ToSnowflake()}");
|
||||
var lastMessage = response.Select(ParseMessage).FirstOrDefault();
|
||||
|
||||
// If the last message doesn't exist or it's outside of range - return
|
||||
if (lastMessage == null || lastMessage.Timestamp < from)
|
||||
{
|
||||
progress?.Report(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get other messages
|
||||
var offsetId = from?.ToSnowflake() ?? "0";
|
||||
while (true)
|
||||
{
|
||||
// Get message batch
|
||||
response = await GetApiResponseAsync(token, "channels", $"{channelId}/messages",
|
||||
"limit=100", $"after={offsetId}");
|
||||
|
||||
// Parse
|
||||
var messages = response
|
||||
.Select(ParseMessage)
|
||||
.Reverse() // reverse because messages appear newest first
|
||||
.ToArray();
|
||||
|
||||
// Break if there are no messages (can happen if messages are deleted during execution)
|
||||
if (!messages.Any())
|
||||
break;
|
||||
|
||||
// Trim messages to range (until last message)
|
||||
var messagesInRange = messages
|
||||
.TakeWhile(m => m.Id != lastMessage.Id && m.Timestamp < lastMessage.Timestamp)
|
||||
.ToArray();
|
||||
|
||||
// Add to result
|
||||
result.AddRange(messagesInRange);
|
||||
|
||||
// Break if messages were trimmed (which means the last message was encountered)
|
||||
if (messagesInRange.Length != messages.Length)
|
||||
break;
|
||||
|
||||
// Report progress (based on the time range of parsed messages compared to total)
|
||||
progress?.Report((result.Last().Timestamp - result.First().Timestamp).TotalSeconds /
|
||||
(lastMessage.Timestamp - result.First().Timestamp).TotalSeconds);
|
||||
|
||||
// Move offset
|
||||
offsetId = result.Last().Id;
|
||||
}
|
||||
|
||||
// Add last message
|
||||
result.Add(lastMessage);
|
||||
|
||||
// Report progress
|
||||
progress?.Report(1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
|
||||
IEnumerable<Message> messages)
|
||||
{
|
||||
// Get channels and roles
|
||||
var channels = guildId != Guild.DirectMessages.Id
|
||||
? await GetGuildChannelsAsync(token, guildId)
|
||||
: Array.Empty<Channel>();
|
||||
var roles = guildId != Guild.DirectMessages.Id
|
||||
? await GetGuildRolesAsync(token, guildId)
|
||||
: Array.Empty<Role>();
|
||||
|
||||
// Get users
|
||||
var userMap = new Dictionary<string, User>();
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// Author
|
||||
userMap[message.Author.Id] = message.Author;
|
||||
|
||||
// Mentioned users
|
||||
foreach (var mentionedUser in message.MentionedUsers)
|
||||
userMap[mentionedUser.Id] = mentionedUser;
|
||||
}
|
||||
|
||||
var users = userMap.Values.ToArray();
|
||||
|
||||
return new Mentionables(users, channels, roles);
|
||||
}
|
||||
|
||||
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||
{
|
||||
// Get messages
|
||||
var messages = await GetChannelMessagesAsync(token, channel.Id, from, to, progress);
|
||||
|
||||
// Get mentionables
|
||||
var mentionables = await GetMentionablesAsync(token, guild.Id, messages);
|
||||
|
||||
return new ChatLog(guild, channel, from, to, messages, mentionables);
|
||||
}
|
||||
|
||||
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Channel channel,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||
{
|
||||
// Get guild
|
||||
var guild = channel.GuildId == Guild.DirectMessages.Id
|
||||
? Guild.DirectMessages
|
||||
: await GetGuildAsync(token, channel.GuildId);
|
||||
|
||||
// Get the chat log
|
||||
return await GetChatLogAsync(token, guild, channel, from, to, progress);
|
||||
}
|
||||
|
||||
public async Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||
{
|
||||
// Get channel
|
||||
var channel = await GetChannelAsync(token, channelId);
|
||||
|
||||
// Get the chat log
|
||||
return await GetChatLogAsync(token, channel, from, to, progress);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private class MessageGroup
|
||||
{
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
||||
{
|
||||
Author = author;
|
||||
Timestamp = timestamp;
|
||||
Messages = messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban;
|
||||
using Scriban.Parsing;
|
||||
using Scriban.Runtime;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private class TemplateLoader : ITemplateLoader
|
||||
{
|
||||
private const string ResourceRootNamespace = "DiscordChatExporter.Core.Resources.ExportTemplates";
|
||||
|
||||
public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName)
|
||||
{
|
||||
return $"{ResourceRootNamespace}.{templateName}";
|
||||
}
|
||||
|
||||
public string GetPath(ExportFormat format)
|
||||
{
|
||||
return $"{ResourceRootNamespace}.{format}.Template.{format.GetFileExtension()}";
|
||||
}
|
||||
|
||||
public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath)
|
||||
{
|
||||
return Assembly.GetExecutingAssembly().GetManifestResourceString(templatePath);
|
||||
}
|
||||
|
||||
public ValueTask<string> LoadAsync(TemplateContext context, SourceSpan callerSpan, string templatePath)
|
||||
{
|
||||
return new ValueTask<string>(Load(context, callerSpan, templatePath));
|
||||
}
|
||||
|
||||
public string Load(ExportFormat format)
|
||||
{
|
||||
return Assembly.GetExecutingAssembly().GetManifestResourceString(GetPath(format));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using DiscordChatExporter.Core.Markdown;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban.Runtime;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private class TemplateModel
|
||||
{
|
||||
private readonly ExportFormat _format;
|
||||
private readonly ChatLog _log;
|
||||
private readonly string _dateFormat;
|
||||
|
||||
public TemplateModel(ExportFormat format, ChatLog log, string dateFormat)
|
||||
{
|
||||
_format = format;
|
||||
_log = log;
|
||||
_dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
||||
=> messages.GroupAdjacentWhile((buffer, message) =>
|
||||
{
|
||||
// Break group if the author changed
|
||||
if (buffer.Last().Author.Id != message.Author.Id)
|
||||
return false;
|
||||
|
||||
// Break group if last message was more than 7 minutes ago
|
||||
if ((message.Timestamp - buffer.Last().Timestamp).TotalMinutes > 7)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}).Select(g => new MessageGroup(g.First().Author, g.First().Timestamp, g));
|
||||
|
||||
private string Format(IFormattable obj, string format)
|
||||
=> obj.ToString(format, CultureInfo.InvariantCulture);
|
||||
|
||||
private string FormatDate(DateTime dateTime) => Format(dateTime, _dateFormat);
|
||||
|
||||
private string FormatMarkdownPlainText(IReadOnlyList<Node> nodes)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node is FormattedNode formattedNode)
|
||||
{
|
||||
var innerText = FormatMarkdownPlainText(formattedNode.Children);
|
||||
buffer.Append($"{formattedNode.Token}{innerText}{formattedNode.Token}");
|
||||
}
|
||||
|
||||
else if (node is MentionNode mentionNode && mentionNode.Type != MentionType.Meta)
|
||||
{
|
||||
if (mentionNode.Type == MentionType.User)
|
||||
{
|
||||
var user = _log.Mentionables.GetUser(mentionNode.Id);
|
||||
buffer.Append($"@{user.Name}");
|
||||
}
|
||||
|
||||
else if (mentionNode.Type == MentionType.Channel)
|
||||
{
|
||||
var channel = _log.Mentionables.GetChannel(mentionNode.Id);
|
||||
buffer.Append($"#{channel.Name}");
|
||||
}
|
||||
|
||||
else if (mentionNode.Type == MentionType.Role)
|
||||
{
|
||||
var role = _log.Mentionables.GetRole(mentionNode.Id);
|
||||
buffer.Append($"@{role.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
else if (node is EmojiNode emojiNode)
|
||||
{
|
||||
buffer.Append(emojiNode.IsCustomEmoji ? $":{emojiNode.Name}:" : node.Lexeme);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
buffer.Append(node.Lexeme);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
private string FormatMarkdownPlainText(string input)
|
||||
=> FormatMarkdownPlainText(MarkdownParser.Parse(input));
|
||||
|
||||
private string FormatMarkdownHtml(IReadOnlyList<Node> nodes, int depth = 0)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node is TextNode textNode)
|
||||
{
|
||||
buffer.Append(textNode.Text.HtmlEncode());
|
||||
}
|
||||
|
||||
else if (node is FormattedNode formattedNode)
|
||||
{
|
||||
var innerHtml = FormatMarkdownHtml(formattedNode.Children, depth + 1);
|
||||
|
||||
if (formattedNode.Formatting == TextFormatting.Bold)
|
||||
buffer.Append($"<strong>{innerHtml}</strong>");
|
||||
|
||||
else if (formattedNode.Formatting == TextFormatting.Italic)
|
||||
buffer.Append($"<em>{innerHtml}</em>");
|
||||
|
||||
else if (formattedNode.Formatting == TextFormatting.Underline)
|
||||
buffer.Append($"<u>{innerHtml}</u>");
|
||||
|
||||
else if (formattedNode.Formatting == TextFormatting.Strikethrough)
|
||||
buffer.Append($"<s>{innerHtml}</s>");
|
||||
|
||||
else if (formattedNode.Formatting == TextFormatting.Spoiler)
|
||||
buffer.Append($"<span class=\"spoiler\">{innerHtml}</span>");
|
||||
}
|
||||
|
||||
else if (node is InlineCodeBlockNode inlineCodeBlockNode)
|
||||
{
|
||||
buffer.Append($"<span class=\"pre pre--inline\">{inlineCodeBlockNode.Code.HtmlEncode()}</span>");
|
||||
}
|
||||
|
||||
else if (node is MultilineCodeBlockNode multilineCodeBlockNode)
|
||||
{
|
||||
// Set language class for syntax highlighting
|
||||
var languageCssClass = multilineCodeBlockNode.Language.IsNotBlank()
|
||||
? "language-" + multilineCodeBlockNode.Language
|
||||
: null;
|
||||
|
||||
buffer.Append(
|
||||
$"<div class=\"pre pre--multiline {languageCssClass}\">{multilineCodeBlockNode.Code.HtmlEncode()}</div>");
|
||||
}
|
||||
|
||||
else if (node is MentionNode mentionNode)
|
||||
{
|
||||
if (mentionNode.Type == MentionType.Meta)
|
||||
{
|
||||
buffer.Append($"<span class=\"mention\">@{mentionNode.Id.HtmlEncode()}</span>");
|
||||
}
|
||||
|
||||
else if (mentionNode.Type == MentionType.User)
|
||||
{
|
||||
var user = _log.Mentionables.GetUser(mentionNode.Id);
|
||||
buffer.Append($"<span class=\"mention\" title=\"{user.FullName}\">@{user.Name.HtmlEncode()}</span>");
|
||||
}
|
||||
|
||||
else if (mentionNode.Type == MentionType.Channel)
|
||||
{
|
||||
var channel = _log.Mentionables.GetChannel(mentionNode.Id);
|
||||
buffer.Append($"<span class=\"mention\">#{channel.Name.HtmlEncode()}</span>");
|
||||
}
|
||||
|
||||
else if (mentionNode.Type == MentionType.Role)
|
||||
{
|
||||
var role = _log.Mentionables.GetRole(mentionNode.Id);
|
||||
buffer.Append($"<span class=\"mention\">@{role.Name.HtmlEncode()}</span>");
|
||||
}
|
||||
}
|
||||
|
||||
else if (node is EmojiNode emojiNode)
|
||||
{
|
||||
// Get emoji image URL
|
||||
var emojiImageUrl = new Emoji(emojiNode.Id, emojiNode.Name, emojiNode.IsAnimated).ImageUrl;
|
||||
|
||||
// Emoji can be jumboable if it's the only top-level node
|
||||
var jumboableCssClass = depth == 0 && nodes.Count == 1
|
||||
? "emoji--large"
|
||||
: null;
|
||||
|
||||
buffer.Append($"<img class=\"emoji {jumboableCssClass}\" alt=\"{emojiNode.Name}\" title=\"{emojiNode.Name}\" src=\"{emojiImageUrl}\" />");
|
||||
}
|
||||
|
||||
else if (node is LinkNode linkNode)
|
||||
{
|
||||
var escapedUrl = Uri.EscapeUriString(linkNode.Url);
|
||||
buffer.Append($"<a href=\"{escapedUrl}\">{linkNode.Title.HtmlEncode()}</a>");
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
private string FormatMarkdownHtml(string input)
|
||||
=> FormatMarkdownHtml(MarkdownParser.Parse(input));
|
||||
|
||||
private string FormatMarkdown(string input)
|
||||
{
|
||||
return _format == ExportFormat.HtmlDark || _format == ExportFormat.HtmlLight
|
||||
? FormatMarkdownHtml(input)
|
||||
: FormatMarkdownPlainText(input);
|
||||
}
|
||||
|
||||
public ScriptObject GetScriptObject()
|
||||
{
|
||||
// Create instance
|
||||
var scriptObject = new ScriptObject();
|
||||
|
||||
// Import model
|
||||
scriptObject.SetValue("Model", _log, true);
|
||||
|
||||
// Import functions
|
||||
scriptObject.Import(nameof(GroupMessages), new Func<IEnumerable<Message>, IEnumerable<MessageGroup>>(GroupMessages));
|
||||
scriptObject.Import(nameof(Format), new Func<IFormattable, string, string>(Format));
|
||||
scriptObject.Import(nameof(FormatDate), new Func<DateTime, string>(FormatDate));
|
||||
scriptObject.Import(nameof(FormatMarkdown), new Func<string, string>(FormatMarkdown));
|
||||
|
||||
return scriptObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban;
|
||||
using Scriban.Runtime;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
public ExportService(SettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
private async Task ExportChatLogSingleAsync(ChatLog chatLog, string filePath, ExportFormat format)
|
||||
{
|
||||
// Create template loader
|
||||
var loader = new TemplateLoader();
|
||||
|
||||
// Get template
|
||||
var templateCode = loader.Load(format);
|
||||
var template = Template.Parse(templateCode);
|
||||
|
||||
// Create template context
|
||||
var context = new TemplateContext
|
||||
{
|
||||
TemplateLoader = loader,
|
||||
MemberRenamer = m => m.Name,
|
||||
MemberFilter = m => true,
|
||||
LoopLimit = int.MaxValue,
|
||||
StrictVariables = true
|
||||
};
|
||||
|
||||
// Create template model
|
||||
var templateModel = new TemplateModel(format, chatLog, _settingsService.DateFormat);
|
||||
context.PushGlobal(templateModel.GetScriptObject());
|
||||
|
||||
// Create directory
|
||||
var dirPath = Path.GetDirectoryName(filePath);
|
||||
if (dirPath.IsNotBlank())
|
||||
Directory.CreateDirectory(dirPath);
|
||||
|
||||
// Render output
|
||||
using (var output = File.CreateText(filePath))
|
||||
{
|
||||
// Configure output
|
||||
context.PushOutput(new TextWriterOutput(output));
|
||||
|
||||
// Render output
|
||||
await context.EvaluateAsync(template.Page);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportChatLogPartitionedAsync(IReadOnlyList<ChatLog> partitions, string filePath, ExportFormat format)
|
||||
{
|
||||
// Split file path into components
|
||||
var dirPath = Path.GetDirectoryName(filePath);
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
|
||||
var fileExt = Path.GetExtension(filePath);
|
||||
|
||||
// Export each partition separately
|
||||
var partitionNumber = 1;
|
||||
foreach (var partition in partitions)
|
||||
{
|
||||
// Compose new file name
|
||||
var partitionFilePath = $"{fileNameWithoutExt} [{partitionNumber} of {partitions.Count}]{fileExt}";
|
||||
|
||||
// Compose full file path
|
||||
if (dirPath.IsNotBlank())
|
||||
partitionFilePath = Path.Combine(dirPath, partitionFilePath);
|
||||
|
||||
// Export
|
||||
await ExportChatLogSingleAsync(partition, partitionFilePath, format);
|
||||
|
||||
// Increment partition number
|
||||
partitionNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExportChatLogAsync(ChatLog chatLog, string filePath, ExportFormat format,
|
||||
int? partitionLimit = null)
|
||||
{
|
||||
// If partitioning is disabled or there are fewer messages in chat log than the limit - process it without partitioning
|
||||
if (partitionLimit == null || partitionLimit <= 0 || chatLog.Messages.Count <= partitionLimit)
|
||||
{
|
||||
await ExportChatLogSingleAsync(chatLog, filePath, format);
|
||||
}
|
||||
// Otherwise split into partitions and export separately
|
||||
else
|
||||
{
|
||||
// Create partitions by grouping up to X adjacent messages into separate chat logs
|
||||
var partitions = chatLog.Messages.GroupAdjacentWhile(g => g.Count < partitionLimit.Value)
|
||||
.Select(g => new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, g, chatLog.Mentionables))
|
||||
.ToArray();
|
||||
|
||||
await ExportChatLogPartitionedAsync(partitions, filePath, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Settings;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public class SettingsService : SettingsManager
|
||||
{
|
||||
public bool IsAutoUpdateEnabled { get; set; } = true;
|
||||
|
||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
||||
|
||||
public AuthToken LastToken { get; set; }
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
public int? LastPartitionLimit { get; set; }
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
Configuration.StorageSpace = StorageSpace.Instance;
|
||||
Configuration.SubDirectoryPath = "";
|
||||
Configuration.FileName = "Settings.dat";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Onova;
|
||||
using Onova.Exceptions;
|
||||
using Onova.Services;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public class UpdateService : IDisposable
|
||||
{
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
private readonly IUpdateManager _updateManager = new UpdateManager(
|
||||
new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"),
|
||||
new ZipPackageExtractor());
|
||||
|
||||
private Version _updateVersion;
|
||||
private bool _updaterLaunched;
|
||||
|
||||
public UpdateService(SettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<Version> CheckPrepareUpdateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// If auto-update is disabled - don't check for updates
|
||||
if (!_settingsService.IsAutoUpdateEnabled)
|
||||
return null;
|
||||
|
||||
// Check for updates
|
||||
var check = await _updateManager.CheckForUpdatesAsync();
|
||||
if (!check.CanUpdate)
|
||||
return null;
|
||||
|
||||
// Prepare the update
|
||||
await _updateManager.PrepareUpdateAsync(check.LastVersion);
|
||||
|
||||
return _updateVersion = check.LastVersion;
|
||||
}
|
||||
catch (UpdaterAlreadyLaunchedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (LockFileNotAcquiredException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void FinalizeUpdate(bool needRestart)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if an update is pending
|
||||
if (_updateVersion == null)
|
||||
return;
|
||||
|
||||
// Check if the updater has already been launched
|
||||
if (_updaterLaunched)
|
||||
return;
|
||||
|
||||
// Launch the updater
|
||||
_updateManager.LaunchUpdater(_updateVersion, needRestart);
|
||||
_updaterLaunched = true;
|
||||
}
|
||||
catch (UpdaterAlreadyLaunchedException)
|
||||
{
|
||||
}
|
||||
catch (LockFileNotAcquiredException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _updateManager.Dispose();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue