looks better i guess

master
Avril 4 years ago
parent 5cc997d0e5
commit fb70eb041e
Signed by: flanchan
GPG Key ID: 284488987C31F630

@ -1,351 +1,351 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Io;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace napdump.Dumpers
{
class Nineball : Dumper
{
readonly IConfiguration browserConfig;
readonly IBrowsingContext context;
readonly ICookieProvider cookies;
public Nineball(DumperConfig config) : base(config)
{
browserConfig = Configuration.Default.WithDefaultCookies().WithDefaultLoader();
cookies = browserConfig.Services.OfType<ICookieProvider>().First();
foreach (var c in config.Cookies ?? Array.Empty<(string Url, string Value)>())
{
cookies.SetCookie(new Url(c.Url), c.Value);
}
context = BrowsingContext.New(browserConfig);
}
private static readonly Regex reBoardName = new Regex(@"^(\/.*?\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex reBoardNameW = new Regex(@"^\/(\w+)\/", RegexOptions.Compiled | RegexOptions.IgnoreCase);
protected async Task GetBoardInfo(BoardInfo bi, IDocument document, CancellationToken token)
{
await Task.Yield();
token.ThrowIfCancellationRequested();
bi.Title = document.QuerySelector("body > threads > h1").InnerHtml;
bi.BoardName = reBoardName.IsMatch(bi.Title) ? reBoardName.Match(bi.Title).Groups[1].Value : bi.Title;
bi.SafeName = reBoardNameW.IsMatch(bi.Title) ? reBoardNameW.Match(bi.Title).Groups[1].Value : "unbound";
bi.Description = document.QuerySelector("#banner_info").TextContent;
bi.Tags = new[] { "meguca", "node", "liveboard" };
}
private static readonly Regex reImageDim = new Regex(@"\((\d+) ([kmg]?b), (\d+)x(\d+)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static bool TryParseImageDimInfo(string info, out long size, out int x, out int y)
{
//Console.WriteLine(info + " " + reImageDim.IsMatch(info));
if(reImageDim.IsMatch(info))
{
var groups = reImageDim.Match(info).Groups;
if(long.TryParse(groups[1].Value, out var rawSize) &&
int.TryParse(groups[3].Value, out x) &&
int.TryParse(groups[4].Value, out y))
{
long multiplier = 1;
switch (groups[2].Value.ToLower().Trim())
{
case "b":
break;
case "kb":
multiplier = 1024;
break;
case "mb":
multiplier = 1024 * 1024;
break;
case "gb":
multiplier = 1024 * 1024 * 1024;
break;
default:
goto bad;
}
size = rawSize & multiplier;
return true;
}
}
bad:
size = default;
x = y = default;
return false;
}
private static readonly Regex reDateTime = new Regex(@"(\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d\d\d\d)\(\w+\)(\d\d):(\d\d)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static bool TryParseDateTime(string htmlDateTime, out DateTime dt)
{
htmlDateTime = htmlDateTime.Trim();
//Console.WriteLine(htmlDateTime + " " + reDateTime.IsMatch(htmlDateTime));
if(reDateTime.IsMatch(htmlDateTime))
{
var groups = reDateTime.Match(htmlDateTime).Groups;
int day = int.Parse(groups[1].Value);
string month = groups[2].Value;
int year = int.Parse(groups[3].Value);
int hour = int.Parse(groups[4].Value);
int minute = int.Parse(groups[5].Value);
try
{
dt = new DateTime(year, month switch
{
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Oct" => 10,
"Nov" => 11,
"Dec" => 12,
_ => throw new InvalidDataException(),
}, day, hour, minute, 0);
return true;
}
catch
{
dt = default;
return false;
}
}
dt = default;
return false;
}
private static readonly Regex reImageDeleted = new Regex(@"^Image deleted by (\w+)$", RegexOptions.Compiled);
private static readonly Regex reImageSpoilered = new Regex(@"^Image spoilered by (\w+)$", RegexOptions.Compiled);
private static readonly Regex rePostDeleted = new Regex(@"^Post deleted by (\w+)$", RegexOptions.Compiled);
private static readonly Regex reUserBanned = new Regex(@"^User banned by (\w+)(?: for (.+))?$", RegexOptions.Compiled);
private static void getModlog(string nodeHtml, out Modlog log)
{
log = new Modlog();
if (nodeHtml == null) return;
try
{
var split = nodeHtml.Split("<br>").Select(x => x.Trim()).Where(x=> x.Length>0);
foreach (var line in split)
{
if (reImageDeleted.IsMatch(line))
log.ImageDeleted = AdminInfo.Create(true, reImageDeleted.Match(line).Groups[1].Value);
if (reImageSpoilered.IsMatch(line))
log.ImageSpoilered = AdminInfo.Create(true, reImageSpoilered.Match(line).Groups[1].Value);
if (rePostDeleted.IsMatch(line))
log.PostDeleted = AdminInfo.Create(true, rePostDeleted.Match(line).Groups[1].Value);
if (reUserBanned.IsMatch(line))
{
var match = reUserBanned.Match(line).Groups;
log.UserBanned = AdminInfo.Create(true, match[1].Value);
if (match[2].Success)
log.BanMessage = AdminInfo.Create(match[2].Value, match[1].Value);
}
}
}
catch(Exception ex)
{
Console.WriteLine("Modlog parsing error: "+ex.Message);
log = new Modlog();
}
}
private static readonly Regex reMailTo = new Regex(@"^mailto:", RegexOptions.Compiled);
protected override async IAsyncEnumerable<PostInfo> GetPosts(ThreadInfo thread, [EnumeratorCancellation] CancellationToken token)
{
var document = await context.OpenAsync(thread.BoardInfo.BoardURL + thread.PostNumber, token);
var section = document.QuerySelector("section");
thread.Locked = section.ClassList.Contains("locked");
(string Name, string Tripcode, string Email, string Capcode) getTripcode(IElement header)
{
var bname = header.QuerySelector("b");
string name, trip, mail, cap;
name = trip = mail = cap = null;
if(bname.FirstChild.NodeName.ToLower() == "a")
{
//Mail link
mail = bname.FirstElementChild.GetAttribute("href");
if (reMailTo.IsMatch(mail))
mail = reMailTo.Replace(mail, "");
bname = bname.FirstElementChild;
}
if(bname.ChildNodes.Length > 1 && bname.ChildNodes[1].NodeName=="CODE")
{
//Has tripcode & name
name = bname.FirstChild.TextContent;
trip = bname.ChildNodes[1].TextContent;
if (bname.ChildNodes.Length > 2)
cap = bname.ChildNodes[2].TextContent;
}
else if(bname.ChildNodes.Length>1)
{
name = bname.FirstChild.TextContent;
cap = bname.ChildNodes[1].TextContent;
}
else if(bname.FirstChild.NodeName.ToLower() == "code")
{
//Tripcode, no name.
trip = bname.FirstChild.TextContent;
}
else
{
//Name, no tripcode
name = bname.FirstChild.TextContent;
}
return (name, trip, mail, cap);
}
//Get thread's modlog.
getModlog(section.QuerySelector("b.modLog")?.InnerHtml, out var threadModlog);
thread.ModLog = threadModlog;
//Get thread's info.
var imageInfo = section.QuerySelector("figure > figcaption > i");
if (imageInfo != null)
{
string imageDimInfo = imageInfo.FirstChild.TextContent;
if (TryParseImageDimInfo(imageDimInfo, out var _imageSize, out var _x, out var _y))
{
thread.ImageSize = _imageSize;
thread.ImageDimensions = (_x, _y);
var imageNameInfo = imageInfo.QuerySelector("a");
thread.ImageURL = imageNameInfo.GetAttribute("href");
thread.ImageFilename = imageNameInfo.GetAttribute("download");
if (TryParseDateTime(section.QuerySelector("header > time").FirstChild.TextContent, out var threadTimestamp))
{
thread.Timestamp = threadTimestamp;
}
else
{
thread.Timestamp = default;
}
(thread.Name, thread.Tripcode, thread.Email, thread.Capcode) = getTripcode(section.QuerySelector("header"));
}
else
{
thread.ImageDimensions = default;
thread.ImageFilename = null;
thread.ImageSize = 0;
thread.ImageURL = null;
}
}
thread.Body = section.QuerySelector("blockquote").InnerHtml;
thread.ThreadURL = document.Url;
thread.Subject = section.QuerySelector("header > h3")?.TextContent;
//Get posts
foreach (var article in section.QuerySelectorAll("article"))
{
var post = new PostInfo()
{
Body = article.QuerySelector("blockquote").InnerHtml,
};
(post.Name, post.Tripcode, post.Email, post.Capcode) = getTripcode(article.QuerySelector("header"));
if (TryParseDateTime(article.QuerySelector("header > time").TextContent, out var _time))
post.Timestamp = _time;
else
post.Timestamp = default;
if (ulong.TryParse(article.QuerySelector("header > nav > a[class=quote]").TextContent, out ulong _postNumber))
post.PostNumber = _postNumber;
else
post.PostNumber = default;
//Get modlog
getModlog(article.QuerySelector("b.modLog")?.InnerHtml, out var postModlog);
post.ModLog = postModlog;
var figure = article.QuerySelector("figure > figcaption > i");
if (figure != null)
{
//Has image
if (TryParseImageDimInfo(figure.FirstChild.TextContent, out var _imageSize, out var _x, out var _y))
{
post.ImageDimensions = (_x, _y);
post.ImageSize = _imageSize;
post.ImageURL = figure.QuerySelector("a").GetAttribute("href");
post.ImageFilename = figure.QuerySelector("a").GetAttribute("download");
}
}
await EncryptIfRequired(post, token);
yield return post;
}
await EncryptIfRequired(thread, token);
}
private async Task EncryptIfRequired(PostInfo post, CancellationToken token)
{
try
{
if (Config.EncryptDeleted != null)
{
if (post.ModLog.ImageDeleted)
{
await post.EncryptImageAsync(Config.EncryptDeleted.Value, token);
}
if (post.ModLog.PostDeleted)
{
await post.EncryptPostAsync(Config.EncryptDeleted.Value, token);
}
}
}catch(Exception ex)
{
Console.WriteLine("Encryption for post "+post.PostNumber+" failed: " + ex.Message+"\n"+ex.StackTrace);
}
}
protected override async IAsyncEnumerable<ThreadInfo> GetThreads(BoardInfo boardInfo, [EnumeratorCancellation] CancellationToken token)
{
var document = await context.OpenAsync(boardInfo.BoardURL + "catalog", token);
await GetBoardInfo(boardInfo, document, token);
var threadLinks = document.QuerySelectorAll("#catalog > article > a[class=history]");
foreach(var link in threadLinks)
{
if (link.HasAttribute("href"))
{
var href = link.GetAttribute("href");
if (ulong.TryParse(href, out ulong postNumber))
{
yield return new ThreadInfo()
{
PostNumber = postNumber,
};
}
}
}
}
}
}
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Io;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace napdump.Dumpers
{
class Nineball : Dumper
{
readonly IConfiguration browserConfig;
readonly IBrowsingContext context;
readonly ICookieProvider cookies;
public Nineball(DumperConfig config) : base(config)
{
browserConfig = Configuration.Default.WithDefaultCookies().WithDefaultLoader();
cookies = browserConfig.Services.OfType<ICookieProvider>().First();
foreach (var c in config.Cookies ?? Array.Empty<(string Url, string Value)>())
{
cookies.SetCookie(new Url(c.Url), c.Value);
}
context = BrowsingContext.New(browserConfig);
}
private static readonly Regex reBoardName = new Regex(@"^(\/.*?\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex reBoardNameW = new Regex(@"^\/(\w+)\/", RegexOptions.Compiled | RegexOptions.IgnoreCase);
protected async Task GetBoardInfo(BoardInfo bi, IDocument document, CancellationToken token)
{
await Task.Yield();
token.ThrowIfCancellationRequested();
bi.Title = document.QuerySelector("body > threads > h1").InnerHtml;
bi.BoardName = reBoardName.IsMatch(bi.Title) ? reBoardName.Match(bi.Title).Groups[1].Value : bi.Title;
bi.SafeName = reBoardNameW.IsMatch(bi.Title) ? reBoardNameW.Match(bi.Title).Groups[1].Value : "unbound";
bi.Description = document.QuerySelector("#banner_info").TextContent;
bi.Tags = new[] { "meguca", "node", "liveboard" };
}
private static readonly Regex reImageDim = new Regex(@"\((\d+) ([kmg]?b), (\d+)x(\d+)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static bool TryParseImageDimInfo(string info, out long size, out int x, out int y)
{
//Console.WriteLine(info + " " + reImageDim.IsMatch(info));
if(reImageDim.IsMatch(info))
{
var groups = reImageDim.Match(info).Groups;
if(long.TryParse(groups[1].Value, out var rawSize) &&
int.TryParse(groups[3].Value, out x) &&
int.TryParse(groups[4].Value, out y))
{
long multiplier = 1;
switch (groups[2].Value.ToLower().Trim())
{
case "b":
break;
case "kb":
multiplier = 1024;
break;
case "mb":
multiplier = 1024 * 1024;
break;
case "gb":
multiplier = 1024 * 1024 * 1024;
break;
default:
goto bad;
}
size = rawSize & multiplier;
return true;
}
}
bad:
size = default;
x = y = default;
return false;
}
private static readonly Regex reDateTime = new Regex(@"(\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d\d\d\d)\(\w+\)(\d\d):(\d\d)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static bool TryParseDateTime(string htmlDateTime, out DateTime dt)
{
htmlDateTime = htmlDateTime.Trim();
//Console.WriteLine(htmlDateTime + " " + reDateTime.IsMatch(htmlDateTime));
if(reDateTime.IsMatch(htmlDateTime))
{
var groups = reDateTime.Match(htmlDateTime).Groups;
int day = int.Parse(groups[1].Value);
string month = groups[2].Value;
int year = int.Parse(groups[3].Value);
int hour = int.Parse(groups[4].Value);
int minute = int.Parse(groups[5].Value);
try
{
dt = new DateTime(year, month switch
{
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Oct" => 10,
"Nov" => 11,
"Dec" => 12,
_ => throw new InvalidDataException(),
}, day, hour, minute, 0);
return true;
}
catch
{
dt = default;
return false;
}
}
dt = default;
return false;
}
private static readonly Regex reImageDeleted = new Regex(@"^Image deleted by (\w+)$", RegexOptions.Compiled);
private static readonly Regex reImageSpoilered = new Regex(@"^Image spoilered by (\w+)$", RegexOptions.Compiled);
private static readonly Regex rePostDeleted = new Regex(@"^Post deleted by (\w+)$", RegexOptions.Compiled);
private static readonly Regex reUserBanned = new Regex(@"^User banned by (\w+)(?: for (.+))?$", RegexOptions.Compiled);
private static void getModlog(string nodeHtml, out Modlog log)
{
log = new Modlog();
if (nodeHtml == null) return;
try
{
var split = nodeHtml.Split("<br>").Select(x => x.Trim()).Where(x=> x.Length>0);
foreach (var line in split)
{
if (reImageDeleted.IsMatch(line))
log.ImageDeleted = AdminInfo.Create(true, reImageDeleted.Match(line).Groups[1].Value);
if (reImageSpoilered.IsMatch(line))
log.ImageSpoilered = AdminInfo.Create(true, reImageSpoilered.Match(line).Groups[1].Value);
if (rePostDeleted.IsMatch(line))
log.PostDeleted = AdminInfo.Create(true, rePostDeleted.Match(line).Groups[1].Value);
if (reUserBanned.IsMatch(line))
{
var match = reUserBanned.Match(line).Groups;
log.UserBanned = AdminInfo.Create(true, match[1].Value);
if (match[2].Success)
log.BanMessage = AdminInfo.Create(match[2].Value, match[1].Value);
}
}
}
catch(Exception ex)
{
Console.WriteLine("Modlog parsing error: "+ex.Message);
log = new Modlog();
}
}
private static readonly Regex reMailTo = new Regex(@"^mailto:", RegexOptions.Compiled);
protected override async IAsyncEnumerable<PostInfo> GetPosts(ThreadInfo thread, [EnumeratorCancellation] CancellationToken token)
{
var document = await context.OpenAsync(thread.BoardInfo.BoardURL + thread.PostNumber, token);
var section = document.QuerySelector("section");
thread.Locked = section.ClassList.Contains("locked");
(string Name, string Tripcode, string Email, string Capcode) getTripcode(IElement header)
{
var bname = header.QuerySelector("b");
string name, trip, mail, cap;
name = trip = mail = cap = null;
if(bname.FirstChild.NodeName.ToLower() == "a")
{
//Mail link
mail = bname.FirstElementChild.GetAttribute("href");
if (reMailTo.IsMatch(mail))
mail = reMailTo.Replace(mail, "");
bname = bname.FirstElementChild;
}
if(bname.ChildNodes.Length > 1 && bname.ChildNodes[1].NodeName=="CODE")
{
//Has tripcode & name
name = bname.FirstChild.TextContent;
trip = bname.ChildNodes[1].TextContent;
if (bname.ChildNodes.Length > 2)
cap = bname.ChildNodes[2].TextContent;
}
else if(bname.ChildNodes.Length>1)
{
name = bname.FirstChild.TextContent;
cap = bname.ChildNodes[1].TextContent;
}
else if(bname.FirstChild.NodeName.ToLower() == "code")
{
//Tripcode, no name.
trip = bname.FirstChild.TextContent;
}
else
{
//Name, no tripcode
name = bname.FirstChild.TextContent;
}
return (name, trip, mail, cap);
}
//Get thread's modlog.
getModlog(section.QuerySelector("b.modLog")?.InnerHtml, out var threadModlog);
thread.ModLog = threadModlog;
//Get thread's info.
var imageInfo = section.QuerySelector("figure > figcaption > i");
if (imageInfo != null)
{
string imageDimInfo = imageInfo.FirstChild.TextContent;
if (TryParseImageDimInfo(imageDimInfo, out var _imageSize, out var _x, out var _y))
{
thread.ImageSize = _imageSize;
thread.ImageDimensions = (_x, _y);
var imageNameInfo = imageInfo.QuerySelector("a");
thread.ImageURL = imageNameInfo.GetAttribute("href");
thread.ImageFilename = imageNameInfo.GetAttribute("download");
if (TryParseDateTime(section.QuerySelector("header > time").FirstChild.TextContent, out var threadTimestamp))
{
thread.Timestamp = threadTimestamp;
}
else
{
thread.Timestamp = default;
}
(thread.Name, thread.Tripcode, thread.Email, thread.Capcode) = getTripcode(section.QuerySelector("header"));
}
else
{
thread.ImageDimensions = default;
thread.ImageFilename = null;
thread.ImageSize = 0;
thread.ImageURL = null;
}
}
thread.Body = section.QuerySelector("blockquote").InnerHtml;
thread.ThreadURL = document.Url;
thread.Subject = section.QuerySelector("header > h3")?.TextContent;
//Get posts
foreach (var article in section.QuerySelectorAll("article"))
{
var post = new PostInfo()
{
Body = article.QuerySelector("blockquote").InnerHtml,
};
(post.Name, post.Tripcode, post.Email, post.Capcode) = getTripcode(article.QuerySelector("header"));
if (TryParseDateTime(article.QuerySelector("header > time").TextContent, out var _time))
post.Timestamp = _time;
else
post.Timestamp = default;
if (ulong.TryParse(article.QuerySelector("header > nav > a[class=quote]").TextContent, out ulong _postNumber))
post.PostNumber = _postNumber;
else
post.PostNumber = default;
//Get modlog
getModlog(article.QuerySelector("b.modLog")?.InnerHtml, out var postModlog);
post.ModLog = postModlog;
var figure = article.QuerySelector("figure > figcaption > i");
if (figure != null)
{
//Has image
if (TryParseImageDimInfo(figure.FirstChild.TextContent, out var _imageSize, out var _x, out var _y))
{
post.ImageDimensions = (_x, _y);
post.ImageSize = _imageSize;
post.ImageURL = figure.QuerySelector("a").GetAttribute("href");
post.ImageFilename = figure.QuerySelector("a").GetAttribute("download");
}
}
await EncryptIfRequired(post, token);
yield return post;
}
await EncryptIfRequired(thread, token);
}
private async Task EncryptIfRequired(PostInfo post, CancellationToken token)
{
try
{
if (Config.EncryptDeleted != null)
{
if (post.ModLog.ImageDeleted)
{
await post.EncryptImageAsync(Config.EncryptDeleted.Value, token);
}
if (post.ModLog.PostDeleted)
{
await post.EncryptPostAsync(Config.EncryptDeleted.Value, token);
}
}
}catch(Exception ex)
{
Console.WriteLine("Encryption for post "+post.PostNumber+" failed: " + ex.Message+"\n"+ex.StackTrace);
}
}
protected override async IAsyncEnumerable<ThreadInfo> GetThreads(BoardInfo boardInfo, [EnumeratorCancellation] CancellationToken token)
{
var document = await context.OpenAsync(boardInfo.BoardURL + "catalog", token);
await GetBoardInfo(boardInfo, document, token);
var threadLinks = document.QuerySelectorAll("#catalog > article > a[class=history]");
foreach(var link in threadLinks)
{
if (link.HasAttribute("href"))
{
var href = link.GetAttribute("href");
if (ulong.TryParse(href, out ulong postNumber))
{
yield return new ThreadInfo()
{
PostNumber = postNumber,
};
}
}
}
}
}
}

@ -1,218 +1,218 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using Tools.Crypto;
namespace napdump
{
class Program
{
public static readonly CancellationTokenSource globalCancel = new CancellationTokenSource();
public const string NineballBaseUrl = "https://nineball.party";
public static readonly string[] NineballBoards = new[] { "/nap/", "/srsbsn/", "/staff/"};
static volatile int totalThreadsDownloaded = 0;
static volatile int totalBoardsDownloaded = 0;
static volatile int totalPostsDownloaded = 0;
static async Task<BoardInfo> GrabBoard(Dumper dumper, string boardUrl, ChannelWriter<ThreadInfo> onNewThread, CancellationToken token)
{
TaskCompletionSource<BoardInfo> get = new TaskCompletionSource<BoardInfo>();
Dumper.Hooks hooks = new Dumper.Hooks()
{
OnBoardRetrieved = (bi) =>
{
if (!token.IsCancellationRequested)
get.SetResult(bi);
},
PrintDebug = false//boardUrl.EndsWith("srsbsn/"),
};
using var _reg_get_cancel = token.Register(() => get.SetException(new OperationCanceledException()));
Console.WriteLine("\r [" + dumper.GetType().Name + "] Downloading " + boardUrl);
try
{
await foreach (var thread in dumper.Parse(boardUrl, hooks).WithCancellation(token))
{
totalPostsDownloaded += thread.Children.Count + 1;
totalThreadsDownloaded += 1;
await onNewThread.WriteAsync(thread, token);
}
return await get.Task;
}
finally
{
Console.WriteLine("\r [" + dumper.GetType().Name + "] Complete " + boardUrl);
}
}
static async Task readOutputs(ChannelReader<ThreadInfo> reader, CancellationToken token)
{
Dictionary<string, int> numberPerBoard = new Dictionary<string, int>();
await foreach(var thread in reader.ReadAllAsync(token))
{
string name = thread.BoardInfo.BoardName;
if (numberPerBoard.ContainsKey(name))
numberPerBoard[name] += 1;
else numberPerBoard.Add(name, 1);
Console.Write($"\r");
foreach(var kv in numberPerBoard)
{
Console.Write($"{kv.Key} - {kv.Value} ");
}
}
Console.WriteLine();
}
static string getTimestamp(DateTime time)
{
return $"{time.Year}-{time.Month}-{time.Day}-{time.Hour}.{time.Minute}.{time.Second}.{time.Millisecond}";
}
static async Task DumpBoardInfo(BoardInfo bi)
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
if (!Directory.Exists("dumps"))
Directory.CreateDirectory("dumps");
var path = Path.Combine("dumps", $"{bi.SafeName ?? "unbound"}-{getTimestamp(DateTime.Now)}.board");
using (var fs = new FileStream(path, FileMode.Create))
{
await Task.Yield();
binf.Serialize(fs, bi);
}
Console.WriteLine($"\r {bi.BoardName} -> {path}");
}
static async Task<DumperConfig> ParseArgs(string[] args)
{
string findarg(string name)
{
for(int i=0;i<args.Length-1;i++)
{
if (args[i].ToLower() == name)
{
var r = args[i + 1];
args = args.Where((_, j) => j != i && j != (i + 1)).ToArray();
return r;
}
}
return null;
}
bool tryarg<T>(string name, TryParser<T> parser, out T value, T def = default)
{
var fa = findarg(name);
if(fa!=null)
{
if(! parser(fa, out value))
{
value = def;
return false;
}
return true;
}
value = def;
return false;
}
static bool defp(string inp, out string op)
{
op = inp;
return true;
}
string login;
string aesKeyfile;
int threads;
tryarg("--login", defp, out login);
tryarg<int>("--threads", int.TryParse, out threads, 3);
tryarg("--encrypt-deleted", defp, out aesKeyfile);
return new DumperConfig(threads, Cookies: login == null ? null : new[] { (NineballBaseUrl, "a=" + login) }, EncryptDeleted: aesKeyfile == null ? (AESKey?)null : (await getKey(aesKeyfile)));
}
private static async Task<AESKey> getKey(string fn)
{
using (var fs = new FileStream(fn, FileMode.Open, FileAccess.Read))
{
return await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Keyfile is password protected: "));
}
}
private delegate bool TryParser<T>(string input, out T value);
static async Task Main(string[] args)
{
if(args.Length==1 && args[0].ToLower()=="--help")
{
Console.WriteLine("napdump.exe [--login <login token `a'>] [--threads <concurrent downloader number>] [--encrypt-deleted <deleted key>]");
return;
}
using var napDownloader = new Dumpers.Nineball(await ParseArgs(args));
Console.CancelKeyPress += (o,e) =>
{
globalCancel.Cancel();
e.Cancel = true;
};
List<Task<BoardInfo>> downloaders = new List<Task<BoardInfo>>();
Channel<ThreadInfo> threads = Channel.CreateUnbounded<ThreadInfo>();
Task outputReader = readOutputs(threads.Reader, globalCancel.Token);
try
{
foreach (var board in NineballBoards)
{
downloaders.Add(Task.Run(async () =>
{
try
{
var bi = await GrabBoard(napDownloader, NineballBaseUrl + board, threads.Writer, globalCancel.Token);
totalBoardsDownloaded += 1;
try
{
await DumpBoardInfo(bi);
}
catch (Exception ex)
{
Console.WriteLine("Failed to dump board " + bi.BoardName + " to file: " + ex.Message);
}
return bi;
}
catch (Exception ex)
{
Console.WriteLine("Failed to download board " + board + ": " + ex.Message);
return default(BoardInfo);
}
}));
}
var boards = (await Task.WhenAll(downloaders)).Where(x => x != null);
Console.WriteLine("\n\nDownloaded Boards:");
foreach (var b in boards)
Console.WriteLine("\t" + b.ToString());
}
catch (Exception ex)
{
Console.WriteLine("\n\nError: " + ex.Message);
return;
}
finally
{
threads.Writer.Complete();
try
{
await outputReader;
}
catch (OperationCanceledException) { }
}
Console.WriteLine("Complete");
Console.WriteLine($"Downloaded {totalBoardsDownloaded} boards, with {totalThreadsDownloaded} threads, containing {totalPostsDownloaded} posts.");
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using Tools.Crypto;
namespace napdump
{
class Program
{
public static readonly CancellationTokenSource globalCancel = new CancellationTokenSource();
public const string NineballBaseUrl = "https://nineball.party";
public static readonly string[] NineballBoards = new[] { "/nap/", "/srsbsn/", "/staff/"};
static volatile int totalThreadsDownloaded = 0;
static volatile int totalBoardsDownloaded = 0;
static volatile int totalPostsDownloaded = 0;
static async Task<BoardInfo> GrabBoard(Dumper dumper, string boardUrl, ChannelWriter<ThreadInfo> onNewThread, CancellationToken token)
{
TaskCompletionSource<BoardInfo> get = new TaskCompletionSource<BoardInfo>();
Dumper.Hooks hooks = new Dumper.Hooks()
{
OnBoardRetrieved = (bi) =>
{
if (!token.IsCancellationRequested)
get.SetResult(bi);
},
PrintDebug = false//boardUrl.EndsWith("srsbsn/"),
};
using var _reg_get_cancel = token.Register(() => get.SetException(new OperationCanceledException()));
Console.WriteLine("\r [" + dumper.GetType().Name + "] Downloading " + boardUrl);
try
{
await foreach (var thread in dumper.Parse(boardUrl, hooks).WithCancellation(token))
{
totalPostsDownloaded += thread.Children.Count + 1;
totalThreadsDownloaded += 1;
await onNewThread.WriteAsync(thread, token);
}
return await get.Task;
}
finally
{
Console.WriteLine("\r [" + dumper.GetType().Name + "] Complete " + boardUrl);
}
}
static async Task readOutputs(ChannelReader<ThreadInfo> reader, CancellationToken token)
{
Dictionary<string, int> numberPerBoard = new Dictionary<string, int>();
await foreach(var thread in reader.ReadAllAsync(token))
{
string name = thread.BoardInfo.BoardName;
if (numberPerBoard.ContainsKey(name))
numberPerBoard[name] += 1;
else numberPerBoard.Add(name, 1);
Console.Write($"\r");
foreach(var kv in numberPerBoard)
{
Console.Write($"{kv.Key} - {kv.Value} ");
}
}
Console.WriteLine();
}
static string getTimestamp(DateTime time)
{
return $"{time.Year}-{time.Month}-{time.Day}-{time.Hour}.{time.Minute}.{time.Second}.{time.Millisecond}";
}
static async Task DumpBoardInfo(BoardInfo bi)
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
if (!Directory.Exists("dumps"))
Directory.CreateDirectory("dumps");
var path = Path.Combine("dumps", $"{bi.SafeName ?? "unbound"}-{getTimestamp(DateTime.Now)}.board");
using (var fs = new FileStream(path, FileMode.Create))
{
await Task.Yield();
binf.Serialize(fs, bi);
}
Console.WriteLine($"\r {bi.BoardName} -> {path}");
}
static async Task<DumperConfig> ParseArgs(string[] args)
{
string findarg(string name)
{
for(int i=0;i<args.Length-1;i++)
{
if (args[i].ToLower() == name)
{
var r = args[i + 1];
args = args.Where((_, j) => j != i && j != (i + 1)).ToArray();
return r;
}
}
return null;
}
bool tryarg<T>(string name, TryParser<T> parser, out T value, T def = default)
{
var fa = findarg(name);
if(fa!=null)
{
if(! parser(fa, out value))
{
value = def;
return false;
}
return true;
}
value = def;
return false;
}
static bool defp(string inp, out string op)
{
op = inp;
return true;
}
string login;
string aesKeyfile;
int threads;
tryarg("--login", defp, out login);
tryarg<int>("--threads", int.TryParse, out threads, 3);
tryarg("--encrypt-deleted", defp, out aesKeyfile);
return new DumperConfig(threads, Cookies: login == null ? null : new[] { (NineballBaseUrl, "a=" + login) }, EncryptDeleted: aesKeyfile == null ? (AESKey?)null : (await getKey(aesKeyfile)));
}
private static async Task<AESKey> getKey(string fn)
{
using (var fs = new FileStream(fn, FileMode.Open, FileAccess.Read))
{
return await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Keyfile is password protected: "));
}
}
private delegate bool TryParser<T>(string input, out T value);
static async Task Main(string[] args)
{
if(args.Length==1 && args[0].ToLower()=="--help")
{
Console.WriteLine("napdump.exe [--login <login token `a'>] [--threads <concurrent downloader number>] [--encrypt-deleted <deleted key>]");
return;
}
using var napDownloader = new Dumpers.Nineball(await ParseArgs(args));
Console.CancelKeyPress += (o,e) =>
{
globalCancel.Cancel();
e.Cancel = true;
};
List<Task<BoardInfo>> downloaders = new List<Task<BoardInfo>>();
Channel<ThreadInfo> threads = Channel.CreateUnbounded<ThreadInfo>();
Task outputReader = readOutputs(threads.Reader, globalCancel.Token);
try
{
foreach (var board in NineballBoards)
{
downloaders.Add(Task.Run(async () =>
{
try
{
var bi = await GrabBoard(napDownloader, NineballBaseUrl + board, threads.Writer, globalCancel.Token);
totalBoardsDownloaded += 1;
try
{
await DumpBoardInfo(bi);
}
catch (Exception ex)
{
Console.WriteLine("Failed to dump board " + bi.BoardName + " to file: " + ex.Message);
}
return bi;
}
catch (Exception ex)
{
Console.WriteLine("Failed to download board " + board + ": " + ex.Message);
return default(BoardInfo);
}
}));
}
var boards = (await Task.WhenAll(downloaders)).Where(x => x != null);
Console.WriteLine("\n\nDownloaded Boards:");
foreach (var b in boards)
Console.WriteLine("\t" + b.ToString());
}
catch (Exception ex)
{
Console.WriteLine("\n\nError: " + ex.Message);
return;
}
finally
{
threads.Writer.Complete();
try
{
await outputReader;
}
catch (OperationCanceledException) { }
}
Console.WriteLine("Complete");
Console.WriteLine($"Downloaded {totalBoardsDownloaded} boards, with {totalThreadsDownloaded} threads, containing {totalPostsDownloaded} posts.");
}
}
}

@ -126,10 +126,11 @@ namespace ndview
if (post.ImageURL != null)
await WriteImageFigure(index, post, token);
await using (await index.TagAsync("blockquote", token))
await WriteBody(index, post, token);
/*await using (await index.TagAsync("blockquote", token))
{
await index.AppendHtml(post.Body);
}
}*/
}
await imageExtractor;
@ -222,6 +223,11 @@ namespace ndview
{
await index.AppendHtml(post.Body);
}
/*if(post.ModLog.UserBanned)
{
await index.AppendHtml("<div class='ban'>USER WAS BANNED FOR THIS POST</div>");
}*/ //TODO: Idk?
}
private async Task WriteThread(HtmlGenerator index, ThreadInfo thread, DirectoryInfo img, bool wasEnc, CancellationToken token)
@ -327,7 +333,7 @@ namespace ndview
}
await using (await index.TagAsync("div", cancel, ("class", "stat")))
{
await index.Append($"Showing {Board.Threads.Count} threads containing {Board.Threads.Select(x => x.Children.Count).Sum()} posts and {Board.Threads.Count + Board.Threads.Select(x => x.Children.Where(y => y.ImageURL != null).Count()).Sum()} images.", cancel);
await index.Append($"Showing {Board.Threads.Count} threads containing {Board.Threads.Select(x => x.Children.Count).Sum()} posts and {Board.Threads.Count + Board.Threads.Select(x => x.Children.Where(y => y.IsImageEncrypted || y.ImageURL != null).Count()).Sum()} images.", cancel);
await index.AppendHtml($"<br />Taken at <time>{Board.DumpTimestamp.ToString()}</time>.<br />Original: <a href='{Board.BoardURL}'>", cancel);
await index.Append(Board.BoardName, cancel);
await index.AppendHtml($"</a>", cancel);
@ -340,6 +346,13 @@ namespace ndview
{
await using(await index.TagAsync("nav", cancel, ("class", "script")))
{
await using(await index.TagAsync("span", cancel)) {
await using(await index.TagAsync("a", cancel, ("href", Board.BoardURL)))
await index.Append(Board.BoardName);
await using(await index.TagAsync("time"))
await index.Append(Board.DumpTimestamp.ToString());
}
await using(await index.TagAsync("ul", cancel)) {
await using(await index.TagAsync("li", cancel))
await using(await index.TagAsync("a", cancel, ("href", "#!"), ("id", "expand_all_threads")))

@ -1,302 +1,304 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using ICSharpCode.SharpZipLib.Zip;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using Tools.Crypto;
namespace ndview
{
class Program
{
static void Usage()
{
Console.WriteLine("ndview [--deleted-key <deleted key>] <archive> [<key file>] <output directory>");
}
static async Task<TempFile> Decrypt(Stream from, Stream keyFile, Func<string> getPasssword, CancellationToken token)
{
using (var enc = new encaes.AesEncryptor(from) { KeepAlive = true })
{
enc.Key = await encaes.AesEncryptor.LoadKey(keyFile, getPasssword, token);
var tempFile = new TempFile();
try
{
await enc.Decrypt(tempFile.Stream, token);
return tempFile;
}
catch (Exception ex)
{
await tempFile.DisposeAsync();
throw ex;
}
}
}
static async Task<TempDirectory> Extract(Stream from, CancellationToken token)
{
var td = new TempDirectory();
try
{
FastZip zip = new FastZip();
await Task.Yield();
token.ThrowIfCancellationRequested();
zip.ExtractZip(from, td.Directory.FullName, FastZip.Overwrite.Always, (x) => true, null, null, false, false);
}
catch (Exception ex)
{
td.Dispose();
throw ex;
}
return td;
}
static (FileInfo BoardFile, DirectoryInfo ImagesDirectory) select(DirectoryInfo folder)
{
FileInfo bi = null;
DirectoryInfo di = null;
foreach (var fi in folder.GetFiles())
{
if (fi.Name.EndsWith(".board"))
{
string nnm = string.Join('.', fi.Name.Split(".")[..^1]);
if (Directory.Exists(Path.Combine(folder.FullName, nnm)))
{
return (fi, new DirectoryInfo(Path.Combine(folder.FullName, nnm)));
}
}
bi = fi;
}
if (bi == null) throw new InvalidDataException("No board info found.");
di = folder.GetDirectories().FirstOrDefault();
if (di == null) throw new InvalidDataException("No images dir found");
return (bi, di);
}
static async Task generate(Stream bif, DirectoryInfo images, DirectoryInfo output, CancellationToken token)
{
var binf = new BinaryFormatter();
await Task.Yield();
token.ThrowIfCancellationRequested();
var bi = (napdump.BoardInfo)binf.Deserialize(bif);
if (!output.Exists) output.Create();
var gen = new PageGenerator(bi, images);
Console.WriteLine($" starting {bi.BoardName} w/ {images.Name} -> {output.FullName}");
await gen.GenerateFull(output, token);
}
static Task<string> ReadPassword(string prompt = null, CancellationToken token = default)
{
TaskCompletionSource<string> comp = new TaskCompletionSource<string>();
List<char> pwd = new List<char>();
bool set = false;
_ = Task.Run(() =>
{
if (prompt != null)
Console.Write(prompt);
try
{
using var _c = token.Register(() =>
{
if (!set)
{
set = true;
comp.SetException(new OperationCanceledException());
}
});
while (true)
{
ConsoleKeyInfo i = Console.ReadKey(true);
if (token.IsCancellationRequested) break;
if (i.Key == ConsoleKey.Enter)
{
Console.WriteLine();
break;
}
else if (i.Key == ConsoleKey.Backspace)
{
if (pwd.Count > 0)
{
pwd.RemoveAt(pwd.Count - 1);
}
}
else if (i.KeyChar != '\u0000')
{
pwd.Add(i.KeyChar);
}
}
if (!set)
{
set = true;
if (token.IsCancellationRequested)
comp.SetException(new OperationCanceledException());
else
comp.SetResult(new string(pwd.ToArray()));
}
}
catch (Exception ex)
{
Console.WriteLine();
pwd.Clear();
if (!set)
{
set = true;
comp.SetException(ex);
}
}
});
return comp.Task;
}
static readonly CancellationTokenSource cancel = new CancellationTokenSource();
public static AESKey? DeletedKey { get; private set; } = null;
static async Task loadAesKey(string from)
{
using (var fs = new FileStream(from, FileMode.Open, FileAccess.Read))
{
DeletedKey = await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Key is password protected: "));
}
}
static Task parseArgs(ref string[] args)
{
string delk = null;
for(int i=0;i<args.Length-1;i++)
{
if(args[i].ToLower() == "--deleted-key")
{
delk = args[i + 1];
args = args.Where((_, j) => j != i && j != (i + 1)).ToArray();
}
}
if (delk == null) return Task.CompletedTask;
else
{
return loadAesKey(delk);
}
}
static async Task Main(string[] args)
{
if (args.Length < 2)
{
Usage();
return;
}
else
{
try
{
await parseArgs(ref args);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load deleted key: " + ex.Message);
return;
}
if (File.Exists(args[0]))
{
Console.CancelKeyPress += (o, e) =>
{
if (!cancel.IsCancellationRequested)
{
cancel.Cancel();
e.Cancel = true;
}
};
try
{
if (await encaes.AesEncryptor.IsEncryptedFile(args[0], cancel.Token))
{
//decrypt & extract
if (args.Length < 3) { Usage(); return; }
TempFile tf = null;
try
{
Console.WriteLine("Decrypting...");
using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read))
{
using (var keyStream = new FileStream(args[1], FileMode.Open, FileAccess.Read))
{
tf = await Decrypt(inputFile, keyStream, () =>
{
var tsk = ReadPassword("Password: ", cancel.Token);
tsk.Wait();
cancel.Token.ThrowIfCancellationRequested();
if (tsk.IsFaulted)
throw tsk.Exception;
return tsk.Result;
}, cancel.Token);
}
}
tf.Stream.Position = 0;
Console.WriteLine("Extracting...");
using (var tempd = await Extract(tf.Stream, cancel.Token))
{
Console.WriteLine("Selecting best matches");
var (boardFile, imagesDir) = select(tempd.Directory);
Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read))
await generate(fs, imagesDir, new DirectoryInfo(args[2]), cancel.Token);
Console.WriteLine("Complete");
}
}
finally
{
tf?.Dispose();
}
}
else
{
//extract
TempDirectory tempd;
using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read))
{
Console.WriteLine("Extracting...");
tempd = await Extract(inputFile, cancel.Token);
}
try
{
Console.WriteLine("Selecting best matches");
var (boardFile, imagesDir) = select(tempd.Directory);
Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read))
await generate(fs, imagesDir, new DirectoryInfo(args[1]), cancel.Token);
Console.WriteLine("Complete");
}
finally
{
tempd.Dispose();
}
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Console.WriteLine($"Error ({ex.GetType().Name}) in generation: {ex.Message}");
}
}
else Console.WriteLine("Input archive must exist.");
}
cancel.Dispose();
}
}
}
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using ICSharpCode.SharpZipLib.Zip;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using Tools.Crypto;
namespace ndview
{
class Program
{
static void Usage()
{
Console.WriteLine("ndview [--deleted-key <deleted key>] <archive> [<key file>] <output directory>");
}
static async Task<TempFile> Decrypt(Stream from, Stream keyFile, Func<string> getPasssword, CancellationToken token)
{
using (var enc = new encaes.AesEncryptor(from) { KeepAlive = true })
{
enc.Key = await encaes.AesEncryptor.LoadKey(keyFile, getPasssword, token);
var tempFile = new TempFile();
try
{
await enc.Decrypt(tempFile.Stream, token);
return tempFile;
}
catch (Exception ex)
{
await tempFile.DisposeAsync();
throw ex;
}
}
}
static async Task<TempDirectory> Extract(Stream from, CancellationToken token)
{
var td = new TempDirectory();
try
{
FastZip zip = new FastZip();
await Task.Yield();
token.ThrowIfCancellationRequested();
zip.ExtractZip(from, td.Directory.FullName, FastZip.Overwrite.Always, (x) => true, null, null, false, false);
}
catch (Exception ex)
{
td.Dispose();
throw ex;
}
return td;
}
static (FileInfo BoardFile, DirectoryInfo ImagesDirectory) select(DirectoryInfo folder)
{
FileInfo bi = null;
DirectoryInfo di = null;
foreach (var fi in folder.GetFiles())
{
if (fi.Name.EndsWith(".board"))
{
string nnm = string.Join('.', fi.Name.Split(".")[..^1]);
if (Directory.Exists(Path.Combine(folder.FullName, nnm)))
{
return (fi, new DirectoryInfo(Path.Combine(folder.FullName, nnm)));
}
}
bi = fi;
}
if (bi == null) throw new InvalidDataException("No board info found.");
di = folder.GetDirectories().FirstOrDefault();
if (di == null) throw new InvalidDataException("No images dir found");
return (bi, di);
}
static async Task generate(Stream bif, DirectoryInfo images, DirectoryInfo output, CancellationToken token)
{
var binf = new BinaryFormatter();
await Task.Yield();
token.ThrowIfCancellationRequested();
var bi = (napdump.BoardInfo)binf.Deserialize(bif);
if (!output.Exists) output.Create();
var gen = new PageGenerator(bi, images);
Console.WriteLine($" starting {bi.BoardName} w/ {images.Name} -> {output.FullName}");
await gen.GenerateFull(output, token);
}
static Task<string> ReadPassword(string prompt = null, CancellationToken token = default)
{
TaskCompletionSource<string> comp = new TaskCompletionSource<string>();
List<char> pwd = new List<char>();
bool set = false;
_ = Task.Run(() =>
{
if (prompt != null)
Console.Write(prompt);
try
{
using var _c = token.Register(() =>
{
if (!set)
{
set = true;
comp.SetException(new OperationCanceledException());
}
});
while (true)
{
ConsoleKeyInfo i = Console.ReadKey(true);
if (token.IsCancellationRequested) break;
if (i.Key == ConsoleKey.Enter)
{
Console.WriteLine();
break;
}
else if (i.Key == ConsoleKey.Backspace)
{
if (pwd.Count > 0)
{
pwd.RemoveAt(pwd.Count - 1);
}
}
else if (i.KeyChar != '\u0000')
{
pwd.Add(i.KeyChar);
}
}
if (!set)
{
set = true;
if (token.IsCancellationRequested)
comp.SetException(new OperationCanceledException());
else
comp.SetResult(new string(pwd.ToArray()));
}
}
catch (Exception ex)
{
Console.WriteLine();
pwd.Clear();
if (!set)
{
set = true;
comp.SetException(ex);
}
}
});
return comp.Task;
}
static readonly CancellationTokenSource cancel = new CancellationTokenSource();
public static AESKey? DeletedKey { get; private set; } = null;
static async Task loadAesKey(string from)
{
using (var fs = new FileStream(from, FileMode.Open, FileAccess.Read))
{
DeletedKey = await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Key is password protected: "));
}
}
static Task parseArgs(ref string[] args)
{
string delk = null;
for(int i=0;i<args.Length-1;i++)
{
if(args[i].ToLower() == "--deleted-key")
{
delk = args[i + 1];
args = args.Where((_, j) => j != i && j != (i + 1)).ToArray();
Console.WriteLine("Set deleted key to "+delk);
}
}
if (delk == null) return Task.CompletedTask;
else
{
return loadAesKey(delk);
}
}
static async Task Main(string[] args)
{
if (args.Length < 2)
{
Usage();
return;
}
else
{
try
{
await parseArgs(ref args);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load deleted key: " + ex.Message);
return;
}
if (File.Exists(args[0]))
{
Console.CancelKeyPress += (o, e) =>
{
if (!cancel.IsCancellationRequested)
{
cancel.Cancel();
e.Cancel = true;
}
};
try
{
if (await encaes.AesEncryptor.IsEncryptedFile(args[0], cancel.Token))
{
//decrypt & extract
if (args.Length < 3) { Usage(); return; }
TempFile tf = null;
try
{
Console.WriteLine("Decrypting...");
using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read))
{
using (var keyStream = new FileStream(args[1], FileMode.Open, FileAccess.Read))
{
tf = await Decrypt(inputFile, keyStream, () =>
{
var tsk = ReadPassword("Password: ", cancel.Token);
tsk.Wait();
cancel.Token.ThrowIfCancellationRequested();
if (tsk.IsFaulted)
throw tsk.Exception;
return tsk.Result;
}, cancel.Token);
}
}
tf.Stream.Position = 0;
Console.WriteLine("Extracting...");
using (var tempd = await Extract(tf.Stream, cancel.Token))
{
Console.WriteLine("Selecting best matches");
var (boardFile, imagesDir) = select(tempd.Directory);
Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read))
await generate(fs, imagesDir, new DirectoryInfo(args[2]), cancel.Token);
Console.WriteLine("Complete");
}
}
finally
{
tf?.Dispose();
}
}
else
{
//extract
TempDirectory tempd;
using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read))
{
Console.WriteLine("Extracting...");
tempd = await Extract(inputFile, cancel.Token);
}
try
{
Console.WriteLine("Selecting best matches");
var (boardFile, imagesDir) = select(tempd.Directory);
Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read))
await generate(fs, imagesDir, new DirectoryInfo(args[1]), cancel.Token);
Console.WriteLine("Complete");
}
finally
{
tempd.Dispose();
}
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Console.WriteLine($"Error ({ex.GetType().Name}) in generation: {ex.Message}");
Console.WriteLine(ex.StackTrace);
}
}
else Console.WriteLine("Input archive must exist.");
}
cancel.Dispose();
}
}
}

