From 97e81ac0f4b72f1fb17c24f7d9025a7254904934 Mon Sep 17 00:00:00 2001 From: Toastie Date: Sat, 18 May 2024 00:08:52 +1200 Subject: [PATCH] Added Ellie Common/Abstractions --- .../_common/Abstractions/AsyncLazy.cs | 19 +++ .../Abstractions/Cache/BotCacheExtensions.cs | 46 ++++++ .../_common/Abstractions/Cache/IBotCache.cs | 47 ++++++ .../Abstractions/Cache/MemoryBotCache.cs | 71 +++++++++ .../Collections/ConcurrentHashSet.cs | 88 ++++++++++ .../Collections/IndexedCollections.cs | 148 +++++++++++++++++ .../_common/Abstractions/EllieRandom.cs | 66 ++++++++ .../Extensions/ArrayExtensions.cs | 62 ++++++++ .../Extensions/EnumerableExtensions.cs | 97 +++++++++++ .../Abstractions/Extensions/Extensions.cs | 7 + .../Extensions/HttpClientExtensions.cs | 35 ++++ .../Extensions/OneOfExtensions.cs | 10 ++ .../Abstractions/Extensions/PipeExtensions.cs | 22 +++ .../Extensions/StringExtensions.cs | 150 ++++++++++++++++++ .../_common/Abstractions/Helpers/LogSetup.cs | 35 ++++ .../Helpers/StandardConversions.cs | 7 + src/EllieBot/_common/Abstractions/Kwum.cs | 100 ++++++++++++ .../Abstractions/PubSub/EventPubSub.cs | 80 ++++++++++ .../_common/Abstractions/PubSub/IPubSub.cs | 10 ++ .../_common/Abstractions/PubSub/ISeria.cs | 7 + .../_common/Abstractions/QueueRunner.cs | 61 +++++++ src/EllieBot/_common/Abstractions/TypedKey.cs | 30 ++++ .../_common/Abstractions/YamlHelper.cs | 48 ++++++ .../Abstractions/creds/IBotCredentials.cs | 78 +++++++++ .../Abstractions/creds/IBotCredsProvider.cs | 8 + .../Abstractions/strings/CommandStrings.cs | 35 ++++ .../Abstractions/strings/IBotStrings.cs | 16 ++ .../strings/IBotStringsExtensions.cs | 17 ++ .../strings/IBotStringsProvider.cs | 28 ++++ .../Abstractions/strings/IStringsSource.cs | 17 ++ .../_common/Abstractions/strings/LocStr.cs | 13 ++ 31 files changed, 1458 insertions(+) create mode 100644 src/EllieBot/_common/Abstractions/AsyncLazy.cs create mode 100644 src/EllieBot/_common/Abstractions/Cache/BotCacheExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Cache/IBotCache.cs create mode 100644 src/EllieBot/_common/Abstractions/Cache/MemoryBotCache.cs create mode 100644 src/EllieBot/_common/Abstractions/Collections/ConcurrentHashSet.cs create mode 100644 src/EllieBot/_common/Abstractions/Collections/IndexedCollections.cs create mode 100644 src/EllieBot/_common/Abstractions/EllieRandom.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/ArrayExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/EnumerableExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/Extensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/HttpClientExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/OneOfExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/PipeExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Extensions/StringExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/Helpers/LogSetup.cs create mode 100644 src/EllieBot/_common/Abstractions/Helpers/StandardConversions.cs create mode 100644 src/EllieBot/_common/Abstractions/Kwum.cs create mode 100644 src/EllieBot/_common/Abstractions/PubSub/EventPubSub.cs create mode 100644 src/EllieBot/_common/Abstractions/PubSub/IPubSub.cs create mode 100644 src/EllieBot/_common/Abstractions/PubSub/ISeria.cs create mode 100644 src/EllieBot/_common/Abstractions/QueueRunner.cs create mode 100644 src/EllieBot/_common/Abstractions/TypedKey.cs create mode 100644 src/EllieBot/_common/Abstractions/YamlHelper.cs create mode 100644 src/EllieBot/_common/Abstractions/creds/IBotCredentials.cs create mode 100644 src/EllieBot/_common/Abstractions/creds/IBotCredsProvider.cs create mode 100644 src/EllieBot/_common/Abstractions/strings/CommandStrings.cs create mode 100644 src/EllieBot/_common/Abstractions/strings/IBotStrings.cs create mode 100644 src/EllieBot/_common/Abstractions/strings/IBotStringsExtensions.cs create mode 100644 src/EllieBot/_common/Abstractions/strings/IBotStringsProvider.cs create mode 100644 src/EllieBot/_common/Abstractions/strings/IStringsSource.cs create mode 100644 src/EllieBot/_common/Abstractions/strings/LocStr.cs diff --git a/src/EllieBot/_common/Abstractions/AsyncLazy.cs b/src/EllieBot/_common/Abstractions/AsyncLazy.cs new file mode 100644 index 0000000..6c86693 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/AsyncLazy.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace Ellie.Common; + +public class AsyncLazy : Lazy> +{ + public AsyncLazy(Func valueFactory) + : base(() => Task.Run(valueFactory)) + { + } + + public AsyncLazy(Func> taskFactory) + : base(() => Task.Run(taskFactory)) + { + } + + public TaskAwaiter GetAwaiter() + => Value.GetAwaiter(); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Cache/BotCacheExtensions.cs b/src/EllieBot/_common/Abstractions/Cache/BotCacheExtensions.cs new file mode 100644 index 0000000..39d5e82 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Cache/BotCacheExtensions.cs @@ -0,0 +1,46 @@ +using OneOf; +using OneOf.Types; + +namespace Ellie.Common; + +public static class BotCacheExtensions +{ + public static async ValueTask GetOrDefaultAsync(this IBotCache cache, TypedKey key) + { + var result = await cache.GetAsync(key); + if (result.TryGetValue(out var val)) + return val; + + return default; + } + + private static TypedKey GetImgKey(Uri uri) + => new($"image:{uri}"); + + public static ValueTask SetImageDataAsync(this IBotCache c, string key, byte[] data) + => c.SetImageDataAsync(new Uri(key), data); + public static async ValueTask SetImageDataAsync(this IBotCache c, Uri key, byte[] data) + => await c.AddAsync(GetImgKey(key), data, expiry: TimeSpan.FromHours(48)); + + public static async ValueTask> GetImageDataAsync(this IBotCache c, Uri key) + => await c.GetAsync(GetImgKey(key)); + + public static async Task GetRatelimitAsync( + this IBotCache c, + TypedKey key, + TimeSpan length) + { + var now = DateTime.UtcNow; + var nowB = now.ToBinary(); + + var cachedValue = await c.GetOrAddAsync(key, + () => Task.FromResult(now.ToBinary()), + expiry: length); + + if (cachedValue == nowB) + return null; + + var diff = now - DateTime.FromBinary(cachedValue); + return length - diff; + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Cache/IBotCache.cs b/src/EllieBot/_common/Abstractions/Cache/IBotCache.cs new file mode 100644 index 0000000..5622c5f --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Cache/IBotCache.cs @@ -0,0 +1,47 @@ +using OneOf; +using OneOf.Types; + +namespace Ellie.Common; + +public interface IBotCache +{ + /// + /// Adds an item to the cache + /// + /// Key to add + /// Value to add to the cache + /// Optional expiry + /// Whether old value should be overwritten + /// Type of the value + /// Returns whether add was sucessful. Always true unless ovewrite = false + ValueTask AddAsync(TypedKey key, T value, TimeSpan? expiry = null, bool overwrite = true); + + /// + /// Get an element from the cache + /// + /// Key + /// Type of the value + /// Either a value or + ValueTask> GetAsync(TypedKey key); + + /// + /// Remove a key from the cache + /// + /// Key to remove + /// Type of the value + /// Whether there was item + ValueTask RemoveAsync(TypedKey key); + + /// + /// Get the key if it exists or add a new one + /// + /// Key to get and potentially add + /// Value creation factory + /// Optional expiry + /// Type of the value + /// The retrieved or newly added value + ValueTask GetOrAddAsync( + TypedKey key, + Func> createFactory, + TimeSpan? expiry = null); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Cache/MemoryBotCache.cs b/src/EllieBot/_common/Abstractions/Cache/MemoryBotCache.cs new file mode 100644 index 0000000..caac44f --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Cache/MemoryBotCache.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Caching.Memory; +using OneOf; +using OneOf.Types; + +// ReSharper disable InconsistentlySynchronizedField + +namespace Ellie.Common; + +public sealed class MemoryBotCache : IBotCache +{ + // needed for overwrites and Delete return value + private readonly object _cacheLock = new object(); + private readonly MemoryCache _cache; + + public MemoryBotCache() + { + _cache = new MemoryCache(new MemoryCacheOptions()); + } + + public ValueTask AddAsync(TypedKey key, T value, TimeSpan? expiry = null, bool overwrite = true) + { + if (overwrite) + { + using var item = _cache.CreateEntry(key.Key); + item.Value = value; + item.AbsoluteExpirationRelativeToNow = expiry; + return new(true); + } + + lock (_cacheLock) + { + if (_cache.TryGetValue(key.Key, out var old) && old is not null) + return new(false); + + using var item = _cache.CreateEntry(key.Key); + item.Value = value; + item.AbsoluteExpirationRelativeToNow = expiry; + return new(true); + } + } + + public async ValueTask GetOrAddAsync( + TypedKey key, + Func> createFactory, + TimeSpan? expiry = null) + => await _cache.GetOrCreateAsync(key.Key, + async ce => + { + ce.AbsoluteExpirationRelativeToNow = expiry; + var val = await createFactory(); + return val; + }); + + public ValueTask> GetAsync(TypedKey key) + { + if (!_cache.TryGetValue(key.Key, out var val) || val is null) + return new(new None()); + + return new((T)val); + } + + public ValueTask RemoveAsync(TypedKey key) + { + lock (_cacheLock) + { + var toReturn = _cache.TryGetValue(key.Key, out var old) && old is not null; + _cache.Remove(key.Key); + return new(toReturn); + } + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Collections/ConcurrentHashSet.cs b/src/EllieBot/_common/Abstractions/Collections/ConcurrentHashSet.cs new file mode 100644 index 0000000..19986be --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Collections/ConcurrentHashSet.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; + +namespace System.Collections.Generic; + +[DebuggerDisplay("{_backingStore.Count}")] +public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollection where T : notnull +{ + private readonly ConcurrentDictionary _backingStore; + + public ConcurrentHashSet() + => _backingStore = new(); + + public ConcurrentHashSet(IEnumerable values, IEqualityComparer? comparer = null) + => _backingStore = new(values.Select(x => new KeyValuePair(x, true)), comparer); + + public IEnumerator GetEnumerator() + => _backingStore.Keys.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + /// Adds the specified item to the . + /// + /// The item to add. + /// + /// true if the items was added to the + /// successfully; false if it already exists. + /// + /// + /// The + /// contains too many items. + /// + public bool Add(T item) + => _backingStore.TryAdd(item, true); + + void ICollection.Add(T item) + => Add(item); + + public void Clear() + => _backingStore.Clear(); + + public bool Contains(T item) + => _backingStore.ContainsKey(item); + + public void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + + if (arrayIndex >= array.Length) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + + CopyToInternal(array, arrayIndex); + } + + private void CopyToInternal(T[] array, int arrayIndex) + { + var len = array.Length; + foreach (var (k, _) in _backingStore) + { + if (arrayIndex >= len) + throw new IndexOutOfRangeException(nameof(arrayIndex)); + + array[arrayIndex++] = k; + } + } + + bool ICollection.Remove(T item) + => TryRemove(item); + + public bool TryRemove(T item) + => _backingStore.TryRemove(item, out _); + + public void RemoveWhere(Func predicate) + { + foreach (var elem in this.Where(predicate)) + TryRemove(elem); + } + + public int Count + => _backingStore.Count; + + public bool IsReadOnly + => false; +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Collections/IndexedCollections.cs b/src/EllieBot/_common/Abstractions/Collections/IndexedCollections.cs new file mode 100644 index 0000000..fe8e2c1 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Collections/IndexedCollections.cs @@ -0,0 +1,148 @@ +using System.Collections; + +namespace Ellie.Common; + +public interface IIndexed +{ + int Index { get; set; } +} + +public class IndexedCollection : IList + where T : class, IIndexed +{ + public List Source { get; } + + public int Count + => Source.Count; + + public bool IsReadOnly + => false; + + public virtual T this[int index] + { + get => Source[index]; + set + { + lock (_locker) + { + value.Index = index; + Source[index] = value; + } + } + } + + private readonly object _locker = new(); + + public IndexedCollection() + => Source = new(); + + public IndexedCollection(IEnumerable source) + { + lock (_locker) + { + Source = source.OrderBy(x => x.Index).ToList(); + UpdateIndexes(); + } + } + + public int IndexOf(T item) + => item?.Index ?? -1; + + public IEnumerator GetEnumerator() + => Source.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => Source.GetEnumerator(); + + public void Add(T item) + { + ArgumentNullException.ThrowIfNull(item); + + lock (_locker) + { + item.Index = Source.Count; + Source.Add(item); + } + } + + public virtual void Clear() + { + lock (_locker) + { + Source.Clear(); + } + } + + public bool Contains(T item) + { + lock (_locker) + { + return Source.Contains(item); + } + } + + public void CopyTo(T[] array, int arrayIndex) + { + lock (_locker) + { + Source.CopyTo(array, arrayIndex); + } + } + + public virtual bool Remove(T item) + { + lock (_locker) + { + if (Source.Remove(item)) + { + for (var i = 0; i < Source.Count; i++) + { + if (Source[i].Index != i) + Source[i].Index = i; + } + + return true; + } + } + + return false; + } + + public virtual void Insert(int index, T item) + { + lock (_locker) + { + Source.Insert(index, item); + for (var i = index; i < Source.Count; i++) + Source[i].Index = i; + } + } + + public virtual void RemoveAt(int index) + { + lock (_locker) + { + Source.RemoveAt(index); + for (var i = index; i < Source.Count; i++) + Source[i].Index = i; + } + } + + public void UpdateIndexes() + { + lock (_locker) + { + for (var i = 0; i < Source.Count; i++) + { + if (Source[i].Index != i) + Source[i].Index = i; + } + } + } + + public static implicit operator List(IndexedCollection x) + => x.Source; + + public List ToList() + => Source.ToList(); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/EllieRandom.cs b/src/EllieBot/_common/Abstractions/EllieRandom.cs new file mode 100644 index 0000000..0ece259 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/EllieRandom.cs @@ -0,0 +1,66 @@ +#nullable disable +using System.Security.Cryptography; + +namespace Ellie.Common; + +public sealed class EllieRandom : Random +{ + private readonly RandomNumberGenerator _rng; + + public EllieRandom() + => _rng = RandomNumberGenerator.Create(); + + public override int Next() + { + var bytes = new byte[sizeof(int)]; + _rng.GetBytes(bytes); + return Math.Abs(BitConverter.ToInt32(bytes, 0)); + } + + /// + /// Generates a random integer between 0 (inclusive) and + /// a specified exclusive upper bound using a cryptographically strong random number generator. + /// + /// Exclusive max value + /// A random number + public override int Next(int maxValue) + => RandomNumberGenerator.GetInt32(maxValue); + + /// + /// Generates a random integer between a specified inclusive lower bound and a + /// specified exclusive upper bound using a cryptographically strong random number generator. + /// + /// Inclusive min value + /// Exclusive max value + /// A random number + public override int Next(int minValue, int maxValue) + => RandomNumberGenerator.GetInt32(minValue, maxValue); + + public long NextLong(long minValue, long maxValue) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); + if (minValue == maxValue) + return minValue; + var bytes = new byte[sizeof(long)]; + _rng.GetBytes(bytes); + var sign = Math.Sign(BitConverter.ToInt64(bytes, 0)); + return (sign * BitConverter.ToInt64(bytes, 0) % (maxValue - minValue)) + minValue; + } + + public override void NextBytes(byte[] buffer) + => _rng.GetBytes(buffer); + + protected override double Sample() + { + var bytes = new byte[sizeof(double)]; + _rng.GetBytes(bytes); + return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1); + } + + public override double NextDouble() + { + var bytes = new byte[sizeof(double)]; + _rng.GetBytes(bytes); + return BitConverter.ToDouble(bytes, 0); + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/ArrayExtensions.cs b/src/EllieBot/_common/Abstractions/Extensions/ArrayExtensions.cs new file mode 100644 index 0000000..dbe2d93 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/ArrayExtensions.cs @@ -0,0 +1,62 @@ +using System.Security.Cryptography; + +namespace Ellie.Common; + +// made for expressions because they almost never get added +// and they get looped through constantly +public static class ArrayExtensions +{ + /// + /// Create a new array from the old array + new element at the end + /// + /// Input array + /// Item to add to the end of the output array + /// Type of the array + /// A new array with the new element at the end + public static T[] With(this T[] input, T added) + { + var newExprs = new T[input.Length + 1]; + Array.Copy(input, 0, newExprs, 0, input.Length); + newExprs[input.Length] = added; + return newExprs; + } + + /// + /// Creates a new array by applying the specified function to every element in the input array + /// + /// Array to modify + /// Function to apply + /// Orignal type of the elements in the array + /// Output type of the elements of the array + /// New array with updated elements + public static TOut[] Map(this TIn[] arr, Func f) + => Array.ConvertAll(arr, x => f(x)); + + /// + /// Creates a new array by applying the specified function to every element in the input array + /// + /// Array to modify + /// Function to apply + /// Orignal type of the elements in the array + /// Output type of the elements of the array + /// New array with updated elements + public static TOut[] Map(this IReadOnlyCollection col, Func f) + { + var toReturn = new TOut[col.Count]; + + var i = 0; + foreach (var item in col) + toReturn[i++] = f(item); + + return toReturn; + } + + public static T? RandomOrDefault(this T[] data) + { + if (data.Length == 0) + return default; + + var index = RandomNumberGenerator.GetInt32(0, data.Length); + return data[index]; + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/EnumerableExtensions.cs b/src/EllieBot/_common/Abstractions/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..4e4c786 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/EnumerableExtensions.cs @@ -0,0 +1,97 @@ +using System.Security.Cryptography; + +namespace Ellie.Common; + +public static class EnumerableExtensions +{ + /// + /// Concatenates the members of a collection, using the specified separator between each member. + /// + /// Collection to join + /// + /// The character to use as a separator. separator is included in the returned string only if + /// values has more than one element. + /// + /// Optional transformation to apply to each element before concatenation. + /// The type of the members of values. + /// + /// A string that consists of the members of values delimited by the separator character. -or- Empty if values has + /// no elements. + /// + public static string Join(this IEnumerable data, char separator, Func? func = null) + => string.Join(separator, data.Select(func ?? (x => x?.ToString() ?? string.Empty))); + + /// + /// Concatenates the members of a collection, using the specified separator between each member. + /// + /// Collection to join + /// + /// The string to use as a separator.separator is included in the returned string only if values + /// has more than one element. + /// + /// Optional transformation to apply to each element before concatenation. + /// The type of the members of values. + /// + /// A string that consists of the members of values delimited by the separator character. -or- Empty if values has + /// no elements. + /// + public static string Join(this IEnumerable data, string separator, Func? func = null) + => string.Join(separator, data.Select(func ?? (x => x?.ToString() ?? string.Empty))); + + /// + /// Randomize element order by performing the Fisher-Yates shuffle + /// + /// Item type + /// Items to shuffle + public static IReadOnlyList Shuffle(this IEnumerable items) + { + var list = items.ToArray(); + var n = list.Length; + while (n-- > 1) + { + var k = RandomNumberGenerator.GetInt32(n); + (list[k], list[n]) = (list[n], list[k]); + } + + return list; + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified + /// has the default concurrency level, has the default initial capacity, + /// and uses the default comparer for the key type. + /// + /// + /// The whose elements are copied to the new + /// . + /// + /// A new instance of the class + public static ConcurrentDictionary ToConcurrent( + this IEnumerable> dict) + where TKey : notnull + => new(dict); + + public static IndexedCollection ToIndexed(this IEnumerable enumerable) + where T : class, IIndexed + => new(enumerable); + + /// + /// Creates a task that will complete when all of the objects in an enumerable + /// collection have completed + /// + /// The tasks to wait on for completion. + /// The type of the completed task. + /// A task that represents the completion of all of the supplied tasks. + public static Task WhenAll(this IEnumerable> tasks) + => Task.WhenAll(tasks); + + /// + /// Creates a task that will complete when all of the objects in an enumerable + /// collection have completed + /// + /// The tasks to wait on for completion. + /// A task that represents the completion of all of the supplied tasks. + public static Task WhenAll(this IEnumerable tasks) + => Task.WhenAll(tasks); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/Extensions.cs b/src/EllieBot/_common/Abstractions/Extensions/Extensions.cs new file mode 100644 index 0000000..487afe7 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/Extensions.cs @@ -0,0 +1,7 @@ +namespace Ellie.Common; + +public static class Extensions +{ + public static long ToTimestamp(this in DateTime value) + => (value.Ticks - 621355968000000000) / 10000000; +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/HttpClientExtensions.cs b/src/EllieBot/_common/Abstractions/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..38e6396 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/HttpClientExtensions.cs @@ -0,0 +1,35 @@ +using System.Net.Http.Headers; + +namespace Ellie.Common; + +public static class HttpClientExtensions +{ + public static HttpClient AddFakeHeaders(this HttpClient http) + { + AddFakeHeaders(http.DefaultRequestHeaders); + return http; + } + + public static void AddFakeHeaders(this HttpHeaders dict) + { + dict.Clear(); + dict.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + dict.Add("User-Agent", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.202 Safari/535.1"); + } + + public static bool IsImage(this HttpResponseMessage msg) + => IsImage(msg, out _); + + public static bool IsImage(this HttpResponseMessage msg, out string? mimeType) + { + mimeType = msg.Content.Headers.ContentType?.MediaType; + if (mimeType is "image/png" or "image/jpeg" or "image/gif") + return true; + + return false; + } + + public static long GetContentLength(this HttpResponseMessage msg) + => msg.Content.Headers.ContentLength ?? long.MaxValue; +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/OneOfExtensions.cs b/src/EllieBot/_common/Abstractions/Extensions/OneOfExtensions.cs new file mode 100644 index 0000000..f9c4cde --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/OneOfExtensions.cs @@ -0,0 +1,10 @@ +using OneOf.Types; +using OneOf; + +namespace Ellie.Common; + +public static class OneOfExtensions +{ + public static bool TryGetValue(this OneOf oneOf, out T value) + => oneOf.TryPickT0(out value, out _); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/PipeExtensions.cs b/src/EllieBot/_common/Abstractions/Extensions/PipeExtensions.cs new file mode 100644 index 0000000..215a829 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/PipeExtensions.cs @@ -0,0 +1,22 @@ +namespace Ellie.Common; + +public delegate TOut PipeFunc(in TIn a); +public delegate TOut PipeFunc(in TIn1 a, in TIn2 b); + +public static class PipeExtensions +{ + public static TOut Pipe(this TIn a, Func fn) + => fn(a); + + public static TOut Pipe(this TIn a, PipeFunc fn) + => fn(a); + + public static TOut Pipe(this (TIn1, TIn2) a, PipeFunc fn) + => fn(a.Item1, a.Item2); + + public static (TIn, TExtra) With(this TIn a, TExtra b) + => (a, b); + + public static async Task Pipe(this Task a, Func fn) + => fn(await a); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Extensions/StringExtensions.cs b/src/EllieBot/_common/Abstractions/Extensions/StringExtensions.cs new file mode 100644 index 0000000..95baadb --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Extensions/StringExtensions.cs @@ -0,0 +1,150 @@ +using EllieBot.Common.Yml; +using System.Text; +using System.Text.RegularExpressions; + +namespace EllieBot.Extensions; + +public static class StringExtensions +{ + private static readonly HashSet _lettersAndDigits = + [ + ..Enumerable.Range(48, 10) + .Concat(Enumerable.Range(65, 26)) + .Concat(Enumerable.Range(97, 26)) + .Select(x => (char)x) + ]; + + private static readonly Regex _filterRegex = new(@"discord(?:\.gg|\.io|\.me|\.li|(?:app)?\.com\/invite)\/(\w+)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex _codePointRegex = + new(@"(\\U(?[a-zA-Z0-9]{8})|\\u(?[a-zA-Z0-9]{4})|\\x(?[a-zA-Z0-9]{2}))", + RegexOptions.Compiled); + + public static string PadBoth(this string str, int length) + { + var spaces = length - str.Length; + var padLeft = (spaces / 2) + str.Length; + return str.PadLeft(padLeft, ' ').PadRight(length, ' '); + } + + public static string StripHtml(this string input) + => Regex.Replace(input, "<.*?>", string.Empty); + + public static string? TrimTo(this string? str, int maxLength, bool hideDots = false) + { + if (hideDots) + { + return str?.Substring(0, Math.Min(str?.Length ?? 0, maxLength)); + } + + if (str is null || str.Length <= maxLength) + return str; + + return string.Concat(str.AsSpan(0, maxLength - 1), "…"); + } + + public static string ToTitleCase(this string str) + { + var tokens = str.Split([" "], StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < tokens.Length; i++) + { + var token = tokens[i]; + tokens[i] = token[..1].ToUpperInvariant() + token[1..]; + } + + return tokens.Join(" ").Replace(" Of ", " of ").Replace(" The ", " the "); + } + + //http://www.dotnetperls.com/levenshtein + public static int LevenshteinDistance(this string s, string t) + { + var n = s.Length; + var m = t.Length; + var d = new int[n + 1, m + 1]; + + // Step 1 + if (n == 0) + return m; + + if (m == 0) + return n; + + // Step 2 + for (var i = 0; i <= n; d[i, 0] = i++) + { + } + + for (var j = 0; j <= m; d[0, j] = j++) + { + } + + // Step 3 + for (var i = 1; i <= n; i++) + //Step 4 + for (var j = 1; j <= m; j++) + { + // Step 5 + var cost = t[j - 1] == s[i - 1] ? 0 : 1; + + // Step 6 + d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); + } + + // Step 7 + return d[n, m]; + } + + public static async Task ToStream(this string str) + { + var ms = new MemoryStream(); + var sw = new StreamWriter(ms); + await sw.WriteAsync(str); + await sw.FlushAsync(); + ms.Position = 0; + return ms; + } + + public static bool IsDiscordInvite(this string str) + => _filterRegex.IsMatch(str); + + public static string Unmention(this string str) + => str.Replace("@", "ම", StringComparison.InvariantCulture); + + public static string SanitizeMentions(this string str, bool sanitizeRoleMentions = false) + { + str = str.Replace("@everyone", "@everyοne", StringComparison.InvariantCultureIgnoreCase) + .Replace("@here", "@һere", StringComparison.InvariantCultureIgnoreCase); + if (sanitizeRoleMentions) + str = str.SanitizeRoleMentions(); + + return str; + } + + public static string SanitizeRoleMentions(this string str) + => str.Replace("<@&", "<ම&", StringComparison.InvariantCultureIgnoreCase); + + public static string SanitizeAllMentions(this string str) + => str.SanitizeMentions().SanitizeRoleMentions(); + + public static string ToBase64(this string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string GetInitials(this string txt, string glue = "") + => txt.Split(' ').Select(x => x.FirstOrDefault()).Join(glue); + + public static bool IsAlphaNumeric(this string txt) + => txt.All(c => _lettersAndDigits.Contains(c)); + + public static string UnescapeUnicodeCodePoints(this string input) + => _codePointRegex.Replace(input, + me => + { + var str = me.Groups["code"].Value; + var newString = str.UnescapeUnicodeCodePoint(); + return newString; + }); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Helpers/LogSetup.cs b/src/EllieBot/_common/Abstractions/Helpers/LogSetup.cs new file mode 100644 index 0000000..8983740 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Helpers/LogSetup.cs @@ -0,0 +1,35 @@ +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using System.Text; + +namespace Ellie.Common; + +public static class LogSetup +{ + public static void SetupLogger(object source) + { + Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("System", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console(LogEventLevel.Information, + theme: GetTheme(), + outputTemplate: + "[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}") + .Enrich.WithProperty("LogSource", source) + .CreateLogger(); + + Console.OutputEncoding = Encoding.UTF8; + } + + private static ConsoleTheme GetTheme() + { + if (Environment.OSVersion.Platform == PlatformID.Unix) + return AnsiConsoleTheme.Code; +#if DEBUG + return AnsiConsoleTheme.Code; +#else + return ConsoleTheme.None; +#endif + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Helpers/StandardConversions.cs b/src/EllieBot/_common/Abstractions/Helpers/StandardConversions.cs new file mode 100644 index 0000000..143c149 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Helpers/StandardConversions.cs @@ -0,0 +1,7 @@ +namespace Ellie.Common; + +public static class StandardConversions +{ + public static double CelsiusToFahrenheit(double cel) + => (cel * 1.8f) + 32; +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/Kwum.cs b/src/EllieBot/_common/Abstractions/Kwum.cs new file mode 100644 index 0000000..267d38b --- /dev/null +++ b/src/EllieBot/_common/Abstractions/Kwum.cs @@ -0,0 +1,100 @@ +using System.Runtime.CompilerServices; + +namespace Ellie.Common; + +// needs proper invalid input check (character array input out of range) +// needs negative number support +// ReSharper disable once InconsistentNaming +#pragma warning disable CS8981 +public readonly struct kwum : IEquatable +#pragma warning restore CS8981 +{ + private const string VALID_CHARACTERS = "23456789abcdefghijkmnpqrstuvwxyz"; + private readonly int _value; + + public kwum(int num) + => _value = num; + + public kwum(in char c) + { + if (!IsValidChar(c)) + throw new ArgumentException("Character needs to be a valid kwum character.", nameof(c)); + + _value = InternalCharToValue(c); + } + + public kwum(in ReadOnlySpan input) + { + _value = 0; + for (var index = 0; index < input.Length; index++) + { + var c = input[index]; + if (!IsValidChar(c)) + throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input)); + + _value += VALID_CHARACTERS.IndexOf(c) * (int)Math.Pow(VALID_CHARACTERS.Length, input.Length - index - 1); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int InternalCharToValue(in char c) + => VALID_CHARACTERS.IndexOf(c); + + public static bool TryParse(in ReadOnlySpan input, out kwum value) + { + value = default; + foreach (var c in input) + { + if (!IsValidChar(c)) + return false; + } + + value = new(input); + return true; + } + + public static kwum operator +(kwum left, kwum right) + => new(left._value + right._value); + + public static bool operator ==(kwum left, kwum right) + => left._value == right._value; + + public static bool operator !=(kwum left, kwum right) + => !(left == right); + + public static implicit operator long(kwum kwum) + => kwum._value; + + public static implicit operator int(kwum kwum) + => kwum._value; + + public static implicit operator kwum(int num) + => new(num); + + public static bool IsValidChar(char c) + => VALID_CHARACTERS.Contains(c); + + public override string ToString() + { + var count = VALID_CHARACTERS.Length; + var localValue = _value; + var arrSize = (int)Math.Log(localValue, count) + 1; + Span chars = new char[arrSize]; + while (localValue > 0) + { + localValue = Math.DivRem(localValue, count, out var rem); + chars[--arrSize] = VALID_CHARACTERS[rem]; + } + + return new(chars); + } + + public override bool Equals(object? obj) + => obj is kwum kw && kw == this; + + public bool Equals(kwum other) + => other == this; + + public override int GetHashCode() + => _value.GetHashCode(); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/PubSub/EventPubSub.cs b/src/EllieBot/_common/Abstractions/PubSub/EventPubSub.cs new file mode 100644 index 0000000..a3d753a --- /dev/null +++ b/src/EllieBot/_common/Abstractions/PubSub/EventPubSub.cs @@ -0,0 +1,80 @@ +namespace Ellie.Common; + +public class EventPubSub : IPubSub +{ + private readonly Dictionary>>> _actions = new(); + private readonly object _locker = new(); + + public Task Sub(in TypedKey key, Func action) + where TData : notnull + { + Func localAction = obj => action((TData)obj); + lock (_locker) + { + if (!_actions.TryGetValue(key.Key, out var keyActions)) + { + keyActions = new(); + _actions[key.Key] = keyActions; + } + + if (!keyActions.TryGetValue(action, out var sameActions)) + { + sameActions = new(); + keyActions[action] = sameActions; + } + + sameActions.Add(localAction); + + return Task.CompletedTask; + } + } + + public Task Pub(in TypedKey key, TData data) + where TData : notnull + { + lock (_locker) + { + if (_actions.TryGetValue(key.Key, out var actions)) + // if this class ever gets used, this needs to be properly implemented + // 1. ignore all valuetasks which are completed + // 2. run all other tasks in parallel + return actions.SelectMany(kvp => kvp.Value).Select(action => action(data).AsTask()).WhenAll(); + + return Task.CompletedTask; + } + } + + public Task Unsub(in TypedKey key, Func action) + { + lock (_locker) + { + // get subscriptions for this action + if (_actions.TryGetValue(key.Key, out var actions)) + // get subscriptions which have the same action hash code + // note: having this as a list allows for multiple subscriptions of + // the same insance's/static method + { + if (actions.TryGetValue(action, out var sameActions)) + { + // remove last subscription + sameActions.RemoveAt(sameActions.Count - 1); + + // if the last subscription was the only subscription + // we can safely remove this action's dictionary entry + if (sameActions.Count == 0) + { + actions.Remove(action); + + // if our dictionary has no more elements after + // removing the entry + // it's safe to remove it from the key's subscriptions + if (actions.Count == 0) + _actions.Remove(key.Key); + } + } + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/PubSub/IPubSub.cs b/src/EllieBot/_common/Abstractions/PubSub/IPubSub.cs new file mode 100644 index 0000000..ea84b6b --- /dev/null +++ b/src/EllieBot/_common/Abstractions/PubSub/IPubSub.cs @@ -0,0 +1,10 @@ +namespace Ellie.Common; + +public interface IPubSub +{ + public Task Pub(in TypedKey key, TData data) + where TData : notnull; + + public Task Sub(in TypedKey key, Func action) + where TData : notnull; +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/PubSub/ISeria.cs b/src/EllieBot/_common/Abstractions/PubSub/ISeria.cs new file mode 100644 index 0000000..76094f4 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/PubSub/ISeria.cs @@ -0,0 +1,7 @@ +namespace Ellie.Common; + +public interface ISeria +{ + byte[] Serialize(T data); + T? Deserialize(byte[]? data); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/QueueRunner.cs b/src/EllieBot/_common/Abstractions/QueueRunner.cs new file mode 100644 index 0000000..6e582cc --- /dev/null +++ b/src/EllieBot/_common/Abstractions/QueueRunner.cs @@ -0,0 +1,61 @@ +using System.Threading.Channels; + +namespace Ellie.Common; + +public sealed class QueueRunner +{ + private readonly Channel> _channel; + private readonly int _delayMs; + + public QueueRunner(int delayMs = 0, int maxCapacity = -1) + { + ArgumentOutOfRangeException.ThrowIfNegative(delayMs); + + _delayMs = delayMs; + _channel = maxCapacity switch + { + 0 or < -1 => throw new ArgumentOutOfRangeException(nameof(maxCapacity)), + -1 => Channel.CreateUnbounded>(new UnboundedChannelOptions() + { + SingleReader = true, + SingleWriter = false, + AllowSynchronousContinuations = true, + }), + _ => Channel.CreateBounded>(new BoundedChannelOptions(maxCapacity) + { + Capacity = maxCapacity, + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + AllowSynchronousContinuations = true + }) + }; + } + + public async Task RunAsync(CancellationToken cancel = default) + { + while (true) + { + var func = await _channel.Reader.ReadAsync(cancel); + + try + { + await func(); + } + catch (Exception ex) + { + Log.Warning(ex, "Exception executing a staggered func: {ErrorMessage}", ex.Message); + } + finally + { + if (_delayMs != 0) + { + await Task.Delay(_delayMs, cancel); + } + } + } + } + + public ValueTask EnqueueAsync(Func action) + => _channel.Writer.WriteAsync(action); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/TypedKey.cs b/src/EllieBot/_common/Abstractions/TypedKey.cs new file mode 100644 index 0000000..ea3103c --- /dev/null +++ b/src/EllieBot/_common/Abstractions/TypedKey.cs @@ -0,0 +1,30 @@ +namespace Ellie.Common; + +public readonly struct TypedKey +{ + public string Key { get; } + + public TypedKey(in string key) + => Key = key; + + public static implicit operator TypedKey(in string input) + => new(input); + + public static implicit operator string(in TypedKey input) + => input.Key; + + public static bool operator ==(in TypedKey left, in TypedKey right) + => left.Key == right.Key; + + public static bool operator !=(in TypedKey left, in TypedKey right) + => !(left == right); + + public override bool Equals(object? obj) + => obj is TypedKey o && o == this; + + public override int GetHashCode() + => Key?.GetHashCode() ?? 0; + + public override string ToString() + => Key; +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/YamlHelper.cs b/src/EllieBot/_common/Abstractions/YamlHelper.cs new file mode 100644 index 0000000..e3c39f0 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/YamlHelper.cs @@ -0,0 +1,48 @@ +#nullable disable +namespace EllieBot.Common.Yml; + +public static class YamlHelper +{ + // https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687 + /// + /// This is modified code from yamldotnet's repo which handles parsing unicode code points + /// it is needed as yamldotnet doesn't support unescaped unicode characters + /// + /// Unicode code point + /// Actual character + public static string UnescapeUnicodeCodePoint(this string point) + { + var character = 0; + + // Scan the character value. + + foreach (var c in point) + { + if (!IsHex(c)) + return point; + + character = (character << 4) + AsHex(c); + } + + // Check the value and write the character. + + if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF) + return point; + + return char.ConvertFromUtf32(character); + } + + public static bool IsHex(char c) + => c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f'); + + public static int AsHex(char c) + { + if (c <= '9') + return c - '0'; + + if (c <= 'F') + return c - 'A' + 10; + + return c - 'a' + 10; + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/creds/IBotCredentials.cs b/src/EllieBot/_common/Abstractions/creds/IBotCredentials.cs new file mode 100644 index 0000000..87db412 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/creds/IBotCredentials.cs @@ -0,0 +1,78 @@ +#nullable disable +namespace EllieBot; + +public interface IBotCredentials +{ + string Token { get; } + string GoogleApiKey { get; } + ICollection OwnerIds { get; set; } + bool UsePrivilegedIntents { get; } + string RapidApiKey { get; } + + Creds.DbOptions Db { get; } + string OsuApiKey { get; } + int TotalShards { get; } + Creds.PatreonSettings Patreon { get; } + string CleverbotApiKey { get; } + string Gpt3ApiKey { get; } + RestartConfig RestartCommand { get; } + Creds.VotesSettings Votes { get; } + string BotListToken { get; } + string RedisOptions { get; } + string LocationIqApiKey { get; } + string TimezoneDbApiKey { get; } + string CoinmarketcapApiKey { get; } + string TrovoClientId { get; } + string CoordinatorUrl { get; set; } + string TwitchClientId { get; set; } + string TwitchClientSecret { get; set; } + GoogleApiConfig Google { get; set; } + BotCacheImplemenation BotCache { get; set; } +} + +public interface IVotesSettings +{ + string TopggServiceUrl { get; set; } + string TopggKey { get; set; } + string DiscordsServiceUrl { get; set; } + string DiscordsKey { get; set; } +} + +public interface IPatreonSettings +{ + public string ClientId { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public string ClientSecret { get; set; } + public string CampaignId { get; set; } +} + +public interface IRestartConfig +{ + string Cmd { get; set; } + string Args { get; set; } +} + +public class RestartConfig : IRestartConfig +{ + public string Cmd { get; set; } + public string Args { get; set; } +} + +public enum BotCacheImplemenation +{ + Memory, + Redis +} + +public interface IDbOptions +{ + string Type { get; set; } + string ConnectionString { get; set; } +} + +public interface IGoogleApiConfig +{ + string SearchId { get; init; } + string ImageSearchId { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/creds/IBotCredsProvider.cs b/src/EllieBot/_common/Abstractions/creds/IBotCredsProvider.cs new file mode 100644 index 0000000..ecc90f0 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/creds/IBotCredsProvider.cs @@ -0,0 +1,8 @@ +namespace EllieBot; + +public interface IBotCredsProvider +{ + public void Reload(); + public IBotCredentials GetCreds(); + public void ModifyCredsFile(Action func); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/strings/CommandStrings.cs b/src/EllieBot/_common/Abstractions/strings/CommandStrings.cs new file mode 100644 index 0000000..6f72ab0 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/strings/CommandStrings.cs @@ -0,0 +1,35 @@ +#nullable disable +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +namespace EllieBot.Services; + +// public sealed record class CommandStrings +// { +// [YamlMember(Alias = "desc")] +// public string Desc { get; set; } +// +// [YamlMember(Alias = "args")] +// public string[] Args { get; set; } +// } + +public sealed record class CommandStrings +{ + [YamlMember(Alias = "desc")] + public string Desc { get; set; } + + [YamlMember(Alias = "ex")] + public string[] Examples { get; set; } + + [YamlMember(Alias = "params")] + public Dictionary[] Params { get; set; } +} + +public sealed record class CommandStringParam +{ + // [YamlMember(Alias = "type", ScalarStyle = ScalarStyle.DoubleQuoted)] + // public string Type { get; set; } + + [YamlMember(Alias = "desc", ScalarStyle = ScalarStyle.DoubleQuoted)] + public string Desc { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/strings/IBotStrings.cs b/src/EllieBot/_common/Abstractions/strings/IBotStrings.cs new file mode 100644 index 0000000..2ded833 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/strings/IBotStrings.cs @@ -0,0 +1,16 @@ +#nullable disable +using System.Globalization; + +namespace EllieBot.Common; + +/// +/// Defines methods to retrieve and reload bot strings +/// +public interface IBotStrings +{ + string GetText(string key, ulong? guildId = null, params object[] data); + string GetText(string key, CultureInfo locale, params object[] data); + void Reload(); + CommandStrings GetCommandStrings(string commandName, ulong? guildId = null); + CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/strings/IBotStringsExtensions.cs b/src/EllieBot/_common/Abstractions/strings/IBotStringsExtensions.cs new file mode 100644 index 0000000..17f9377 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/strings/IBotStringsExtensions.cs @@ -0,0 +1,17 @@ +#nullable disable +using System.Globalization; + +namespace EllieBot.Common; + +public static class BotStringsExtensions +{ + // this one is for pipe fun, see PipeExtensions.cs + public static string GetText(this IBotStrings strings, in LocStr str, in ulong guildId) + => strings.GetText(str.Key, guildId, str.Params); + + public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null) + => strings.GetText(str.Key, guildId, str.Params); + + public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture) + => strings.GetText(str.Key, culture, str.Params); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/strings/IBotStringsProvider.cs b/src/EllieBot/_common/Abstractions/strings/IBotStringsProvider.cs new file mode 100644 index 0000000..87cab9f --- /dev/null +++ b/src/EllieBot/_common/Abstractions/strings/IBotStringsProvider.cs @@ -0,0 +1,28 @@ +#nullable disable +namespace EllieBot.Services; + +/// +/// Implemented by classes which provide localized strings in their own ways +/// +public interface IBotStringsProvider +{ + /// + /// Gets localized string + /// + /// Language name + /// String key + /// Localized string + string GetText(string localeName, string key); + + /// + /// Reloads string cache + /// + void Reload(); + + /// + /// Gets command arg examples and description + /// + /// Language name + /// Command name + CommandStrings GetCommandStrings(string localeName, string commandName); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/strings/IStringsSource.cs b/src/EllieBot/_common/Abstractions/strings/IStringsSource.cs new file mode 100644 index 0000000..a2715f0 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/strings/IStringsSource.cs @@ -0,0 +1,17 @@ +#nullable disable + +namespace EllieBot.Services; + +/// +/// Basic interface used for classes implementing strings loading mechanism +/// +public interface IStringsSource +{ + /// + /// Gets all response strings + /// + /// Dictionary(localename, Dictionary(key, response)) + Dictionary> GetResponseStrings(); + + Dictionary> GetCommandStrings(); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Abstractions/strings/LocStr.cs b/src/EllieBot/_common/Abstractions/strings/LocStr.cs new file mode 100644 index 0000000..78a8bb7 --- /dev/null +++ b/src/EllieBot/_common/Abstractions/strings/LocStr.cs @@ -0,0 +1,13 @@ +namespace EllieBot; + +public readonly struct LocStr +{ + public readonly string Key; + public readonly object[] Params; + + public LocStr(string key, params object[] data) + { + Key = key; + Params = data; + } +} \ No newline at end of file