You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

528 lines
18 KiB

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<T>
{
public readonly T Value;
public readonly string By;
public readonly bool Exists;
public bool Equals(AdminInfo<T> 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> 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<T> info)
=> info.Value;
public static bool operator ==(AdminInfo<T> left, AdminInfo<T> right)
{
return left.Equals(right);
}
public static bool operator !=(AdminInfo<T> left, AdminInfo<T> right)
{
return !(left == right);
}
}
public static class AdminInfo
{
public static AdminInfo<T> Create<T>(T value, string by)
=> new AdminInfo<T>(value, by);
public static AdminInfo<T> None<T>()
=> default;
}
[Serializable]
public struct Modlog
{
public AdminInfo<bool> ImageDeleted;
public AdminInfo<bool> ImageSpoilered;
public AdminInfo<bool> PostDeleted;
public AdminInfo<bool> UserBanned;
public AdminInfo<string> 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<PostInfo> children = new List<PostInfo>();
public IReadOnlyCollection<PostInfo> Children => new ReadOnlyCollection<PostInfo>(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<PostInfo> 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<object> 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<byte[]> 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<ThreadInfo> childThreads = new List<ThreadInfo>();
internal void AddChildThread(ThreadInfo threadInfo)
{
childThreads.Add(threadInfo);
}
public ReadOnlyCollection<ThreadInfo> Threads => new ReadOnlyCollection<ThreadInfo>(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}}}";
}
}
}