Implement nodeinfo
This commit is contained in:
parent
c35b62e2f2
commit
068fe25d9d
8 changed files with 296 additions and 6 deletions
|
@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore;
|
||||||
namespace Iceshrimp.Backend.Controllers;
|
namespace Iceshrimp.Backend.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[UseNewtonsoftJson]
|
||||||
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
||||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||||
public class ActivityPubController(DatabaseContext db, APUserRenderer userRenderer) : Controller {
|
public class ActivityPubController(DatabaseContext db, APUserRenderer userRenderer) : Controller {
|
||||||
|
|
73
Iceshrimp.Backend/Controllers/Attributes/UseJsonAttribute.cs
Normal file
73
Iceshrimp.Backend/Controllers/Attributes/UseJsonAttribute.cs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers.Attributes;
|
||||||
|
|
||||||
|
public abstract class UseJsonAttribute : Attribute, IAsyncActionFilter {
|
||||||
|
public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UseNewtonsoftJsonAttribute : UseJsonAttribute;
|
||||||
|
|
||||||
|
internal class JsonInputMultiFormatter : TextInputFormatter {
|
||||||
|
public JsonInputMultiFormatter() {
|
||||||
|
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
||||||
|
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
||||||
|
SupportedMediaTypes.Add("text/json");
|
||||||
|
SupportedMediaTypes.Add("application/json");
|
||||||
|
SupportedMediaTypes.Add("application/*+json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||||
|
InputFormatterContext context, Encoding encoding) {
|
||||||
|
var mvcOpt = context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||||
|
var formatters = mvcOpt.InputFormatters;
|
||||||
|
TextInputFormatter? formatter;
|
||||||
|
|
||||||
|
var endpoint = context.HttpContext.GetEndpoint();
|
||||||
|
if (endpoint?.Metadata.GetMetadata<UseNewtonsoftJsonAttribute>() != null)
|
||||||
|
// We can't use OfType<NewtonsoftJsonInputFormatter> because NewtonsoftJsonPatchInputFormatter exists
|
||||||
|
formatter = (NewtonsoftJsonInputFormatter?)formatters
|
||||||
|
.FirstOrDefault(f => typeof(NewtonsoftJsonInputFormatter) == f.GetType());
|
||||||
|
else
|
||||||
|
// Default to System.Text.Json
|
||||||
|
formatter = formatters.OfType<SystemTextJsonInputFormatter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (formatter == null) throw new Exception("Failed to resolve formatter");
|
||||||
|
|
||||||
|
var result = await formatter.ReadRequestBodyAsync(context, encoding);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class JsonOutputMultiFormatter : TextOutputFormatter {
|
||||||
|
public JsonOutputMultiFormatter() {
|
||||||
|
SupportedEncodings.Add(Encoding.UTF8);
|
||||||
|
SupportedEncodings.Add(Encoding.Unicode);
|
||||||
|
SupportedMediaTypes.Add("text/json");
|
||||||
|
SupportedMediaTypes.Add("application/json");
|
||||||
|
SupportedMediaTypes.Add("application/*+json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) {
|
||||||
|
var mvcOpt = context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||||
|
var formatters = mvcOpt.OutputFormatters;
|
||||||
|
TextOutputFormatter? formatter;
|
||||||
|
|
||||||
|
var endpoint = context.HttpContext.GetEndpoint();
|
||||||
|
if (endpoint?.Metadata.GetMetadata<UseNewtonsoftJsonAttribute>() != null)
|
||||||
|
formatter = formatters.OfType<NewtonsoftJsonOutputFormatter>().FirstOrDefault();
|
||||||
|
else
|
||||||
|
// Default to System.Text.Json
|
||||||
|
formatter = formatters.OfType<SystemTextJsonOutputFormatter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (formatter == null) throw new Exception("Failed to resolve formatter");
|
||||||
|
|
||||||
|
await formatter.WriteResponseBodyAsync(context, selectedEncoding);
|
||||||
|
}
|
||||||
|
}
|
75
Iceshrimp.Backend/Controllers/NodeInfoController.cs
Normal file
75
Iceshrimp.Backend/Controllers/NodeInfoController.cs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
|
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/nodeinfo")]
|
||||||
|
public class NodeInfoController(IOptions<Config.InstanceSection> config) : Controller {
|
||||||
|
[HttpGet("2.1")]
|
||||||
|
[HttpGet("2.0")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(WebFingerResponse))]
|
||||||
|
public IActionResult GetNodeInfo() {
|
||||||
|
var instance = config.Value;
|
||||||
|
var result = new NodeInfoResponse {
|
||||||
|
Version = instance.Version,
|
||||||
|
Software = new NodeInfoResponse.NodeInfoSoftware {
|
||||||
|
Version = instance.Version,
|
||||||
|
Name = "Iceshrimp.NET",
|
||||||
|
Homepage = new Uri("https://iceshrimp.dev/iceshrimp/iceshrimp-rewrite"),
|
||||||
|
Repository = Request.Path.Value?.EndsWith("2.1") ?? false
|
||||||
|
? new Uri("https://iceshrimp.dev/iceshrimp/iceshrimp-rewrite")
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
Protocols = ["activitypub"],
|
||||||
|
Services = new NodeInfoResponse.NodeInfoServices {
|
||||||
|
Inbound = [],
|
||||||
|
Outbound = ["atom1.0", "rss2.0"]
|
||||||
|
},
|
||||||
|
Usage = new NodeInfoResponse.NodeInfoUsage {
|
||||||
|
//FIXME Implement members
|
||||||
|
Users = new NodeInfoResponse.NodeInfoUsers {
|
||||||
|
Total = 0,
|
||||||
|
ActiveMonth = 0,
|
||||||
|
ActiveHalfYear = 0
|
||||||
|
},
|
||||||
|
LocalComments = 0,
|
||||||
|
LocalPosts = 0
|
||||||
|
},
|
||||||
|
Metadata = new NodeInfoResponse.NodeInfoMetadata {
|
||||||
|
//FIXME Implement members
|
||||||
|
NodeName = "todo",
|
||||||
|
NodeDescription = "todo",
|
||||||
|
Maintainer = new NodeInfoResponse.Maintainer {
|
||||||
|
Name = "todo",
|
||||||
|
Email = "todo"
|
||||||
|
},
|
||||||
|
Languages = [],
|
||||||
|
TosUrl = "todo",
|
||||||
|
RepositoryUrl = new Uri("https://iceshrimp.dev/iceshrimp/iceshrimp-rewrite"),
|
||||||
|
FeedbackUrl = new Uri("https://iceshrimp.dev/iceshrimp/iceshrimp-rewrite/issues"),
|
||||||
|
ThemeColor = "#000000",
|
||||||
|
DisableRegistration = true,
|
||||||
|
DisableLocalTimeline = false,
|
||||||
|
DisableRecommendedTimeline = false,
|
||||||
|
DisableGlobalTimeline = false,
|
||||||
|
EmailRequiredForSignup = false,
|
||||||
|
PostEditing = false,
|
||||||
|
PostImports = false,
|
||||||
|
EnableHCaptcha = false,
|
||||||
|
EnableRecaptcha = false,
|
||||||
|
MaxNoteTextLength = 0,
|
||||||
|
MaxCaptionTextLength = 0,
|
||||||
|
EnableGithubIntegration = false,
|
||||||
|
EnableDiscordIntegration = false,
|
||||||
|
EnableEmail = false
|
||||||
|
},
|
||||||
|
OpenRegistrations = false
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,26 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("nodeinfo")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NodeInfoIndexResponse))]
|
||||||
|
public IActionResult NodeInfo() {
|
||||||
|
var response = new NodeInfoIndexResponse {
|
||||||
|
Links = [
|
||||||
|
new WebFingerLink {
|
||||||
|
Rel = "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||||
|
Href = $"https://{config.Value.WebDomain}/nodeinfo/2.1"
|
||||||
|
},
|
||||||
|
new WebFingerLink {
|
||||||
|
Rel = "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||||
|
Href = $"https://{config.Value.WebDomain}/nodeinfo/2.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("host-meta")]
|
[HttpGet("host-meta")]
|
||||||
[Produces("application/xrd+xml")]
|
[Produces("application/xrd+xml")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
|
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
|
||||||
|
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.WebFinger;
|
namespace Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
||||||
public sealed class WebFingerLink {
|
public sealed class WebFingerLink {
|
||||||
[J("rel")] [JR] public string Rel { get; set; } = null!;
|
[J("rel")] [JR] public string Rel { get; set; } = null!;
|
||||||
[J("type")] public string? Type { get; set; }
|
|
||||||
[J("href")] public string? Href { get; set; }
|
[J("type")]
|
||||||
[J("template")] public string? Template { get; set; }
|
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Type { get; set; }
|
||||||
|
|
||||||
|
[J("href")]
|
||||||
|
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Href { get; set; }
|
||||||
|
|
||||||
|
[J("template")]
|
||||||
|
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Template { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")]
|
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")]
|
||||||
|
@ -17,5 +28,82 @@ public sealed class WebFingerLink {
|
||||||
public sealed class WebFingerResponse {
|
public sealed class WebFingerResponse {
|
||||||
[J("links")] [JR] public List<WebFingerLink> Links { get; set; } = null!;
|
[J("links")] [JR] public List<WebFingerLink> Links { get; set; } = null!;
|
||||||
[J("subject")] [JR] public string Subject { get; set; } = null!;
|
[J("subject")] [JR] public string Subject { get; set; } = null!;
|
||||||
[J("aliases")] public List<string> Aliases { get; set; } = [];
|
|
||||||
|
[J("aliases")]
|
||||||
|
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public List<string>? Aliases { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NodeInfoIndexResponse {
|
||||||
|
[J("links")] [JR] public List<WebFingerLink> Links { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NodeInfoResponse {
|
||||||
|
[J("version")] public required string Version { get; set; }
|
||||||
|
[J("software")] public required NodeInfoSoftware Software { get; set; }
|
||||||
|
[J("protocols")] public required List<string> Protocols { get; set; }
|
||||||
|
[J("services")] public required NodeInfoServices Services { get; set; }
|
||||||
|
[J("usage")] public required NodeInfoUsage Usage { get; set; }
|
||||||
|
[J("metadata")] public required NodeInfoMetadata Metadata { get; set; }
|
||||||
|
[J("openRegistrations")] public required bool OpenRegistrations { get; set; }
|
||||||
|
|
||||||
|
public class NodeInfoMetadata {
|
||||||
|
[J("nodeName")] public required string NodeName { get; set; }
|
||||||
|
[J("nodeDescription")] public required string NodeDescription { get; set; }
|
||||||
|
[J("maintainer")] public required Maintainer Maintainer { get; set; }
|
||||||
|
[J("langs")] public required List<object> Languages { get; set; }
|
||||||
|
[J("tosUrl")] public required object TosUrl { get; set; }
|
||||||
|
[J("repositoryUrl")] public required Uri RepositoryUrl { get; set; }
|
||||||
|
[J("feedbackUrl")] public required Uri FeedbackUrl { get; set; }
|
||||||
|
[J("themeColor")] public required string ThemeColor { get; set; }
|
||||||
|
[J("disableRegistration")] public required bool DisableRegistration { get; set; }
|
||||||
|
[J("disableLocalTimeline")] public required bool DisableLocalTimeline { get; set; }
|
||||||
|
[J("disableRecommendedTimeline")] public required bool DisableRecommendedTimeline { get; set; }
|
||||||
|
[J("disableGlobalTimeline")] public required bool DisableGlobalTimeline { get; set; }
|
||||||
|
[J("emailRequiredForSignup")] public required bool EmailRequiredForSignup { get; set; }
|
||||||
|
[J("postEditing")] public required bool PostEditing { get; set; }
|
||||||
|
[J("postImports")] public required bool PostImports { get; set; }
|
||||||
|
[J("enableHcaptcha")] public required bool EnableHCaptcha { get; set; }
|
||||||
|
[J("enableRecaptcha")] public required bool EnableRecaptcha { get; set; }
|
||||||
|
[J("maxNoteTextLength")] public required long MaxNoteTextLength { get; set; }
|
||||||
|
[J("maxCaptionTextLength")] public required long MaxCaptionTextLength { get; set; }
|
||||||
|
[J("enableGithubIntegration")] public required bool EnableGithubIntegration { get; set; }
|
||||||
|
[J("enableDiscordIntegration")] public required bool EnableDiscordIntegration { get; set; }
|
||||||
|
[J("enableEmail")] public required bool EnableEmail { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Maintainer {
|
||||||
|
[J("name")] public required string Name { get; set; }
|
||||||
|
[J("email")] public required string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NodeInfoServices {
|
||||||
|
[J("inbound")] public required List<object> Inbound { get; set; }
|
||||||
|
[J("outbound")] public required List<string> Outbound { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NodeInfoSoftware {
|
||||||
|
[J("name")] public required string Name { get; set; }
|
||||||
|
[J("version")] public required string Version { get; set; }
|
||||||
|
[J("homepage")] public required Uri Homepage { get; set; }
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// This is only part of nodeinfo 2.1
|
||||||
|
/// </remarks>
|
||||||
|
[J("repository")]
|
||||||
|
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public Uri? Repository { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NodeInfoUsage {
|
||||||
|
[J("users")] public required NodeInfoUsers Users { get; set; }
|
||||||
|
[J("localPosts")] public required long LocalPosts { get; set; }
|
||||||
|
[J("localComments")] public required long LocalComments { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NodeInfoUsers {
|
||||||
|
[J("total")] public required long Total { get; set; }
|
||||||
|
[J("activeHalfyear")] public required long ActiveHalfYear { get; set; }
|
||||||
|
[J("activeMonth")] public required long ActiveMonth { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
30
Iceshrimp.Backend/Core/Helpers/MvcBuilderExtensions.cs
Normal file
30
Iceshrimp.Backend/Core/Helpers/MvcBuilderExtensions.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using Iceshrimp.Backend.Controllers.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Helpers;
|
||||||
|
|
||||||
|
public static class MvcBuilderExtensions {
|
||||||
|
public static IMvcBuilder AddMultiFormatter(this IMvcBuilder builder) {
|
||||||
|
builder.Services.AddOptions<MvcOptions>()
|
||||||
|
.PostConfigure<IOptions<JsonOptions>, IOptions<MvcNewtonsoftJsonOptions>, ArrayPool<char>,
|
||||||
|
ObjectPoolProvider, ILoggerFactory>((opts, jsonOpts, _, _, _, loggerFactory) => {
|
||||||
|
// We need to re-add these one since .AddNewtonsoftJson() removes them
|
||||||
|
if(!opts.InputFormatters.OfType<SystemTextJsonInputFormatter>().Any()){
|
||||||
|
var systemInputLogger = loggerFactory.CreateLogger<SystemTextJsonInputFormatter>();
|
||||||
|
opts.InputFormatters.Add(new SystemTextJsonInputFormatter(jsonOpts.Value, systemInputLogger));
|
||||||
|
}
|
||||||
|
if(!opts.OutputFormatters.OfType<SystemTextJsonOutputFormatter>().Any()){
|
||||||
|
opts.OutputFormatters.Add(new SystemTextJsonOutputFormatter(jsonOpts.Value.JsonSerializerOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.InputFormatters.Insert(0, new JsonInputMultiFormatter());
|
||||||
|
opts.OutputFormatters.Insert(0, new JsonOutputMultiFormatter());
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,9 @@ builder.Configuration
|
||||||
.AddIniFile(Environment.GetEnvironmentVariable("ICESHRIMP_CONFIG_OVERRIDES") ?? "configuration.overrides.ini",
|
.AddIniFile(Environment.GetEnvironmentVariable("ICESHRIMP_CONFIG_OVERRIDES") ?? "configuration.overrides.ini",
|
||||||
true, true);
|
true, true);
|
||||||
|
|
||||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
builder.Services.AddControllers()
|
||||||
|
.AddNewtonsoftJson() //TODO: remove once dotNetRdf switches to System.Text.Json
|
||||||
|
.AddMultiFormatter();
|
||||||
builder.Services.AddApiVersioning(options => {
|
builder.Services.AddApiVersioning(options => {
|
||||||
options.DefaultApiVersion = new ApiVersion(1);
|
options.DefaultApiVersion = new ApiVersion(1);
|
||||||
options.ReportApiVersions = true;
|
options.ReportApiVersions = true;
|
||||||
|
|
|
@ -36,4 +36,5 @@
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Iceshrimp/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Iceshrimp/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=nodeinfo/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=webfinger/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=webfinger/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
Loading…
Add table
Reference in a new issue