Compare commits

..

14 Commits
5.3.7 ... v5

Author SHA1 Message Date
Kwoth
6c7ac44ed0 Edit README.md 2025-03-18 12:23:35 +00:00
Kwoth
542cdc2d0d fix: OpenAI apis will now correctly print an error to the console if the request fails 2025-02-02 12:52:25 +00:00
Kwoth
02fa501530 docs: Updated LICENSE.md to reserve all rights, removed some missing files 2025-02-02 08:33:37 +00:00
Kwoth
556142c5ce .delete will now accept a message link 2025-02-02 07:56:21 +00:00
Kwoth
3db5a71d01 fix: fixed .stock command, most likely
fix: fixed 2 character captchas, again, most likely
2025-01-31 04:39:48 +00:00
Kwoth
06f692283b dev: CI will no longer execute everything on every push. Only tests, docker and medusa. Full build with artifacts only on releases. 2025-01-30 12:14:14 +00:00
Kwoth
49ff0dd27a add: added .todo archive done <name>, to create an alternative to .todo archive add <name> in case you want to create an archive of only currently completed todos
docs: Updated CHANGELOG.md, upped version to 5.3.9
2025-01-30 11:52:13 +00:00
Kwoth
2053296154 Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2025-01-30 11:42:56 +00:00
Kwoth
42fc0c263d fix: global nadeko captcha patron add will show 12.5% of the time now, down from 20%, and be smaller
change: increased todo and archive limits slightly
2025-01-30 11:42:22 +00:00
Kwoth
cf1d950308 fix: global nadeko captcha patron add will show 12.5% of the time now, down from 20%, and be smaller 2025-01-30 10:52:42 +00:00
Kwoth
0fdccea31c fix: fixed captcha cutting off 2025-01-30 10:50:26 +00:00
Kwoth
2f8f62afcb fix: fixed .stock command, probably 2025-01-30 10:00:00 +00:00
Kwoth
570f39d4f8 change: remind now has a 1 year max timeout, up from 2 months 2025-01-29 07:57:31 +00:00
Kwoth
40f1774655 fix: fixed .temprole not giving the role 2025-01-27 20:03:13 +00:00
25 changed files with 314 additions and 124 deletions

5
.gitignore vendored
View File

@@ -370,3 +370,8 @@ __pycache__/
### VisualStudio Patch ### ### VisualStudio Patch ###
build/ build/
site/ site/
## AI
.aider.*
PROMPT.md

View File

@@ -29,6 +29,10 @@ variables:
build: build:
stage: build stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: $CI_COMMIT_TAG
script: script:
- | - |
VERSION_STRING="" VERSION_STRING=""
@@ -54,6 +58,8 @@ upload-builds:
stage: upload-builds stage: upload-builds
image: alpine:latest image: alpine:latest
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
script: script:
- apk add --no-cache curl tar zip - apk add --no-cache curl tar zip
@@ -83,6 +89,8 @@ release:
stage: release stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest image: registry.gitlab.com/gitlab-org/release-cli:latest
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
script: script:
- | - |
@@ -130,7 +138,6 @@ publish-medusa-package:
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script: script:
- LAST_TAG=$(git describe --tags --abbrev=0) - LAST_TAG=$(git describe --tags --abbrev=0)
- if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-alpha$CI_COMMIT_SHORT_SHA"; fi - if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-alpha$CI_COMMIT_SHORT_SHA"; fi
@@ -162,6 +169,8 @@ docker-build:
- docker push "$CI_REGISTRY_IMAGE${tag}" - docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists # Run this job in a branch where a Dockerfile exists
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
exists: exists:
- Dockerfile - Dockerfile

View File

@@ -2,6 +2,32 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.3.9] - 30.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 nadeko 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] - 27.01.2025
## Fixed
- `.temprole` now correctly adds a role
- `.h temprole` also shows the correct overload now
## [5.3.7] - 21.01.2025 ## [5.3.7] - 21.01.2025
## Changed ## Changed

View File

