refactor: update for api 9

This commit is contained in:
Anna 2023-09-28 21:09:50 -04:00
parent 672e83e186
commit 3eda6e5e57
Signed by: anna
GPG Key ID: D0943384CD9F87D1
23 changed files with 1046 additions and 1042 deletions

View File

@ -1,6 +1,6 @@
namespace ExpandedSearchInfo.Configs {
public abstract class BaseConfig {
public bool Enabled { get; set; } = true;
public bool DefaultExpanded { get; set; } = true;
}
}
namespace ExpandedSearchInfo.Configs;
public abstract class BaseConfig {
public bool Enabled { get; set; } = true;
public bool DefaultExpanded { get; set; } = true;
}

View File

@ -1,4 +1,4 @@
namespace ExpandedSearchInfo.Configs {
public class CarrdConfig : BaseConfig {
}
namespace ExpandedSearchInfo.Configs;
public class CarrdConfig : BaseConfig {
}

View File

@ -1,10 +1,10 @@
using Newtonsoft.Json;
namespace ExpandedSearchInfo.Configs {
public class FListConfig : BaseConfig {
[JsonConstructor]
public FListConfig() {
this.Enabled = false;
}
namespace ExpandedSearchInfo.Configs;
public class FListConfig : BaseConfig {
[JsonConstructor]
public FListConfig() {
this.Enabled = false;
}
}
}

View File

@ -1,4 +1,4 @@
namespace ExpandedSearchInfo.Configs {
public class PastebinConfig : BaseConfig {
}
namespace ExpandedSearchInfo.Configs;
public class PastebinConfig : BaseConfig {
}

View File

@ -1,4 +1,4 @@
namespace ExpandedSearchInfo.Configs {
public class PlainTextConfig : BaseConfig {
}
namespace ExpandedSearchInfo.Configs;
public class PlainTextConfig : BaseConfig {
}

View File

@ -1,4 +1,4 @@
namespace ExpandedSearchInfo.Configs {
public class RefsheetConfig : BaseConfig {
}
namespace ExpandedSearchInfo.Configs;
public class RefsheetConfig : BaseConfig {
}

View File

@ -1,47 +1,46 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Logging;
namespace ExpandedSearchInfo {
public class GameFunctions : IDisposable {
private Plugin Plugin { get; }
namespace ExpandedSearchInfo;
private delegate byte SearchInfoDownloadedDelegate(IntPtr data, IntPtr a2, IntPtr searchInfoPtr, IntPtr a4);
public class GameFunctions : IDisposable {
private Plugin Plugin { get; }
private readonly Hook<SearchInfoDownloadedDelegate>? _searchInfoDownloadedHook;
private delegate byte SearchInfoDownloadedDelegate(IntPtr data, IntPtr a2, IntPtr searchInfoPtr, IntPtr a4);
internal delegate void ReceiveSearchInfoEventDelegate(uint objectId, SeString info);
private readonly Hook<SearchInfoDownloadedDelegate>? _searchInfoDownloadedHook;
internal event ReceiveSearchInfoEventDelegate? ReceiveSearchInfo;
internal delegate void ReceiveSearchInfoEventDelegate(uint objectId, SeString info);
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
internal event ReceiveSearchInfoEventDelegate? ReceiveSearchInfo;
var sidPtr = this.Plugin.SigScanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 56 48 83 EC 20 49 8B E8 8B DA");
this._searchInfoDownloadedHook = Hook<SearchInfoDownloadedDelegate>.FromAddress(sidPtr, this.SearchInfoDownloaded);
this._searchInfoDownloadedHook.Enable();
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
var sidPtr = this.Plugin.SigScanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 56 48 83 EC 20 49 8B E8 8B DA");
this._searchInfoDownloadedHook = this.Plugin.GameInteropProvider.HookFromAddress<SearchInfoDownloadedDelegate>(sidPtr, this.SearchInfoDownloaded);
this._searchInfoDownloadedHook.Enable();
}
public void Dispose() {
this._searchInfoDownloadedHook?.Dispose();
}
private unsafe byte SearchInfoDownloaded(IntPtr data, IntPtr a2, IntPtr searchInfoPtr, IntPtr a4) {
var result = this._searchInfoDownloadedHook!.Original(data, a2, searchInfoPtr, a4);
try {
// Updated: 4.5
var actorId = *(uint*) (data + 48);
var searchInfo = Util.ReadRawSeString(searchInfoPtr);
this.ReceiveSearchInfo?.Invoke(actorId, searchInfo);
} catch (Exception ex) {
Plugin.Log.Error(ex, "Error in SearchInfoDownloaded hook");
}
public void Dispose() {
this._searchInfoDownloadedHook?.Dispose();
}
private unsafe byte SearchInfoDownloaded(IntPtr data, IntPtr a2, IntPtr searchInfoPtr, IntPtr a4) {
var result = this._searchInfoDownloadedHook!.Original(data, a2, searchInfoPtr, a4);
try {
// Updated: 4.5
var actorId = *(uint*) (data + 48);
var searchInfo = Util.ReadRawSeString(searchInfoPtr);
this.ReceiveSearchInfo?.Invoke(actorId, searchInfo);
} catch (Exception ex) {
PluginLog.LogError(ex, "Error in SearchInfoDownloaded hook");
}
return result;
}
return result;
}
}

View File

@ -1,57 +1,62 @@
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace ExpandedSearchInfo {
// ReSharper disable once ClassNeverInstantiated.Global
public class Plugin : IDalamudPlugin {
public string Name => "Expanded Search Info";
namespace ExpandedSearchInfo;
[PluginService]
internal DalamudPluginInterface Interface { get; init; } = null!;
// ReSharper disable once ClassNeverInstantiated.Global
public class Plugin : IDalamudPlugin {
internal static string Name => "Expanded Search Info";
[PluginService]
internal CommandManager CommandManager { get; init; } = null!;
[PluginService]
internal static IPluginLog Log { get; private set; } = null!;
[PluginService]
internal GameGui GameGui { get; init; } = null!;
[PluginService]
internal DalamudPluginInterface Interface { get; init; } = null!;
[PluginService]
internal ObjectTable ObjectTable { get; init; } = null!;
[PluginService]
internal ICommandManager CommandManager { get; init; } = null!;
[PluginService]
internal SigScanner SigScanner { get; init; } = null!;
[PluginService]
internal IGameGui GameGui { get; init; } = null!;
internal PluginConfiguration Config { get; }
internal GameFunctions Functions { get; }
internal SearchInfoRepository Repository { get; }
private PluginUi Ui { get; }
[PluginService]
internal IObjectTable ObjectTable { get; init; } = null!;
public Plugin() {
this.Config = (PluginConfiguration?) this.Interface.GetPluginConfig() ?? new PluginConfiguration();
this.Config.Initialise(this);
[PluginService]
internal ISigScanner SigScanner { get; init; } = null!;
this.Functions = new GameFunctions(this);
this.Repository = new SearchInfoRepository(this);
this.Ui = new PluginUi(this);
[PluginService]
internal IGameInteropProvider GameInteropProvider { get; init; } = null!;
this.CommandManager.AddHandler("/esi", new CommandInfo(this.OnCommand) {
HelpMessage = "Toggles Expanded Search Info's configuration window",
});
}
internal PluginConfiguration Config { get; }
internal GameFunctions Functions { get; }
internal SearchInfoRepository Repository { get; }
private PluginUi Ui { get; }
public void Dispose() {
this.CommandManager.RemoveHandler("/esi");
this.Ui.Dispose();
this.Repository.Dispose();
this.Functions.Dispose();
}
public Plugin() {
this.Config = (PluginConfiguration?) this.Interface.GetPluginConfig() ?? new PluginConfiguration();
this.Config.Initialise(this);
private void OnCommand(string command, string arguments) {
this.Ui.ConfigVisible = !this.Ui.ConfigVisible;
}
this.Functions = new GameFunctions(this);
this.Repository = new SearchInfoRepository(this);
this.Ui = new PluginUi(this);
this.CommandManager.AddHandler("/esi", new CommandInfo(this.OnCommand) {
HelpMessage = "Toggles Expanded Search Info's configuration window",
});
}
}
public void Dispose() {
this.CommandManager.RemoveHandler("/esi");
this.Ui.Dispose();
this.Repository.Dispose();
this.Functions.Dispose();
}
private void OnCommand(string command, string arguments) {
this.Ui.ConfigVisible = !this.Ui.ConfigVisible;
}
}

View File

@ -2,30 +2,30 @@
using Dalamud.Configuration;
using ExpandedSearchInfo.Configs;
namespace ExpandedSearchInfo {
[Serializable]
public class PluginConfiguration : IPluginConfiguration {
private Plugin Plugin { get; set; } = null!;
namespace ExpandedSearchInfo;
public int Version { get; set; } = 1;
[Serializable]
public class PluginConfiguration : IPluginConfiguration {
private Plugin Plugin { get; set; } = null!;
public ProviderConfigs Configs { get; set; } = new();
public int Version { get; set; } = 1;
internal void Initialise(Plugin plugin) {
this.Plugin = plugin;
}
public ProviderConfigs Configs { get; set; } = new();
internal void Save() {
this.Plugin.Interface.SavePluginConfig(this);
}
internal void Initialise(Plugin plugin) {
this.Plugin = plugin;
}
[Serializable]
public class ProviderConfigs {
public CarrdConfig Carrd { get; set; } = new();
public FListConfig FList { get; set; } = new();
public PastebinConfig Pastebin { get; set; } = new();
public PlainTextConfig PlainText { get; set; } = new();
public RefsheetConfig Refsheet { get; set; } = new();
internal void Save() {
this.Plugin.Interface.SavePluginConfig(this);
}
}
[Serializable]
public class ProviderConfigs {
public CarrdConfig Carrd { get; set; } = new();
public FListConfig FList { get; set; } = new();
public PastebinConfig Pastebin { get; set; } = new();
public PlainTextConfig PlainText { get; set; } = new();
public RefsheetConfig Refsheet { get; set; } = new();
}

View File

@ -2,232 +2,233 @@
using System.Diagnostics;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
namespace ExpandedSearchInfo {
public class PluginUi : IDisposable {
private Plugin Plugin { get; }
namespace ExpandedSearchInfo;
private bool _configVisible;
public class PluginUi : IDisposable {
private Plugin Plugin { get; }
internal bool ConfigVisible {
get => this._configVisible;
set => this._configVisible = value;
private bool _configVisible;
internal bool ConfigVisible {
get => this._configVisible;
set => this._configVisible = value;
}
internal PluginUi(Plugin plugin) {
this.Plugin = plugin;
this.Plugin.Interface.UiBuilder.Draw += this.Draw;
this.Plugin.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
}
private void OnOpenConfigUi() {
this.ConfigVisible = true;
}
public void Dispose() {
this.Plugin.Interface.UiBuilder.OpenConfigUi -= this.OnOpenConfigUi;
this.Plugin.Interface.UiBuilder.Draw -= this.Draw;
}
private static bool IconButton(FontAwesomeIcon icon, string? id = null) {
ImGui.PushFont(UiBuilder.IconFont);
var text = icon.ToIconString();
if (id != null) {
text += $"##{id}";
}
internal PluginUi(Plugin plugin) {
this.Plugin = plugin;
var result = ImGui.Button(text);
this.Plugin.Interface.UiBuilder.Draw += this.Draw;
this.Plugin.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
ImGui.PopFont();
return result;
}
private void Draw() {
this.DrawConfig();
this.DrawExpandedSearchInfo();
}
private void DrawConfig() {
if (!this.ConfigVisible) {
return;
}
private void OnOpenConfigUi() {
this.ConfigVisible = true;
ImGui.SetNextWindowSize(new Vector2(500, -1), ImGuiCond.FirstUseEver);
if (!ImGui.Begin($"{Plugin.Name} settings", ref this._configVisible)) {
return;
}
public void Dispose() {
this.Plugin.Interface.UiBuilder.OpenConfigUi -= this.OnOpenConfigUi;
this.Plugin.Interface.UiBuilder.Draw -= this.Draw;
ImGui.PushTextWrapPos();
if (ImGui.Button("Clear cache")) {
this.Plugin.Repository.SearchInfos.Clear();
}
private static bool IconButton(FontAwesomeIcon icon, string? id = null) {
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine();
var text = icon.ToIconString();
if (id != null) {
text += $"##{id}";
}
var cached = this.Plugin.Repository.SearchInfos.Count;
var playersString = cached switch {
1 => "One player",
_ => $"{cached} players",
};
ImGui.TextUnformatted($"{playersString} in the cache");
var result = ImGui.Button(text);
ImGui.Spacing();
ImGui.PopFont();
ImGui.TextUnformatted("Expanded Search Info downloads information contained in search infos once and caches it for later retrieval. If you want to clear this cache, click the button above. You can also clear individual players from the cache with the button in their expanded info.");
return result;
}
ImGui.Separator();
private void Draw() {
this.DrawConfig();
this.DrawExpandedSearchInfo();
}
private void DrawConfig() {
if (!this.ConfigVisible) {
if (ImGui.CollapsingHeader("Providers", ImGuiTreeNodeFlags.DefaultOpen)) {
if (!ImGui.BeginTabBar("ESI tabs")) {
return;
}
ImGui.SetNextWindowSize(new Vector2(500, -1), ImGuiCond.FirstUseEver);
foreach (var provider in this.Plugin.Repository.AllProviders) {
if (!ImGui.BeginTabItem($"{provider.Name}##esi-provider")) {
continue;
}
if (!ImGui.Begin($"{this.Plugin.Name} settings", ref this._configVisible)) {
return;
ImGui.Columns(2);
ImGui.SetColumnWidth(0, ImGui.GetWindowWidth() / 3);
ImGui.TextUnformatted(provider.Description);
ImGui.NextColumn();
var enabled = provider.Config.Enabled;
if (ImGui.Checkbox($"Enabled##{provider.Name}", ref enabled)) {
provider.Config.Enabled = enabled;
this.Plugin.Config.Save();
}
var defaultOpen = provider.Config.DefaultExpanded;
if (ImGui.Checkbox($"Open by default##{provider.Name}", ref defaultOpen)) {
provider.Config.DefaultExpanded = defaultOpen;
this.Plugin.Config.Save();
}
provider.DrawConfig();
ImGui.Columns(1);
ImGui.EndTabItem();
}
ImGui.PushTextWrapPos();
ImGui.EndTabBar();
}
if (ImGui.Button("Clear cache")) {
this.Plugin.Repository.SearchInfos.Clear();
ImGui.PopTextWrapPos();
ImGui.End();
}
private unsafe void DrawExpandedSearchInfo() {
// check if the examine window is open
var addonPtr = this.Plugin.GameGui.GetAddonByName("CharacterInspect", 1);
if (addonPtr == IntPtr.Zero) {
return;
}
var addon = (AtkUnitBase*) addonPtr;
if (!addon->IsVisible) {
return;
}
// get examine window info
var rootNode = addon->RootNode;
if (rootNode == null) {
return;
}
var width = rootNode->Width * addon->Scale;
var height = rootNode->Height * addon->Scale;
var x = addon->X;
var y = addon->Y;
// check the last actor id recorded (should be who the examine window is showing)
var actorId = this.Plugin.Repository.LastObjectId;
if (actorId == 0 || !this.Plugin.Repository.SearchInfos.TryGetValue(actorId, out var expanded)) {
return;
}
// set window size
ImGui.SetNextWindowSizeConstraints(
new Vector2(0, 0),
new Vector2(ImGui.GetIO().DisplaySize.X / 4, height)
);
ImGui.SetNextWindowSize(new Vector2(-1, -1));
if (!ImGui.Begin(Plugin.Name, ImGuiWindowFlags.NoTitleBar)) {
ImGui.End();
return;
}
ImGui.PushTextWrapPos(ImGui.GetIO().DisplaySize.X / 4 - 24);
// show a section for each extracted section
for (var i = 0; i < expanded.Sections.Count; i++) {
var section = expanded.Sections[i];
var flags = section.Provider.Config.DefaultExpanded switch {
true => ImGuiTreeNodeFlags.DefaultOpen,
false => ImGuiTreeNodeFlags.None,
};
if (!ImGui.CollapsingHeader($"{section.Name}##{i}", flags)) {
continue;
}
ImGui.TreePush();
if (IconButton(FontAwesomeIcon.ExternalLinkAlt, $"open-{i}")) {
Process.Start(new ProcessStartInfo {
FileName = section.Uri.ToString(),
UseShellExecute = true,
});
}
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted("Open in browser.");
ImGui.EndTooltip();
}
ImGui.SameLine();
var cached = this.Plugin.Repository.SearchInfos.Count;
var playersString = cached switch {
1 => "One player",
_ => $"{cached} players",
};
ImGui.TextUnformatted($"{playersString} in the cache");
ImGui.Spacing();
ImGui.TextUnformatted("Expanded Search Info downloads information contained in search infos once and caches it for later retrieval. If you want to clear this cache, click the button above. You can also clear individual players from the cache with the button in their expanded info.");
ImGui.Separator();
if (ImGui.CollapsingHeader("Providers", ImGuiTreeNodeFlags.DefaultOpen)) {
if (!ImGui.BeginTabBar("ESI tabs")) {
return;
}
foreach (var provider in this.Plugin.Repository.AllProviders) {
if (!ImGui.BeginTabItem($"{provider.Name}##esi-provider")) {
continue;
}
ImGui.Columns(2);
ImGui.SetColumnWidth(0, ImGui.GetWindowWidth() / 3);
ImGui.TextUnformatted(provider.Description);
ImGui.NextColumn();
var enabled = provider.Config.Enabled;
if (ImGui.Checkbox($"Enabled##{provider.Name}", ref enabled)) {
provider.Config.Enabled = enabled;
this.Plugin.Config.Save();
}
var defaultOpen = provider.Config.DefaultExpanded;
if (ImGui.Checkbox($"Open by default##{provider.Name}", ref defaultOpen)) {
provider.Config.DefaultExpanded = defaultOpen;
this.Plugin.Config.Save();
}
provider.DrawConfig();
ImGui.Columns(1);
ImGui.EndTabItem();
}
ImGui.EndTabBar();
if (IconButton(FontAwesomeIcon.Redo, $"refresh-{i}")) {
this.Plugin.Repository.SearchInfos.TryRemove(actorId, out _);
}
ImGui.PopTextWrapPos();
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted("Clear the cache. Re-examine this character to redownload information.");
ImGui.EndTooltip();
}
ImGui.End();
section.Draw();
ImGui.TreePop();
}
private unsafe void DrawExpandedSearchInfo() {
// check if the examine window is open
var addonPtr = this.Plugin.GameGui.GetAddonByName("CharacterInspect", 1);
if (addonPtr == IntPtr.Zero) {
return;
}
ImGui.PopTextWrapPos();
var addon = (AtkUnitBase*) addonPtr;
if (!addon->IsVisible) {
return;
}
// determine whether to show on the left or right of the examine window based on space available
var display = ImGui.GetIO().DisplaySize;
var actualWidth = ImGui.GetWindowWidth();
// get examine window info
var rootNode = addon->RootNode;
if (rootNode == null) {
return;
}
var xPos = x + width + actualWidth > display.X
? x - actualWidth
: x + width;
ImGui.SetWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(xPos, y));
var width = rootNode->Width * addon->Scale;
var height = rootNode->Height * addon->Scale;
var x = addon->X;
var y = addon->Y;
// check the last actor id recorded (should be who the examine window is showing)
var actorId = this.Plugin.Repository.LastObjectId;
if (actorId == 0 || !this.Plugin.Repository.SearchInfos.TryGetValue(actorId, out var expanded)) {
return;
}
// set window size
ImGui.SetNextWindowSizeConstraints(
new Vector2(0, 0),
new Vector2(ImGui.GetIO().DisplaySize.X / 4, height)
);
ImGui.SetNextWindowSize(new Vector2(-1, -1));
if (!ImGui.Begin(this.Plugin.Name, ImGuiWindowFlags.NoTitleBar)) {
ImGui.End();
return;
}
ImGui.PushTextWrapPos(ImGui.GetIO().DisplaySize.X / 4 - 24);
// show a section for each extracted section
for (var i = 0; i < expanded.Sections.Count; i++) {
var section = expanded.Sections[i];
var flags = section.Provider.Config.DefaultExpanded switch {
true => ImGuiTreeNodeFlags.DefaultOpen,
false => ImGuiTreeNodeFlags.None,
};
if (!ImGui.CollapsingHeader($"{section.Name}##{i}", flags)) {
continue;
}
ImGui.TreePush();
if (IconButton(FontAwesomeIcon.ExternalLinkAlt, $"open-{i}")) {
Process.Start(new ProcessStartInfo {
FileName = section.Uri.ToString(),
UseShellExecute = true,
});
}
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted("Open in browser.");
ImGui.EndTooltip();
}
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.Redo, $"refresh-{i}")) {
this.Plugin.Repository.SearchInfos.TryRemove(actorId, out _);
}
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted("Clear the cache. Re-examine this character to redownload information.");
ImGui.EndTooltip();
}
section.Draw();
ImGui.TreePop();
}
ImGui.PopTextWrapPos();
// determine whether to show on the left or right of the examine window based on space available
var display = ImGui.GetIO().DisplaySize;
var actualWidth = ImGui.GetWindowWidth();
var xPos = x + width + actualWidth > display.X
? x - actualWidth
: x + width;
ImGui.SetWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(xPos, y));
ImGui.End();
}
ImGui.End();
}
}
}

View File

@ -8,30 +8,30 @@ using AngleSharp.Html.Parser;
using ExpandedSearchInfo.Configs;
using ExpandedSearchInfo.Sections;
namespace ExpandedSearchInfo.Providers {
public abstract class BaseHtmlProvider : IProvider {
private IBrowsingContext Context { get; } = BrowsingContext.New();
namespace ExpandedSearchInfo.Providers;
public abstract string Name { get; }
public abstract class BaseHtmlProvider : IProvider {
private IBrowsingContext Context { get; } = BrowsingContext.New();
public abstract string Description { get; }
public abstract string Name { get; }
public abstract BaseConfig Config { get; }
public abstract string Description { get; }
public abstract bool ExtractsUris { get; }
public abstract BaseConfig Config { get; }
public abstract void DrawConfig();
public abstract bool ExtractsUris { get; }
public abstract bool Matches(Uri uri);
public abstract void DrawConfig();
public abstract IEnumerable<Uri>? ExtractUris(uint objectId, string info);
public abstract bool Matches(Uri uri);
public abstract Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response);
public abstract IEnumerable<Uri>? ExtractUris(uint objectId, string info);
protected async Task<IHtmlDocument> DownloadDocument(HttpResponseMessage response) {
var html = await response.Content.ReadAsStringAsync();
var parser = this.Context.GetService<IHtmlParser>();
return await parser.ParseDocumentAsync(html);
}
public abstract Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response);
protected async Task<IHtmlDocument> DownloadDocument(HttpResponseMessage response) {
var html = await response.Content.ReadAsStringAsync();
var parser = this.Context.GetService<IHtmlParser>();
return await parser.ParseDocumentAsync(html);
}
}
}

View File

@ -7,85 +7,85 @@ using AngleSharp.Dom;
using ExpandedSearchInfo.Configs;
using ExpandedSearchInfo.Sections;
namespace ExpandedSearchInfo.Providers {
public class CarrdProvider : BaseHtmlProvider {
private static readonly string[] Domains = {
".carrd.co",
".crd.co",
".carrd.com",
};
namespace ExpandedSearchInfo.Providers;
private Plugin Plugin { get; }
public class CarrdProvider : BaseHtmlProvider {
private static readonly string[] Domains = {
".carrd.co",
".crd.co",
".carrd.com",
};
public override string Name => "Carrd";
private Plugin Plugin { get; }
public override string Description => "This provider provides information for carrd.co URLs and their aliases.";
public override string Name => "Carrd";
public override BaseConfig Config => this.Plugin.Config.Configs.Carrd;
public override string Description => "This provider provides information for carrd.co URLs and their aliases.";
public override bool ExtractsUris => false;
public override BaseConfig Config => this.Plugin.Config.Configs.Carrd;
internal CarrdProvider(Plugin plugin) {
this.Plugin = plugin;
}
public override bool ExtractsUris => false;
public override void DrawConfig() {
}
internal CarrdProvider(Plugin plugin) {
this.Plugin = plugin;
}
public override bool Matches(Uri uri) => Domains.Any(domain => uri.Host.EndsWith(domain));
public override void DrawConfig() {
}
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public override bool Matches(Uri uri) => Domains.Any(domain => uri.Host.EndsWith(domain));
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
var text = string.Empty;
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
IElement? lastList = null;
var listNum = 1;
var text = string.Empty;
foreach (var element in document.QuerySelectorAll("p, [id ^= 'text']")) {
// check if this element is in an li
var inLi = element.ParentElement?.TagName == "LI";
// if the first element in a li, we need to prefix it
if (inLi && element.PreviousSibling == null) {
// check if this element is in the same list as the last list element we checked
if (element.ParentElement != lastList) {
// if not, update the last list and reset the counter
lastList = element.ParentElement;
listNum = 1;
}
IElement? lastList = null;
var listNum = 1;
// check if this list is an ol or ul
var isOl = element.ParentElement?.ParentElement?.TagName == "OL";
if (isOl) {
// use the list number for ol
text += $"{listNum++}. ";
} else {
// use a dash for ul
text += "- ";
}
foreach (var element in document.QuerySelectorAll("p, [id ^= 'text']")) {
// check if this element is in an li
var inLi = element.ParentElement?.TagName == "LI";
// if the first element in a li, we need to prefix it
if (inLi && element.PreviousSibling == null) {
// check if this element is in the same list as the last list element we checked
if (element.ParentElement != lastList) {
// if not, update the last list and reset the counter
lastList = element.ParentElement;
listNum = 1;
}
// add the text from each child node
foreach (var node in element.ChildNodes) {
text += node.Text();
// add an extra newline if the node is a br
if (node is IElement { TagName: "BR" }) {
text += '\n';
}
// check if this list is an ol or ul
var isOl = element.ParentElement?.ParentElement?.TagName == "OL";
if (isOl) {
// use the list number for ol
text += $"{listNum++}. ";
} else {
// use a dash for ul
text += "- ";
}
// add a newline after every element
text += '\n';
}
return new TextSection(
this,
$"{document.Title} (Carrd)",
response.RequestMessage!.RequestUri!,
text
);
// add the text from each child node
foreach (var node in element.ChildNodes) {
text += node.Text();
// add an extra newline if the node is a br
if (node is IElement { TagName: "BR" }) {
text += '\n';
}
}
// add a newline after every element
text += '\n';
}
return new TextSection(
this,
$"{document.Title} (Carrd)",
response.RequestMessage!.RequestUri!,
text
);
}
}
}

View File

@ -8,117 +8,117 @@ using AngleSharp.Dom;
using ExpandedSearchInfo.Configs;
using ExpandedSearchInfo.Sections;
namespace ExpandedSearchInfo.Providers {
public class FListProvider : BaseHtmlProvider {
private Plugin Plugin { get; }
namespace ExpandedSearchInfo.Providers;
public override string Name => "F-List (18+)";
public class FListProvider : BaseHtmlProvider {
private Plugin Plugin { get; }
public override string Description => "This provider provides information for F-List URLs. It also searches for F-List profiles matching the character's name if /c/ is in their search info.";
public override string Name => "F-List (18+)";
public override BaseConfig Config => this.Plugin.Config.Configs.FList;
public override string Description => "This provider provides information for F-List URLs. It also searches for F-List profiles matching the character's name if /c/ is in their search info.";
public override bool ExtractsUris => true;
public override BaseConfig Config => this.Plugin.Config.Configs.FList;
internal FListProvider(Plugin plugin) {
this.Plugin = plugin;
}
public override bool ExtractsUris => true;
public override void DrawConfig() {
}
public override bool Matches(Uri uri) => uri.Host is "www.f-list.net" or "f-list.net" && uri.AbsolutePath.StartsWith("/c/");
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) {
if (!info.ToLowerInvariant().Contains("c/")) {
return null;
}
var obj = this.Plugin.ObjectTable.FirstOrDefault(obj => obj.ObjectId == objectId);
if (obj == null) {
return null;
}
var safeName = obj.Name.ToString().Replace("'", "");
return new[] {
new Uri($"https://www.f-list.net/c/{Uri.EscapeUriString(safeName)}"),
};
}
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
var error = document.QuerySelector("#DisplayedMessage");
if (error != null) {
var errorText = error.Text();
if (errorText.Contains("No such character exists")) {
return null;
}
if (errorText.Contains("has been banned")) {
return null;
}
}
var stats = new List<Tuple<string, string>>();
var statBox = document.QuerySelector(".statbox");
if (statBox != null) {
foreach (var stat in statBox.Children) {
if (!stat.Matches(".taglabel")) {
continue;
}
var name = stat.Text().Trim().Trim(' ', '\r', '\n', '\t', ':');
var value = stat.NextSibling.Text().Trim(' ', '\r', '\n', '\t', ':');
stats.Add(new Tuple<string, string>(name, value));
}
}
var info = string.Empty;
var formatted = document.QuerySelector("#tabs-1 > .FormattedBlock");
if (formatted != null) {
foreach (var child in formatted.ChildNodes) {
info += child.Text();
if (child is IElement childElem && childElem.TagName != "BR") {
info += "\n";
}
}
}
// remove bbcode and turn special characters into normal ascii
info = info.StripBbCode().Normalize(NormalizationForm.FormKD);
var fave = KinkSection(document, "Character_FetishlistFave");
var yes = KinkSection(document, "Character_FetishlistYes");
var maybe = KinkSection(document, "Character_FetishlistMaybe");
var no = KinkSection(document, "Character_FetishlistNo");
var charName = document.Title.Split('-')[2].Trim();
return new FListSection(
this,
$"{charName} (F-List)",
response.RequestMessage!.RequestUri!,
info,
stats,
fave,
yes,
maybe,
no
);
}
private static List<Tuple<string, string>> KinkSection(IParentNode document, string id) {
var kinks = new List<Tuple<string, string>>();
var kinkElems = document.QuerySelectorAll($"#{id} > a");
foreach (var kink in kinkElems) {
var name = kink.Text().Trim();
var value = kink.Attributes.GetNamedItem("rel")?.Value ?? "";
kinks.Add(new Tuple<string, string>(name, value));
}
return kinks;
}
internal FListProvider(Plugin plugin) {
this.Plugin = plugin;
}
}
public override void DrawConfig() {
}
public override bool Matches(Uri uri) => uri.Host is "www.f-list.net" or "f-list.net" && uri.AbsolutePath.StartsWith("/c/");
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) {
if (!info.ToLowerInvariant().Contains("c/")) {
return null;
}
var obj = this.Plugin.ObjectTable.FirstOrDefault(obj => obj.ObjectId == objectId);
if (obj == null) {
return null;
}
var safeName = obj.Name.ToString().Replace("'", "");
return new[] {
new Uri($"https://www.f-list.net/c/{Uri.EscapeUriString(safeName)}"),
};
}
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
var error = document.QuerySelector("#DisplayedMessage");
if (error != null) {
var errorText = error.Text();
if (errorText.Contains("No such character exists")) {
return null;
}
if (errorText.Contains("has been banned")) {
return null;
}
}
var stats = new List<Tuple<string, string>>();
var statBox = document.QuerySelector(".statbox");
if (statBox != null) {
foreach (var stat in statBox.Children) {
if (!stat.Matches(".taglabel")) {
continue;
}
var name = stat.Text().Trim().Trim(' ', '\r', '\n', '\t', ':');
var value = stat.NextSibling.Text().Trim(' ', '\r', '\n', '\t', ':');
stats.Add(new Tuple<string, string>(name, value));
}
}
var info = string.Empty;
var formatted = document.QuerySelector("#tabs-1 > .FormattedBlock");
if (formatted != null) {
foreach (var child in formatted.ChildNodes) {
info += child.Text();
if (child is IElement childElem && childElem.TagName != "BR") {
info += "\n";
}
}
}
// remove bbcode and turn special characters into normal ascii
info = info.StripBbCode().Normalize(NormalizationForm.FormKD);
var fave = KinkSection(document, "Character_FetishlistFave");
var yes = KinkSection(document, "Character_FetishlistYes");
var maybe = KinkSection(document, "Character_FetishlistMaybe");
var no = KinkSection(document, "Character_FetishlistNo");
var charName = document.Title.Split('-')[2].Trim();
return new FListSection(
this,
$"{charName} (F-List)",
response.RequestMessage!.RequestUri!,
info,
stats,
fave,
yes,
maybe,
no
);
}
private static List<Tuple<string, string>> KinkSection(IParentNode document, string id) {
var kinks = new List<Tuple<string, string>>();
var kinkElems = document.QuerySelectorAll($"#{id} > a");
foreach (var kink in kinkElems) {
var name = kink.Text().Trim();
var value = kink.Attributes.GetNamedItem("rel")?.Value ?? "";
kinks.Add(new Tuple<string, string>(name, value));
}
return kinks;
}
}

View File

@ -5,52 +5,52 @@ using System.Threading.Tasks;
using ExpandedSearchInfo.Configs;
using ExpandedSearchInfo.Sections;
namespace ExpandedSearchInfo.Providers {
public interface IProvider {
string Name { get; }
namespace ExpandedSearchInfo.Providers;
string Description { get; }
public interface IProvider {
string Name { get; }
BaseConfig Config { get; }
string Description { get; }
/// <summary>
/// If this provider is capable of parsing the search info for custom Uris, this should be true.
///
/// Note that normal Uris are parsed by the plugin itself, so this can remain false for providers
/// that only handle normal Uris.
/// </summary>
bool ExtractsUris { get; }
BaseConfig Config { get; }
void DrawConfig();
/// <summary>
/// If this provider is capable of parsing the search info for custom Uris, this should be true.
///
/// Note that normal Uris are parsed by the plugin itself, so this can remain false for providers
/// that only handle normal Uris.
/// </summary>
bool ExtractsUris { get; }
/// <summary>
/// Determine if this provider should run on the given Uri.
/// </summary>
/// <param name="uri">Uri to test</param>
/// <returns>true if this provider's Extract method should be run for the HTTP response from this Uri</returns>
bool Matches(Uri uri);
void DrawConfig();
/// <summary>
/// For providers that require Uris, this can return null.
/// For providers that don't require Uris, this must return a Uri extracted from the given search info.
/// </summary>
/// <param name="objectId">The actor ID associated with the search info</param>
/// <param name="info">A character's full search info</param>
/// <returns>null for providers that require Uris, a Uri for providers that don't</returns>
IEnumerable<Uri>? ExtractUris(uint objectId, string info);
/// <summary>
/// Determine if this provider should run on the given Uri.
/// </summary>
/// <param name="uri">Uri to test</param>
/// <returns>true if this provider's Extract method should be run for the HTTP response from this Uri</returns>
bool Matches(Uri uri);
/// <summary>
/// Extract the search info to be displayed given the HTTP response from a Uri.
/// </summary>
/// <param name="response">HTTP response from a Uri</param>
/// <returns>null if search info could not be extracted or the search info as a string if it could</returns>
Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response);
/// <summary>
/// For providers that require Uris, this can return null.
/// For providers that don't require Uris, this must return a Uri extracted from the given search info.
/// </summary>
/// <param name="objectId">The actor ID associated with the search info</param>
/// <param name="info">A character's full search info</param>
/// <returns>null for providers that require Uris, a Uri for providers that don't</returns>
IEnumerable<Uri>? ExtractUris(uint objectId, string info);
/// <summary>
/// Modify any requests made for this provider before they are sent.
/// </summary>
/// <param name="request">HTTP request about to be sent</param>
void ModifyRequest(HttpRequestMessage request) {
}
/// <summary>
/// Extract the search info to be displayed given the HTTP response from a Uri.
/// </summary>
/// <param name="response">HTTP response from a Uri</param>
/// <returns>null if search info could not be extracted or the search info as a string if it could</returns>
Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response);
/// <summary>
/// Modify any requests made for this provider before they are sent.
/// </summary>
/// <param name="request">HTTP request about to be sent</param>
void ModifyRequest(HttpRequestMessage request) {
}
}
}

View File

@ -7,46 +7,46 @@ using System.Threading.Tasks;
using ExpandedSearchInfo.Configs;
using ExpandedSearchInfo.Sections;
namespace ExpandedSearchInfo.Providers {
public class PastebinProvider : IProvider {
private static readonly Regex Matcher = new(@"pb:(\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
namespace ExpandedSearchInfo.Providers;
private Plugin Plugin { get; }
public class PastebinProvider : IProvider {
private static readonly Regex Matcher = new(@"pb:(\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string Name => "pastebin.com";
private Plugin Plugin { get; }
public string Description => "This provider provides information from pastebin.com URLs. It also works on tags such as \"pb:pasteid\".";
public string Name => "pastebin.com";
public BaseConfig Config => this.Plugin.Config.Configs.Pastebin;
public string Description => "This provider provides information from pastebin.com URLs. It also works on tags such as \"pb:pasteid\".";
public bool ExtractsUris => true;
public BaseConfig Config => this.Plugin.Config.Configs.Pastebin;
public PastebinProvider(Plugin plugin) {
this.Plugin = plugin;
}
public bool ExtractsUris => true;
public void DrawConfig() {
}
public bool Matches(Uri uri) => uri.Host == "pastebin.com" && uri.AbsolutePath.Length > 1;
public IEnumerable<Uri>? ExtractUris(uint objectId, string info) {
var matches = Matcher.Matches(info);
return matches.Count == 0
? null
: from Match match in matches select match.Groups[1].Value into id select new Uri($"https://pastebin.com/raw/{id}");
}
public async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
if (response.Content.Headers.ContentType?.MediaType != "text/plain") {
return null;
}
var id = response.RequestMessage!.RequestUri!.AbsolutePath.Split('/').LastOrDefault();
var info = await response.Content.ReadAsStringAsync();
return new TextSection(this, $"Pastebin ({id})", response.RequestMessage.RequestUri, info);
}
public PastebinProvider(Plugin plugin) {
this.Plugin = plugin;
}
}
public void DrawConfig() {
}
public bool Matches(Uri uri) => uri.Host == "pastebin.com" && uri.AbsolutePath.Length > 1;
public IEnumerable<Uri>? ExtractUris(uint objectId, string info) {
var matches = Matcher.Matches(info);
return matches.Count == 0
? null
: from Match match in matches select match.Groups[1].Value into id select new Uri($"https://pastebin.com/raw/{id}");
}
public async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
if (response.Content.Headers.ContentType?.MediaType != "text/plain") {
return null;
}
var id = response.RequestMessage!.RequestUri!.AbsolutePath.Split('/').LastOrDefault();
var info = await response.Content.ReadAsStringAsync();
return new TextSection(this, $"Pastebin ({id})", response.RequestMessage.RequestUri, info);
}
}

View File

@ -6,42 +6,42 @@ using System.Threading.Tasks;
using ExpandedSearchInfo.Configs;
using ExpandedSearchInfo.Sections;
namespace ExpandedSearchInfo.Providers {
public class PlainTextProvider : IProvider {
private Plugin Plugin { get; }
namespace ExpandedSearchInfo.Providers;
public string Name => "Plain text";
public class PlainTextProvider : IProvider {
private Plugin Plugin { get; }
public string Description => "This provider provides information for any URL that provides plain text.";
public string Name => "Plain text";
public BaseConfig Config => this.Plugin.Config.Configs.PlainText;
public string Description => "This provider provides information for any URL that provides plain text.";
public bool ExtractsUris => false;
public BaseConfig Config => this.Plugin.Config.Configs.PlainText;
internal PlainTextProvider(Plugin plugin) {
this.Plugin = plugin;
}
public bool ExtractsUris => false;
public void DrawConfig() {
}
public bool Matches(Uri uri) => true;
public IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
if (response.Content.Headers.ContentType?.MediaType != "text/plain") {
return null;
}
var info = await response.Content.ReadAsStringAsync();
var uri = response.RequestMessage!.RequestUri!;
return new TextSection(this, $"Text##{uri}", uri, info);
}
public void ModifyRequest(HttpRequestMessage request) {
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
}
internal PlainTextProvider(Plugin plugin) {
this.Plugin = plugin;
}
}
public void DrawConfig() {
}
public bool Matches(Uri uri) => true;
public IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
if (response.Content.Headers.ContentType?.MediaType != "text/plain") {
return null;
}
var info = await response.Content.ReadAsStringAsync();
var uri = response.RequestMessage!.RequestUri!;
return new TextSection(this, $"Text##{uri}", uri, info);
}
public void ModifyRequest(HttpRequestMessage request) {
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
}
}

View File

@ -8,155 +8,155 @@ using ExpandedSearchInfo.Sections;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace ExpandedSearchInfo.Providers {
public class RefsheetProvider : BaseHtmlProvider {
private const string JsonLineStart = "var props = ";
namespace ExpandedSearchInfo.Providers;
private Plugin Plugin { get; }
public class RefsheetProvider : BaseHtmlProvider {
private const string JsonLineStart = "var props = ";
public override string Name => "Refsheet";
private Plugin Plugin { get; }
public override string Description => "This provider provides information for refsheet.net and ref.st URLs.";
public override string Name => "Refsheet";
public override BaseConfig Config => this.Plugin.Config.Configs.Refsheet;
public override string Description => "This provider provides information for refsheet.net and ref.st URLs.";
public override bool ExtractsUris => false;
public override BaseConfig Config => this.Plugin.Config.Configs.Refsheet;
internal RefsheetProvider(Plugin plugin) {
this.Plugin = plugin;
}
public override bool ExtractsUris => false;
public override void DrawConfig() {
}
public override bool Matches(Uri uri) => uri.Host is "refsheet.net" or "ref.st";
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
// refsheet provides all the content but... uses js to display it?
// find the script containing the json and use it
var script = document.QuerySelectorAll("script[type = 'text/javascript']").LastOrDefault();
if (script == null) {
return null;
}
var jsonLine = script.InnerHtml.Split('\n')
.Select(line => line.Trim())
.FirstOrDefault(line => line.StartsWith(JsonLineStart));
if (jsonLine == null) {
return null;
}
var json = jsonLine.Substring(JsonLineStart.Length, jsonLine.Length - JsonLineStart.Length - 1);
var parsed = JsonConvert.DeserializeObject<RefsheetData>(json);
if (parsed == null) {
return null;
}
var character = parsed.EagerLoad.Character;
// get character name
var name = character.Name;
// get all attributes
var attributes = new List<Tuple<string, string>>();
// handle built-in attributes first
if (!string.IsNullOrWhiteSpace(character.Gender)) {
attributes.Add(new Tuple<string, string>("Gender", character.Gender));
}
if (!string.IsNullOrWhiteSpace(character.Species)) {
attributes.Add(new Tuple<string, string>("Species", character.Species));
}
if (!string.IsNullOrWhiteSpace(character.Height)) {
attributes.Add(new Tuple<string, string>("Height", character.Height));
}
if (!string.IsNullOrWhiteSpace(character.Weight)) {
attributes.Add(new Tuple<string, string>("Weight", character.Weight));
}
if (!string.IsNullOrWhiteSpace(character.BodyType)) {
attributes.Add(new Tuple<string, string>("Body type", character.BodyType));
}
if (!string.IsNullOrWhiteSpace(character.Personality)) {
attributes.Add(new Tuple<string, string>("Personality", character.Personality));
}
// then look for custom attributes
foreach (var attr in character.CustomAttributes) {
attributes.Add(new Tuple<string, string>(attr.Name, attr.Value));
}
// get important notes
var notes = character.SpecialNotes;
// get cards
var cards = new List<Tuple<string, string>>();
// get about card
if (!string.IsNullOrWhiteSpace(character.Profile)) {
cards.Add(new Tuple<string, string>($"About {character.Name}", character.Profile));
}
// get likes/dislikes cards
if (!string.IsNullOrWhiteSpace(character.Likes)) {
cards.Add(new Tuple<string, string>("Likes", character.Likes));
}
if (!string.IsNullOrWhiteSpace(character.Dislikes)) {
cards.Add(new Tuple<string, string>("Dislikes", character.Dislikes));
}
return new RefsheetSection(
this,
$"{name} (Refsheet)",
response.RequestMessage!.RequestUri!,
attributes,
notes,
cards
);
}
#pragma warning disable 8618
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
private class RefsheetData {
public RefsheetEagerLoad EagerLoad { get; set; }
}
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
private class RefsheetEagerLoad {
public RefsheetCharacter Character { get; set; }
}
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
private class RefsheetCharacter {
public string Name { get; set; }
public string Profile { get; set; }
public string Gender { get; set; }
public string Species { get; set; }
public string Height { get; set; }
public string Weight { get; set; }
public string BodyType { get; set; }
public string Personality { get; set; }
public string SpecialNotes { get; set; }
public string Likes { get; set; }
public string Dislikes { get; set; }
public List<RefsheetCustomAttribute> CustomAttributes { get; set; }
}
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
private class RefsheetCustomAttribute {
public string Name { get; set; }
public string Value { get; set; }
public string Id { get; set; }
}
#pragma warning restore 8618
internal RefsheetProvider(Plugin plugin) {
this.Plugin = plugin;
}
}
public override void DrawConfig() {
}
public override bool Matches(Uri uri) => uri.Host is "refsheet.net" or "ref.st";
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
// refsheet provides all the content but... uses js to display it?
// find the script containing the json and use it
var script = document.QuerySelectorAll("script[type = 'text/javascript']").LastOrDefault();
if (script == null) {
return null;
}
var jsonLine = script.InnerHtml.Split('\n')
.Select(line => line.Trim())
.FirstOrDefault(line => line.StartsWith(JsonLineStart));
if (jsonLine == null) {
return null;
}
var json = jsonLine.Substring(JsonLineStart.Length, jsonLine.Length - JsonLineStart.Length - 1);
var parsed = JsonConvert.DeserializeObject<RefsheetData>(json);
if (parsed == null) {
return null;
}
var character = parsed.EagerLoad.Character;
// get character name
var name = character.Name;
// get all attributes
var attributes = new List<Tuple<string, string>>();
// handle built-in attributes first
if (!string.IsNullOrWhiteSpace(character.Gender)) {
attributes.Add(new Tuple<string, string>("Gender", character.Gender));
}
if (!string.IsNullOrWhiteSpace(character.Species)) {
attributes.Add(new Tuple<string, string>("Species", character.Species));
}
if (!string.IsNullOrWhiteSpace(character.Height)) {
attributes.Add(new Tuple<string, string>("Height", character.Height));
}
if (!string.IsNullOrWhiteSpace(character.Weight)) {
attributes.Add(new Tuple<string, string>("Weight", character.Weight));
}
if (!string.IsNullOrWhiteSpace(character.BodyType)) {
attributes.Add(new Tuple<string, string>("Body type", character.BodyType));
}
if (!string.IsNullOrWhiteSpace(character.Personality)) {
attributes.Add(new Tuple<string, string>("Personality", character.Personality));
}
// then look for custom attributes
foreach (var attr in character.CustomAttributes) {
attributes.Add(new Tuple<string, string>(attr.Name, attr.Value));
}
// get important notes
var notes = character.SpecialNotes;
// get cards
var cards = new List<Tuple<string, string>>();
// get about card
if (!string.IsNullOrWhiteSpace(character.Profile)) {
cards.Add(new Tuple<string, string>($"About {character.Name}", character.Profile));
}
// get likes/dislikes cards
if (!string.IsNullOrWhiteSpace(character.Likes)) {
cards.Add(new Tuple<string, string>("Likes", character.Likes));
}
if (!string.IsNullOrWhiteSpace(character.Dislikes)) {
cards.Add(new Tuple<string, string>("Dislikes", character.Dislikes));
}
return new RefsheetSection(
this,
$"{name} (Refsheet)",
response.RequestMessage!.RequestUri!,
attributes,
notes,
cards
);
}
#pragma warning disable 8618
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
private class RefsheetData {
public RefsheetEagerLoad EagerLoad { get; set; }
}
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
private class RefsheetEagerLoad {
public RefsheetCharacter Character { get; set; }
}
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
private class RefsheetCharacter {
public string Name { get; set; }
public string Profile { get; set; }
public string Gender { get; set; }
public string Species { get; set; }
public string Height { get; set; }
public string Weight { get; set; }
public string BodyType { get; set; }
public string Personality { get; set; }
public string SpecialNotes { get; set; }
public string Likes { get; set; }
public string Dislikes { get; set; }
public List<RefsheetCustomAttribute> CustomAttributes { get; set; }
}
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
private class RefsheetCustomAttribute {
public string Name { get; set; }
public string Value { get; set; }
public string Id { get; set; }
}
#pragma warning restore 8618
}

View File

@ -7,199 +7,198 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Logging;
using ExpandedSearchInfo.Providers;
using ExpandedSearchInfo.Sections;
using Nager.PublicSuffix;
namespace ExpandedSearchInfo {
public class ExpandedSearchInfo {
public string Info { get; }
public List<ISearchInfoSection> Sections { get; }
namespace ExpandedSearchInfo;
public ExpandedSearchInfo(string info, List<ISearchInfoSection> sections) {
this.Info = info;
this.Sections = sections;
public class ExpandedSearchInfo {
public string Info { get; }
public List<ISearchInfoSection> Sections { get; }
public ExpandedSearchInfo(string info, List<ISearchInfoSection> sections) {
this.Info = info;
this.Sections = sections;
}
}
public class SearchInfoRepository : IDisposable {
private Plugin Plugin { get; }
private DomainParser Parser { get; }
internal ConcurrentDictionary<uint, ExpandedSearchInfo> SearchInfos { get; } = new();
internal uint LastObjectId { get; private set; }
private List<IProvider> Providers { get; } = new();
internal IEnumerable<IProvider> AllProviders => this.Providers;
internal SearchInfoRepository(Plugin plugin) {
this.Plugin = plugin;
// create the public suffix list parser
var provider = new WebTldRuleProvider();
if (!provider.CacheProvider.IsCacheValid()) {
provider.BuildAsync().GetAwaiter().GetResult();
}
this.Parser = new DomainParser(provider);
// add providers
this.AddProviders();
// listen for search info
this.Plugin.Functions.ReceiveSearchInfo += this.ProcessSearchInfo;
}
public class SearchInfoRepository : IDisposable {
private Plugin Plugin { get; }
private DomainParser Parser { get; }
internal ConcurrentDictionary<uint, ExpandedSearchInfo> SearchInfos { get; } = new();
internal uint LastObjectId { get; private set; }
public void Dispose() {
this.Plugin.Functions.ReceiveSearchInfo -= this.ProcessSearchInfo;
}
private List<IProvider> Providers { get; } = new();
internal IEnumerable<IProvider> AllProviders => this.Providers;
private void AddProviders() {
this.Providers.Add(new PastebinProvider(this.Plugin));
this.Providers.Add(new CarrdProvider(this.Plugin));
this.Providers.Add(new FListProvider(this.Plugin));
this.Providers.Add(new RefsheetProvider(this.Plugin));
this.Providers.Add(new PlainTextProvider(this.Plugin));
}
internal SearchInfoRepository(Plugin plugin) {
this.Plugin = plugin;
private void ProcessSearchInfo(uint objectId, SeString raw) {
this.LastObjectId = objectId;
// create the public suffix list parser
var provider = new WebTldRuleProvider();
if (!provider.CacheProvider.IsCacheValid()) {
provider.BuildAsync().GetAwaiter().GetResult();
}
var info = raw.TextValue;
this.Parser = new DomainParser(provider);
// add providers
this.AddProviders();
// listen for search info
this.Plugin.Functions.ReceiveSearchInfo += this.ProcessSearchInfo;
// if empty search info, short circuit
if (string.IsNullOrWhiteSpace(info)) {
// remove any existing search info
this.SearchInfos.TryRemove(objectId, out _);
return;
}
public void Dispose() {
this.Plugin.Functions.ReceiveSearchInfo -= this.ProcessSearchInfo;
}
private void AddProviders() {
this.Providers.Add(new PastebinProvider(this.Plugin));
this.Providers.Add(new CarrdProvider(this.Plugin));
this.Providers.Add(new FListProvider(this.Plugin));
this.Providers.Add(new RefsheetProvider(this.Plugin));
this.Providers.Add(new PlainTextProvider(this.Plugin));
}
private void ProcessSearchInfo(uint objectId, SeString raw) {
this.LastObjectId = objectId;
var info = raw.TextValue;
// if empty search info, short circuit
if (string.IsNullOrWhiteSpace(info)) {
// remove any existing search info
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// check to see if info has changed
#if RELEASE
// check to see if info has changed
#if RELEASE
if (this.SearchInfos.TryGetValue(objectId, out var existing)) {
if (existing.Info == info) {
return;
}
}
#endif
#endif
Task.Run(async () => {
try {
await this.DoExtraction(objectId, info);
} catch (Exception ex) {
PluginLog.LogError(ex, "Error in extraction thread");
}
});
}
private async Task DoExtraction(uint objectId, string info) {
var downloadUris = new List<Uri>();
// extract uris from the search info with providers
var extractedUris = this.Providers
.Where(provider => provider.Config.Enabled && provider.ExtractsUris)
.Select(provider => provider.ExtractUris(objectId, info))
.Where(uris => uris != null)
.SelectMany(uris => uris!);
// add the extracted uris to the list
downloadUris.AddRange(extractedUris);
// go word-by-word and try to parse a uri
foreach (var word in info.Split(' ', '\n', '\r')) {
Uri found;
try {
found = new UriBuilder(word.Trim()).Uri;
} catch (UriFormatException) {
continue;
}
// make sure the hostname is a valid domain
try {
if (!this.Parser.IsValidDomain(found.Host)) {
continue;
}
} catch (ParseException) {
continue;
}
downloadUris.Add(found);
Task.Run(async () => {
try {
await this.DoExtraction(objectId, info);
} catch (Exception ex) {
Plugin.Log.Error(ex, "Error in extraction thread");
}
// if there were no uris found or extracted, remove existing search info and stop
if (downloadUris.Count == 0) {
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// do the downloads
await this.DownloadAndExtract(objectId, info, downloadUris);
}
private async Task DownloadAndExtract(uint objectId, string info, IEnumerable<Uri> uris) {
var handler = new HttpClientHandler {
UseCookies = true,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 5,
};
handler.CookieContainer.Add(new Cookie("warning", "1", "/", "www.f-list.net"));
var version = this.GetType().Assembly.GetName().Version?.ToString(3) ?? "unknown";
var client = new HttpClient(handler) {
DefaultRequestHeaders = {
UserAgent = { new ProductInfoHeaderValue("ExpandedSearchInfo", version) },
},
};
var sections = new List<ISearchInfoSection>();
// run through each extracted uri
foreach (var uri in uris) {
if (uri.Scheme is not ("http" or "https")) {
continue;
}
// find the providers that run on this uri
var matching = this.Providers
.Where(provider => provider.Config.Enabled && provider.Matches(uri))
.ToList();
// skip the uri if no providers
if (matching.Count == 0) {
continue;
}
// get the http response from the uri and make sure it's ok
var req = new HttpRequestMessage(HttpMethod.Get, uri);
foreach (var provider in matching) {
provider.ModifyRequest(req);
}
var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
if (resp.StatusCode != HttpStatusCode.OK) {
continue;
}
// go through each provider in order and take the first one that provides info
foreach (var provider in matching) {
var extracted = await provider.ExtractInfo(resp);
if (extracted == null) {
continue;
}
sections.Add(extracted);
break;
}
}
// remove expanded search info if no sections resulted
if (sections.Count == 0) {
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// otherwise set the expanded search info for this actor id
this.SearchInfos[objectId] = new ExpandedSearchInfo(info, sections);
}
});
}
}
private async Task DoExtraction(uint objectId, string info) {
var downloadUris = new List<Uri>();
// extract uris from the search info with providers
var extractedUris = this.Providers
.Where(provider => provider.Config.Enabled && provider.ExtractsUris)
.Select(provider => provider.ExtractUris(objectId, info))
.Where(uris => uris != null)
.SelectMany(uris => uris!);
// add the extracted uris to the list
downloadUris.AddRange(extractedUris);
// go word-by-word and try to parse a uri
foreach (var word in info.Split(' ', '\n', '\r')) {
Uri found;
try {
found = new UriBuilder(word.Trim()).Uri;
} catch (UriFormatException) {
continue;
}
// make sure the hostname is a valid domain
try {
if (!this.Parser.IsValidDomain(found.Host)) {
continue;
}
} catch (ParseException) {
continue;
}
downloadUris.Add(found);
}
// if there were no uris found or extracted, remove existing search info and stop
if (downloadUris.Count == 0) {
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// do the downloads
await this.DownloadAndExtract(objectId, info, downloadUris);
}
private async Task DownloadAndExtract(uint objectId, string info, IEnumerable<Uri> uris) {
var handler = new HttpClientHandler {
UseCookies = true,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 5,
};
handler.CookieContainer.Add(new Cookie("warning", "1", "/", "www.f-list.net"));
var version = this.GetType().Assembly.GetName().Version?.ToString(3) ?? "unknown";
var client = new HttpClient(handler) {
DefaultRequestHeaders = {
UserAgent = { new ProductInfoHeaderValue("ExpandedSearchInfo", version) },
},
};
var sections = new List<ISearchInfoSection>();
// run through each extracted uri
foreach (var uri in uris) {
if (uri.Scheme is not ("http" or "https")) {
continue;
}
// find the providers that run on this uri
var matching = this.Providers
.Where(provider => provider.Config.Enabled && provider.Matches(uri))
.ToList();
// skip the uri if no providers
if (matching.Count == 0) {
continue;
}
// get the http response from the uri and make sure it's ok
var req = new HttpRequestMessage(HttpMethod.Get, uri);
foreach (var provider in matching) {
provider.ModifyRequest(req);
}
var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
if (resp.StatusCode != HttpStatusCode.OK) {
continue;
}
// go through each provider in order and take the first one that provides info
foreach (var provider in matching) {
var extracted = await provider.ExtractInfo(resp);
if (extracted == null) {
continue;
}
sections.Add(extracted);
break;
}
}
// remove expanded search info if no sections resulted
if (sections.Count == 0) {
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// otherwise set the expanded search info for this actor id
this.SearchInfos[objectId] = new ExpandedSearchInfo(info, sections);
}
}

View File

@ -4,67 +4,67 @@ using System.Linq;
using ExpandedSearchInfo.Providers;
using ImGuiNET;
namespace ExpandedSearchInfo.Sections {
public class FListSection : ISearchInfoSection {
public IProvider Provider { get; }
public string Name { get; }
public Uri Uri { get; }
namespace ExpandedSearchInfo.Sections;
private string Info { get; }
private List<Tuple<string, string>> Stats { get; }
private List<Tuple<string, string>> Fave { get; }
private List<Tuple<string, string>> Yes { get; }
private List<Tuple<string, string>> Maybe { get; }
private List<Tuple<string, string>> No { get; }
public class FListSection : ISearchInfoSection {
public IProvider Provider { get; }
public string Name { get; }
public Uri Uri { get; }
internal FListSection(IProvider provider, string name, Uri uri, string info, List<Tuple<string, string>> stats, List<Tuple<string, string>> fave, List<Tuple<string, string>> yes, List<Tuple<string, string>> maybe, List<Tuple<string, string>> no) {
this.Provider = provider;
this.Name = name;
this.Uri = uri;
private string Info { get; }
private List<Tuple<string, string>> Stats { get; }
private List<Tuple<string, string>> Fave { get; }
private List<Tuple<string, string>> Yes { get; }
private List<Tuple<string, string>> Maybe { get; }
private List<Tuple<string, string>> No { get; }
this.Info = info;
this.Stats = stats;
this.Fave = fave;
this.Yes = yes;
this.Maybe = maybe;
this.No = no;
internal FListSection(IProvider provider, string name, Uri uri, string info, List<Tuple<string, string>> stats, List<Tuple<string, string>> fave, List<Tuple<string, string>> yes, List<Tuple<string, string>> maybe, List<Tuple<string, string>> no) {
this.Provider = provider;
this.Name = name;
this.Uri = uri;
this.Info = info;
this.Stats = stats;
this.Fave = fave;
this.Yes = yes;
this.Maybe = maybe;
this.No = no;
}
public void Draw() {
if (ImGui.CollapsingHeader($"Stats##{this.Name}")) {
var stats = string.Join("\n", this.Stats.Select(entry => $"{entry.Item1}: {entry.Item2}"));
ImGui.TextUnformatted(stats);
}
public void Draw() {
if (ImGui.CollapsingHeader($"Stats##{this.Name}")) {
var stats = string.Join("\n", this.Stats.Select(entry => $"{entry.Item1}: {entry.Item2}"));
ImGui.TextUnformatted(stats);
}
if (ImGui.CollapsingHeader($"Info##{this.Name}", ImGuiTreeNodeFlags.DefaultOpen)) {
Util.DrawLines(this.Info);
}
this.DrawKinkSection("Fave", this.Fave);
this.DrawKinkSection("Yes", this.Yes);
this.DrawKinkSection("Maybe", this.Maybe);
this.DrawKinkSection("No", this.No);
if (ImGui.CollapsingHeader($"Info##{this.Name}", ImGuiTreeNodeFlags.DefaultOpen)) {
Util.DrawLines(this.Info);
}
private void DrawKinkSection(string sectionName, IEnumerable<Tuple<string, string>> kinks) {
if (!ImGui.CollapsingHeader($"{sectionName}##{this.Name}")) {
return;
this.DrawKinkSection("Fave", this.Fave);
this.DrawKinkSection("Yes", this.Yes);
this.DrawKinkSection("Maybe", this.Maybe);
this.DrawKinkSection("No", this.No);
}
private void DrawKinkSection(string sectionName, IEnumerable<Tuple<string, string>> kinks) {
if (!ImGui.CollapsingHeader($"{sectionName}##{this.Name}")) {
return;
}
foreach (var (name, description) in kinks) {
ImGui.TextUnformatted(name);
if (!ImGui.IsItemHovered()) {
continue;
}
foreach (var (name, description) in kinks) {
ImGui.TextUnformatted(name);
if (!ImGui.IsItemHovered()) {
continue;
}
ImGui.BeginTooltip();
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetIO().DisplaySize.X / 8);
ImGui.TextUnformatted(description);
ImGui.PopTextWrapPos();
ImGui.PushTextWrapPos(ImGui.GetIO().DisplaySize.X / 8);
ImGui.TextUnformatted(description);
ImGui.PopTextWrapPos();
ImGui.EndTooltip();
}
ImGui.EndTooltip();
}
}
}
}

View File

@ -1,12 +1,12 @@
using System;
using ExpandedSearchInfo.Providers;
namespace ExpandedSearchInfo.Sections {
public interface ISearchInfoSection {
IProvider Provider { get; }
string Name { get; }
Uri Uri { get; }
namespace ExpandedSearchInfo.Sections;
void Draw();
}
}
public interface ISearchInfoSection {
IProvider Provider { get; }
string Name { get; }
Uri Uri { get; }
void Draw();
}

View File

@ -3,43 +3,43 @@ using System.Collections.Generic;
using ExpandedSearchInfo.Providers;
using ImGuiNET;
namespace ExpandedSearchInfo.Sections {
public class RefsheetSection : ISearchInfoSection {
public IProvider Provider { get; }
public string Name { get; }
public Uri Uri { get; }
namespace ExpandedSearchInfo.Sections;
private List<Tuple<string, string>> Attributes { get; }
private string Notes { get; }
private List<Tuple<string, string>> Cards { get; }
public class RefsheetSection : ISearchInfoSection {
public IProvider Provider { get; }
public string Name { get; }
public Uri Uri { get; }
internal RefsheetSection(IProvider provider, string name, Uri uri, List<Tuple<string, string>> attributes, string notes, List<Tuple<string, string>> cards) {
this.Provider = provider;
this.Name = name;
this.Uri = uri;
this.Attributes = attributes;
this.Notes = notes;
this.Cards = cards;
private List<Tuple<string, string>> Attributes { get; }
private string Notes { get; }
private List<Tuple<string, string>> Cards { get; }
internal RefsheetSection(IProvider provider, string name, Uri uri, List<Tuple<string, string>> attributes, string notes, List<Tuple<string, string>> cards) {
this.Provider = provider;
this.Name = name;
this.Uri = uri;
this.Attributes = attributes;
this.Notes = notes;
this.Cards = cards;
}
public void Draw() {
if (ImGui.CollapsingHeader($"Attributes##{this.Name}")) {
foreach (var (key, value) in this.Attributes) {
ImGui.TextUnformatted($"{key}: {value}");
}
}
public void Draw() {
if (ImGui.CollapsingHeader($"Attributes##{this.Name}")) {
foreach (var (key, value) in this.Attributes) {
ImGui.TextUnformatted($"{key}: {value}");
}
if (ImGui.CollapsingHeader($"Notes##{this.Name}")) {
Util.DrawLines(this.Notes);
}
foreach (var (cardName, cardContent) in this.Cards) {
if (!ImGui.CollapsingHeader($"{cardName}##{this.Name}")) {
continue;
}
if (ImGui.CollapsingHeader($"Notes##{this.Name}")) {
Util.DrawLines(this.Notes);
}
foreach (var (cardName, cardContent) in this.Cards) {
if (!ImGui.CollapsingHeader($"{cardName}##{this.Name}")) {
continue;
}
Util.DrawLines(cardContent);
}
Util.DrawLines(cardContent);
}
}
}
}

View File

@ -1,23 +1,23 @@
using System;
using ExpandedSearchInfo.Providers;
namespace ExpandedSearchInfo.Sections {
public class TextSection : ISearchInfoSection {
public IProvider Provider { get; }
public string Name { get; }
public Uri Uri { get; }
namespace ExpandedSearchInfo.Sections;
private string Info { get; }
public class TextSection : ISearchInfoSection {
public IProvider Provider { get; }
public string Name { get; }
public Uri Uri { get; }
internal TextSection(IProvider provider, string name, Uri uri, string info) {
this.Provider = provider;
this.Name = name;
this.Uri = uri;
this.Info = info;
}
private string Info { get; }
public void Draw() {
Util.DrawLines(this.Info);
}
internal TextSection(IProvider provider, string name, Uri uri, string info) {
this.Provider = provider;
this.Name = name;
this.Uri = uri;
this.Info = info;
}
}
public void Draw() {
Util.DrawLines(this.Info);
}
}

View File

@ -4,29 +4,29 @@ using System.Text.RegularExpressions;
using Dalamud.Game.Text.SeStringHandling;
using ImGuiNET;
namespace ExpandedSearchInfo {
internal static class Util {
private static readonly Regex BbCodeTag = new(@"\[/?\w+(?:=.+?)?\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
namespace ExpandedSearchInfo;
internal static unsafe SeString ReadRawSeString(IntPtr data) {
var bytes = new List<byte>();
internal static class Util {
private static readonly Regex BbCodeTag = new(@"\[/?\w+(?:=.+?)?\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
var ptr = (byte*) data;
while (*ptr != 0) {
bytes.Add(*ptr);
ptr += 1;
}
internal static unsafe SeString ReadRawSeString(IntPtr data) {
var bytes = new List<byte>();
return SeString.Parse(bytes.ToArray());
var ptr = (byte*) data;
while (*ptr != 0) {
bytes.Add(*ptr);
ptr += 1;
}
internal static string StripBbCode(this string input) => BbCodeTag.Replace(input, "");
return SeString.Parse(bytes.ToArray());
}
internal static void DrawLines(string input) {
// FIXME: this is a workaround for imgui breaking on extremely long strings
foreach (var line in input.Split(new[] {"\n", "\r", "\r\n"}, StringSplitOptions.None)) {
ImGui.TextUnformatted(line);
}
internal static string StripBbCode(this string input) => BbCodeTag.Replace(input, "");
internal static void DrawLines(string input) {
// FIXME: this is a workaround for imgui breaking on extremely long strings
foreach (var line in input.Split(new[] {"\n", "\r", "\r\n"}, StringSplitOptions.None)) {
ImGui.TextUnformatted(line);
}
}
}
}