Use nullable

pull/236/head
Alexey Golub 5 years ago
parent 1bf9d9e2e2
commit e5a2852165

@ -7,7 +7,6 @@ using CliFx.Utilities;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Core.Services.Helpers;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Cli.Commands
{
@ -22,7 +21,7 @@ namespace DiscordChatExporter.Cli.Commands
public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark;
[CommandOption("output", 'o', Description = "Output file or directory path.")]
public string OutputPath { get; set; }
public string? OutputPath { get; set; }
[CommandOption("after",Description = "Limit to messages sent after this date.")]
public DateTimeOffset? After { get; set; }
@ -34,7 +33,7 @@ namespace DiscordChatExporter.Cli.Commands
public int? PartitionLimit { get; set; }
[CommandOption("dateformat", Description = "Date format used in output.")]
public string DateFormat { get; set; }
public string? DateFormat { get; set; }
protected ExportCommandBase(SettingsService settingsService, DataService dataService, ExportService exportService)
: base(dataService)
@ -46,8 +45,8 @@ namespace DiscordChatExporter.Cli.Commands
protected async Task ExportChannelAsync(IConsole console, Channel channel)
{
// Configure settings
if (!DateFormat.IsNullOrWhiteSpace())
SettingsService.DateFormat = DateFormat;
if (!string.IsNullOrWhiteSpace(DateFormat))
SettingsService.DateFormat = DateFormat!;
console.Output.Write($"Exporting channel [{channel.Name}]... ");
var progress = console.CreateProgressTicker();
@ -57,7 +56,7 @@ namespace DiscordChatExporter.Cli.Commands
// Generate file path if not set or is a directory
var filePath = OutputPath;
if (filePath.IsNullOrWhiteSpace() || ExportHelper.IsDirectoryPath(filePath))
if (string.IsNullOrWhiteSpace(filePath) || ExportHelper.IsDirectoryPath(filePath))
{
// Generate default file name
var fileName = ExportHelper.GetDefaultExportFileName(ExportFormat, chatLog.Guild,

@ -6,13 +6,14 @@
<Version>2.15</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (c) Alexey Golub</Copyright>
<Nullable>enable</Nullable>
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliFx" Version="0.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup>
<ItemGroup>

@ -2,10 +2,11 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup>
</Project>

@ -16,9 +16,9 @@ namespace DiscordChatExporter.Core.Markdown.Internal
{
}
public ParsedMatch<T> Match(StringPart stringPart)
public ParsedMatch<T>? Match(StringPart stringPart)
{
ParsedMatch<T> earliestMatch = null;
ParsedMatch<T>? earliestMatch = null;
// Try to match the input with each matcher and get the match with the lowest start index
foreach (var matcher in _matchers)

@ -2,6 +2,6 @@
{
internal interface IMatcher<T>
{
ParsedMatch<T> Match(StringPart stringPart);
ParsedMatch<T>? Match(StringPart stringPart);
}
}

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
namespace DiscordChatExporter.Core.Markdown.Internal
@ -23,7 +19,7 @@ namespace DiscordChatExporter.Core.Markdown.Internal
{
}
public ParsedMatch<T> Match(StringPart stringPart)
public ParsedMatch<T>? Match(StringPart stringPart)
{
var match = _regex.Match(stringPart.Target, stringPart.StartIndex, stringPart.Length);
if (!match.Success)

@ -20,7 +20,7 @@ namespace DiscordChatExporter.Core.Markdown.Internal
{
}
public ParsedMatch<T> Match(StringPart stringPart)
public ParsedMatch<T>? Match(StringPart stringPart)
{
var index = stringPart.Target.IndexOf(_needle, stringPart.StartIndex, stringPart.Length, _comparison);

@ -3,7 +3,6 @@ using System.Linq;
using System.Text.RegularExpressions;
using DiscordChatExporter.Core.Markdown.Internal;
using DiscordChatExporter.Core.Markdown.Nodes;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Markdown
{
@ -125,7 +124,7 @@ namespace DiscordChatExporter.Core.Markdown
// Capture <:lul:123456> or <a:lul:123456>
private static readonly IMatcher<Node> CustomEmojiNodeMatcher = new RegexMatcher<Node>(
new Regex("<(a)?:(.+?):(\\d+?)>", DefaultRegexOptions),
m => new EmojiNode(m.Groups[3].Value, m.Groups[2].Value, !m.Groups[1].Value.IsNullOrWhiteSpace()));
m => new EmojiNode(m.Groups[3].Value, m.Groups[2].Value, !string.IsNullOrWhiteSpace(m.Groups[1].Value)));
/* Links */

@ -1,18 +1,16 @@
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Markdown.Nodes
namespace DiscordChatExporter.Core.Markdown.Nodes
{
public class EmojiNode : Node
{
public string Id { get; }
public string? Id { get; }
public string Name { get; }
public bool IsAnimated { get; }
public bool IsCustomEmoji => !Id.IsNullOrWhiteSpace();
public bool IsCustomEmoji => !string.IsNullOrWhiteSpace(Id);
public EmojiNode(string id, string name, bool isAnimated)
public EmojiNode(string? id, string name, bool isAnimated)
{
Id = id;
Name = name;

@ -6,17 +6,17 @@
{
public string Id { get; }
public string ParentId { get; }
public string? ParentId { get; }
public string GuildId { get; }
public string? GuildId { get; }
public string Name { get; }
public string Topic { get; }
public string? Topic { get; }
public ChannelType Type { get; }
public Channel(string id, string parentId, string guildId, string name, string topic, ChannelType type)
public Channel(string id, string? parentId, string? guildId, string name, string? topic, ChannelType type)
{
Id = id;
ParentId = parentId;

@ -2,10 +2,11 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup>
</Project>

@ -8,28 +8,29 @@ namespace DiscordChatExporter.Core.Models
public class Embed
{
public string Title { get; }
public string? Title { get; }
public string Url { get; }
public string? Url { get; }
public DateTimeOffset? Timestamp { get; }
// TODO: this should be nullable and default color should be set in CSS
public Color Color { get; }
public EmbedAuthor Author { get; }
public EmbedAuthor? Author { get; }
public string Description { get; }
public string? Description { get; }
public IReadOnlyList<EmbedField> Fields { get; }
public EmbedImage Thumbnail { get; }
public EmbedImage? Thumbnail { get; }
public EmbedImage Image { get; }
public EmbedImage? Image { get; }
public EmbedFooter Footer { get; }
public EmbedFooter? Footer { get; }
public Embed(string title, string url, DateTimeOffset? timestamp, Color color, EmbedAuthor author, string description,
IReadOnlyList<EmbedField> fields, EmbedImage thumbnail, EmbedImage image, EmbedFooter footer)
public Embed(string? title, string? url, DateTimeOffset? timestamp, Color color, EmbedAuthor? author, string? description,
IReadOnlyList<EmbedField> fields, EmbedImage? thumbnail, EmbedImage? image, EmbedFooter? footer)
{
Title = title;
Url = url;
@ -43,6 +44,6 @@ namespace DiscordChatExporter.Core.Models
Footer = footer;
}
public override string ToString() => Title;
public override string ToString() => Title ?? "<untitled embed>";
}
}

@ -4,19 +4,19 @@ namespace DiscordChatExporter.Core.Models
public class EmbedAuthor
{
public string Name { get; }
public string? Name { get; }
public string Url { get; }
public string? Url { get; }
public string IconUrl { get; }
public string? IconUrl { get; }
public EmbedAuthor(string name, string url, string iconUrl)
public EmbedAuthor(string? name, string? url, string? iconUrl)
{
Name = name;
Url = url;
IconUrl = iconUrl;
}
public override string ToString() => Name;
public override string ToString() => Name ?? "<unnamed author>";
}
}

@ -6,9 +6,9 @@ namespace DiscordChatExporter.Core.Models
{
public string Text { get; }
public string IconUrl { get; }
public string? IconUrl { get; }
public EmbedFooter(string text, string iconUrl)
public EmbedFooter(string text, string? iconUrl)
{
Text = text;
IconUrl = iconUrl;

@ -4,13 +4,13 @@ namespace DiscordChatExporter.Core.Models
public class EmbedImage
{
public string Url { get; }
public string? Url { get; }
public int? Width { get; }
public int? Height { get; }
public EmbedImage(string url, int? width, int? height)
public EmbedImage(string? url, int? width, int? height)
{
Url = url;
Height = height;

@ -8,7 +8,7 @@ namespace DiscordChatExporter.Core.Models
public partial class Emoji
{
public string Id { get; }
public string? Id { get; }
public string Name { get; }
@ -16,7 +16,7 @@ namespace DiscordChatExporter.Core.Models
public string ImageUrl { get; }
public Emoji(string id, string name, bool isAnimated)
public Emoji(string? id, string name, bool isAnimated)
{
Id = id;
Name = name;
@ -37,10 +37,10 @@ namespace DiscordChatExporter.Core.Models
private static string GetTwemojiName(string emoji) =>
GetCodePoints(emoji).Select(i => i.ToString("x")).JoinToString("-");
public static string GetImageUrl(string id, string name, bool isAnimated)
public static string GetImageUrl(string? id, string name, bool isAnimated)
{
// Custom emoji
if (!id.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(id))
{
// Animated
if (isAnimated)

@ -1,8 +1,6 @@
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Models
namespace DiscordChatExporter.Core.Models
{
// https://discordapp.com/developers/docs/resources/guild#guild-object
// https://discordapp.string.IsNullOrWhiteSpace(com/developers/docs/resources/guild#guild-object
public partial class Guild
{
@ -10,11 +8,11 @@ namespace DiscordChatExporter.Core.Models
public string Name { get; }
public string IconHash { get; }
public string? IconHash { get; }
public string IconUrl { get; }
public Guild(string id, string name, string iconHash)
public Guild(string id, string name, string? iconHash)
{
Id = id;
Name = name;
@ -28,9 +26,9 @@ namespace DiscordChatExporter.Core.Models
public partial class Guild
{
public static string GetIconUrl(string id, string iconHash)
public static string GetIconUrl(string id, string? iconHash)
{
return !iconHash.IsNullOrWhiteSpace()
return !string.IsNullOrWhiteSpace(iconHash)
? $"https://cdn.discordapp.com/icons/{id}/{iconHash}.png"
: "https://cdn.discordapp.com/embed/avatars/0.png";
}

@ -19,7 +19,7 @@ namespace DiscordChatExporter.Core.Models
public DateTimeOffset? EditedTimestamp { get; }
public string Content { get; }
public string? Content { get; }
public IReadOnlyList<Attachment> Attachments { get; }
@ -32,7 +32,7 @@ namespace DiscordChatExporter.Core.Models
public bool IsPinned { get; }
public Message(string id, string channelId, MessageType type, User author, DateTimeOffset timestamp,
DateTimeOffset? editedTimestamp, string content, IReadOnlyList<Attachment> attachments,
DateTimeOffset? editedTimestamp, string? content, IReadOnlyList<Attachment> attachments,
IReadOnlyList<Embed> embeds, IReadOnlyList<Reaction> reactions, IReadOnlyList<User> mentionedUsers,
bool isPinned)
{
@ -50,6 +50,6 @@ namespace DiscordChatExporter.Core.Models
IsPinned = isPinned;
}
public override string ToString() => Content;
public override string ToString() => Content ?? "<message without content>";
}
}

@ -1,5 +1,4 @@
using System;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Models
{
@ -15,13 +14,13 @@ namespace DiscordChatExporter.Core.Models
public string FullName { get; }
public string AvatarHash { get; }
public string? AvatarHash { get; }
public string AvatarUrl { get; }
public bool IsBot { get; }
public User(string id, int discriminator, string name, string avatarHash, bool isBot)
public User(string id, int discriminator, string name, string? avatarHash, bool isBot)
{
Id = id;
Discriminator = discriminator;
@ -40,10 +39,10 @@ namespace DiscordChatExporter.Core.Models
{
public static string GetFullName(string name, int discriminator) => $"{name}#{discriminator:0000}";
public static string GetAvatarUrl(string id, int discriminator, string avatarHash)
public static string GetAvatarUrl(string id, int discriminator, string? avatarHash)
{
// Custom avatar
if (!avatarHash.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(avatarHash))
{
// Animated
if (avatarHash.StartsWith("a_", StringComparison.Ordinal))

@ -96,7 +96,7 @@ namespace DiscordChatExporter.Core.Rendering
await RenderFieldAsync(writer, FormatDate(message.Timestamp));
// Content
await RenderFieldAsync(writer, FormatMarkdown(message.Content));
await RenderFieldAsync(writer, FormatMarkdown(message.Content ?? ""));
// Attachments
var formattedAttachments = message.Attachments.Select(a => a.Url).JoinToString(",");

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

@ -97,7 +97,7 @@ namespace DiscordChatExporter.Core.Rendering
if (node is MultiLineCodeBlockNode multilineCodeBlockNode)
{
// Set CSS class for syntax highlighting
var highlightCssClass = !multilineCodeBlockNode.Language.IsNullOrWhiteSpace()
var highlightCssClass = !string.IsNullOrWhiteSpace(multilineCodeBlockNode.Language)
? $"language-{multilineCodeBlockNode.Language}"
: "nohighlight";
@ -153,7 +153,7 @@ namespace DiscordChatExporter.Core.Rendering
// Extract message ID if the link points to a Discord message
var linkedMessageId = Regex.Match(linkNode.Url, "^https?://discordapp.com/channels/.*?/(\\d+)/?$").Groups[1].Value;
return linkedMessageId.IsNullOrWhiteSpace()
return string.IsNullOrWhiteSpace(linkedMessageId)
? $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\">{HtmlEncode(linkNode.Title)}</a>"
: $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">{HtmlEncode(linkNode.Title)}</a>";
}
@ -165,7 +165,7 @@ namespace DiscordChatExporter.Core.Rendering
private string FormatMarkdown(IReadOnlyList<Node> nodes, bool isTopLevel)
{
// Emojis are jumbo if all top-level nodes are emoji nodes or whitespace text nodes
var isJumbo = isTopLevel && nodes.All(n => n is EmojiNode || n is TextNode textNode && textNode.Text.IsNullOrWhiteSpace());
var isJumbo = isTopLevel && nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
return nodes.Select(n => FormatMarkdown(n, isJumbo)).JoinToString("");
}

@ -40,7 +40,7 @@ namespace DiscordChatExporter.Core.Rendering
return $"before {FormatDate(before.Value)}";
// Neither
return null;
return "";
}
private string FormatMarkdown(Node node)
@ -131,43 +131,43 @@ namespace DiscordChatExporter.Core.Rendering
await writer.WriteLineAsync("{Embed}");
// Author name
if (!(embed.Author?.Name).IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Author?.Name))
await writer.WriteLineAsync(embed.Author?.Name);
// URL
if (!embed.Url.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Url))
await writer.WriteLineAsync(embed.Url);
// Title
if (!embed.Title.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Title))
await writer.WriteLineAsync(FormatMarkdown(embed.Title));
// Description
if (!embed.Description.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Description))
await writer.WriteLineAsync(FormatMarkdown(embed.Description));
// Fields
foreach (var field in embed.Fields)
{
// Name
if (!field.Name.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(field.Name))
await writer.WriteLineAsync(field.Name);
// Value
if (!field.Value.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(field.Value))
await writer.WriteLineAsync(field.Value);
}
// Thumbnail URL
if (!(embed.Thumbnail?.Url).IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url))
await writer.WriteLineAsync(embed.Thumbnail?.Url);
// Image URL
if (!(embed.Image?.Url).IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Image?.Url))
await writer.WriteLineAsync(embed.Image?.Url);
// Footer text
if (!(embed.Footer?.Text).IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
await writer.WriteLineAsync(embed.Footer?.Text);
await writer.WriteLineAsync();
@ -201,7 +201,8 @@ namespace DiscordChatExporter.Core.Rendering
await RenderMessageHeaderAsync(writer, message);
// Content
await writer.WriteLineAsync(FormatMarkdown(message.Content));
if (!string.IsNullOrWhiteSpace(message.Content))
await writer.WriteLineAsync(FormatMarkdown(message.Content));
// Separator
await writer.WriteLineAsync();

@ -12,10 +12,10 @@ namespace DiscordChatExporter.Core.Services
{
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>();
var id = json["id"]!.Value<string>();
var discriminator = json["discriminator"]!.Value<int>();
var name = json["username"]!.Value<string>();
var avatarHash = json["avatar"]!.Value<string>();
var isBot = json["bot"]?.Value<bool>() ?? false;
return new User(id, discriminator, name, avatarHash, isBot);
@ -23,9 +23,9 @@ namespace DiscordChatExporter.Core.Services
private Guild ParseGuild(JToken json)
{
var id = json["id"].Value<string>();
var name = json["name"].Value<string>();
var iconHash = json["icon"].Value<string>();
var id = json["id"]!.Value<string>();
var name = json["name"]!.Value<string>();
var iconHash = json["icon"]!.Value<string>();
return new Guild(id, name, iconHash);
}
@ -33,23 +33,23 @@ namespace DiscordChatExporter.Core.Services
private Channel ParseChannel(JToken json)
{
// Get basic data
var id = json["id"].Value<string>();
var id = json["id"]!.Value<string>();
var parentId = json["parent_id"]?.Value<string>();
var type = (ChannelType) json["type"].Value<int>();
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.IsNullOrWhiteSpace())
if (string.IsNullOrWhiteSpace(guildId))
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.IsNullOrWhiteSpace())
if (string.IsNullOrWhiteSpace(name))
name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
return new Channel(id, parentId, guildId, name, topic, type);
@ -57,20 +57,20 @@ namespace DiscordChatExporter.Core.Services
private Role ParseRole(JToken json)
{
var id = json["id"].Value<string>();
var name = json["name"].Value<string>();
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 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 fileName = json["filename"]!.Value<string>();
var fileSizeBytes = json["size"]!.Value<long>();
var fileSize = new FileSize(fileSizeBytes);
@ -88,8 +88,8 @@ namespace DiscordChatExporter.Core.Services
private EmbedField ParseEmbedField(JToken json)
{
var name = json["name"].Value<string>();
var value = json["value"].Value<string>();
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);
@ -106,7 +106,7 @@ namespace DiscordChatExporter.Core.Services
private EmbedFooter ParseEmbedFooter(JToken json)
{
var text = json["text"].Value<string>();
var text = json["text"]!.Value<string>();
var iconUrl = json["icon_url"]?.Value<string>();
return new EmbedFooter(text, iconUrl);
@ -122,23 +122,23 @@ namespace DiscordChatExporter.Core.Services
// Get color
var color = json["color"] != null
? Color.FromArgb(json["color"].Value<int>()).ResetAlpha()
? 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;
var author = json["author"] != null ? ParseEmbedAuthor(json["author"]!) : null;
// Get fields
var fields = json["fields"].EmptyIfNull().Select(ParseEmbedField).ToArray();
var fields = (json["fields"] ?? Enumerable.Empty<JToken>()).Select(ParseEmbedField).ToArray();
// Get thumbnail
var thumbnail = json["thumbnail"] != null ? ParseEmbedImage(json["thumbnail"]) : null;
var thumbnail = json["thumbnail"] != null ? ParseEmbedImage(json["thumbnail"]!) : null;
// Get image
var image = json["image"] != null ? ParseEmbedImage(json["image"]) : null;
var image = json["image"] != null ? ParseEmbedImage(json["image"]!) : null;
// Get footer
var footer = json["footer"] != null ? ParseEmbedFooter(json["footer"]) : null;
var footer = json["footer"] != null ? ParseEmbedFooter(json["footer"]!) : null;
return new Embed(title, url, timestamp, color, author, description, fields, thumbnail, image, footer);
}
@ -146,7 +146,7 @@ namespace DiscordChatExporter.Core.Services
private Emoji ParseEmoji(JToken json)
{
var id = json["id"]?.Value<string>();
var name = json["name"]?.Value<string>();
var name = json["name"]!.Value<string>();
var isAnimated = json["animated"]?.Value<bool>() ?? false;
return new Emoji(id, name, isAnimated);
@ -154,8 +154,8 @@ namespace DiscordChatExporter.Core.Services
private Reaction ParseReaction(JToken json)
{
var count = json["count"].Value<int>();
var emoji = ParseEmoji(json["emoji"]);
var count = json["count"]!.Value<int>();
var emoji = ParseEmoji(json["emoji"]!);
return new Reaction(count, emoji);
}
@ -163,12 +163,12 @@ namespace DiscordChatExporter.Core.Services
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>().ToDateTimeOffset();
var id = json["id"]!.Value<string>();
var channelId = json["channel_id"]!.Value<string>();
var timestamp = json["timestamp"]!.Value<DateTime>().ToDateTimeOffset();
var editedTimestamp = json["edited_timestamp"]?.Value<DateTime?>()?.ToDateTimeOffset();
var content = json["content"].Value<string>();
var type = (MessageType) json["type"].Value<int>();
var content = json["content"]!.Value<string>();
var type = (MessageType) json["type"]!.Value<int>();
// Workarounds for non-default types
if (type == MessageType.RecipientAdd)
@ -187,22 +187,22 @@ namespace DiscordChatExporter.Core.Services
content = "Joined the server.";
// Get author
var author = ParseUser(json["author"]);
var author = ParseUser(json["author"]!);
// Get attachments
var attachments = json["attachments"].EmptyIfNull().Select(ParseAttachment).ToArray();
var attachments = (json["attachments"] ?? Enumerable.Empty<JToken>()).Select(ParseAttachment).ToArray();
// Get embeds
var embeds = json["embeds"].EmptyIfNull().Select(ParseEmbed).ToArray();
var embeds = (json["embeds"] ?? Enumerable.Empty<JToken>()).Select(ParseEmbed).ToArray();
// Get reactions
var reactions = json["reactions"].EmptyIfNull().Select(ParseReaction).ToArray();
var reactions = (json["reactions"] ?? Enumerable.Empty<JToken>()).Select(ParseReaction).ToArray();
// Get mentioned users
var mentionedUsers = json["mentions"].EmptyIfNull().Select(ParseUser).ToArray();
var mentionedUsers = (json["mentions"] ?? Enumerable.Empty<JToken>()).Select(ParseUser).ToArray();
// Get whether this message is pinned
var isPinned = json["pinned"].Value<bool>();
var isPinned = json["pinned"]!.Value<bool>();
return new Message(id, channelId, type, author, timestamp, editedTimestamp, content, attachments, embeds,
reactions, mentionedUsers, isPinned);

@ -45,7 +45,7 @@ namespace DiscordChatExporter.Core.Services
var value = parameter.SubstringAfter("=");
// Skip empty values
if (value.IsNullOrWhiteSpace())
if (string.IsNullOrWhiteSpace(value))
continue;
request.RequestUri = request.RequestUri.SetQueryParameter(key, value);
@ -53,6 +53,7 @@ namespace DiscordChatExporter.Core.Services
// 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)
@ -119,7 +120,7 @@ namespace DiscordChatExporter.Core.Services
}
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(AuthToken token, string channelId,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null)
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{
var result = new List<Message>();
@ -211,7 +212,7 @@ namespace DiscordChatExporter.Core.Services
}
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null)
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{
// Get messages
var messages = await GetChannelMessagesAsync(token, channel.Id, after, before, progress);
@ -223,19 +224,19 @@ namespace DiscordChatExporter.Core.Services
}
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Channel channel,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null)
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{
// Get guild
var guild = channel.GuildId == Guild.DirectMessages.Id
? Guild.DirectMessages
: await GetGuildAsync(token, channel.GuildId);
var guild = !string.IsNullOrWhiteSpace(channel.GuildId)
? await GetGuildAsync(token, channel.GuildId)
: Guild.DirectMessages;
// Get the chat log
return await GetChatLogAsync(token, guild, channel, after, before, progress);
}
public async Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null)
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{
// Get channel
var channel = await GetChannelAsync(token, channelId);

@ -2,13 +2,14 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Failsafe" Version="1.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Onova" Version="2.4.5" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Onova" Version="2.5.1" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
</ItemGroup>

@ -38,7 +38,7 @@ namespace DiscordChatExporter.Core.Services
{
// Create output directory
var dirPath = Path.GetDirectoryName(filePath);
if (!dirPath.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(dirPath))
Directory.CreateDirectory(dirPath);
// Render chat log to output file
@ -74,7 +74,7 @@ namespace DiscordChatExporter.Core.Services
var partitionFilePath = $"{fileNameWithoutExt} [{partitionNumber} of {partitions.Length}]{fileExt}";
// Compose full file path
if (!dirPath.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(dirPath))
partitionFilePath = Path.Combine(dirPath, partitionFilePath);
// Export

@ -3,7 +3,6 @@ using System.IO;
using System.Linq;
using System.Text;
using DiscordChatExporter.Core.Models;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Services.Helpers
{
@ -12,7 +11,7 @@ namespace DiscordChatExporter.Core.Services.Helpers
public static bool IsDirectoryPath(string path) =>
path.Last() == Path.DirectorySeparatorChar ||
path.Last() == Path.AltDirectorySeparatorChar ||
Path.GetExtension(path).IsNullOrWhiteSpace() && !File.Exists(path);
string.IsNullOrWhiteSpace(Path.GetExtension(path)) && !File.Exists(path);
public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel,
DateTimeOffset? after = null, DateTimeOffset? before = null)

@ -9,8 +9,10 @@ namespace DiscordChatExporter.Core.Services
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
public AuthToken LastToken { get; set; }
public AuthToken? LastToken { get; set; }
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
public int? LastPartitionLimit { get; set; }
public SettingsService()

@ -1,6 +1,16 @@
namespace DiscordChatExporter.Gui
using System;
using System.Reflection;
namespace DiscordChatExporter.Gui
{
public partial class App
{
private static readonly Assembly Assembly = typeof(App).Assembly;
public static string Name => Assembly.GetName().Name!;
public static Version Version => Assembly.GetName().Version!;
public static string VersionString => Version.ToString(3);
}
}

@ -1,11 +1,14 @@
using System.Windows;
using System.Windows.Threading;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Gui.ViewModels;
using DiscordChatExporter.Gui.ViewModels.Framework;
using Stylet;
using StyletIoC;
#if !DEBUG
using System.Windows;
using System.Windows.Threading;
#endif
namespace DiscordChatExporter.Gui
{
public class Bootstrapper : Bootstrapper<RootViewModel>

@ -14,7 +14,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is DateTimeOffset dateTimeOffsetValue)
return dateTimeOffsetValue.DateTime;
return default;
return default(DateTime);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
@ -22,7 +22,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is DateTime dateTimeValue)
return new DateTimeOffset(dateTimeValue);
return default;
return default(DateTimeOffset);
}
}
}

@ -10,12 +10,12 @@ namespace DiscordChatExporter.Gui.Converters
{
public static ExportFormatToStringConverter Instance { get; } = new ExportFormatToStringConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ExportFormat exportFormatValue)
return exportFormatValue.GetDisplayName();
return default;
return default(string);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

@ -14,7 +14,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is bool boolValue)
return !boolValue;
return default;
return default(bool);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
@ -22,7 +22,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is bool boolValue)
return !boolValue;
return default;
return default(bool);
}
}
}

@ -7,6 +7,7 @@
<Version>2.15</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (c) Alexey Golub</Copyright>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ApplicationIcon>../favicon.ico</ApplicationIcon>
</PropertyGroup>
@ -19,11 +20,11 @@
<PackageReference Include="Gress" Version="1.1.1" />
<PackageReference Include="MaterialDesignColors" Version="1.2.0" />
<PackageReference Include="MaterialDesignThemes" Version="2.6.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.0.1" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="1.1.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.1.3" />
<PackageReference Include="Stylet" Version="1.3.0" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup>
<ItemGroup>

@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Services;
using Onova;
using Onova.Exceptions;
using Onova.Services;
@ -13,10 +12,10 @@ namespace DiscordChatExporter.Gui.Services
new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"),
new ZipPackageExtractor());
private Version _updateVersion;
private Version? _updateVersion;
private bool _updaterLaunched;
public async Task<Version> CheckForUpdatesAsync()
public async Task<Version?> CheckForUpdatesAsync()
{
var check = await _updateManager.CheckForUpdatesAsync();
return check.CanUpdate ? check.LastVersion : null;

@ -7,7 +7,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Components
{
public Channel Model { get; set; }
public string Category { get; set; }
public string? Category { get; set; }
}
public partial class ChannelViewModel

@ -6,7 +6,6 @@ using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Core.Services.Helpers;
using DiscordChatExporter.Gui.ViewModels.Components;
using DiscordChatExporter.Gui.ViewModels.Framework;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
{
@ -21,12 +20,12 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
public bool IsSingleChannel => Channels.Count == 1;
public string OutputPath { get; set; }
public string? OutputPath { get; set; }
public IReadOnlyList<ExportFormat> AvailableFormats =>
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
public ExportFormat SelectedFormat { get; set; } = ExportFormat.HtmlDark;
public ExportFormat SelectedFormat { get; set; }
public DateTimeOffset? After { get; set; }
@ -38,11 +37,6 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
{
_dialogManager = dialogManager;
_settingsService = settingsService;
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
// Persist preferences
SelectedFormat = _settingsService.LastExportFormat;
@ -85,7 +79,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
}
// If canceled - return
if (OutputPath.IsNullOrWhiteSpace())
if (string.IsNullOrWhiteSpace(OutputPath))
return;
// Close dialog

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading.Tasks;
using MaterialDesignThemes.Wpf;
using Microsoft.Win32;
@ -22,10 +23,10 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
// Set up event routing that will close the view when called from viewmodel
void OnDialogOpened(object sender, DialogOpenedEventArgs openArgs)
void OnDialogOpened(object? sender, DialogOpenedEventArgs openArgs)
{
// Delegate to close the dialog and unregister event handler
void OnScreenClosed(object o, CloseEventArgs closeArgs)
void OnScreenClosed(object? o, EventArgs closeArgs)
{
openArgs.Session.Close();
dialogScreen.Closed -= OnScreenClosed;
@ -41,7 +42,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
return dialogScreen.DialogResult;
}
public string PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
{
// Create dialog
var dialog = new SaveFileDialog
@ -56,7 +57,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
return dialog.ShowDialog() == true ? dialog.FileName : null;
}
public string PromptDirectoryPath(string defaultDirPath = "")
public string? PromptDirectoryPath(string defaultDirPath = "")
{
// Create dialog
var dialog = new VistaFolderBrowserDialog

@ -1,22 +1,18 @@
using Stylet;
using System;
using Stylet;
namespace DiscordChatExporter.Gui.ViewModels.Framework
{
public abstract class DialogScreen<T> : Screen
public abstract class DialogScreen<T> : PropertyChangedBase
{
public T DialogResult { get; private set; }
public event EventHandler? Closed;
public void Close(T dialogResult = default)
{
// Set the result
DialogResult = dialogResult;
// If there is a parent - ask them to close this dialog
if (Parent != null)
RequestClose(Equals(dialogResult, default(T)));
// Otherwise close ourselves
else
((IScreenState) this).Close();
Closed?.Invoke(this, EventArgs.Empty);
}
}

@ -7,8 +7,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
{
public static class Extensions
{
public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model,
string category = null)
public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model, string? category = null)
{
var viewModel = factory.CreateChannelViewModel();
viewModel.Model = model;

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services;
@ -38,13 +37,13 @@ namespace DiscordChatExporter.Gui.ViewModels
public bool IsBotToken { get; set; }
public string TokenValue { get; set; }
public string? TokenValue { get; set; }
public IReadOnlyList<GuildViewModel> AvailableGuilds { get; private set; }
public IReadOnlyList<GuildViewModel>? AvailableGuilds { get; private set; }
public GuildViewModel SelectedGuild { get; set; }
public GuildViewModel? SelectedGuild { get; set; }
public IReadOnlyList<ChannelViewModel> SelectedChannels { get; set; }
public IReadOnlyList<ChannelViewModel>? SelectedChannels { get; set; }
public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager,
SettingsService settingsService, UpdateService updateService, DataService dataService,
@ -58,8 +57,7 @@ namespace DiscordChatExporter.Gui.ViewModels
_exportService = exportService;
// Set title
var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
DisplayName = $"DiscordChatExporter v{version}";
DisplayName = $"{App.Name} v{App.VersionString}";
// Update busy state when progress manager changes
ProgressManager.Bind(o => o.IsActive, (sender, args) => IsBusy = ProgressManager.IsActive);
@ -83,7 +81,7 @@ namespace DiscordChatExporter.Gui.ViewModels
return;
// Notify user of an update and prepare it
Notifications.Enqueue($"Downloading update to DiscordChatExporter v{updateVersion}...");
Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
await _updateService.PrepareUpdateAsync(updateVersion);
// Prompt user to install update (otherwise install it when application exits)
@ -140,7 +138,7 @@ namespace DiscordChatExporter.Gui.ViewModels
await _dialogManager.ShowDialogAsync(dialog);
}
public bool CanPopulateGuildsAndChannels => !IsBusy && !TokenValue.IsNullOrWhiteSpace();
public bool CanPopulateGuildsAndChannels => !IsBusy && !string.IsNullOrWhiteSpace(TokenValue);
public async void PopulateGuildsAndChannels()
{
@ -150,7 +148,7 @@ namespace DiscordChatExporter.Gui.ViewModels
try
{
// Sanitize token
TokenValue = TokenValue.Trim('"');
TokenValue = TokenValue!.Trim('"');
// Create token
var token = new AuthToken(
@ -253,15 +251,15 @@ namespace DiscordChatExporter.Gui.ViewModels
}
}
public bool CanExportChannels => !IsBusy && !SelectedChannels.IsNullOrEmpty();
public bool CanExportChannels => !IsBusy && SelectedGuild != null && SelectedChannels != null && SelectedChannels.Any();
public async void ExportChannels()
{
// Get last used token
var token = _settingsService.LastToken;
var token = _settingsService.LastToken!;
// Create dialog
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild!, SelectedChannels!);
// Show dialog, if canceled - return
if (await _dialogManager.ShowDialogAsync(dialog) != true)
@ -281,7 +279,7 @@ namespace DiscordChatExporter.Gui.ViewModels
try
{
// Generate file path if necessary
var filePath = dialog.OutputPath;
var filePath = dialog.OutputPath!;
if (ExportHelper.IsDirectoryPath(filePath))
{
// Generate default file name

Loading…
Cancel
Save