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
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}}}";
|
|
}
|
|
|
|
}
|
|
}
|