diff --git a/napdump/Dumpers/Nineball.cs b/napdump/Dumpers/Nineball.cs index b4f1fbe..6464da7 100644 --- a/napdump/Dumpers/Nineball.cs +++ b/napdump/Dumpers/Nineball.cs @@ -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().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("
").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 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 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().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("
").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 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 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, + }; + } + } + } + } + } +} diff --git a/napdump/Program.cs b/napdump/Program.cs index 719b56b..67ebb5e 100644 --- a/napdump/Program.cs +++ b/napdump/Program.cs @@ -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 GrabBoard(Dumper dumper, string boardUrl, ChannelWriter onNewThread, CancellationToken token) - { - TaskCompletionSource get = new TaskCompletionSource(); - 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 reader, CancellationToken token) - { - Dictionary numberPerBoard = new Dictionary(); - 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 ParseArgs(string[] args) - { - string findarg(string name) - { - for(int i=0;i j != i && j != (i + 1)).ToArray(); - return r; - } - } - return null; - } - bool tryarg(string name, TryParser 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("--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 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(string input, out T value); - - static async Task Main(string[] args) - { - if(args.Length==1 && args[0].ToLower()=="--help") - { - Console.WriteLine("napdump.exe [--login ] [--threads ] [--encrypt-deleted ]"); - return; - } - using var napDownloader = new Dumpers.Nineball(await ParseArgs(args)); - - Console.CancelKeyPress += (o,e) => - { - globalCancel.Cancel(); - e.Cancel = true; - }; - - List> downloaders = new List>(); - Channel threads = Channel.CreateUnbounded(); - 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 GrabBoard(Dumper dumper, string boardUrl, ChannelWriter onNewThread, CancellationToken token) + { + TaskCompletionSource get = new TaskCompletionSource(); + 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 reader, CancellationToken token) + { + Dictionary numberPerBoard = new Dictionary(); + 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 ParseArgs(string[] args) + { + string findarg(string name) + { + for(int i=0;i j != i && j != (i + 1)).ToArray(); + return r; + } + } + return null; + } + bool tryarg(string name, TryParser 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("--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 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(string input, out T value); + + static async Task Main(string[] args) + { + if(args.Length==1 && args[0].ToLower()=="--help") + { + Console.WriteLine("napdump.exe [--login ] [--threads ] [--encrypt-deleted ]"); + return; + } + using var napDownloader = new Dumpers.Nineball(await ParseArgs(args)); + + Console.CancelKeyPress += (o,e) => + { + globalCancel.Cancel(); + e.Cancel = true; + }; + + List> downloaders = new List>(); + Channel threads = Channel.CreateUnbounded(); + 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."); + + } + } +} diff --git a/ndview/PageGenerator.cs b/ndview/PageGenerator.cs index 3a4e7c5..811567e 100644 --- a/ndview/PageGenerator.cs +++ b/ndview/PageGenerator.cs @@ -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("
USER WAS BANNED FOR THIS POST
"); + }*/ //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($"
Taken at .
Original: ", cancel); await index.Append(Board.BoardName, cancel); await index.AppendHtml($"", 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"))) diff --git a/ndview/Program.cs b/ndview/Program.cs index a403f76..5131790 100644 --- a/ndview/Program.cs +++ b/ndview/Program.cs @@ -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 ] [] "); - } - - static async Task Decrypt(Stream from, Stream keyFile, Func 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 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 ReadPassword(string prompt = null, CancellationToken token = default) - { - TaskCompletionSource comp = new TaskCompletionSource(); - List pwd = new List(); - - 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 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 ] [] "); + } + + static async Task Decrypt(Stream from, Stream keyFile, Func 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 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 ReadPassword(string prompt = null, CancellationToken token = default) + { + TaskCompletionSource comp = new TaskCompletionSource(); + List pwd = new List(); + + 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 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(); + + } + } +} diff --git a/ndview/Properties/static/css/base.css b/ndview/Properties/static/css/base.css index b019422..0c36d5f 100644 --- a/ndview/Properties/static/css/base.css +++ b/ndview/Properties/static/css/base.css @@ -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; + } }