@@ -1,4 +1,4 @@
Copyright 2023 Kwoth Copyright 2025 Breaker
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -12,8 +12,6 @@ ProjectSection(SolutionItems) = preProject
README.md = README.md README.md = README.md
.gitlab-ci.yml = .gitlab-ci.yml .gitlab-ci.yml = .gitlab-ci.yml
Dockerfile = Dockerfile Dockerfile = Dockerfile
migrate.ps1 = migrate.ps1
remove-migration.ps1 = remove-migration.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NadekoBot", "src\NadekoBot\NadekoBot.csproj", "{45EC1473-C678-4857-A544-07DFE0D0B478}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NadekoBot", "src\NadekoBot\NadekoBot.csproj", "{45EC1473-C678-4857-A544-07DFE0D0B478}"

View File

@@ -1,9 +1,4 @@
[![nadeko0](https://cdn.nadeko.bot/tutorial/docs-top.png)](https://nadeko.bot/)
[![nadeko1](https://cdn.nadeko.bot/tutorial/docs-mid.png)](https://invite.nadeko.bot/) - Nadeko has been moved to https://github.com/nadeko-bot/nadekobot
[![nadeko2](https://cdn.nadeko.bot/tutorial/docs-bot.png)](https://nadeko.bot/commands) - This repo will stay here as a v3, v4 and v5 archive
### Useful links
- [Self hosting Guides and Docs](https://nadekobot.readthedocs.io/en/latest)
- [Discord support server](https://discord.nadeko.bot)

View File

@@ -301,6 +301,16 @@ public partial class Administration : NadekoModule<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 : NadekoModule<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)
{ {
@@ -450,7 +461,8 @@ public partial class Administration : NadekoModule<AdministrationService>
public async Task SetServerBanner([Leftover] string img = null) public async Task SetServerBanner([Leftover] string img = null)
{ {
// Tier2 or higher is required to set a banner. // Tier2 or higher is required to set a banner.
if (ctx.Guild.PremiumTier is PremiumTier.Tier1 or PremiumTier.None) return; if (ctx.Guild.PremiumTier is PremiumTier.Tier1 or PremiumTier.None)
return;
var result = await _service.SetServerBannerAsync(ctx.Guild, img); var result = await _service.SetServerBannerAsync(ctx.Guild, img);

View File

@@ -221,7 +221,7 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
public async Task TempRole(ParsedTimespan timespan, IUser user, [Leftover] IRole role) public async Task TempRole(ParsedTimespan timespan, IGuildUser user, [Leftover] IRole role)
{ {
if (!await CheckRoleHierarchy(role)) if (!await CheckRoleHierarchy(role))
{ {
@@ -231,6 +231,7 @@ public partial class Administration
return; return;
} }
await user.AddRoleAsync(role);
await _tempRoleService.AddTempRoleAsync(ctx.Guild.Id, role.Id, user.Id, timespan.Time); await _tempRoleService.AddTempRoleAsync(ctx.Guild.Id, role.Id, user.Id, timespan.Time);

View File

@@ -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_NADEKO #if GLOBAL_NADEKO
if (_rng.Next(0, 5) == 0) if (_rng.Next(0, 8) == 0)
toSend = toSend toSend = toSend
.Confirm("[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha."); .Text("*[Sub on Patreon](https://patreon.com/nadekobot) 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;

View File

@@ -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);

View File

@@ -17,7 +17,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);
@@ -39,7 +39,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,
@@ -52,7 +52,7 @@ public sealed class CaptchaService(FontProvider fonts, IBotCache cache, IPatrona
public string GeneratePassword() public string GeneratePassword()
{ {
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3)); var num = _rng.Next((int)Math.Pow(32, 2) + 1, (int)Math.Pow(32, 3));
return new kwum(num).ToString(); return new kwum(num).ToString();
} }

View File

@@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Modules.Games; namespace NadekoBot.Modules.Games;
public sealed class FishCatch public sealed class FishCatch
{ {
[Key] [Key]
public int Id { get; set; } public int Id { get; set; }

View File

@@ -33,9 +33,9 @@ public partial class Games
.File(stream, "timely.png"); .File(stream, "timely.png");
#if GLOBAL_NADEKO #if GLOBAL_NADEKO
if (_rng.Next(0, 5) == 0) if (_rng.Next(0, 8) == 0)
toSend = toSend toSend = toSend
.Confirm("[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha."); .Text("*[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha.*");
#endif #endif
var captcha = await toSend.SendAsync(); var captcha = await toSend.SendAsync();

View File

@@ -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 NadekoBot.Modules.Searches; namespace NadekoBot.Modules.Searches;
@@ -9,54 +11,57 @@ namespace NadekoBot.Modules.Searches;
public sealed class DefaultStockDataService : IStockDataService, INService public sealed class DefaultStockDataService : IStockDataService, INService
{ {
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 sum = await GetNasdaqDataResponse<NasdaqSummaryResponse>(
$"https://api.nasdaq.com/api/quote/{query}/summary?assetclass=stocks");
var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}"; if (sum?.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 info = await GetNasdaqDataResponse<NasdaqInfoResponse>(
$"https://api.nasdaq.com/api/quote/{query}/info?assetclass=stocks");
var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']") if (info?.Data?.PrimaryData is not { } pd)
?.TextContent; return default;
var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']") var priceStr = pd.LastSalePrice;
?.TextContent
?? "0";
var price = document.QuerySelector("fin-streamer.livePrice > span")
?.TextContent
?? "0";
return new() return new()
{ {
Name = tickerName, Name = info.Data.CompanyName,
Symbol = query, Symbol = sum.Data.Symbol,
Price = double.Parse(price, NumberStyles.Any, CultureInfo.InvariantCulture), Price = double.Parse(priceStr?.Substring(1) ?? "0", NumberStyles.Any, CultureInfo.InvariantCulture),
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, INService
} }
} }
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, INService
.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();
} }
} }

View File

@@ -0,0 +1,20 @@
namespace NadekoBot.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; }
}
}
}

View File

@@ -0,0 +1,6 @@
namespace NadekoBot.Modules.Searches;
public sealed class NasdaqDataResponse<T>
{
public required T? Data { get; init; }
}

View File

@@ -0,0 +1,15 @@
namespace NadekoBot.Modules.Searches;
public sealed class NasdaqInfoResponse
{
public required string Symbol { get; init; }
public required string CompanyName {get; init; }
public required NasdaqInfoPrimaryData PrimaryData { get; init; }
public sealed class NasdaqInfoPrimaryData
{
public required string LastSalePrice{ get; init; }
public required string PercentageChange { get; init; }
public required string DeltaIndicator { get; init; }
}
}

View File

@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches;
public sealed class NasdaqSummaryResponse
{
public required string Symbol { get; init; }
public required NasdaqSummaryResponseData SummaryData { 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; }
}
}
}

View File

@@ -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)