@ -119,13 +119,25 @@ a:hover {
nav {
position: fixed;
float: right;
right: 10px;
top: 10px;
background-color: white;
padding: 5px;
border: solid 1px;
border-color: black;
right: 0;
top: 0;
width: 100%;
background: rgba(214,218,240,.7);
border-bottom: 1px solid #b7c5d9;
}
nav > span {
display:inline-block;
padding-left: 10px;
}
nav ul {
display: flex;
justify-content: center;
list-style-type: none;
margin: 0;
padding: 0;
float:right;
}
nav :not(a)
@ -141,7 +153,7 @@ nav ul {
padding: 0;
}
nav ul::before {
content: "< ";
content: "[ ";
}
nav ul li {
padding: 0 2px;
@ -150,5 +162,32 @@ nav ul li:not(:last-child)::after {
content: " | ";
}
nav ul::after {
content: " >";
content: " ]";
}
nav > span > time {
padding-left: 10px;
padding-right: 10px;
}
article > header {
margin-left: 10px;
padding-top: 1px;
}
a, a:visited {
color: blue;
}
@media only screen and (max-width: 792px)
{
nav > span {
display:none!important;
}
}
@media only screen and (max-width: 542px)
{
nav {
display:none!important;
}
}

Loading…
Cancel
Save