using AngleSharp; using AngleSharp.Dom; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using System.Linq; using System.Runtime.Serialization; using System.IO; using Tools; using System.Text.RegularExpressions; using System.Security.Cryptography; using Tools.Crypto; using System.Runtime.Serialization.Formatters.Binary; namespace napdump { [Serializable] public readonly struct AdminInfo { public readonly T Value; public readonly string By; public readonly bool Exists; public bool Equals(AdminInfo info) => info.By == By && (ReferenceEquals(info.Value, Value) || (info.Value?.Equals(this.Value) ?? false)); public override int GetHashCode() { return (Value?.GetHashCode() ?? 0) ^ (By?.GetHashCode() ?? 0); } public override bool Equals(object obj) { return obj is AdminInfo t && Equals(t); } internal AdminInfo(T value, string by) { Value = value; By = by; Exists = true; } public override string ToString() { if (Exists) return (Value?.ToString() ?? "null") + "(" + By + ")"; else return "None"; } public static implicit operator T(AdminInfo info) => info.Value; public static bool operator ==(AdminInfo left, AdminInfo right) { return left.Equals(right); } public static bool operator !=(AdminInfo left, AdminInfo right) { return !(left == right); } } public static class AdminInfo { public static AdminInfo Create(T value, string by) => new AdminInfo(value, by); public static AdminInfo None() => default; } [Serializable] public struct Modlog { public AdminInfo ImageDeleted; public AdminInfo ImageSpoilered; public AdminInfo PostDeleted; public AdminInfo UserBanned; public AdminInfo BanMessage; public override string ToString() { return $"{{ImageDeleted:{ImageDeleted}, ImageSpoilered:{ImageSpoilered}, PostDeleted:{PostDeleted}, UserBanned:{UserBanned}, BanMessage:'{BanMessage}'}}"; } public bool Equals(in Modlog log) { return ImageDeleted.Equals(log.ImageDeleted) && ImageSpoilered.Equals(log.ImageSpoilered) && PostDeleted.Equals(log.PostDeleted) && UserBanned.Equals(log.UserBanned) && BanMessage.Equals(log.BanMessage); } public override bool Equals(object obj) { return obj is Modlog log && this.Equals(log); } public override int GetHashCode() { return ImageDeleted.GetHashCode() ^ ImageSpoilered.GetHashCode() ^ PostDeleted.GetHashCode() ^ UserBanned.GetHashCode() ^ BanMessage.GetHashCode(); } public static bool operator ==(Modlog left, Modlog right) { return left.Equals(right); } public static bool operator !=(Modlog left, Modlog right) { return !(left == right); } } [Serializable] public class ThreadInfo : PostInfo { public override ThreadInfo Parent { get => null; internal set => throw new InvalidOperationException("Thread can have no parent thread"); } private readonly List children = new List(); public IReadOnlyCollection Children => new ReadOnlyCollection(children); public string ThreadURL { get; set; } public bool Locked { get; set; } protected override void Blank() { base.Blank(); ThreadURL = default; Locked = default; children.Clear(); } protected override void CopyFrom(PostInfo other) { base.CopyFrom(other); if (other is ThreadInfo thread) { ThreadURL = thread.ThreadURL; Locked = thread.Locked; children.Clear(); children.AddRange(thread.children); } else throw new InvalidOperationException("Not a thread."); } internal void AddChildPost(PostInfo post) { children.Add(post); } internal void AddChildPosts(IEnumerable posts) { children.AddRange(posts); } ~ThreadInfo() { children.Clear(); } public override async Task DecryptPostAsync(AESKey with, CancellationToken cancel = default) { var post = await DecryptAsync(with, cancel) as ThreadInfo ?? throw new InvalidOperationException("Not a thread."); CopyFrom(post); } public override async Task EncryptPostAsync(AESKey with, CancellationToken cancel = default) { var data = await EncryptAsync(with, cancel); Blank(); EncryptedData = data; } public ThreadInfo() : base() { } protected override StringBuilder PropertyString() { var sb = base.PropertyString(); if (!IsEncrypted) { AppendProp(sb, "Thread-Url", ThreadURL); AppendProp(sb, "Locked", Locked ? Locked.ToString() : null); if (children.Count > 0) AppendProp(sb, "Children", $"({children.Count})[" + string.Join(',', Children.Select(x => x.PostNumber.ToString() + " ")).Trim() + "]"); } return sb; } } [Serializable] public struct ImageInfo { public string ImageURL { get; set; } public string ImageFilename { get; set; } public long ImageSize { get; set; } public (int Width, int Height) ImageDimensions { get; set; } public bool Equals(in ImageInfo image) { return ImageURL == image.ImageURL && ImageFilename == image.ImageFilename && ImageSize == image.ImageSize && ImageDimensions == image.ImageDimensions; } public override bool Equals(object obj) { return obj is ImageInfo info && Equals(info); } public override int GetHashCode() { return (ImageURL?.GetHashCode() ?? 0) ^ (ImageFilename?.GetHashCode() ?? 0) ^ (ImageSize.GetHashCode()) ^ ImageDimensions.GetHashCode(); } public static bool operator ==(ImageInfo left, ImageInfo right) { return left.Equals(right); } public static bool operator !=(ImageInfo left, ImageInfo right) { return !(left == right); } } [Serializable] public class PostInfo #if DEBUG && false : ISerializable, IBinaryWritable, ITextWritable #endif { [field: NonSerialized] public BoardInfo BoardInfo { get; internal set; } [field: NonSerialized] public virtual ThreadInfo Parent { get; internal set; } protected virtual void Blank() { Subject = default; Body = default; Name = default; Tripcode = default; Email = default; Capcode = default; Timestamp = default; ImageField = default; ModLog = default; Extra = default; EncryptedData = default; EncryptedImageData = default; } protected virtual void CopyFrom(PostInfo other) { PostNumber = other.PostNumber; Subject = other.Subject; Body = other.Body; Name = other.Name; Tripcode = other.Tripcode; Email = other.Email; Capcode = other.Capcode; Timestamp = other.Timestamp; ImageField = other.ImageField; ModLog = other.ModLog; Extra = other.Extra; EncryptedData = other.EncryptedData; EncryptedImageData = other.EncryptedImageData; } public ulong PostNumber { get; set; } public string Subject { get; set; } public string Body { get; set; } public string Name { get; set; } public string Tripcode { get; set; } public string Email { get; set; } public string Capcode { get; set; } public DateTime Timestamp { get; set; } protected ImageInfo ImageField; public ImageInfo Image { get => IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : ImageField; set => ImageField = IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : value; } public string ImageURL { get => IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : ImageField.ImageURL; set => ImageField.ImageURL = IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : value; } public string ImageFilename { get => IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : ImageField.ImageFilename; set => ImageField.ImageFilename = IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : value; } public long ImageSize { get => IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : ImageField.ImageSize; set => ImageField.ImageSize = IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : value; } public (int Width, int Height) ImageDimensions { get => IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : ImageField.ImageDimensions; set => ImageField.ImageDimensions = IsImageEncrypted ? throw new InvalidOperationException("Image is encrypted.") : value; } public Modlog ModLog { get; set; } = default; public string Extra { get; set; } = ""; public bool IsEncrypted => EncryptedData != null; protected byte[] EncryptedData { get; set; } = null; protected byte[] EncryptedImageData { get; private set; } = null; public bool IsImageEncrypted => EncryptedImageData != null; public async Task EncryptImageAsync(AESKey with, CancellationToken token=default) { if (IsImageEncrypted) throw new InvalidOperationException("Already encrypted image."); using(var inp = new MemoryStream()) { var binf = new BinaryFormatter(); binf.Serialize(inp, ImageField); inp.Position = 0; using (var enc = new encaes.AesEncryptor(inp) { KeepAlive = true }) { enc.Key = with; using (var oup = new MemoryStream()) { await enc.Encrypt(oup, token); oup.Position = 0; EncryptedImageData = oup.ToArray(); ImageField = default; } } } } public async Task DecryptImageAsync(AESKey with, CancellationToken token=default) { if (!IsImageEncrypted) throw new InvalidOperationException("Image is not encrypted."); using(var inp = new MemoryStream(EncryptedImageData)) { inp.Position = 0; using (var dec = new encaes.AesEncryptor(inp) { KeepAlive = true }) { dec.Key = with; using (var oup = new MemoryStream()) { await dec.Decrypt(oup, token); oup.Position = 0; var binf = new BinaryFormatter(); ImageField = (ImageInfo)binf.Deserialize(oup); EncryptedImageData = null; } } } } protected async Task DecryptAsync(AESKey with, CancellationToken token=default) { if (!IsEncrypted) throw new InvalidOperationException("Not encrypted."); using (var inp = new MemoryStream()) { await inp.WriteAllAsync(EncryptedData, token); inp.Position = 0; using (var enc = new encaes.AesEncryptor(inp) { KeepAlive = true }) { enc.Key = with; using (var oup = new MemoryStream()) { await enc.Decrypt(oup, token); oup.Position = 0; var binf = new BinaryFormatter(); return binf.Deserialize(oup); } } } } protected async Task EncryptAsync(AESKey with, CancellationToken cancel = default) { using (var ms = new MemoryStream()) { var binf = new BinaryFormatter(); await Task.Yield(); binf.Serialize(ms, this); ms.Position = 0; using (var enc = new encaes.AesEncryptor(ms) { KeepAlive = true }) { enc.Key = with; using (var op = new MemoryStream()) { await enc.Encrypt(op, cancel); op.Position = 0; return op.ToArray(); } } } } public virtual async Task DecryptPostAsync(AESKey with, CancellationToken cancel = default) { var post = await DecryptAsync(with, cancel) as PostInfo ?? throw new InvalidOperationException("Not a post."); CopyFrom(post); } public virtual async Task EncryptPostAsync(AESKey with, CancellationToken cancel = default) { var data = await EncryptAsync(with, cancel); Blank(); EncryptedData = data; } protected static StringBuilder AppendProp(StringBuilder to, string name, string value) { if (to == null) { to = new StringBuilder(); to.Append($"[{name}={value}]: {{"); } else { if (value != null) to.Append($"{name}={EnsureLength(value, 32)}, "); else return to; } to.Append("\n\t"); return to; } protected static string CompleteProp(StringBuilder from) { string vl = from.ToString().Trim(); if (vl.EndsWith(",")) return vl.Substring(0, vl.Length - 1) + "}"; else return vl + "}"; } protected static string EnsureLength(string str, int i) { if (str.Length < i) return str; else return str.Substring(0, i) + "(...)"; } protected virtual StringBuilder PropertyString() { StringBuilder sb = AppendProp(null, "PostNumber", PostNumber.ToString()); AppendProp(sb, "Board", BoardInfo?.ToString()); AppendProp(sb, "Parent", Parent?.ToString()); AppendProp(sb, "Image-Encrypted", IsImageEncrypted ? "Yes" : null); AppendProp(sb, "Encrypted", IsEncrypted ? "Yes" : null); if (!IsEncrypted) { AppendProp(sb, "Name", Name); AppendProp(sb, "Tripcode", Tripcode); AppendProp(sb, "Email", Email); AppendProp(sb, "Subject", Subject); AppendProp(sb, "Image-URL", ImageField.ImageURL); AppendProp(sb, "Image-Filename", ImageField.ImageFilename); AppendProp(sb, "Image-Size", ImageField.ImageSize switch { default(long) => null, _ => ImageField.ImageSize.ToString() }); AppendProp(sb, "Image-Dimensions", ImageField.ImageURL != null ? $"({ImageField.ImageDimensions.Width} . {ImageField.ImageDimensions.Height})" : null); AppendProp(sb, "Timestamp", Timestamp.ToString()); AppendProp(sb, "Body", Body.ToString()); } return sb; } public override sealed string ToString() { return CompleteProp(PropertyString()); } } [Serializable] public class BoardInfo { public string BoardURL { get; internal set; } public string Title { get; set; } public string Description { get; set; } public string BoardName { get; set; } public string SafeName { get; set; } public DateTime DumpTimestamp { get; set; } = DateTime.Now; private readonly List childThreads = new List(); internal void AddChildThread(ThreadInfo threadInfo) { childThreads.Add(threadInfo); } public ReadOnlyCollection Threads => new ReadOnlyCollection(childThreads); ~BoardInfo() { childThreads.Clear(); } public string[] Tags { get; set; } public override string ToString() { return $@"(BoardInfo){{Taken={DumpTimestamp.ToString()}, URL={BoardURL}, Title={Title}, Desc={Description}, Name={BoardName} ({SafeName}), Tags={string.Join(' ', Tags ?? new string[] { })}, Threads={childThreads.Count}}}"; } } }