View File

@@ -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))
{ {

View File

@@ -6,8 +6,8 @@ namespace NadekoBot.Modules.Utility;
public sealed class TodoService : INService public sealed class TodoService : INService
{ {
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 : INService
.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 : INService
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();
@@ -204,4 +204,5 @@ public sealed class TodoService : INService
.Where(x => x.UserId == userId && x.Id == todoId) .Where(x => x.UserId == userId && x.Id == todoId)
.FirstOrDefaultAsyncLinqToDB(); .FirstOrDefaultAsyncLinqToDB();
} }
} }

View File

@@ -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.7</Version> <Version>5.3.9</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View File

@@ -30,4 +30,5 @@ public static class SocketMessageComponentExtensions
string text, string text,
bool ephemeral = false) bool ephemeral = false)
=> smc.RespondAsync(sender, text, MsgType.Ok, ephemeral); => smc.RespondAsync(sender, text, MsgType.Ok, ephemeral);
} }

View File

@@ -1441,6 +1441,11 @@ todoarchivedelete:
- del - del
- remove - remove
- rm - rm
todoarchivedone:
- done
- compelete
- finish
- completed
todoedit: todoedit:
- edit - edit
- change - change

View File

@@ -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:
@@ -4852,11 +4867,11 @@ temprole:
- '15m @User Jail' - '15m @User Jail'
- '7d @Newbie Trial Member' - '7d @Newbie Trial Member'
params: params:
- days: - time:
desc: "The time after which the role is automatically removed." desc: "The time after which the role is automatically removed."
- user: user:
desc: "The user to give the role to." desc: "The user to give the role to."
- role: role:
desc: "The role to give to the user." desc: "The role to give to the user."
minesweeper: minesweeper:
desc: |- desc: |-