Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
138d3441e4 | |||
86a4a1ca99 | |||
fb2642904d | |||
bc4ab57a67 | |||
96ce7b192e | |||
4fb4a2d0c3 | |||
c6ea3fd36f | |||
491e9b5a6f | |||
34ba6e782b | |||
ba1bc1732e |
18 changed files with 275 additions and 101 deletions
.gitignoreCHANGELOG.mdLICENSE
src/EllieBot
EllieBot.csproj
Modules
data
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -20,6 +20,7 @@ src/EllieBot/credentials.json
|
||||||
src/EllieBot/old_credentials.json
|
src/EllieBot/old_credentials.json
|
||||||
src/EllieBot/credentials.json.bak
|
src/EllieBot/credentials.json.bak
|
||||||
src/EllieBot/data/EllieBot.db
|
src/EllieBot/data/EllieBot.db
|
||||||
|
# scripts
|
||||||
ellie-menu.ps1
|
ellie-menu.ps1
|
||||||
package.sh
|
package.sh
|
||||||
|
|
||||||
|
@ -372,3 +373,8 @@ __pycache__/
|
||||||
### VisualStudio Patch ###
|
### VisualStudio Patch ###
|
||||||
build/
|
build/
|
||||||
site/
|
site/
|
||||||
|
|
||||||
|
## AI
|
||||||
|
|
||||||
|
.aider.*
|
||||||
|
PROMPT.md
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -2,6 +2,25 @@
|
||||||
|
|
||||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
|
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## [5.3.9] - 31.01.2025
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added `.todo archive done <name>`
|
||||||
|
- Creates an archive of only currently completed todos
|
||||||
|
- An alternative to ".todo archive add <name>" which moves all todos to an archive
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Increased todo and archive limits slightly
|
||||||
|
- Global ellie captcha patron ad will show 12.5% of the time now, down from 20%, and be smaller
|
||||||
|
- `.remind` now has a 1 year max timeout, up from 2 months
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Captcha is now slightly bigger, with larger margin, to mitigate phone edge issues
|
||||||
|
- Fixed `.stock` command, unless there is some ip blocking going on
|
||||||
|
|
||||||
## [5.3.8] - 29.01.2025
|
## [5.3.8] - 29.01.2025
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
@ -195,7 +214,7 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except da
|
||||||
|
|
||||||
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands
|
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands
|
||||||
- `.sar autodel`
|
- `.sar autodel`
|
||||||
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
|
- Toggles the automatic deletion of the user's message and Ellie's confirmations for .iam and .iamn commands.
|
||||||
- `.sar ad`
|
- `.sar ad`
|
||||||
- Adds a role to the list of self-assignable roles. You can also specify a group.
|
- Adds a role to the list of self-assignable roles. You can also specify a group.
|
||||||
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role
|
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -186,7 +186,7 @@
|
||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2024 Toastie_t0ast
|
Copyright 2025 Toastie_t0ast
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>5.3.8</Version>
|
<Version>5.3.9</Version>
|
||||||
|
|
||||||
<!-- Output/build -->
|
<!-- Output/build -->
|
||||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
|
|
|
@ -97,9 +97,9 @@ public partial class Administration : EllieModule<AdministrationService>
|
||||||
var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
|
var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle(GetText(strs.server_delmsgoncmd))
|
.WithTitle(GetText(strs.server_delmsgoncmd))
|
||||||
.WithDescription(enabled ? "✅" : "❌");
|
.WithDescription(enabled ? "✅" : "❌");
|
||||||
|
|
||||||
var str = string.Join("\n",
|
var str = string.Join("\n",
|
||||||
channels.Select(x =>
|
channels.Select(x =>
|
||||||
|
@ -301,6 +301,16 @@ public partial class Administration : EllieModule<AdministrationService>
|
||||||
public Task Delete(ulong messageId, ParsedTimespan timespan = null)
|
public Task Delete(ulong messageId, ParsedTimespan timespan = null)
|
||||||
=> Delete((ITextChannel)ctx.Channel, messageId, timespan);
|
=> Delete((ITextChannel)ctx.Channel, messageId, timespan);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Delete(MessageLink messageLink, ParsedTimespan timespan = null)
|
||||||
|
{
|
||||||
|
if (messageLink.Channel is not ITextChannel tc)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Delete(tc, messageLink.Message.Id, timespan);
|
||||||
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async Task Delete(ITextChannel channel, ulong messageId, ParsedTimespan timespan = null)
|
public async Task Delete(ITextChannel channel, ulong messageId, ParsedTimespan timespan = null)
|
||||||
|
@ -373,7 +383,8 @@ public partial class Administration : EllieModule<AdministrationService>
|
||||||
if (ctx.Channel is not SocketTextChannel stc)
|
if (ctx.Channel is not SocketTextChannel stc)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var t = stc.Threads.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase));
|
var t = stc.Threads.FirstOrDefault(
|
||||||
|
x => string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
if (t is null)
|
if (t is null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -162,15 +162,15 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
|
|
||||||
if (password is not null)
|
if (password is not null)
|
||||||
{
|
{
|
||||||
var img = GetPasswordImage(password);
|
var img = _captchaService.GetPasswordImage(password);
|
||||||
await using var stream = await img.ToStreamAsync();
|
await using var stream = await img.ToStreamAsync();
|
||||||
var toSend = Response()
|
var toSend = Response()
|
||||||
.File(stream, "timely.png");
|
.File(stream, "timely.png");
|
||||||
|
|
||||||
#if GLOBAL_ELLIE
|
#if GLOBAL_ELLIE
|
||||||
if (_rng.Next(0, 5) == 0)
|
if (_rng.Next(0, 8) == 0)
|
||||||
toSend = toSend
|
toSend = toSend
|
||||||
.Confirm("[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.");
|
.Text("*[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.*");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var captchaMessage = await toSend.SendAsync();
|
var captchaMessage = await toSend.SendAsync();
|
||||||
|
@ -194,39 +194,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
await ClaimTimely();
|
await ClaimTimely();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Image<Rgba32> GetPasswordImage(string password)
|
|
||||||
{
|
|
||||||
var img = new Image<Rgba32>(50, 24);
|
|
||||||
|
|
||||||
var font = _fonts.NotoSans.CreateFont(22);
|
|
||||||
var outlinePen = new SolidPen(Color.Black, 0.5f);
|
|
||||||
var strikeoutRun = new RichTextRun
|
|
||||||
{
|
|
||||||
Start = 0,
|
|
||||||
End = password.GetGraphemeCount(),
|
|
||||||
Font = font,
|
|
||||||
StrikeoutPen = new SolidPen(Color.White, 4),
|
|
||||||
TextDecorations = TextDecorations.Strikeout
|
|
||||||
};
|
|
||||||
// draw password on the image
|
|
||||||
img.Mutate(x =>
|
|
||||||
{
|
|
||||||
x.DrawText(new RichTextOptions(font)
|
|
||||||
{
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
FallbackFontFamilies = _fonts.FallBackFonts,
|
|
||||||
Origin = new(25, 12),
|
|
||||||
TextRuns = [strikeoutRun]
|
|
||||||
},
|
|
||||||
password,
|
|
||||||
Brushes.Solid(Color.White),
|
|
||||||
outlinePen);
|
|
||||||
});
|
|
||||||
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ClaimTimely()
|
private async Task ClaimTimely()
|
||||||
{
|
{
|
||||||
var period = Config.Timely.Cooldown;
|
var period = Config.Timely.Cooldown;
|
||||||
|
|
|
@ -111,8 +111,20 @@ public partial class OpenAiApiSession : IChatterBotSession
|
||||||
});
|
});
|
||||||
|
|
||||||
var dataString = await data.Content.ReadAsStringAsync();
|
var dataString = await data.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
data.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Failed to get response from OpenAI: {Message}", ex.Message);
|
||||||
|
return new Error<string>("Failed to get response from OpenAI");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
|
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
|
||||||
|
|
||||||
// Log.Information("Received response: {Response} ", dataString);
|
// Log.Information("Received response: {Response} ", dataString);
|
||||||
|
|
|
@ -16,7 +16,7 @@ public sealed class CaptchaService(FontProvider fonts, IBotCache cache, IPatrona
|
||||||
|
|
||||||
public Image<Rgba32> GetPasswordImage(string password)
|
public Image<Rgba32> GetPasswordImage(string password)
|
||||||
{
|
{
|
||||||
var img = new Image<Rgba32>(50, 24);
|
var img = new Image<Rgba32>(60, 34);
|
||||||
|
|
||||||
var font = fonts.NotoSans.CreateFont(22);
|
var font = fonts.NotoSans.CreateFont(22);
|
||||||
var outlinePen = new SolidPen(Color.Black, 0.5f);
|
var outlinePen = new SolidPen(Color.Black, 0.5f);
|
||||||
|
@ -38,7 +38,7 @@ public sealed class CaptchaService(FontProvider fonts, IBotCache cache, IPatrona
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
FallbackFontFamilies = fonts.FallBackFonts,
|
FallbackFontFamilies = fonts.FallBackFonts,
|
||||||
Origin = new(25, 12),
|
Origin = new(30, 15),
|
||||||
TextRuns = [strikeoutRun]
|
TextRuns = [strikeoutRun]
|
||||||
},
|
},
|
||||||
password,
|
password,
|
||||||
|
|
|
@ -33,9 +33,9 @@ public partial class Games
|
||||||
.File(stream, "timely.png");
|
.File(stream, "timely.png");
|
||||||
|
|
||||||
#if GLOBAL_ELLIE
|
#if GLOBAL_ELLIE
|
||||||
if (_rng.Next(0, 5) == 0)
|
if (_rng.Next(0, 8) == 0)
|
||||||
toSend = toSend
|
toSend = toSend
|
||||||
.Confirm("[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.");
|
.Text("*[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.*");
|
||||||
#endif
|
#endif
|
||||||
var captcha = await toSend.SendAsync();
|
var captcha = await toSend.SendAsync();
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using CsvHelper.Configuration;
|
using CsvHelper.Configuration;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace EllieBot.Modules.Searches;
|
namespace EllieBot.Modules.Searches;
|
||||||
|
@ -9,54 +11,57 @@ namespace EllieBot.Modules.Searches;
|
||||||
public sealed class DefaultStockDataService : IStockDataService, IEService
|
public sealed class DefaultStockDataService : IStockDataService, IEService
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IBotCache _cache;
|
||||||
|
|
||||||
public DefaultStockDataService(IHttpClientFactory httpClientFactory)
|
public DefaultStockDataService(IHttpClientFactory httpClientFactory, IBotCache cache)
|
||||||
=> _httpClientFactory = httpClientFactory;
|
=> (_httpClientFactory, _cache) = (httpClientFactory, cache);
|
||||||
|
|
||||||
|
private static TypedKey<StockData> GetStockDataKey(string query)
|
||||||
|
=> new($"stockdata:{query}");
|
||||||
|
|
||||||
public async Task<StockData?> GetStockDataAsync(string query)
|
public async Task<StockData?> GetStockDataAsync(string query)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(query);
|
||||||
|
|
||||||
|
return await _cache.GetOrAddAsync(GetStockDataKey(query.Trim().ToLowerInvariant()),
|
||||||
|
() => GetStockDataInternalAsync(query),
|
||||||
|
expiry: TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StockData?> GetStockDataInternalAsync(string query)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!query.IsAlphaNumeric())
|
if (!query.IsAlphaNumeric())
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
using var http = _httpClientFactory.CreateClient();
|
var info = await GetNasdaqDataResponse<NasdaqSummaryResponse>(
|
||||||
|
$"https://api.nasdaq.com/api/quote/{query}/summary?assetclass=stocks");
|
||||||
|
|
||||||
var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}";
|
if (info?.Data is not { } d || d.SummaryData is not { } sd)
|
||||||
|
|
||||||
var config = Configuration.Default.WithDefaultLoader();
|
|
||||||
using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage);
|
|
||||||
|
|
||||||
var tickerName = document.QuerySelector("div.top > .left > .container > h1")
|
|
||||||
?.TextContent;
|
|
||||||
|
|
||||||
if (tickerName is null)
|
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var marketcap = document
|
var closePrice = double.Parse(sd.PreviousClose.Value?.Substring(1) ?? "0",
|
||||||
.QuerySelector("li > span > fin-streamer[data-field='marketCap']")
|
NumberStyles.Any,
|
||||||
?.TextContent;
|
CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var price = d.BidAsk.Bid.Value.IndexOf('*') is var idx and > 0
|
||||||
var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']")
|
&& double.TryParse(d.BidAsk.Bid.Value.Substring(1, idx - 1),
|
||||||
?.TextContent;
|
NumberStyles.Any,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']")
|
out var bid)
|
||||||
?.TextContent
|
? bid
|
||||||
?? "0";
|
: double.NaN;
|
||||||
|
|
||||||
var price = document.QuerySelector("fin-streamer.livePrice > span")
|
|
||||||
?.TextContent
|
|
||||||
?? "0";
|
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Name = tickerName,
|
Name = query,
|
||||||
Symbol = query,
|
Symbol = info.Data.Symbol,
|
||||||
Price = double.Parse(price, NumberStyles.Any, CultureInfo.InvariantCulture),
|
Price = price,
|
||||||
Close = double.Parse(close, NumberStyles.Any, CultureInfo.InvariantCulture),
|
Close = closePrice,
|
||||||
MarketCap = marketcap,
|
MarketCap = sd.MarketCap.Value,
|
||||||
DailyVolume = (long)double.Parse(volume ?? "0", NumberStyles.Any, CultureInfo.InvariantCulture),
|
DailyVolume =
|
||||||
|
(long)double.Parse(sd.AverageVolume.Value ?? "0", NumberStyles.Any, CultureInfo.InvariantCulture),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -66,6 +71,36 @@ public sealed class DefaultStockDataService : IStockDataService, IEService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<NasdaqDataResponse<T>?> GetNasdaqDataResponse<T>(string url)
|
||||||
|
{
|
||||||
|
using var httpClient = _httpClientFactory.CreateClient("google:search");
|
||||||
|
|
||||||
|
var req = new HttpRequestMessage(HttpMethod.Get,
|
||||||
|
url)
|
||||||
|
{
|
||||||
|
Headers =
|
||||||
|
{
|
||||||
|
{ "Host", "api.nasdaq.com" },
|
||||||
|
{ "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0" },
|
||||||
|
{ "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" },
|
||||||
|
{ "Accept-Language", "en-US,en;q=0.5" },
|
||||||
|
{ "Accept-Encoding", "gzip, deflate, br, zstd" },
|
||||||
|
{ "Connection", "keep-alive" },
|
||||||
|
{ "Upgrade-Insecure-Requests", "1" },
|
||||||
|
{ "Sec-Fetch-Dest", "document" },
|
||||||
|
{ "Sec-Fetch-Mode", "navigate" },
|
||||||
|
{ "Sec-Fetch-Site", "none" },
|
||||||
|
{ "Sec-Fetch-User", "?1" },
|
||||||
|
{ "Priority", "u=0, i" },
|
||||||
|
{ "TE", "trailers" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var res = await httpClient.SendAsync(req);
|
||||||
|
|
||||||
|
var info = await res.Content.ReadFromJsonAsync<NasdaqDataResponse<T>>();
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<SymbolData>> SearchSymbolAsync(string query)
|
public async Task<IReadOnlyCollection<SymbolData>> SearchSymbolAsync(string query)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
@ -91,22 +126,37 @@ public sealed class DefaultStockDataService : IStockDataService, IEService
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CsvConfiguration _csvConfig = new(CultureInfo.InvariantCulture);
|
private static TypedKey<IReadOnlyCollection<CandleData>> GetCandleDataKey(string query)
|
||||||
|
=> new($"candledata:{query}");
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<CandleData>> GetCandleDataAsync(string query)
|
public async Task<IReadOnlyCollection<CandleData>> GetCandleDataAsync(string query)
|
||||||
|
=> await _cache.GetOrAddAsync(GetCandleDataKey(query),
|
||||||
|
async () => await GetCandleDataInternalAsync(query),
|
||||||
|
expiry: TimeSpan.FromHours(4))
|
||||||
|
?? [];
|
||||||
|
|
||||||
|
public async Task<IReadOnlyCollection<CandleData>> GetCandleDataInternalAsync(string query)
|
||||||
{
|
{
|
||||||
using var http = _httpClientFactory.CreateClient();
|
using var http = _httpClientFactory.CreateClient();
|
||||||
await using var resStream = await http.GetStreamAsync(
|
|
||||||
$"https://query1.finance.yahoo.com/v7/finance/download/{query}"
|
|
||||||
+ $"?period1={DateTime.UtcNow.Subtract(30.Days()).ToTimestamp()}"
|
|
||||||
+ $"&period2={DateTime.UtcNow.ToTimestamp()}"
|
|
||||||
+ "&interval=1d");
|
|
||||||
|
|
||||||
using var textReader = new StreamReader(resStream);
|
var now = DateTime.UtcNow;
|
||||||
using var csv = new CsvReader(textReader, _csvConfig);
|
var fromdate = now.Subtract(30.Days()).ToString("yyyy-MM-dd");
|
||||||
var records = csv.GetRecords<YahooFinanceCandleData>().ToArray();
|
var todate = now.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
return records
|
var res = await GetNasdaqDataResponse<NasdaqChartResponse>(
|
||||||
.Map(static x => new CandleData(x.Open, x.Close, x.High, x.Low, x.Volume));
|
$"https://api.nasdaq.com/api/quote/{query}/chart?assetclass=stocks"
|
||||||
|
+ $"&fromdate={fromdate}"
|
||||||
|
+ $"&todate={todate}");
|
||||||
|
|
||||||
|
if (res?.Data?.Chart is not { } chart)
|
||||||
|
return Array.Empty<CandleData>();
|
||||||
|
|
||||||
|
|
||||||
|
return chart.Select(d => new CandleData(d.Z.Open,
|
||||||
|
d.Z.Close,
|
||||||
|
d.Z.High,
|
||||||
|
d.Z.Low,
|
||||||
|
(long)double.Parse(d.Z.Volume, NumberStyles.Any, CultureInfo.InvariantCulture)))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace EllieBot.Modules.Searches;
|
||||||
|
|
||||||
|
public sealed class NasdaqChartResponse
|
||||||
|
{
|
||||||
|
public required NasdaqChartResponseData[] Chart { get; init; }
|
||||||
|
|
||||||
|
public sealed class NasdaqChartResponseData
|
||||||
|
{
|
||||||
|
public required CandleData Z { get; init; }
|
||||||
|
|
||||||
|
public sealed class CandleData
|
||||||
|
{
|
||||||
|
public required decimal High { get; init; }
|
||||||
|
public required decimal Low { get; init; }
|
||||||
|
public required decimal Open { get; init; }
|
||||||
|
public required decimal Close { get; init; }
|
||||||
|
public required string Volume { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace EllieBot.Modules.Searches;
|
||||||
|
|
||||||
|
public sealed class NasdaqDataResponse<T>
|
||||||
|
{
|
||||||
|
public required T? Data { get; init; }
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Searches;
|
||||||
|
|
||||||
|
public sealed class NasdaqSummaryResponse
|
||||||
|
{
|
||||||
|
public required string Symbol { get; init; }
|
||||||
|
|
||||||
|
public required NasdaqSummaryResponseData SummaryData { get; init; }
|
||||||
|
public required NasdaqSummaryBidAsk BidAsk { get; init; }
|
||||||
|
|
||||||
|
public sealed class NasdaqSummaryBidAsk
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Bid * Size")]
|
||||||
|
public required NasdaqBid Bid { get; init; }
|
||||||
|
|
||||||
|
public sealed class NasdaqBid
|
||||||
|
{
|
||||||
|
public required string Value { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NasdaqSummaryResponseData
|
||||||
|
{
|
||||||
|
public required PreviousCloseData PreviousClose { get; init; }
|
||||||
|
public required MarketCapData MarketCap { get; init; }
|
||||||
|
public required AverageVolumeData AverageVolume { get; init; }
|
||||||
|
|
||||||
|
public sealed class PreviousCloseData
|
||||||
|
{
|
||||||
|
public required string Value { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MarketCapData
|
||||||
|
{
|
||||||
|
public required string Value { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AverageVolumeData
|
||||||
|
{
|
||||||
|
public required string Value { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -183,7 +183,7 @@ public partial class Utility
|
||||||
{
|
{
|
||||||
var time = DateTime.UtcNow + ts;
|
var time = DateTime.UtcNow + ts;
|
||||||
|
|
||||||
if (ts > TimeSpan.FromDays(60))
|
if (ts > TimeSpan.FromDays(366))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (ctx.Guild is not null)
|
if (ctx.Guild is not null)
|
||||||
|
|
|
@ -150,7 +150,26 @@ public partial class Utility
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task TodoArchiveAdd([Leftover] string name)
|
public async Task TodoArchiveAdd([Leftover] string name)
|
||||||
{
|
{
|
||||||
var result = await _service.ArchiveTodosAsync(ctx.User.Id, name);
|
var result = await _service.ArchiveTodosAsync(ctx.User.Id, name, false);
|
||||||
|
if (result == ArchiveTodoResult.NoTodos)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.todo_no_todos).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == ArchiveTodoResult.MaxLimitReached)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.todo_archive_max_limit).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task TodoArchiveDone([Leftover] string name)
|
||||||
|
{
|
||||||
|
var result = await _service.ArchiveTodosAsync(ctx.User.Id, name, true);
|
||||||
if (result == ArchiveTodoResult.NoTodos)
|
if (result == ArchiveTodoResult.NoTodos)
|
||||||
{
|
{
|
||||||
await Response().Error(strs.todo_no_todos).SendAsync();
|
await Response().Error(strs.todo_no_todos).SendAsync();
|
||||||
|
@ -193,7 +212,7 @@ public partial class Utility
|
||||||
|
|
||||||
foreach (var archivedList in items)
|
foreach (var archivedList in items)
|
||||||
{
|
{
|
||||||
eb.AddField($"id: {archivedList.Id.ToString()}", archivedList.Name, true);
|
eb.AddField($"id: {new kwum(archivedList.Id)}", archivedList.Name, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return eb;
|
return eb;
|
||||||
|
@ -202,7 +221,7 @@ public partial class Utility
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task TodoArchiveShow(int id)
|
public async Task TodoArchiveShow(kwum id)
|
||||||
{
|
{
|
||||||
var list = await _service.GetArchivedTodoListAsync(ctx.User.Id, id);
|
var list = await _service.GetArchivedTodoListAsync(ctx.User.Id, id);
|
||||||
if (list == null || list.Items.Count == 0)
|
if (list == null || list.Items.Count == 0)
|
||||||
|
@ -234,7 +253,7 @@ public partial class Utility
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task TodoArchiveDelete(int id)
|
public async Task TodoArchiveDelete(kwum id)
|
||||||
{
|
{
|
||||||
if (!await _service.ArchiveDeleteAsync(ctx.User.Id, id))
|
if (!await _service.ArchiveDeleteAsync(ctx.User.Id, id))
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,8 +6,8 @@ namespace EllieBot.Modules.Utility;
|
||||||
|
|
||||||
public sealed class TodoService : IEService
|
public sealed class TodoService : IEService
|
||||||
{
|
{
|
||||||
private const int ARCHIVE_MAX_COUNT = 9;
|
private const int ARCHIVE_MAX_COUNT = 18;
|
||||||
private const int TODO_MAX_COUNT = 27;
|
private const int TODO_MAX_COUNT = 36;
|
||||||
|
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ public sealed class TodoService : IEService
|
||||||
.DeleteAsync();
|
.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ArchiveTodoResult> ArchiveTodosAsync(ulong userId, string name)
|
public async Task<ArchiveTodoResult> ArchiveTodosAsync(ulong userId, string name, bool onlyDone)
|
||||||
{
|
{
|
||||||
// create a new archive
|
// create a new archive
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ public sealed class TodoService : IEService
|
||||||
|
|
||||||
var updated = await ctx
|
var updated = await ctx
|
||||||
.GetTable<TodoModel>()
|
.GetTable<TodoModel>()
|
||||||
.Where(x => x.UserId == userId && x.ArchiveId == null)
|
.Where(x => x.UserId == userId && (!onlyDone || x.IsDone) && x.ArchiveId == null)
|
||||||
.Set(x => x.ArchiveId, inserted.Id)
|
.Set(x => x.ArchiveId, inserted.Id)
|
||||||
.UpdateAsync();
|
.UpdateAsync();
|
||||||
|
|
||||||
|
|
|
@ -1441,6 +1441,11 @@ todoarchivedelete:
|
||||||
- del
|
- del
|
||||||
- remove
|
- remove
|
||||||
- rm
|
- rm
|
||||||
|
todoarchivedone:
|
||||||
|
- done
|
||||||
|
- compelete
|
||||||
|
- finish
|
||||||
|
- completed
|
||||||
todoedit:
|
todoedit:
|
||||||
- edit
|
- edit
|
||||||
- change
|
- change
|
||||||
|
|
|
@ -4134,7 +4134,11 @@ edit:
|
||||||
text:
|
text:
|
||||||
desc: "The new text content of the edited message."
|
desc: "The new text content of the edited message."
|
||||||
delete:
|
delete:
|
||||||
desc: Deletes a single message given the channel and message ID. If channel is ommited, message will be searched for in the current channel. You can also specify time parameter after which the message will be deleted (up to 7 days). This timer won't persist through bot restarts.
|
desc: |-
|
||||||
|
Deletes a single message given the channel and message ID, or a message link.
|
||||||
|
If channel is omitted, message will be searched for in the current channel.
|
||||||
|
You can also specify time parameter after which the message will be deleted (up to 7 days).
|
||||||
|
This timer won't persist through bot restarts.
|
||||||
ex:
|
ex:
|
||||||
- '#chat 771562360594628608'
|
- '#chat 771562360594628608'
|
||||||
- 771562360594628608
|
- 771562360594628608
|
||||||
|
@ -4144,6 +4148,10 @@ delete:
|
||||||
desc: "The id of a specific message within a channel, used to target the deletion operation."
|
desc: "The id of a specific message within a channel, used to target the deletion operation."
|
||||||
time:
|
time:
|
||||||
desc: "The duration after which the message should be automatically deleted."
|
desc: "The duration after which the message should be automatically deleted."
|
||||||
|
- messageLink:
|
||||||
|
desc: "The link of the message to delete. It must be on the same server."
|
||||||
|
time:
|
||||||
|
desc: "The duration after which the message should be automatically deleted."
|
||||||
- channel:
|
- channel:
|
||||||
desc: "The channel where the message is located or should be searched for."
|
desc: "The channel where the message is located or should be searched for."
|
||||||
messageId:
|
messageId:
|
||||||
|
@ -4524,6 +4532,13 @@ todoarchiveadd:
|
||||||
params:
|
params:
|
||||||
- name:
|
- name:
|
||||||
desc: "The name of the archive to be created."
|
desc: "The name of the archive to be created."
|
||||||
|
todoarchivedone:
|
||||||
|
desc: Creates a new archive with the specified name using only completed current todos.
|
||||||
|
ex:
|
||||||
|
- Success!
|
||||||
|
params:
|
||||||
|
- name:
|
||||||
|
desc: "The name of the archive to be created."
|
||||||
todoarchivelist:
|
todoarchivelist:
|
||||||
desc: Lists all archived todo lists.
|
desc: Lists all archived todo lists.
|
||||||
ex:
|
ex:
|
||||||
|
|
Loading…
Add table
Reference in a new issue