forked from EllieBotDevs/elliebot
Compare commits
43 commits
Author | SHA1 | Date | |
---|---|---|---|
68e736ceb8 | |||
d3c90ab59f | |||
4a19d91a4f | |||
53af8f940d | |||
6891869046 | |||
aba5c4fbfd | |||
7d8f61ecea | |||
524c97d9bf | |||
c058d180ae | |||
57a5993064 | |||
aa06f62258 | |||
07df2ed450 | |||
aaf8522987 | |||
d414ecda2d | |||
99a8030898 | |||
28f21bae12 | |||
177257da12 | |||
051daebada | |||
d96039d20c | |||
0a1797700c | |||
e4afa1e385 | |||
69f45cd592 | |||
ea1c8c56e3 | |||
4c2b42ab7f | |||
97fe14cf5a | |||
b1d28296f0 | |||
9fe75d930f | |||
69a02e0e15 | |||
8d08595a9f | |||
1cbaaed944 | |||
a583c7d763 | |||
7246c982df | |||
d26efb3c8c | |||
16025b74e3 | |||
55e3a80405 | |||
4d3bdc2481 | |||
a1bf03ad40 | |||
2d3c7de8e7 | |||
9044a04c87 | |||
b3d2785cec | |||
2f740e96b8 | |||
ae8a63eeac | |||
4549b1f4e4 |
145 changed files with 6202 additions and 2409 deletions
.editorconfig.gitignoreCHANGELOG.md
src
Ellie.Marmalade
EllieBot.GrpcApiBase/protos
EllieBot.VotesApi
Common
Controllers
DockerfileEllieBot.VotesApi.csprojProgram.csProtos
Services
Startup.csappsettings.jsonEllieBot
Db
EllieBot.csprojEllieBot.csproj.DotSettingsMigrations
PostgreSql
20250323021916_linkfixer.sql20250324230804_quests.sql20250327001838_fishitems.sql20250327001912_init.Designer.cs20250327001912_init.csPostgreSqlContextModelSnapshot.cs
Sqlite
Modules
Administration
Gambling
AnimalRacing
Bank
Gambling.csGamblingConfig.csGamblingConfigService.csGamblingService.csPlantPick
Shop
VoteRewardService.csWaifus
Games
ChatterBot
Fish
Db
FishCommands.csFishConfig.csFishConfigService.csFishItem.csFishItemCommands.csFishItemService.csFishResult.csFishService.csFishingCommands.csstrings.jsonHangman
NCanvas
Quests
Quest.csQuestCommands.csQuestEvent.csQuestEventType.csQuestIds.cs
QuestModels
BankerQuest.csBetFlowersQuest.csBetQuest.csCatchFishQuest.csCatchQualityQuest.csCatchTrashQuest.csCheckLeaderboardsQuest.csGiftWaifuQuest.csGiveFlowersQuest.csHangmanWinQuest.csIQuest.csJoinAnimalRaceQuest.csPlantPickQuest.csSetPixelsQuest.csWellInformed.cs
QuestService.cs
208
.editorconfig
Normal file
208
.editorconfig
Normal file
|
@ -0,0 +1,208 @@
|
|||
|
||||
[*]
|
||||
charset = utf-8-bom
|
||||
end_of_line = crlf
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
dotnet_naming_rule.constants_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.constants_rule.resharper_description = Constant fields (not private)
|
||||
dotnet_naming_rule.constants_rule.resharper_guid = 669e5282-fb4b-4e90-91e7-07d269d04b60
|
||||
dotnet_naming_rule.constants_rule.severity = suggestion
|
||||
dotnet_naming_rule.constants_rule.style = all_upper_style
|
||||
dotnet_naming_rule.constants_rule.symbols = constants_symbols
|
||||
dotnet_naming_rule.interfaces_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces
|
||||
dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7
|
||||
dotnet_naming_rule.interfaces_rule.severity = suggestion
|
||||
dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style
|
||||
dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
|
||||
dotnet_naming_rule.local_constants_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.local_constants_rule.resharper_description = Local constants
|
||||
dotnet_naming_rule.local_constants_rule.resharper_guid = a4f433b8-abcd-4e55-a08f-82e78cef0f0c
|
||||
dotnet_naming_rule.local_constants_rule.severity = suggestion
|
||||
dotnet_naming_rule.local_constants_rule.style = all_upper_style
|
||||
dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols
|
||||
dotnet_naming_rule.private_constants_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.private_constants_rule.resharper_description = Constant fields (private)
|
||||
dotnet_naming_rule.private_constants_rule.resharper_guid = 236f7aa5-7b06-43ca-bf2a-9b31bfcff09a
|
||||
dotnet_naming_rule.private_constants_rule.severity = suggestion
|
||||
dotnet_naming_rule.private_constants_rule.style = all_upper_style
|
||||
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
|
||||
dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.private_instance_fields_rule.resharper_description = Instance fields (private)
|
||||
dotnet_naming_rule.private_instance_fields_rule.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c
|
||||
dotnet_naming_rule.private_instance_fields_rule.severity = suggestion
|
||||
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
|
||||
dotnet_naming_rule.private_static_fields_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.private_static_fields_rule.resharper_description = Static fields (private)
|
||||
dotnet_naming_rule.private_static_fields_rule.resharper_exclusive_prefixes_suffixes = true
|
||||
dotnet_naming_rule.private_static_fields_rule.resharper_guid = f9fce829-e6f4-4cb2-80f1-5497c44f51df
|
||||
dotnet_naming_rule.private_static_fields_rule.severity = suggestion
|
||||
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
|
||||
dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.private_static_readonly_rule.resharper_description = Static readonly fields (private)
|
||||
dotnet_naming_rule.private_static_readonly_rule.resharper_guid = 15b5b1f1-457c-4ca6-b278-5615aedc07d3
|
||||
dotnet_naming_rule.private_static_readonly_rule.severity = suggestion
|
||||
dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
|
||||
dotnet_naming_rule.type_parameters_rule.import_to_resharper = True
|
||||
dotnet_naming_rule.type_parameters_rule.resharper_description = Type parameters
|
||||
dotnet_naming_rule.type_parameters_rule.resharper_guid = 2c62818f-621b-4425-adc9-78611099bfcb
|
||||
dotnet_naming_rule.type_parameters_rule.severity = suggestion
|
||||
dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style
|
||||
dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols
|
||||
dotnet_naming_style.all_upper_style.capitalization = all_upper
|
||||
dotnet_naming_style.all_upper_style.word_separator = _
|
||||
dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case
|
||||
dotnet_naming_style.i_upper_camel_case_style.required_prefix = I
|
||||
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
|
||||
dotnet_naming_style.lower_camel_case_style.required_prefix = _
|
||||
dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case
|
||||
dotnet_naming_style.t_upper_camel_case_style.required_prefix = T
|
||||
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
|
||||
dotnet_naming_symbols.constants_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.constants_symbols.required_modifiers = const
|
||||
dotnet_naming_symbols.constants_symbols.resharper_applicable_kinds = constant_field
|
||||
dotnet_naming_symbols.constants_symbols.resharper_required_modifiers = any
|
||||
dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface
|
||||
dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any
|
||||
dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local
|
||||
dotnet_naming_symbols.local_constants_symbols.required_modifiers = const
|
||||
dotnet_naming_symbols.local_constants_symbols.resharper_applicable_kinds = local_constant
|
||||
dotnet_naming_symbols.local_constants_symbols.resharper_required_modifiers = any
|
||||
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
|
||||
dotnet_naming_symbols.private_constants_symbols.resharper_applicable_kinds = constant_field
|
||||
dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers = any
|
||||
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds = field,readonly_field
|
||||
dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers = instance
|
||||
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
|
||||
dotnet_naming_symbols.private_static_fields_symbols.resharper_applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_fields_symbols.resharper_required_modifiers = static
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = readonly,static
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds = readonly_field
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers = static
|
||||
dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
|
||||
dotnet_naming_symbols.type_parameters_symbols.resharper_applicable_kinds = type_parameter
|
||||
dotnet_naming_symbols.type_parameters_symbols.resharper_required_modifiers = any
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
|
||||
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_autodetect_indent_settings = true
|
||||
resharper_csharp_wrap_arguments_style = chop_if_long
|
||||
resharper_force_attribute_style = join
|
||||
resharper_formatter_off_tag = @formatter:off
|
||||
resharper_formatter_on_tag = @formatter:on
|
||||
resharper_formatter_tags_enabled = true
|
||||
resharper_keep_existing_embedded_arrangement = false
|
||||
resharper_keep_existing_expr_member_arrangement = false
|
||||
resharper_method_or_operator_body = expression_body
|
||||
resharper_object_creation_when_type_not_evident = target_typed
|
||||
resharper_place_accessorholder_attribute_on_same_line = false
|
||||
resharper_place_accessor_attribute_on_same_line = false
|
||||
resharper_place_expr_method_on_single_line = false
|
||||
resharper_place_expr_property_on_single_line = false
|
||||
resharper_place_field_attribute_on_same_line = false
|
||||
resharper_place_simple_embedded_statement_on_same_line = false
|
||||
resharper_show_autodetect_configure_formatting_tip = false
|
||||
resharper_use_indent_from_vs = false
|
||||
resharper_wrap_before_arrow_with_expressions = true
|
||||
|
||||
# ReSharper inspection severities
|
||||
resharper_arrange_constructor_or_destructor_body_highlighting = hint
|
||||
resharper_arrange_method_or_operator_body_highlighting = suggestion
|
||||
resharper_arrange_object_creation_when_type_evident_highlighting = none
|
||||
resharper_arrange_redundant_parentheses_highlighting = suggestion
|
||||
resharper_arrange_this_qualifier_highlighting = hint
|
||||
resharper_arrange_type_member_modifiers_highlighting = none
|
||||
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
|
||||
resharper_built_in_type_reference_style_highlighting = hint
|
||||
resharper_check_namespace_highlighting = none
|
||||
resharper_class_never_instantiated_global_highlighting = hint
|
||||
resharper_convert_to_primary_constructor_highlighting = none
|
||||
resharper_convert_to_using_declaration_highlighting = warning
|
||||
resharper_convert_type_check_pattern_to_null_check_highlighting = hint
|
||||
resharper_empty_general_catch_clause_highlighting = none
|
||||
resharper_function_never_returns_highlighting = suggestion
|
||||
resharper_inconsistent_naming_highlighting = suggestion
|
||||
resharper_invert_if_highlighting = none
|
||||
resharper_lambda_expression_can_be_made_static_highlighting = hint
|
||||
resharper_mvc_action_not_resolved_highlighting = warning
|
||||
resharper_mvc_area_not_resolved_highlighting = warning
|
||||
resharper_mvc_controller_not_resolved_highlighting = warning
|
||||
resharper_mvc_masterpage_not_resolved_highlighting = warning
|
||||
resharper_mvc_partial_view_not_resolved_highlighting = warning
|
||||
resharper_mvc_template_not_resolved_highlighting = warning
|
||||
resharper_mvc_view_component_not_resolved_highlighting = warning
|
||||
resharper_mvc_view_component_view_not_resolved_highlighting = warning
|
||||
resharper_mvc_view_not_resolved_highlighting = warning
|
||||
resharper_non_readonly_member_in_get_hash_code_highlighting = suggestion
|
||||
resharper_not_accessed_field_local_highlighting = suggestion
|
||||
resharper_out_parameter_value_is_always_discarded_local_highlighting = suggestion
|
||||
resharper_private_field_can_be_converted_to_local_variable_highlighting = suggestion
|
||||
resharper_razor_assembly_not_resolved_highlighting = warning
|
||||
resharper_redundant_anonymous_type_property_name_highlighting = suggestion
|
||||
resharper_redundant_base_qualifier_highlighting = warning
|
||||
resharper_redundant_pattern_parentheses_highlighting = none
|
||||
resharper_redundant_record_class_keyword_highlighting = suggestion
|
||||
resharper_replace_with_single_call_to_first_or_default_highlighting = hint
|
||||
resharper_suggest_var_or_type_built_in_types_highlighting = hint
|
||||
resharper_suggest_var_or_type_elsewhere_highlighting = hint
|
||||
resharper_suggest_var_or_type_simple_types_highlighting = hint
|
||||
resharper_unused_method_return_value_local_highlighting = suggestion
|
||||
resharper_use_await_using_highlighting = warning
|
||||
resharper_web_config_module_not_resolved_highlighting = warning
|
||||
resharper_web_config_type_not_resolved_highlighting = warning
|
||||
resharper_web_config_wrong_module_highlighting = warning
|
||||
|
||||
[{*.har,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{*.bash,*.sh,*.zsh}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.proto]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,cg,cginc,compute,cs,cshtml,dtd,fx,fxh,hlsl,hlsli,hlslinc,master,nuspec,paml,razor,resw,resx,shaderFoundry,skin,urtshader,usf,ush,vb,xaml,xamlx,xoml,xsd}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
tab_width = 4
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -371,8 +371,7 @@ site/
|
|||
|
||||
.aider.*
|
||||
PROMPT.md
|
||||
.aider*
|
||||
.windsurfrules
|
||||
.*rules
|
||||
|
||||
## Python pip/env files
|
||||
Pipfile
|
||||
|
|
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -2,6 +2,62 @@
|
|||
|
||||
*a,c,f,r,o*
|
||||
|
||||
## [6.1.0] - 30.03.2025
|
||||
|
||||
### Added
|
||||
- Added Quest System!
|
||||
- Each user gets a couple of daily quests to complete
|
||||
- There are 10-15 different quests, each day you'll get 3
|
||||
- Upon completion of all dailies, the user will get a boost to timely and vote
|
||||
- `.quests` to see your quests
|
||||
- Added Fishing Items!
|
||||
- `.fishop` to see a list of all available items for sale
|
||||
- `.fibuy` to buy an item
|
||||
- `.finv` to see your inventory
|
||||
- `.fiuse` to use an item. You can equip one of each item, except potions
|
||||
- You can equip one of each item
|
||||
- You can equip any number of potions, but they have limited duration and cant be unequiped
|
||||
- `.fili` will show your equipped item names, nad `.fish` will show bonuses
|
||||
- Added `.fishlb` to see the top anglers
|
||||
- Added `.notify <channel> nicecatch <message>` event
|
||||
- It will show all rare fish/trash and all max star fish caught on any server
|
||||
- You can use `.notifyphs nicecatch` to see the list of placeholders you can use while setting a message
|
||||
- Example: `.notify #fishfeed nicecatch %user% just caught a %event.fish.stars% %event.fish.name% %event.fish.emoji%`
|
||||
- Added prices to `.nczoom`
|
||||
- Voting re-added, `.votefeed` to see all votes. Non-trivial setup required, check commits
|
||||
- owner only `.massping` command for special situations
|
||||
|
||||
### Changed
|
||||
- .notify commands now require Manage Messages permission
|
||||
- .notify will now let you know if you can't set a notify message due to a missing channel
|
||||
- `.say` will no longer reply
|
||||
- `.vote` and `.timely` will now show active bonuses
|
||||
- `.lcha` (live channel) limit increased to 5
|
||||
- `.nc` will now show instructions
|
||||
|
||||
### Fixed
|
||||
- Fixed `.antispamignore` restart persistence
|
||||
- Fixed `.notify` events. Only levelup used to work
|
||||
- Fixed `.hangman` misalignment
|
||||
- Fixed bank quest
|
||||
|
||||
## [6.0.13] - 23.03.2025
|
||||
|
||||
### Added
|
||||
- Added `.linkfix <old> <new>` command
|
||||
- If bot sees a message with the old link, it will reply to the message with a fixed (new) link
|
||||
- ex: `.linkfix twitter.com vxtwitter.com`
|
||||
- Added `.roleicon role <icon_url / server_emoji>` command to set the icon of a role
|
||||
- Added a captcha option for `.fish`
|
||||
|
||||
### Fixed
|
||||
- Fixed youtube stream notifications in case invalid channel was provided
|
||||
- `.lcha` (live channel) will now let you override an existing channel template even if you're at the limit
|
||||
- Fixed `.shop` commands
|
||||
|
||||
### Removed
|
||||
- removed `.xpglb` as it is no longer used
|
||||
|
||||
## [6.0.12] - 20.03.2025
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net.Core" Version="3.17.1" />
|
||||
<PackageReference Include="Discord.Net.Core" Version="3.17.2" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="15.1.6" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -12,6 +12,8 @@ service GrpcXp {
|
|||
|
||||
rpc AddReward(AddRewardRequest) returns (AddRewardReply);
|
||||
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
|
||||
|
||||
rpc GetUserXp(GetUserXpRequest) returns (GetUserXpReply);
|
||||
}
|
||||
|
||||
message GetXpLbRequest {
|
||||
|
@ -75,4 +77,18 @@ message DeleteRewardRequest {
|
|||
|
||||
message DeleteRewardReply {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message GetUserXpRequest {
|
||||
uint64 guildId = 1;
|
||||
uint64 userId = 2;
|
||||
}
|
||||
|
||||
message GetUserXpReply {
|
||||
int64 xp = 1;
|
||||
int64 requiredXp = 2;
|
||||
int64 level = 3;
|
||||
string club = 4;
|
||||
string clubIcon = 5;
|
||||
int32 rank = 6;
|
||||
}
|
71
src/EllieBot.GrpcApiBase/protos/xpshop.proto
Normal file
71
src/EllieBot.GrpcApiBase/protos/xpshop.proto
Normal file
|
@ -0,0 +1,71 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "EllieBot.GrpcApi";
|
||||
|
||||
package greet;
|
||||
|
||||
service GrpcXpShop {
|
||||
rpc AddXpShopItem (AddXpShopItemRequest) returns (AddXpShopItemReply);
|
||||
rpc GetShopItems (GetShopItemsRequest) returns (GetShopItemsReply);
|
||||
rpc UseShopItem (UseShopItemRequest) returns (UseShopItemReply);
|
||||
rpc BuyShopItem (BuyShopItemRequest) returns (BuyShopItemReply);
|
||||
}
|
||||
|
||||
message UseShopItemRequest {
|
||||
uint64 userId = 1;
|
||||
string uniqueName = 2;
|
||||
GrpcXpShopItemType itemType = 3;
|
||||
}
|
||||
|
||||
message UseShopItemReply {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message BuyShopItemRequest {
|
||||
uint64 userId = 1;
|
||||
string uniqueName = 2;
|
||||
GrpcXpShopItemType itemType = 3;
|
||||
}
|
||||
|
||||
message BuyShopItemReply {
|
||||
bool success = 1;
|
||||
optional BuyShopItemError Error = 2;
|
||||
}
|
||||
|
||||
enum BuyShopItemError {
|
||||
NotEnough = 0;
|
||||
AlreadyOwned = 1;
|
||||
Unknown = 2;
|
||||
}
|
||||
|
||||
message AddXpShopItemRequest {
|
||||
XpShopItem item = 1;
|
||||
string uniqueName = 2;
|
||||
GrpcXpShopItemType itemType = 3;
|
||||
}
|
||||
|
||||
message AddXpShopItemReply {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message GetShopItemsRequest {
|
||||
|
||||
}
|
||||
|
||||
message GetShopItemsReply {
|
||||
repeated XpShopItem bgs = 1;
|
||||
repeated XpShopItem frames = 2;
|
||||
}
|
||||
|
||||
message XpShopItem {
|
||||
string Name = 1;
|
||||
string Description = 2;
|
||||
int64 Price = 3;
|
||||
string FullUrl = 4;
|
||||
string PreviewUrl = 5;
|
||||
}
|
||||
|
||||
enum GrpcXpShopItemType {
|
||||
Bg = 0;
|
||||
Frame = 1;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
@ -14,6 +15,7 @@ namespace EllieBot.VotesApi
|
|||
public const string SchemeName = "AUTHORIZATION_SCHEME";
|
||||
public const string DiscordsClaim = "DISCORDS_CLAIM";
|
||||
public const string TopggClaim = "TOPGG_CLAIM";
|
||||
public const string DiscordbotlistClaim = "DISCORDBOTLIST_CLAIM";
|
||||
|
||||
private readonly IConfiguration _conf;
|
||||
|
||||
|
@ -24,17 +26,58 @@ namespace EllieBot.VotesApi
|
|||
: base(options, logger, encoder)
|
||||
=> _conf = conf;
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
return AuthenticateResult.Fail("Authorization header missing");
|
||||
}
|
||||
|
||||
var authToken = authHeader.ToString().Trim();
|
||||
if (string.IsNullOrWhiteSpace(authToken))
|
||||
{
|
||||
return AuthenticateResult.Fail("Authorization token empty");
|
||||
}
|
||||
|
||||
var claims = new List<Claim>();
|
||||
|
||||
if (_conf[ConfKeys.DISCORDS_KEY].Trim() == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new(DiscordsClaim, "true"));
|
||||
var discsKey = _conf[ConfKeys.DISCORDS_KEY]?.Trim();
|
||||
var topggKey = _conf[ConfKeys.TOPGG_KEY]?.Trim();
|
||||
var dblKey = _conf[ConfKeys.DISCORDBOTLIST_KEY]?.Trim();
|
||||
|
||||
if (_conf[ConfKeys.TOPGG_KEY] == Request.Headers["Authorization"].ToString().Trim())
|
||||
if (!string.IsNullOrWhiteSpace(discsKey)
|
||||
&& System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(discsKey),
|
||||
Encoding.UTF8.GetBytes(authToken)))
|
||||
{
|
||||
claims.Add(new Claim(DiscordsClaim, "true"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(topggKey)
|
||||
&& System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(topggKey),
|
||||
Encoding.UTF8.GetBytes(authToken)))
|
||||
{
|
||||
claims.Add(new Claim(TopggClaim, "true"));
|
||||
}
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(new(new(new ClaimsIdentity(claims)), SchemeName)));
|
||||
if (!string.IsNullOrWhiteSpace(dblKey)
|
||||
&& System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(dblKey),
|
||||
Encoding.UTF8.GetBytes(authToken)))
|
||||
{
|
||||
claims.Add(new Claim(DiscordbotlistClaim, "true"));
|
||||
}
|
||||
|
||||
if (claims.Count == 0)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid authorization token");
|
||||
}
|
||||
|
||||
return AuthenticateResult.Success(
|
||||
new AuthenticationTicket(
|
||||
new ClaimsPrincipal(new ClaimsIdentity(claims)),
|
||||
SchemeName));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,5 +4,6 @@
|
|||
{
|
||||
public const string DISCORDS_KEY = "DiscordsKey";
|
||||
public const string TOPGG_KEY = "TopGGKey";
|
||||
public const string DISCORDBOTLIST_KEY = "DiscordbotListKey";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class DiscordbotlistVoteWebhookModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The avatar hash of the user
|
||||
/// </summary>
|
||||
public string Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username of the user who voted
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user who voted
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -12,12 +12,6 @@
|
|||
/// </summary>
|
||||
public string Bot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains totalVotes, votesMonth, votes24, hasVoted - a list of IDs of users who have voted this month, and
|
||||
/// Voted24 - a list of IDs of users who have voted today
|
||||
/// </summary>
|
||||
public string Votes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of event, whether it is a vote event or test event
|
||||
/// </summary>
|
|
@ -4,5 +4,6 @@
|
|||
{
|
||||
public const string DiscordsAuth = "DiscordsAuth";
|
||||
public const string TopggAuth = "TopggAuth";
|
||||
public const string DiscordbotlistAuth = "DiscordbotlistAuth";
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class DiscordsController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<DiscordsController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public DiscordsController(ILogger<DiscordsController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewDiscordsVotesAsync();
|
||||
if (votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes", votes.Count);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class TopGgController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TopGgController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public TopGgController(ILogger<TopGgController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewTopGgVotesAsync();
|
||||
if (votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes", votes.Count);
|
||||
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,33 +2,32 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
using EllieBot.GrpcVotesApi;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class WebhookController : ControllerBase
|
||||
public class WebhookController(ILogger<WebhookController> logger, VoteService.VoteServiceClient client)
|
||||
: ControllerBase
|
||||
{
|
||||
private readonly ILogger<WebhookController> _logger;
|
||||
private readonly IVotesCache _votesCache;
|
||||
|
||||
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_votesCache = votesCache;
|
||||
}
|
||||
|
||||
[HttpPost("/discordswebhook")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IActionResult> DiscordsWebhook([FromBody] DiscordsVoteWebhookModel data)
|
||||
{
|
||||
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
if ((data.Type?.Contains("vote") ?? false) == false)
|
||||
return Ok();
|
||||
|
||||
logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"discords.com");
|
||||
|
||||
await _votesCache.AddNewDiscordsVote(data.User);
|
||||
await client.VoteReceivedAsync(new GrpcVoteData()
|
||||
{
|
||||
Type = VoteType.Discords,
|
||||
UserId = data.User,
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -36,12 +35,34 @@ namespace EllieBot.VotesApi.Controllers
|
|||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IActionResult> TopggWebhook([FromBody] TopggVoteWebhookModel data)
|
||||
{
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"top.gg");
|
||||
|
||||
await _votesCache.AddNewTopggVote(data.User);
|
||||
await client.VoteReceivedAsync(new GrpcVoteData()
|
||||
{
|
||||
Type = VoteType.Topgg,
|
||||
UserId = data.User,
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("/discordbotlistwebhook")]
|
||||
[Authorize(Policy = Policies.DiscordbotlistAuth)]
|
||||
public async Task<IActionResult> DiscordbotlistWebhook([FromBody] DiscordbotlistVoteWebhookModel data)
|
||||
{
|
||||
logger.LogInformation("User {UserId} has voted for Bot on {Platform}",
|
||||
data.Id,
|
||||
"discordbotlist.com");
|
||||
|
||||
await client.VoteReceivedAsync(new GrpcVoteData()
|
||||
{
|
||||
Type = VoteType.Discordbotlist,
|
||||
UserId = data.Id,
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/EllieBot.VotesApi/EllieBot.VotesApi.csproj", "EllieBot.VotesApi/"]
|
||||
RUN dotnet restore "src/EllieBot.VotesApi/EllieBot.VotesApi.csproj"
|
||||
COPY . .
|
||||
COPY ["EllieBot.VotesApi.csproj", "EllieBot.VotesApi/"]
|
||||
RUN dotnet restore "EllieBot.VotesApi/EllieBot.VotesApi.csproj"
|
||||
COPY . "EllieBot.VotesApi/"
|
||||
WORKDIR "/src/EllieBot.VotesApi"
|
||||
RUN dotnet build "EllieBot.VotesApi.csproj" -c Release -o /app/build
|
||||
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.70.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos/vote.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using EllieBot.VotesApi;
|
||||
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
|
||||
static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
||||
static IHostBuilder CreateHostBuilder(string[] args)
|
||||
=> Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(c => c.AddEnvironmentVariables())
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder
|
||||
.UseStartup<Startup>();
|
||||
});
|
24
src/EllieBot.VotesApi/Protos/vote.proto
Normal file
24
src/EllieBot.VotesApi/Protos/vote.proto
Normal file
|
@ -0,0 +1,24 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package vote;
|
||||
|
||||
option csharp_namespace = "EllieBot.GrpcVotesApi";
|
||||
|
||||
enum VoteType {
|
||||
TOPGG = 0;
|
||||
DISCORDBOTLIST = 1;
|
||||
DISCORDS = 2;
|
||||
}
|
||||
|
||||
message GrpcVoteData {
|
||||
string userId = 1;
|
||||
VoteType type = 2;
|
||||
}
|
||||
|
||||
message GrpcVoteResult {
|
||||
|
||||
}
|
||||
|
||||
service VoteService {
|
||||
rpc VoteReceived (GrpcVoteData) returns (GrpcVoteResult);
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace EllieBot.VotesApi.Services
|
||||
{
|
||||
public class FileVotesCache : IVotesCache
|
||||
{
|
||||
// private const string STATS_FILE = "store/stats.json";
|
||||
private const string TOPGG_FILE = "store/topgg.json";
|
||||
private const string DISCORDS_FILE = "store/discords.json";
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public FileVotesCache()
|
||||
{
|
||||
if (!Directory.Exists("store"))
|
||||
Directory.CreateDirectory("store");
|
||||
|
||||
if (!File.Exists(TOPGG_FILE))
|
||||
File.WriteAllText(TOPGG_FILE, "[]");
|
||||
|
||||
if (!File.Exists(DISCORDS_FILE))
|
||||
File.WriteAllText(DISCORDS_FILE, "[]");
|
||||
}
|
||||
|
||||
public ITask AddNewTopggVote(string userId)
|
||||
=> AddNewVote(TOPGG_FILE, userId);
|
||||
|
||||
public ITask AddNewDiscordsVote(string userId)
|
||||
=> AddNewVote(DISCORDS_FILE, userId);
|
||||
|
||||
private async ITask AddNewVote(string file, string userId)
|
||||
{
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
var votes = await GetVotesAsync(file);
|
||||
votes.Add(userId);
|
||||
await File.WriteAllTextAsync(file, JsonSerializer.Serialize(votes));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewTopGgVotesAsync()
|
||||
{
|
||||
var votes = await EvictTopggVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewDiscordsVotesAsync()
|
||||
{
|
||||
var votes = await EvictDiscordsVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
private ITask<List<Vote>> EvictTopggVotes()
|
||||
=> EvictVotes(TOPGG_FILE);
|
||||
|
||||
private ITask<List<Vote>> EvictDiscordsVotes()
|
||||
=> EvictVotes(DISCORDS_FILE);
|
||||
|
||||
private async ITask<List<Vote>> EvictVotes(string file)
|
||||
{
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
|
||||
var ids = await GetVotesAsync(file);
|
||||
await File.WriteAllTextAsync(file, "[]");
|
||||
|
||||
return ids?
|
||||
.Select(x => (Ok: ulong.TryParse(x, out var r), Id: r))
|
||||
.Where(x => x.Ok)
|
||||
.Select(x => new Vote
|
||||
{
|
||||
UserId = x.Id
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ITask<IList<string>> GetVotesAsync(string file)
|
||||
{
|
||||
await using var fs = File.Open(file, FileMode.Open);
|
||||
var votes = await JsonSerializer.DeserializeAsync<List<string>>(fs);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace EllieBot.VotesApi.Services
|
||||
{
|
||||
public interface IVotesCache
|
||||
{
|
||||
ITask<IList<Vote>> GetNewTopGgVotesAsync();
|
||||
ITask<IList<Vote>> GetNewDiscordsVotesAsync();
|
||||
ITask AddNewTopggVote(string userId);
|
||||
ITask AddNewDiscordsVote(string userId);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using System;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using EllieBot.VotesApi.Services;
|
||||
using EllieBot.GrpcVotesApi;
|
||||
|
||||
namespace EllieBot.VotesApi
|
||||
{
|
||||
|
@ -21,11 +22,16 @@ namespace EllieBot.VotesApi
|
|||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
services.AddSingleton<IVotesCache, FileVotesCache>();
|
||||
services.AddSwaggerGen(static c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "EllieBot.VotesApi", Version = "v1" });
|
||||
});
|
||||
|
||||
services.AddGrpcClient<VoteService.VoteServiceClient>(options =>
|
||||
{
|
||||
options.Address = new Uri(Configuration["BotGrpcHost"]!);
|
||||
})
|
||||
.ConfigureChannel((sp, c) =>
|
||||
{
|
||||
c.Credentials = ChannelCredentials.Insecure;
|
||||
c.ServiceProvider = sp;
|
||||
});
|
||||
|
||||
services
|
||||
.AddAuthentication(opts =>
|
||||
|
@ -40,9 +46,18 @@ namespace EllieBot.VotesApi
|
|||
opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName)
|
||||
.RequireAssertion(static _ => false)
|
||||
.Build();
|
||||
opts.AddPolicy(Policies.DiscordsAuth, static policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
|
||||
opts.AddPolicy(Policies.TopggAuth, static policy => policy.RequireClaim(AuthHandler.TopggClaim));
|
||||
opts.AddPolicy(Policies.DiscordsAuth,
|
||||
static policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
|
||||
opts.AddPolicy(Policies.TopggAuth,
|
||||
static policy => policy.RequireClaim(AuthHandler.TopggClaim));
|
||||
opts.AddPolicy(Policies.DiscordbotlistAuth,
|
||||
static policy => policy.RequireClaim(AuthHandler.DiscordbotlistClaim));
|
||||
});
|
||||
|
||||
services.AddCors(x => x.AddDefaultPolicy(cpb =>
|
||||
cpb.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyOrigin()));
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
@ -51,8 +66,6 @@ namespace EllieBot.VotesApi
|
|||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(static c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "EllieBot.VotesApi v1"));
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
|
|
@ -8,5 +8,7 @@
|
|||
},
|
||||
"DiscordsKey": "my_discords_key",
|
||||
"TopGGKey": "my_topgg_key",
|
||||
"DiscordBotListKey": "my_discordbotlist_key",
|
||||
"BotGrpcHost": "http://127.0.0.1:59384",
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ public abstract class EllieContext : DbContext
|
|||
{
|
||||
// load all entities from current assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(EllieContext).Assembly);
|
||||
|
||||
|
||||
#region Notify
|
||||
|
||||
modelBuilder.Entity<Notify>(e =>
|
||||
|
@ -113,8 +113,8 @@ public abstract class EllieContext : DbContext
|
|||
#region GuildColors
|
||||
|
||||
modelBuilder.Entity<GuildColors>()
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique(true);
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique(true);
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -123,7 +123,7 @@ public abstract class EllieContext : DbContext
|
|||
modelBuilder.Entity<ButtonRole>(br =>
|
||||
{
|
||||
br.HasIndex(x => x.GuildId)
|
||||
.IsUnique(false);
|
||||
.IsUnique(false);
|
||||
|
||||
br.HasAlternateKey(x => new
|
||||
{
|
||||
|
@ -145,27 +145,27 @@ public abstract class EllieContext : DbContext
|
|||
});
|
||||
|
||||
sg.HasMany(x => x.Roles)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Sar>()
|
||||
.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.RoleId
|
||||
});
|
||||
.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.RoleId
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SarAutoDelete>()
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique();
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rakeback
|
||||
|
||||
modelBuilder.Entity<Rakeback>()
|
||||
.HasKey(x => x.UserId);
|
||||
.HasKey(x => x.UserId);
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -174,14 +174,14 @@ public abstract class EllieContext : DbContext
|
|||
modelBuilder.Entity<UserBetStats>(ubs =>
|
||||
{
|
||||
ubs.HasIndex(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.Game
|
||||
})
|
||||
.IsUnique();
|
||||
{
|
||||
x.UserId,
|
||||
x.Game
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
ubs.HasIndex(x => x.MaxWin)
|
||||
.IsUnique(false);
|
||||
.IsUnique(false);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
@ -189,22 +189,22 @@ public abstract class EllieContext : DbContext
|
|||
#region Flag Translate
|
||||
|
||||
modelBuilder.Entity<FlagTranslateChannel>()
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.ChannelId
|
||||
})
|
||||
.IsUnique();
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.ChannelId
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
#region NCanvas
|
||||
|
||||
modelBuilder.Entity<NCPixel>()
|
||||
.HasAlternateKey(x => x.Position);
|
||||
.HasAlternateKey(x => x.Position);
|
||||
|
||||
modelBuilder.Entity<NCPixel>()
|
||||
.HasIndex(x => x.OwnerId);
|
||||
.HasIndex(x => x.OwnerId);
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -221,10 +221,11 @@ public abstract class EllieContext : DbContext
|
|||
var configEntity = modelBuilder.Entity<GuildConfig>();
|
||||
|
||||
configEntity.HasIndex(c => c.GuildId)
|
||||
.IsUnique();
|
||||
.IsUnique();
|
||||
|
||||
configEntity.Property(x => x.VerboseErrors)
|
||||
.HasDefaultValue(true);
|
||||
.HasDefaultValue(true);
|
||||
|
||||
// end shop
|
||||
|
||||
modelBuilder.Entity<PlantedCurrency>().HasIndex(x => x.MessageId).IsUnique();
|
||||
|
@ -270,19 +271,19 @@ public abstract class EllieContext : DbContext
|
|||
modelBuilder.Entity<DiscordUser>(du =>
|
||||
{
|
||||
du.Property(x => x.IsClubAdmin)
|
||||
.HasDefaultValue(false);
|
||||
.HasDefaultValue(false);
|
||||
|
||||
du.Property(x => x.TotalXp)
|
||||
.HasDefaultValue(0);
|
||||
.HasDefaultValue(0);
|
||||
|
||||
du.Property(x => x.CurrencyAmount)
|
||||
.HasDefaultValue(0);
|
||||
.HasDefaultValue(0);
|
||||
|
||||
du.HasAlternateKey(w => w.UserId);
|
||||
du.HasOne(x => x.Club)
|
||||
.WithMany(x => x.Members)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
.WithMany(x => x.Members)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
|
||||
du.HasIndex(x => x.TotalXp);
|
||||
du.HasIndex(x => x.CurrencyAmount);
|
||||
|
@ -308,11 +309,11 @@ public abstract class EllieContext : DbContext
|
|||
|
||||
var xps = modelBuilder.Entity<UserXpStats>();
|
||||
xps.HasIndex(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.GuildId
|
||||
})
|
||||
.IsUnique();
|
||||
{
|
||||
x.UserId,
|
||||
x.GuildId
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
xps.HasIndex(x => x.UserId);
|
||||
xps.HasIndex(x => x.GuildId);
|
||||
|
@ -324,49 +325,49 @@ public abstract class EllieContext : DbContext
|
|||
|
||||
var ci = modelBuilder.Entity<ClubInfo>();
|
||||
ci.HasOne(x => x.Owner)
|
||||
.WithOne()
|
||||
.HasForeignKey<ClubInfo>(x => x.OwnerId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
.WithOne()
|
||||
.HasForeignKey<ClubInfo>(x => x.OwnerId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
ci.HasIndex(x => new
|
||||
{
|
||||
x.Name
|
||||
})
|
||||
.IsUnique();
|
||||
{
|
||||
x.Name
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
#region ClubManytoMany
|
||||
|
||||
modelBuilder.Entity<ClubApplicants>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
t.ClubId,
|
||||
t.UserId
|
||||
});
|
||||
.HasKey(t => new
|
||||
{
|
||||
t.ClubId,
|
||||
t.UserId
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ClubApplicants>()
|
||||
.HasOne(pt => pt.User)
|
||||
.WithMany();
|
||||
.HasOne(pt => pt.User)
|
||||
.WithMany();
|
||||
|
||||
modelBuilder.Entity<ClubApplicants>()
|
||||
.HasOne(pt => pt.Club)
|
||||
.WithMany(x => x.Applicants);
|
||||
.HasOne(pt => pt.Club)
|
||||
.WithMany(x => x.Applicants);
|
||||
|
||||
modelBuilder.Entity<ClubBans>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
t.ClubId,
|
||||
t.UserId
|
||||
});
|
||||
.HasKey(t => new
|
||||
{
|
||||
t.ClubId,
|
||||
t.UserId
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ClubBans>()
|
||||
.HasOne(pt => pt.User)
|
||||
.WithMany();
|
||||
.HasOne(pt => pt.User)
|
||||
.WithMany();
|
||||
|
||||
modelBuilder.Entity<ClubBans>()
|
||||
.HasOne(pt => pt.Club)
|
||||
.WithMany(x => x.Bans);
|
||||
.HasOne(pt => pt.Club)
|
||||
.WithMany(x => x.Bans);
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -375,16 +376,16 @@ public abstract class EllieContext : DbContext
|
|||
modelBuilder.Entity<CurrencyTransaction>(e =>
|
||||
{
|
||||
e.HasIndex(x => x.UserId)
|
||||
.IsUnique(false);
|
||||
.IsUnique(false);
|
||||
|
||||
e.Property(x => x.OtherId)
|
||||
.HasDefaultValueSql(CurrencyTransactionOtherIdDefaultValue);
|
||||
.HasDefaultValueSql(CurrencyTransactionOtherIdDefaultValue);
|
||||
|
||||
e.Property(x => x.Type)
|
||||
.IsRequired();
|
||||
.IsRequired();
|
||||
|
||||
e.Property(x => x.Extra)
|
||||
.IsRequired();
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
@ -399,21 +400,21 @@ public abstract class EllieContext : DbContext
|
|||
|
||||
modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();
|
||||
modelBuilder.Entity<BanTemplate>()
|
||||
.Property(x => x.PruneDays)
|
||||
.HasDefaultValue(null)
|
||||
.IsRequired(false);
|
||||
.Property(x => x.PruneDays)
|
||||
.HasDefaultValue(null)
|
||||
.IsRequired(false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Perm Override
|
||||
|
||||
modelBuilder.Entity<DiscordPermOverride>()
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.Command
|
||||
})
|
||||
.IsUnique();
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.Command
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -430,14 +431,14 @@ public abstract class EllieContext : DbContext
|
|||
modelBuilder.Entity<ReactionRoleV2>(rr2 =>
|
||||
{
|
||||
rr2.HasIndex(x => x.GuildId)
|
||||
.IsUnique(false);
|
||||
.IsUnique(false);
|
||||
|
||||
rr2.HasIndex(x => new
|
||||
{
|
||||
x.MessageId,
|
||||
x.Emote
|
||||
})
|
||||
.IsUnique();
|
||||
{
|
||||
x.MessageId,
|
||||
x.Emote
|
||||
})
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
@ -447,18 +448,18 @@ public abstract class EllieContext : DbContext
|
|||
modelBuilder.Entity<LogSetting>(ls => ls.HasIndex(x => x.GuildId).IsUnique());
|
||||
|
||||
modelBuilder.Entity<LogSetting>(ls => ls
|
||||
.HasMany(x => x.LogIgnores)
|
||||
.WithOne(x => x.LogSetting)
|
||||
.OnDelete(DeleteBehavior.Cascade));
|
||||
.HasMany(x => x.LogIgnores)
|
||||
.WithOne(x => x.LogSetting)
|
||||
.OnDelete(DeleteBehavior.Cascade));
|
||||
|
||||
modelBuilder.Entity<IgnoredLogItem>(ili => ili
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.LogSettingId,
|
||||
x.LogItemId,
|
||||
x.ItemType
|
||||
})
|
||||
.IsUnique());
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.LogSettingId,
|
||||
x.LogItemId,
|
||||
x.ItemType
|
||||
})
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -511,12 +512,12 @@ public abstract class EllieContext : DbContext
|
|||
{
|
||||
// user can own only one of each item
|
||||
x.HasIndex(model => new
|
||||
{
|
||||
model.UserId,
|
||||
model.ItemType,
|
||||
model.ItemKey
|
||||
})
|
||||
.IsUnique();
|
||||
{
|
||||
model.UserId,
|
||||
model.ItemType,
|
||||
model.ItemKey
|
||||
})
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
@ -524,27 +525,27 @@ public abstract class EllieContext : DbContext
|
|||
#region AutoPublish
|
||||
|
||||
modelBuilder.Entity<AutoPublishChannel>(apc => apc
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique());
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
#region GamblingStats
|
||||
|
||||
modelBuilder.Entity<GamblingStats>(gs => gs
|
||||
.HasIndex(x => x.Feature)
|
||||
.IsUnique());
|
||||
.HasIndex(x => x.Feature)
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sticky Roles
|
||||
|
||||
modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.UserId
|
||||
})
|
||||
.IsUnique());
|
||||
{
|
||||
x.GuildId,
|
||||
x.UserId
|
||||
})
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -552,35 +553,35 @@ public abstract class EllieContext : DbContext
|
|||
#region Giveaway
|
||||
|
||||
modelBuilder.Entity<GiveawayModel>()
|
||||
.HasMany(x => x.Participants)
|
||||
.WithOne()
|
||||
.HasForeignKey(x => x.GiveawayId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.HasMany(x => x.Participants)
|
||||
.WithOne()
|
||||
.HasForeignKey(x => x.GiveawayId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<GiveawayUser>(gu => gu
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GiveawayId,
|
||||
x.UserId
|
||||
})
|
||||
.IsUnique());
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GiveawayId,
|
||||
x.UserId
|
||||
})
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Todo
|
||||
|
||||
modelBuilder.Entity<TodoModel>()
|
||||
.HasKey(x => x.Id);
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<TodoModel>()
|
||||
.HasIndex(x => x.UserId)
|
||||
.IsUnique(false);
|
||||
.HasIndex(x => x.UserId)
|
||||
.IsUnique(false);
|
||||
|
||||
modelBuilder.Entity<ArchivedTodoListModel>()
|
||||
.HasMany(x => x.Items)
|
||||
.WithOne()
|
||||
.HasForeignKey(x => x.ArchiveId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.HasMany(x => x.Items)
|
||||
.WithOne()
|
||||
.HasForeignKey(x => x.ArchiveId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -588,11 +589,11 @@ public abstract class EllieContext : DbContext
|
|||
|
||||
modelBuilder
|
||||
.Entity<GreetSettings>(gs => gs.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.GreetType
|
||||
})
|
||||
.IsUnique());
|
||||
{
|
||||
x.GuildId,
|
||||
x.GreetType
|
||||
})
|
||||
.IsUnique());
|
||||
|
||||
modelBuilder.Entity<GreetSettings>(gs =>
|
||||
{
|
||||
|
@ -619,6 +620,6 @@ public abstract class EllieContext : DbContext
|
|||
#endif
|
||||
|
||||
optionsBuilder.ConfigureWarnings(x => x.Log(RelationalEventId.PendingModelChangesWarning)
|
||||
.Ignore());
|
||||
.Ignore());
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
@ -31,39 +30,38 @@ public sealed class GuildFilterConfigEntityConfiguration : IEntityTypeConfigurat
|
|||
public class GuildConfig : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public string Prefix { get; set; }
|
||||
public string? Prefix { get; set; } = null;
|
||||
|
||||
public bool DeleteMessageOnCommand { get; set; }
|
||||
|
||||
public string AutoAssignRoleIds { get; set; }
|
||||
public bool DeleteMessageOnCommand { get; set; } = false;
|
||||
|
||||
public string? AutoAssignRoleIds { get; set; } = null;
|
||||
public bool VerbosePermissions { get; set; } = true;
|
||||
public string PermissionRole { get; set; }
|
||||
public string? PermissionRole { get; set; } = null;
|
||||
|
||||
//filtering
|
||||
public string MuteRoleName { get; set; }
|
||||
public string? MuteRoleName { get; set; } = null;
|
||||
|
||||
// chatterbot
|
||||
public bool CleverbotEnabled { get; set; }
|
||||
public bool CleverbotEnabled { get; set; } = false;
|
||||
|
||||
// aliases
|
||||
public bool WarningsInitialized { get; set; }
|
||||
public bool WarningsInitialized { get; set; } = false;
|
||||
|
||||
public ulong? GameVoiceChannel { get; set; }
|
||||
public ulong? GameVoiceChannel { get; set; } = null;
|
||||
public bool VerboseErrors { get; set; } = true;
|
||||
|
||||
|
||||
public bool NotifyStreamOffline { get; set; }
|
||||
public bool DeleteStreamOnlineMessage { get; set; }
|
||||
public int WarnExpireHours { get; set; }
|
||||
public bool NotifyStreamOffline { get; set; } = true;
|
||||
public bool DeleteStreamOnlineMessage { get; set; } = false;
|
||||
public int WarnExpireHours { get; set; } = 0;
|
||||
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;
|
||||
|
||||
public bool DisableGlobalExpressions { get; set; } = false;
|
||||
|
||||
public bool StickyRoles { get; set; }
|
||||
|
||||
public string TimeZoneId { get; set; }
|
||||
public string Locale { get; set; }
|
||||
public bool StickyRoles { get; set; } = false;
|
||||
|
||||
public string? TimeZoneId { get; set; } = null;
|
||||
public string? Locale { get; set; } = null;
|
||||
|
||||
public List<Permissionv2> Permissions { get; set; } = [];
|
||||
}
|
|
@ -21,5 +21,6 @@ public enum NotifyType
|
|||
Protection = 1, Prot = 1,
|
||||
AddRoleReward = 2,
|
||||
RemoveRoleReward = 3,
|
||||
NiceCatch = 4,
|
||||
// BigWin = 4,
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>6.0.12</Version>
|
||||
<Version>6.1.0</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</PackageReference>
|
||||
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Discord.Net" Version="3.17.1" />
|
||||
<PackageReference Include="Discord.Net" Version="3.17.2" />
|
||||
<PackageReference Include="CoreCLR-NCalc" Version="3.1.253" />
|
||||
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
|
||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3653" />
|
||||
|
@ -124,6 +124,9 @@
|
|||
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
|
||||
<GrpcServices>Client</GrpcServices>
|
||||
</Protobuf>
|
||||
<Protobuf Include="../EllieBot.VotesApi/Protos/*.proto">
|
||||
<GrpcServices>Server</GrpcServices>
|
||||
</Protobuf>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Canti/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cbtnrole/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cclub/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Ccurrency/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cexpr/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cfilter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cgiveaway/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cncanvas/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cpunish/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Croles/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cslowmode/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Csupport/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Ctodo/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cuntimer/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=db_005Cmodels_005Cxp/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cadministration_005Cnotify_005Cmodels/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
@ -0,0 +1,16 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE linkfix (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
olddomain text NOT NULL,
|
||||
newdomain text NOT NULL,
|
||||
CONSTRAINT pk_linkfix PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX ix_linkfix_guildid_olddomain ON linkfix (guildid, olddomain);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250323021916_linkfixer', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
20
src/EllieBot/Migrations/PostgreSql/20250324230804_quests.sql
Normal file
20
src/EllieBot/Migrations/PostgreSql/20250324230804_quests.sql
Normal file
|
@ -0,0 +1,20 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE userquest (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
questnumber integer NOT NULL,
|
||||
userid numeric(20,0) NOT NULL,
|
||||
questid integer NOT NULL,
|
||||
progress bigint NOT NULL,
|
||||
iscompleted boolean NOT NULL,
|
||||
dateassigned timestamp without time zone NOT NULL,
|
||||
CONSTRAINT pk_userquest PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_userquest_userid ON userquest (userid);
|
||||
|
||||
CREATE UNIQUE INDEX ix_userquest_userid_questnumber_dateassigned ON userquest (userid, questnumber, dateassigned);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250324230804_quests', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,25 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE userfishstats DROP COLUMN bait;
|
||||
|
||||
ALTER TABLE userfishstats DROP COLUMN pole;
|
||||
|
||||
ALTER TABLE userquest ALTER COLUMN progress TYPE integer;
|
||||
|
||||
CREATE TABLE userfishitem (
|
||||
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
userid numeric(20,0) NOT NULL,
|
||||
itemtype integer NOT NULL,
|
||||
itemid integer NOT NULL,
|
||||
isequipped boolean NOT NULL,
|
||||
usesleft integer,
|
||||
expiresat timestamp without time zone,
|
||||
CONSTRAINT pk_userfishitem PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_userfishitem_userid ON userfishitem (userid);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250327001838_fishitems', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||
namespace EllieBot.Migrations.PostgreSql
|
||||
{
|
||||
[DbContext(typeof(PostgreSqlContext))]
|
||||
[Migration("20250319010930_init")]
|
||||
[Migration("20250327001912_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -867,57 +867,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("discorduser", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allowtarget");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("autodeletetrigger");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("containsanywhere");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("dmresponse");
|
||||
|
||||
b.Property<decimal?>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reactions");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("response");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("trigger");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_expressions");
|
||||
|
||||
b.ToTable("expressions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1541,6 +1490,39 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("imageonlychannels", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LinkFix", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("NewDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("newdomain");
|
||||
|
||||
b.Property<string>("OldDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("olddomain");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_linkfix");
|
||||
|
||||
b.HasIndex("GuildId", "OldDomain")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_linkfix_guildid_olddomain");
|
||||
|
||||
b.ToTable("linkfix", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1829,6 +1811,57 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("ncpixel", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allowtarget");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("autodeletetrigger");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("containsanywhere");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("dmresponse");
|
||||
|
||||
b.Property<decimal?>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reactions");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("response");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("trigger");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_expressions");
|
||||
|
||||
b.ToTable("expressions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2955,6 +2988,52 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("unroletimer", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserQuest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("DateAssigned")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateassigned");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("iscompleted");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<int>("QuestId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("questid");
|
||||
|
||||
b.Property<int>("QuestNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("questnumber");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userquest");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_userquest_userid");
|
||||
|
||||
b.HasIndex("UserId", "QuestNumber", "DateAssigned")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_userquest_userid_questnumber_dateassigned");
|
||||
|
||||
b.ToTable("userquest", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserXpStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -3437,6 +3516,48 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("xpshopowneditem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.Fish.Db.UserFishItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("expiresat");
|
||||
|
||||
b.Property<bool>("IsEquipped")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("isequipped");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemid");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemtype");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.Property<int?>("UsesLeft")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("usesleft");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userfishitem");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_userfishitem_userid");
|
||||
|
||||
b.ToTable("userfishitem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -3480,14 +3601,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("Bait")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bait");
|
||||
|
||||
b.Property<int?>("Pole")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("pole");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skill");
|
||||
|
@ -4151,4 +4264,4 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -550,6 +550,21 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.PrimaryKey("pk_imageonlychannels", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "linkfix",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
olddomain = table.Column<string>(type: "text", nullable: false),
|
||||
newdomain = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_linkfix", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "livechannelconfig",
|
||||
columns: table => new
|
||||
|
@ -1094,6 +1109,24 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.PrimaryKey("pk_userbetstats", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userfishitem",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
itemtype = table.Column<int>(type: "integer", nullable: false),
|
||||
itemid = table.Column<int>(type: "integer", nullable: false),
|
||||
isequipped = table.Column<bool>(type: "boolean", nullable: false),
|
||||
usesleft = table.Column<int>(type: "integer", nullable: true),
|
||||
expiresat = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_userfishitem", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userfishstats",
|
||||
columns: table => new
|
||||
|
@ -1101,15 +1134,31 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
skill = table.Column<int>(type: "integer", nullable: false),
|
||||
pole = table.Column<int>(type: "integer", nullable: true),
|
||||
bait = table.Column<int>(type: "integer", nullable: true)
|
||||
skill = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_userfishstats", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userquest",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
questnumber = table.Column<int>(type: "integer", nullable: false),
|
||||
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
questid = table.Column<int>(type: "integer", nullable: false),
|
||||
progress = table.Column<int>(type: "integer", nullable: false),
|
||||
iscompleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
dateassigned = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_userquest", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userrole",
|
||||
columns: table => new
|
||||
|
@ -1999,6 +2048,12 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
column: "channelid",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_linkfix_guildid_olddomain",
|
||||
table: "linkfix",
|
||||
columns: new[] { "guildid", "olddomain" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_livechannelconfig_guildid",
|
||||
table: "livechannelconfig",
|
||||
|
@ -2212,12 +2267,28 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
columns: new[] { "userid", "game" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userfishitem_userid",
|
||||
table: "userfishitem",
|
||||
column: "userid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userfishstats_userid",
|
||||
table: "userfishstats",
|
||||
column: "userid",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userquest_userid",
|
||||
table: "userquest",
|
||||
column: "userid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userquest_userid_questnumber_dateassigned",
|
||||
table: "userquest",
|
||||
columns: new[] { "userid", "questnumber", "dateassigned" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userrole_guildid",
|
||||
table: "userrole",
|
||||
|
@ -2501,6 +2572,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "imageonlychannels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "linkfix");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "livechannelconfig");
|
||||
|
||||
|
@ -2597,9 +2671,15 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "userbetstats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userfishitem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userfishstats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userquest");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userrole");
|
||||
|
||||
|
@ -2679,4 +2759,4 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
name: "discorduser");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -864,57 +864,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("discorduser", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allowtarget");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("autodeletetrigger");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("containsanywhere");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("dmresponse");
|
||||
|
||||
b.Property<decimal?>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reactions");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("response");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("trigger");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_expressions");
|
||||
|
||||
b.ToTable("expressions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1538,6 +1487,39 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("imageonlychannels", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LinkFix", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("NewDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("newdomain");
|
||||
|
||||
b.Property<string>("OldDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("olddomain");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_linkfix");
|
||||
|
||||
b.HasIndex("GuildId", "OldDomain")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_linkfix_guildid_olddomain");
|
||||
|
||||
b.ToTable("linkfix", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1826,6 +1808,57 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("ncpixel", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allowtarget");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("autodeletetrigger");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("containsanywhere");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("dmresponse");
|
||||
|
||||
b.Property<decimal?>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reactions");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("response");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("trigger");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_expressions");
|
||||
|
||||
b.ToTable("expressions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2952,6 +2985,52 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("unroletimer", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserQuest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("DateAssigned")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateassigned");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("iscompleted");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<int>("QuestId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("questid");
|
||||
|
||||
b.Property<int>("QuestNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("questnumber");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userquest");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_userquest_userid");
|
||||
|
||||
b.HasIndex("UserId", "QuestNumber", "DateAssigned")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_userquest_userid_questnumber_dateassigned");
|
||||
|
||||
b.ToTable("userquest", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserXpStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -3434,6 +3513,48 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("xpshopowneditem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.Fish.Db.UserFishItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("expiresat");
|
||||
|
||||
b.Property<bool>("IsEquipped")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("isequipped");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemid");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemtype");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.Property<int?>("UsesLeft")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("usesleft");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userfishitem");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_userfishitem_userid");
|
||||
|
||||
b.ToTable("userfishitem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -3477,14 +3598,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("Bait")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bait");
|
||||
|
||||
b.Property<int?>("Pole")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("pole");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skill");
|
||||
|
@ -4148,4 +4261,4 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
src/EllieBot/Migrations/Sqlite/20250323021857_linkfixer.sql
Normal file
15
src/EllieBot/Migrations/Sqlite/20250323021857_linkfixer.sql
Normal file
|
@ -0,0 +1,15 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "LinkFix" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_LinkFix" PRIMARY KEY AUTOINCREMENT,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"OldDomain" TEXT NOT NULL,
|
||||
"NewDomain" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_LinkFix_GuildId_OldDomain" ON "LinkFix" ("GuildId", "OldDomain");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250323021857_linkfixer', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
19
src/EllieBot/Migrations/Sqlite/20250324230801_quests.sql
Normal file
19
src/EllieBot/Migrations/Sqlite/20250324230801_quests.sql
Normal file
|
@ -0,0 +1,19 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "UserQuest" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_UserQuest" PRIMARY KEY AUTOINCREMENT,
|
||||
"QuestNumber" INTEGER NOT NULL,
|
||||
"UserId" INTEGER NOT NULL,
|
||||
"QuestId" INTEGER NOT NULL,
|
||||
"Progress" INTEGER NOT NULL,
|
||||
"IsCompleted" INTEGER NOT NULL,
|
||||
"DateAssigned" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_UserQuest_UserId" ON "UserQuest" ("UserId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_UserQuest_UserId_QuestNumber_DateAssigned" ON "UserQuest" ("UserId", "QuestNumber", "DateAssigned");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250324230801_quests', '9.0.1');
|
||||
|
||||
COMMIT;
|
43
src/EllieBot/Migrations/Sqlite/20250327001835_fishitems.sql
Normal file
43
src/EllieBot/Migrations/Sqlite/20250327001835_fishitems.sql
Normal file
|
@ -0,0 +1,43 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "UserFishItem" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_UserFishItem" PRIMARY KEY AUTOINCREMENT,
|
||||
"UserId" INTEGER NOT NULL,
|
||||
"ItemType" INTEGER NOT NULL,
|
||||
"ItemId" INTEGER NOT NULL,
|
||||
"IsEquipped" INTEGER NOT NULL,
|
||||
"UsesLeft" INTEGER NULL,
|
||||
"ExpiresAt" TEXT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_UserFishItem_UserId" ON "UserFishItem" ("UserId");
|
||||
|
||||
CREATE TABLE "ef_temp_UserFishStats" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_UserFishStats" PRIMARY KEY AUTOINCREMENT,
|
||||
"Skill" INTEGER NOT NULL,
|
||||
"UserId" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_UserFishStats" ("Id", "Skill", "UserId")
|
||||
SELECT "Id", "Skill", "UserId"
|
||||
FROM "UserFishStats";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "UserFishStats";
|
||||
|
||||
ALTER TABLE "ef_temp_UserFishStats" RENAME TO "UserFishStats";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE UNIQUE INDEX "IX_UserFishStats_UserId" ON "UserFishStats" ("UserId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250327001835_fishitems', '9.0.1');
|
|
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace EllieBot.Migrations.Sqlite
|
||||
{
|
||||
[DbContext(typeof(SqliteContext))]
|
||||
[Migration("20250319010920_init")]
|
||||
[Migration("20250327001909_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -649,44 +649,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("DiscordUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Expressions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1151,6 +1113,31 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("ImageOnlyChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LinkFix", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NewDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OldDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GuildId", "OldDomain")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LinkFix");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1366,6 +1353,44 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("NCPixel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Expressions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2202,6 +2227,40 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("UnroleTimer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserQuest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateAssigned")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QuestId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QuestNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "QuestNumber", "DateAssigned")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserQuest");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserXpStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2557,6 +2616,37 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("XpShopOwnedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.Fish.Db.UserFishItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsEquipped")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UsesLeft")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserFishItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2588,12 +2678,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Bait")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Pole")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -3156,4 +3240,4 @@ namespace EllieBot.Migrations.Sqlite
|
|||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -550,6 +550,21 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.PrimaryKey("PK_ImageOnlyChannels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LinkFix",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
OldDomain = table.Column<string>(type: "TEXT", nullable: false),
|
||||
NewDomain = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LinkFix", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LiveChannelConfig",
|
||||
columns: table => new
|
||||
|
@ -1096,6 +1111,24 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.PrimaryKey("PK_UserBetStats", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserFishItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ItemType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ItemId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
IsEquipped = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
UsesLeft = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserFishItem", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserFishStats",
|
||||
columns: table => new
|
||||
|
@ -1103,15 +1136,31 @@ namespace EllieBot.Migrations.Sqlite
|
|||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Skill = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Pole = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Bait = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
Skill = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserFishStats", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserQuest",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
QuestNumber = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
QuestId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Progress = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DateAssigned = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserQuest", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRole",
|
||||
columns: table => new
|
||||
|
@ -2001,6 +2050,12 @@ namespace EllieBot.Migrations.Sqlite
|
|||
column: "ChannelId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LinkFix_GuildId_OldDomain",
|
||||
table: "LinkFix",
|
||||
columns: new[] { "GuildId", "OldDomain" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LiveChannelConfig_GuildId",
|
||||
table: "LiveChannelConfig",
|
||||
|
@ -2214,12 +2269,28 @@ namespace EllieBot.Migrations.Sqlite
|
|||
columns: new[] { "UserId", "Game" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserFishItem_UserId",
|
||||
table: "UserFishItem",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserFishStats_UserId",
|
||||
table: "UserFishStats",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserQuest_UserId",
|
||||
table: "UserQuest",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserQuest_UserId_QuestNumber_DateAssigned",
|
||||
table: "UserQuest",
|
||||
columns: new[] { "UserId", "QuestNumber", "DateAssigned" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRole_GuildId",
|
||||
table: "UserRole",
|
||||
|
@ -2503,6 +2574,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "ImageOnlyChannels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LinkFix");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LiveChannelConfig");
|
||||
|
||||
|
@ -2599,9 +2673,15 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "UserBetStats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserFishItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserFishStats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserQuest");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRole");
|
||||
|
||||
|
@ -2681,4 +2761,4 @@ namespace EllieBot.Migrations.Sqlite
|
|||
name: "DiscordUser");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -646,44 +646,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("DiscordUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Expressions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1148,6 +1110,31 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("ImageOnlyChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LinkFix", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NewDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OldDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GuildId", "OldDomain")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LinkFix");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1363,6 +1350,44 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("NCPixel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowTarget")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AutoDeleteTrigger")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ContainsAnywhere")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DmResponse")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Reactions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Trigger")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Expressions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2199,6 +2224,40 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("UnroleTimer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserQuest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateAssigned")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QuestId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QuestNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "QuestNumber", "DateAssigned")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserQuest");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserXpStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2554,6 +2613,37 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("XpShopOwnedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.Fish.Db.UserFishItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsEquipped")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UsesLeft")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserFishItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2585,12 +2675,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Bait")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Pole")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -3153,4 +3237,4 @@ namespace EllieBot.Migrations.Sqlite
|
|||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -124,7 +124,7 @@ public partial class Administration : EllieModule<AdministrationService>
|
|||
[Priority(1)]
|
||||
public async Task Delmsgoncmd(Server _ = Server.Server)
|
||||
{
|
||||
var enabled = await _service.ToggleDeleteMessageOnCommand(ctx.Guild.Id);
|
||||
var enabled = await _service.ToggleDelMsgOnCmd(ctx.Guild.Id);
|
||||
if (enabled)
|
||||
{
|
||||
await Response().Confirm(strs.delmsg_on).SendAsync();
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Administration._common.results;
|
||||
|
@ -10,8 +8,8 @@ namespace EllieBot.Modules.Administration;
|
|||
|
||||
public class AdministrationService : IEService, IReadyExecutor
|
||||
{
|
||||
private ConcurrentHashSet<ulong> deleteMessagesOnCommand;
|
||||
private ConcurrentDictionary<ulong, bool> delMsgOnCmdChannels;
|
||||
private ConcurrentHashSet<ulong> _deleteMessagesOnCommand;
|
||||
private ConcurrentDictionary<ulong, bool> _delMsgOnCmdChannels;
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly IReplacementService _repSvc;
|
||||
|
@ -39,31 +37,32 @@ public class AdministrationService : IEService, IReadyExecutor
|
|||
public async Task OnReadyAsync()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
deleteMessagesOnCommand = new(await uow.GetTable<GuildConfig>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId) && x.DeleteMessageOnCommand)
|
||||
.Select(x => x.GuildId)
|
||||
.ToListAsyncLinqToDB());
|
||||
_deleteMessagesOnCommand = new(await uow.GetTable<GuildConfig>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId) &&
|
||||
x.DeleteMessageOnCommand)
|
||||
.Select(x => x.GuildId)
|
||||
.ToListAsyncLinqToDB());
|
||||
|
||||
delMsgOnCmdChannels = (await uow.GetTable<DelMsgOnCmdChannel>()
|
||||
.Where(x => deleteMessagesOnCommand.Contains(x.GuildId))
|
||||
.ToDictionaryAsyncLinqToDB(x => x.ChannelId, x => x.State))
|
||||
.ToConcurrent();
|
||||
_delMsgOnCmdChannels = (await uow.GetTable<DelMsgOnCmdChannel>()
|
||||
.Where(x => _deleteMessagesOnCommand.Contains(x.GuildId))
|
||||
.ToDictionaryAsyncLinqToDB(x => x.ChannelId, x => x.State))
|
||||
.ToConcurrent();
|
||||
|
||||
_cmdHandler.CommandExecuted += DelMsgOnCmd_Handler;
|
||||
}
|
||||
|
||||
public async Task<(bool DelMsgOnCmd, IEnumerable<DelMsgOnCmdChannel> channels)> GetDelMsgOnCmdData(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var conf = await uow.GetTable<GuildConfig>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.Select(x => x.DeleteMessageOnCommand)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.Select(x => x.DeleteMessageOnCommand)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
|
||||
var channels = await uow.GetTable<DelMsgOnCmdChannel>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return (conf, channels);
|
||||
}
|
||||
|
@ -76,50 +75,50 @@ public class AdministrationService : IEService, IReadyExecutor
|
|||
_ = Task.Run(async () =>
|
||||
{
|
||||
//wat ?!
|
||||
if (delMsgOnCmdChannels.TryGetValue(channel.Id, out var state))
|
||||
if (_delMsgOnCmdChannels.TryGetValue(channel.Id, out var state))
|
||||
{
|
||||
if (state && cmd.Name != "prune" && cmd.Name != "pick")
|
||||
{
|
||||
_logService.AddDeleteIgnore(msg.Id);
|
||||
try
|
||||
{ await msg.DeleteAsync(); }
|
||||
catch { }
|
||||
{
|
||||
await msg.DeleteAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
//if state is false, that means do not do it
|
||||
}
|
||||
else if (deleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick")
|
||||
else if (_deleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick")
|
||||
{
|
||||
_logService.AddDeleteIgnore(msg.Id);
|
||||
try
|
||||
{ await msg.DeleteAsync(); }
|
||||
catch { }
|
||||
{
|
||||
await msg.DeleteAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleDeleteMessageOnCommand(ulong guildId)
|
||||
public async Task<bool> ToggleDelMsgOnCmd(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var conf = await uow.GetTable<GuildConfig>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateWithOutputAsync(x => new()
|
||||
{
|
||||
DeleteMessageOnCommand = !x.DeleteMessageOnCommand
|
||||
}, (old, newVal) => newVal);
|
||||
var gc = uow.GuildConfigsForId(guildId);
|
||||
gc.DeleteMessageOnCommand = !gc.DeleteMessageOnCommand;
|
||||
|
||||
if (conf.Length == 0)
|
||||
return false;
|
||||
|
||||
var val = conf[0].DeleteMessageOnCommand;
|
||||
|
||||
if (val)
|
||||
deleteMessagesOnCommand.Add(guildId);
|
||||
if (gc.DeleteMessageOnCommand)
|
||||
_deleteMessagesOnCommand.Add(guildId);
|
||||
else
|
||||
deleteMessagesOnCommand.TryRemove(guildId);
|
||||
_deleteMessagesOnCommand.TryRemove(guildId);
|
||||
|
||||
return val;
|
||||
await uow.SaveChangesAsync();
|
||||
return gc.DeleteMessageOnCommand;
|
||||
}
|
||||
|
||||
public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState)
|
||||
|
@ -151,7 +150,7 @@ public class AdministrationService : IEService, IReadyExecutor
|
|||
}
|
||||
|
||||
old.State = newState == Administration.State.Enable;
|
||||
delMsgOnCmdChannels[chId] = newState == Administration.State.Enable;
|
||||
_delMsgOnCmdChannels[chId] = newState == Administration.State.Enable;
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
|
@ -162,11 +161,11 @@ public class AdministrationService : IEService, IReadyExecutor
|
|||
}
|
||||
else if (newState == Administration.State.Enable)
|
||||
{
|
||||
delMsgOnCmdChannels[chId] = true;
|
||||
_delMsgOnCmdChannels[chId] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
delMsgOnCmdChannels.TryRemove(chId, out _);
|
||||
_delMsgOnCmdChannels.TryRemove(chId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,5 +248,6 @@ public class AdministrationService : IEService, IReadyExecutor
|
|||
return SetServerIconResult.Success;
|
||||
}
|
||||
|
||||
private bool IsValidUri(string img) => !string.IsNullOrWhiteSpace(img) && Uri.IsWellFormedUriString(img, UriKind.Absolute);
|
||||
private bool IsValidUri(string img)
|
||||
=> !string.IsNullOrWhiteSpace(img) && Uri.IsWellFormedUriString(img, UriKind.Absolute);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
#nullable disable
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Games;
|
||||
|
||||
namespace EllieBot.Modules.Administration.Services;
|
||||
|
||||
public record struct NiceCatchNotifyModel(
|
||||
ulong UserId,
|
||||
FishData Fish,
|
||||
string Stars
|
||||
) : INotifyModel<NiceCatchNotifyModel>
|
||||
{
|
||||
public static string KeyName
|
||||
=> "notify.nicecatch";
|
||||
|
||||
public static NotifyType NotifyType
|
||||
=> NotifyType.NiceCatch;
|
||||
|
||||
public const string PH_EMOJI = "fish.emoji";
|
||||
public const string PH_IMAGE = "fish.image";
|
||||
public const string PH_NAME = "fish.name";
|
||||
public const string PH_STARS = "fish.stars";
|
||||
public const string PH_FLUFF = "fish.fluff";
|
||||
|
||||
public bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = UserId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<NotifyModelPlaceholderData<NiceCatchNotifyModel>> GetReplacements()
|
||||
{
|
||||
return
|
||||
[
|
||||
new(PH_EMOJI, static (data, _) => data.Fish.Emoji),
|
||||
new(PH_IMAGE, static (data, _) => data.Fish.Image),
|
||||
new(PH_NAME, static (data, _) => data.Fish.Name),
|
||||
new(PH_STARS, static (data, _) => data.Stars),
|
||||
new(PH_FLUFF, static (data, _) => data.Fish.Fluff),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ public partial class Administration
|
|||
public class NotifyCommands : EllieModule<NotifyService>
|
||||
{
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task Notify()
|
||||
{
|
||||
await Response()
|
||||
|
@ -38,11 +38,12 @@ public partial class Administration
|
|||
NotifyType.Protection => strs.notify_desc_protection,
|
||||
NotifyType.AddRoleReward => strs.notify_desc_addrolerew,
|
||||
NotifyType.RemoveRoleReward => strs.notify_desc_removerolerew,
|
||||
NotifyType.NiceCatch => strs.notify_desc_nicecatch,
|
||||
_ => strs.notify_desc_not_found
|
||||
};
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task Notify(NotifyType nType)
|
||||
{
|
||||
// show msg
|
||||
|
@ -76,12 +77,12 @@ public partial class Administration
|
|||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task Notify(NotifyType nType, [Leftover] string message)
|
||||
=> await NotifyInternalAsync(nType, null, message);
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task Notify(NotifyType nType, IMessageChannel channel, [Leftover] string message)
|
||||
=> await NotifyInternalAsync(nType, channel, message);
|
||||
|
||||
|
@ -89,6 +90,14 @@ public partial class Administration
|
|||
{
|
||||
var result = await _service.EnableAsync(ctx.Guild.Id, channel?.Id, nType, message);
|
||||
|
||||
if(!result)
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.notify_cant_set)
|
||||
.SendAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
var outChannel = channel is null ? "origin" : $"<#{channel.Id}>";
|
||||
await Response()
|
||||
.Confirm(strs.notify_on(outChannel, Format.Bold(nType.ToString())))
|
||||
|
@ -96,7 +105,7 @@ public partial class Administration
|
|||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task NotifyPhs(NotifyType nType)
|
||||
{
|
||||
var data = _service.GetRegisteredModel(nType);
|
||||
|
@ -111,7 +120,7 @@ public partial class Administration
|
|||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task NotifyList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
|
@ -139,7 +148,7 @@ public partial class Administration
|
|||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task NotifyClear(NotifyType nType)
|
||||
{
|
||||
await _service.DisableAsync(ctx.Guild.Id, nType);
|
||||
|
|
|
@ -42,12 +42,11 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
RegisterModel<ProtectionNotifyModel>();
|
||||
RegisterModel<AddRoleRewardNotifyModel>();
|
||||
RegisterModel<RemoveRoleRewardNotifyModel>();
|
||||
RegisterModel<NiceCatchNotifyModel>();
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
RegisterModels();
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
_events = (await uow.GetTable<Notify>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId,
|
||||
|
@ -57,9 +56,8 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
.GroupBy(x => x.Type)
|
||||
.ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
|
||||
.ToConcurrent();
|
||||
|
||||
|
||||
await SubscribeToEvent<LevelUpNotifyModel>();
|
||||
|
||||
RegisterModels();
|
||||
}
|
||||
|
||||
private async Task SubscribeToEvent<T>()
|
||||
|
@ -75,7 +73,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
{
|
||||
if (isShardLocal)
|
||||
{
|
||||
await OnEvent(data);
|
||||
_ = Task.Run(async () => await OnEvent(data));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -160,7 +158,15 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
IUser? user = null;
|
||||
if (model.TryGetUserId(out var userId))
|
||||
{
|
||||
user = guild.GetUser(userId) ?? _client.GetUser(userId);
|
||||
try
|
||||
{
|
||||
user = guild.GetUser(userId)
|
||||
?? await _client.GetUserAsync(userId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
var rctx = new ReplacementContext(guild: guild, channel: channel, user: user);
|
||||
|
@ -283,7 +289,10 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
var data = new NotifyModelData(T.NotifyType,
|
||||
T.SupportsOriginTarget,
|
||||
T.GetReplacements().Map(x => x.Name));
|
||||
|
||||
_models[T.NotifyType] = data;
|
||||
|
||||
_pubSub.Sub<T>(new(T.KeyName), async (data) => await OnEvent(data));
|
||||
}
|
||||
|
||||
public NotifyModelData GetRegisteredModel(NotifyType nType)
|
||||
|
|
|
@ -391,7 +391,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
{
|
||||
var obj = new AntiSpamIgnore
|
||||
{
|
||||
ChannelId = channelId
|
||||
ChannelId = channelId,
|
||||
};
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
@ -529,16 +529,18 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
};
|
||||
}
|
||||
|
||||
var spamConfigs = await uow.GetTable<AntiSpamSetting>()
|
||||
var spamConfigs = await uow.Set<AntiSpamSetting>()
|
||||
.AsNoTracking()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => gids.Contains(x.GuildId))
|
||||
.Include(x => x.IgnoredChannels)
|
||||
.ToListAsyncEF();
|
||||
|
||||
foreach (var config in spamConfigs)
|
||||
{
|
||||
_antiSpamGuilds[config.GuildId] = new()
|
||||
{
|
||||
AntiSpamSettings = config,
|
||||
UserStats = new()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@ public partial class Administration
|
|||
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
|
||||
var progress = GetProgressTracker(progressMsg);
|
||||
|
||||
var result = await _service.PruneWhere(ctx.Channel,
|
||||
var result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
ctx.Channel,
|
||||
100,
|
||||
x => x.Author.Id == ctx.Client.CurrentUser.Id,
|
||||
progress);
|
||||
|
@ -66,13 +68,17 @@ public partial class Administration
|
|||
|
||||
PruneResult result;
|
||||
if (opts.Safe)
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
(ITextChannel)ctx.Channel,
|
||||
100,
|
||||
x => x.Author.Id == user.Id && !x.IsPinned,
|
||||
progress,
|
||||
opts.After);
|
||||
else
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
(ITextChannel)ctx.Channel,
|
||||
100,
|
||||
x => x.Author.Id == user.Id,
|
||||
progress,
|
||||
|
@ -107,13 +113,17 @@ public partial class Administration
|
|||
|
||||
PruneResult result;
|
||||
if (opts.Safe)
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
ctx.Channel,
|
||||
count,
|
||||
x => !x.IsPinned && x.Id != progressMsg.Id,
|
||||
progress,
|
||||
opts.After);
|
||||
else
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
ctx.Channel,
|
||||
count,
|
||||
x => x.Id != progressMsg.Id,
|
||||
progress,
|
||||
|
@ -133,13 +143,14 @@ public partial class Administration
|
|||
await progressMsg.ModifyAsync(props =>
|
||||
{
|
||||
props.Embed = CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription(GetText(strs.prune_progress(deleted, total)))
|
||||
.Build();
|
||||
.WithPendingColor()
|
||||
.WithDescription(GetText(strs.prune_progress(deleted, total)))
|
||||
.Build();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -182,7 +193,9 @@ public partial class Administration
|
|||
PruneResult result;
|
||||
if (opts.Safe)
|
||||
{
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
ctx.Channel,
|
||||
count,
|
||||
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
|
||||
progress,
|
||||
|
@ -191,7 +204,9 @@ public partial class Administration
|
|||
}
|
||||
else
|
||||
{
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere(
|
||||
ctx.User.Id,
|
||||
ctx.Channel,
|
||||
count,
|
||||
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
|
||||
progress,
|
||||
|
@ -233,7 +248,7 @@ public partial class Administration
|
|||
msg.DeleteAfter(5);
|
||||
break;
|
||||
case PruneResult.FeatureLimit:
|
||||
var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||
var msg2 = await Response().Pending(strs.prune_patron).SendAsync();
|
||||
msg2.DeleteAfter(10);
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -1,23 +1,13 @@
|
|||
#nullable disable
|
||||
using EllieBot.Modules.Patronage;
|
||||
|
||||
namespace EllieBot.Modules.Administration.Services;
|
||||
|
||||
public class PruneService : IEService
|
||||
public class PruneService(ILogCommandService logService) : IEService
|
||||
{
|
||||
//channelids where prunes are currently occuring
|
||||
private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
|
||||
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
|
||||
private readonly ILogCommandService _logService;
|
||||
private readonly IPatronageService _ps;
|
||||
|
||||
public PruneService(ILogCommandService logService, IPatronageService ps)
|
||||
{
|
||||
_logService = logService;
|
||||
_ps = ps;
|
||||
}
|
||||
|
||||
public async Task<PruneResult> PruneWhere(
|
||||
ulong runningUserId,
|
||||
IMessageChannel channel,
|
||||
int amount,
|
||||
Func<IMessage, bool> predicate,
|
||||
|
@ -37,11 +27,6 @@ public class PruneService : IEService
|
|||
|
||||
try
|
||||
{
|
||||
if (channel is ITextChannel tc && !await _ps.LimitHitAsync(LimitedFeatureName.Prune, tc.Guild.OwnerId))
|
||||
{
|
||||
return PruneResult.FeatureLimit;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
IMessage[] msgs;
|
||||
IMessage lastMessage = null;
|
||||
|
@ -67,7 +52,7 @@ public class PruneService : IEService
|
|||
var singleDeletable = new List<IMessage>();
|
||||
foreach (var x in msgs)
|
||||
{
|
||||
_logService.AddDeleteIgnore(x.Id);
|
||||
logService.AddDeleteIgnore(x.Id);
|
||||
|
||||
if (now - x.CreatedAt < _twoWeeks)
|
||||
bulkDeletable.Add(x);
|
||||
|
|
|
@ -57,8 +57,7 @@ public partial class Administration
|
|||
_ => ctx.OkAsync(),
|
||||
async fl =>
|
||||
{
|
||||
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
||||
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||
await msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -48,9 +48,9 @@ public partial class Administration
|
|||
});
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
|
||||
Format.Bold(targetUser.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
|
||||
Format.Bold(targetUser.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -73,9 +73,9 @@ public partial class Administration
|
|||
{
|
||||
await targetUser.RemoveRoleAsync(roleToRemove);
|
||||
await Response()
|
||||
.Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
|
||||
Format.Bold(targetUser.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
|
||||
Format.Bold(targetUser.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -226,8 +226,8 @@ public partial class Administration
|
|||
if (!await CheckRoleHierarchy(role))
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.hierarchy)
|
||||
.SendAsync();
|
||||
.Error(strs.hierarchy)
|
||||
.SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -236,10 +236,83 @@ public partial class Administration
|
|||
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.temp_role_added(user.Mention,
|
||||
Format.Bold(role.Name),
|
||||
TimestampTag.FromDateTime(DateTime.UtcNow.Add(timespan.Time), TimestampTagStyles.Relative)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.temp_role_added(user.Mention,
|
||||
Format.Bold(role.Name),
|
||||
TimestampTag.FromDateTime(DateTime.UtcNow.Add(timespan.Time), TimestampTagStyles.Relative)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
public Task RoleIcon(IRole role, Emote emote)
|
||||
=> RoleIcon(role, emote.Url);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
public async Task RoleIcon(IRole role, [Leftover] string iconUrl)
|
||||
{
|
||||
if (!await CheckRoleHierarchy(role))
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(iconUrl))
|
||||
{
|
||||
await Response().Error(strs.userrole_icon_invalid).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the URL format
|
||||
if (!Uri.TryCreate(iconUrl, UriKind.Absolute, out var uri))
|
||||
{
|
||||
await Response().Error(strs.userrole_icon_invalid).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the image
|
||||
using var httpClient = new HttpClient();
|
||||
using var response = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
// Check if the response is successful
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await Response().Error(strs.userrole_icon_fail).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check content type - must be image/png or image/jpeg
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType?.ToLower();
|
||||
if (contentType != "image/png"
|
||||
&& contentType != "image/jpeg"
|
||||
&& contentType != "image/webp")
|
||||
{
|
||||
await Response().Error(strs.userrole_icon_fail).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file size - Discord limit is 256KB
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
if (contentLength is > 256 * 1024)
|
||||
{
|
||||
await Response().Error(strs.userrole_icon_fail).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the image to a memory stream
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
await using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
// Create Discord image from stream
|
||||
using var discordImage = new Image(memoryStream);
|
||||
|
||||
// Upload the image to Discord
|
||||
await role.ModifyAsync(r => r.Icon = discordImage);
|
||||
|
||||
await Response().Confirm(strs.userrole_icon_success(Format.Bold(role.Name))).SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -494,6 +494,9 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||
.WithFooter(CurrentTime(usr.Guild))
|
||||
.WithOkColor();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(reason))
|
||||
embed.WithDescription(reason);
|
||||
|
||||
await _sender.Response(logChannel).Embed(embed).SendAsync();
|
||||
}
|
||||
catch
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#nullable disable
|
||||
using EllieBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
|
||||
using EllieBot.Modules.Games.Common;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Common.AnimalRacing;
|
||||
|
||||
|
@ -35,12 +36,18 @@ public sealed class AnimalRace : IDisposable
|
|||
private readonly ICurrencyService _currency;
|
||||
private readonly RaceOptions _options;
|
||||
private readonly Queue<RaceAnimal> _animalsQueue;
|
||||
private readonly QuestService _quests;
|
||||
|
||||
public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable<RaceAnimal> availableAnimals)
|
||||
public AnimalRace(
|
||||
RaceOptions options,
|
||||
ICurrencyService currency,
|
||||
IEnumerable<RaceAnimal> availableAnimals,
|
||||
QuestService quests)
|
||||
{
|
||||
_currency = currency;
|
||||
_options = options;
|
||||
_animalsQueue = new(availableAnimals);
|
||||
_quests = quests;
|
||||
MaxUsers = _animalsQueue.Count;
|
||||
|
||||
if (_animalsQueue.Count == 0)
|
||||
|
@ -60,7 +67,10 @@ public sealed class AnimalRace : IDisposable
|
|||
|
||||
await Start();
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
});
|
||||
|
||||
public async Task<AnimalRacingUser> JoinRace(ulong userId, string userName, long bet = 0)
|
||||
|
@ -93,7 +103,10 @@ public sealed class AnimalRace : IDisposable
|
|||
|
||||
return user;
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Start()
|
||||
|
@ -104,7 +117,9 @@ public sealed class AnimalRace : IDisposable
|
|||
foreach (var user in _users)
|
||||
{
|
||||
if (user.Bet > 0)
|
||||
await _currency.AddAsync(user.UserId, (long)(user.Bet + BASE_MULTIPLIER), new("animalrace", "refund"));
|
||||
await _currency.AddAsync(user.UserId,
|
||||
(long)(user.Bet * BASE_MULTIPLIER),
|
||||
new("animalrace", "refund"));
|
||||
}
|
||||
|
||||
_ = OnStartingFailed?.Invoke(this);
|
||||
|
@ -112,6 +127,11 @@ public sealed class AnimalRace : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
foreach (var user in _users)
|
||||
{
|
||||
await _quests.ReportActionAsync(user.UserId, QuestEventType.RaceJoined);
|
||||
}
|
||||
|
||||
_ = OnStarted?.Invoke(this);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ using EllieBot.Modules.Gambling.Common;
|
|||
using EllieBot.Modules.Gambling.Common.AnimalRacing;
|
||||
using EllieBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
|
||||
using EllieBot.Modules.Gambling.Services;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
using EllieBot.Modules.Games.Services;
|
||||
|
||||
namespace EllieBot.Modules.Gambling;
|
||||
|
@ -17,6 +18,7 @@ public partial class Gambling
|
|||
private readonly ICurrencyService _cs;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly GamesConfigService _gamesConf;
|
||||
private readonly QuestService _quests;
|
||||
|
||||
private IUserMessage raceMessage;
|
||||
|
||||
|
@ -24,12 +26,14 @@ public partial class Gambling
|
|||
ICurrencyService cs,
|
||||
DiscordSocketClient client,
|
||||
GamblingConfigService gamblingConf,
|
||||
GamesConfigService gamesConf)
|
||||
GamesConfigService gamesConf,
|
||||
QuestService quests)
|
||||
: base(gamblingConf)
|
||||
{
|
||||
_cs = cs;
|
||||
_client = client;
|
||||
_gamesConf = gamesConf;
|
||||
_quests = quests;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -39,11 +43,11 @@ public partial class Gambling
|
|||
{
|
||||
var (options, _) = OptionsParser.ParseFrom(new RaceOptions(), args);
|
||||
|
||||
var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle());
|
||||
var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle(), _quests);
|
||||
if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar))
|
||||
return Response()
|
||||
.Error(GetText(strs.animal_race), GetText(strs.animal_race_already_started))
|
||||
.SendAsync();
|
||||
.Error(GetText(strs.animal_race), GetText(strs.animal_race_already_started))
|
||||
.SendAsync();
|
||||
|
||||
ar.Initialize();
|
||||
|
||||
|
@ -61,7 +65,9 @@ public partial class Gambling
|
|||
raceMessage = null;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -74,22 +80,22 @@ public partial class Gambling
|
|||
if (race.FinishedUsers[0].Bet > 0)
|
||||
{
|
||||
return Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.animal_race))
|
||||
.WithDescription(GetText(strs.animal_race_won_money(
|
||||
Format.Bold(winner.Username),
|
||||
winner.Animal.Icon,
|
||||
N(race.FinishedUsers[0].Bet * race.Multi))))
|
||||
.WithFooter($"x{race.Multi:F2}"))
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.animal_race))
|
||||
.WithDescription(GetText(strs.animal_race_won_money(
|
||||
Format.Bold(winner.Username),
|
||||
winner.Animal.Icon,
|
||||
N(race.FinishedUsers[0].Bet * race.Multi))))
|
||||
.WithFooter($"x{race.Multi:F2}"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
ar.Dispose();
|
||||
return Response()
|
||||
.Confirm(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon)))
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
ar.OnStartingFailed += Ar_OnStartingFailed;
|
||||
|
@ -99,10 +105,10 @@ public partial class Gambling
|
|||
_client.MessageReceived += ClientMessageReceived;
|
||||
|
||||
return Response()
|
||||
.Confirm(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_starting(options.StartTime)),
|
||||
footer: GetText(strs.animal_race_join_instr(prefix)))
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_starting(options.StartTime)),
|
||||
footer: GetText(strs.animal_race_join_instr(prefix)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private Task Ar_OnStarted(AnimalRace race)
|
||||
|
@ -110,9 +116,9 @@ public partial class Gambling
|
|||
if (race.Users.Count == race.MaxUsers)
|
||||
return Response().Confirm(GetText(strs.animal_race), GetText(strs.animal_race_full)).SendAsync();
|
||||
return Response()
|
||||
.Confirm(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_starting_with_x(race.Users.Count)))
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_starting_with_x(race.Users.Count)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private async Task Ar_OnStateUpdate(AnimalRace race)
|
||||
|
@ -133,10 +139,10 @@ public partial class Gambling
|
|||
else
|
||||
{
|
||||
await msg.ModifyAsync(x => x.Embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.animal_race))
|
||||
.WithDescription(text)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
.WithTitle(GetText(strs.animal_race))
|
||||
.WithDescription(text)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,15 +172,15 @@ public partial class Gambling
|
|||
if (amount > 0)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(GetText(strs.animal_race_join_bet(ctx.User.Mention,
|
||||
user.Animal.Icon,
|
||||
amount + CurrencySign)))
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.animal_race_join_bet(ctx.User.Mention,
|
||||
user.Animal.Icon,
|
||||
amount + CurrencySign)))
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
await Response()
|
||||
.Confirm(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon))
|
||||
.SendAsync();
|
||||
.Confirm(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon))
|
||||
.SendAsync();
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Bank;
|
||||
|
||||
public sealed class BankService : IBankService, IEService
|
||||
public sealed class BankService(
|
||||
ICurrencyService _cur,
|
||||
DbService _db,
|
||||
QuestService quests) : IBankService, IEService
|
||||
{
|
||||
private readonly ICurrencyService _cur;
|
||||
private readonly DbService _db;
|
||||
|
||||
public BankService(ICurrencyService cur, DbService db)
|
||||
{
|
||||
_cur = cur;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<bool> AwardAsync(ulong userId, long amount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
|
||||
|
@ -37,7 +32,7 @@ public sealed class BankService : IBankService, IEService
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> TakeAsync(ulong userId, long amount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
|
||||
|
@ -50,7 +45,7 @@ public sealed class BankService : IBankService, IEService
|
|||
{
|
||||
Balance = old.Balance - amount
|
||||
});
|
||||
|
||||
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
|
@ -63,20 +58,28 @@ public sealed class BankService : IBankService, IEService
|
|||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.Set<BankUser>()
|
||||
.ToLinqToDBTable()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Balance = amount
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
Balance = old.Balance + amount
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId
|
||||
});
|
||||
.ToLinqToDBTable()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Balance = amount
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
Balance = old.Balance + amount
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId
|
||||
});
|
||||
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.BankAction,
|
||||
new()
|
||||
{
|
||||
{ "type", "deposit" },
|
||||
{ "amount", amount.ToString() }
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -87,16 +90,23 @@ public sealed class BankService : IBankService, IEService
|
|||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var rows = await ctx.Set<BankUser>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId && x.Balance >= amount)
|
||||
.UpdateAsync((old) => new()
|
||||
{
|
||||
Balance = old.Balance - amount
|
||||
});
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId && x.Balance >= amount)
|
||||
.UpdateAsync((old) => new()
|
||||
{
|
||||
Balance = old.Balance - amount
|
||||
});
|
||||
|
||||
if (rows > 0)
|
||||
{
|
||||
await _cur.AddAsync(userId, amount, new("bank", "withdraw"));
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.BankAction,
|
||||
new()
|
||||
{
|
||||
{ "type", "withdraw" },
|
||||
{ "amount", amount.ToString() }
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -106,10 +116,18 @@ public sealed class BankService : IBankService, IEService
|
|||
public async Task<long> GetBalanceAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return (await ctx.Set<BankUser>()
|
||||
.ToLinqToDBTable()
|
||||
.FirstOrDefaultAsync(x => x.UserId == userId))
|
||||
?.Balance
|
||||
?? 0;
|
||||
var res = (await ctx.Set<BankUser>()
|
||||
.ToLinqToDBTable()
|
||||
.FirstOrDefaultAsync(x => x.UserId == userId))
|
||||
?.Balance
|
||||
?? 0;
|
||||
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.BankAction,
|
||||
new()
|
||||
{
|
||||
{ "type", "balance" }
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ using System.Text;
|
|||
using EllieBot.Modules.Gambling.Rps;
|
||||
using EllieBot.Common.TypeReaders;
|
||||
using EllieBot.Modules.Games;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
using EllieBot.Modules.Patronage;
|
||||
|
||||
namespace EllieBot.Modules.Gambling;
|
||||
|
@ -35,6 +36,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
private readonly RakebackService _rb;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly CaptchaService _captchaService;
|
||||
private readonly VoteRewardService _vrs;
|
||||
private readonly QuestService _quests;
|
||||
|
||||
public Gambling(
|
||||
IGamblingService gs,
|
||||
|
@ -50,7 +53,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
GamblingTxTracker gamblingTxTracker,
|
||||
RakebackService rb,
|
||||
IBotCache cache,
|
||||
CaptchaService captchaService)
|
||||
CaptchaService captchaService,
|
||||
VoteRewardService vrs,
|
||||
QuestService quests)
|
||||
: base(configService)
|
||||
{
|
||||
_gs = gs;
|
||||
|
@ -65,6 +70,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
_captchaService = captchaService;
|
||||
_ps = patronage;
|
||||
_rng = new EllieRandom();
|
||||
_vrs = vrs;
|
||||
_quests = quests;
|
||||
|
||||
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
||||
_enUsCulture.NumberDecimalDigits = 0;
|
||||
|
@ -131,6 +138,67 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await ClaimTimely();
|
||||
});
|
||||
|
||||
[Cmd]
|
||||
public async Task Vote()
|
||||
{
|
||||
var reward = Config.VoteReward;
|
||||
if (reward <= 0)
|
||||
{
|
||||
if (Config.Timely.Amount > 0 && Config.Timely.Cooldown > 0)
|
||||
{
|
||||
await Timely();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var (amount, msg) = await _service.GetAmountAndMessage(ctx.User.Id, reward);
|
||||
|
||||
var prepend = GetText(strs.vote_suggest(Format.Bold(N(amount))));
|
||||
msg = prepend + "\n\n" + msg;
|
||||
|
||||
var inter = CreateRemindMeInteraction(12) as EllieButtonInteractionHandler;
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(msg);
|
||||
|
||||
var cb = new ComponentBuilder();
|
||||
|
||||
// Add vote platform buttons if any are configured
|
||||
if (Config.VotePlatforms.Length > 0)
|
||||
{
|
||||
var row = new ActionRowBuilder();
|
||||
// Loop through each vote platform and create a URL button for it
|
||||
foreach (var platform in Config.VotePlatforms)
|
||||
{
|
||||
// Create a URL button for each platform
|
||||
// The platform string should be in format "Label|URL"
|
||||
var parts = platform.Split('|', 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var label = parts[0];
|
||||
var url = parts[1];
|
||||
|
||||
// Add a URL button to the component builder
|
||||
row.WithButton(label, style: ButtonStyle.Link, url: url);
|
||||
}
|
||||
}
|
||||
cb.AddRow(row);
|
||||
}
|
||||
if (!_service.UserHasTimelyReminder(ctx.User.Id))
|
||||
{
|
||||
var secondRow = new ActionRowBuilder();
|
||||
secondRow.WithButton(inter.Button);
|
||||
cb.AddRow(secondRow);
|
||||
var sent = await ctx.Channel.SendMessageAsync(embed: eb.Build(), components: cb?.Build());
|
||||
await inter.RunAsync(sent);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ctx.Channel.SendMessageAsync(embed: eb.Build(), components: cb?.Build());
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Timely()
|
||||
{
|
||||
|
@ -138,10 +206,17 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
var period = Config.Timely.Cooldown;
|
||||
if (val <= 0 || period <= 0)
|
||||
{
|
||||
if (Config.VoteReward > 0)
|
||||
{
|
||||
await Vote();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response().Error(strs.timely_none).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
||||
if (Config.Timely.ProtType == TimelyProt.Button)
|
||||
{
|
||||
var interaction = CreateTimelyInteraction();
|
||||
|
@ -149,7 +224,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await msg.DeleteAsync();
|
||||
return;
|
||||
}
|
||||
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||
|
||||
if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||
{
|
||||
var password = await _captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
|
||||
|
@ -209,56 +285,19 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
|
||||
var val = Config.Timely.Amount;
|
||||
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
|
||||
var guildUsers = await boostGuilds
|
||||
.Select(async gid =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var guild = await _client.Rest.GetGuildAsync(gid, false);
|
||||
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
|
||||
return (guild, user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
||||
var booster = userInfo != default;
|
||||
|
||||
if (booster)
|
||||
val += Config.BoostBonus.BaseTimelyBonus;
|
||||
|
||||
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
||||
|
||||
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
||||
|
||||
val += (int)(val * percentBonus);
|
||||
|
||||
var inter = CreateRemindMeInteraction(period);
|
||||
|
||||
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
||||
var prepend = GetText(strs.timely(N(val), period));
|
||||
var (newVal, msg) = await _service.GetAmountAndMessage(ctx.User.Id, val);
|
||||
|
||||
var msg = GetText(strs.timely(N(val), period));
|
||||
if (booster || percentBonus > float.Epsilon)
|
||||
{
|
||||
msg += "\n\n";
|
||||
if (booster)
|
||||
msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
|
||||
msg = prepend + "\n\n" + msg;
|
||||
|
||||
if (percentBonus > float.Epsilon)
|
||||
msg +=
|
||||
$"*+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/elliebot) pledge! <:hart:746995901758832712>*";
|
||||
await _cs.AddAsync(ctx.User.Id, newVal, new("timely", "claim"));
|
||||
|
||||
await Response().Confirm(msg).Interaction(inter).SendAsync();
|
||||
}
|
||||
else
|
||||
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
|
||||
await Response().Confirm(msg).Interaction(inter).SendAsync();
|
||||
}
|
||||
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task TimelyReset()
|
||||
|
@ -911,7 +950,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
|
||||
|
||||
public enum GambleTestTarget
|
||||
{
|
||||
Slot,
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling.Common;
|
|||
public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; } = 12;
|
||||
public int Version { get; set; } = 13;
|
||||
|
||||
[Comment("""Currency settings""")]
|
||||
public CurrencyConfig Currency { get; set; }
|
||||
|
@ -63,6 +63,20 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
|
|||
This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting
|
||||
""")]
|
||||
public long VoteReward { get; set; } = 100;
|
||||
|
||||
[Comment("""
|
||||
Id of the channel to send a message to after a user votes
|
||||
""")]
|
||||
public ulong? VoteFeedChannelId { get; set; }
|
||||
|
||||
[Comment("""
|
||||
List of platforms for which the bot will give currency rewards.
|
||||
Format: PLATFORM|URL
|
||||
Supported platforms: topgg, discords, discordbotlist
|
||||
You will have to have VotesApi running on the same machine.
|
||||
Format example: Top.gg|https://top.gg/bot/YOUR_BOT_ID/vote
|
||||
""")]
|
||||
public string[] VotePlatforms { get; set; } = [];
|
||||
|
||||
[Comment("""Slot config""")]
|
||||
public SlotsConfig Slots { get; set; }
|
||||
|
|
|
@ -12,12 +12,6 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
|
|||
public override string Name
|
||||
=> "gambling";
|
||||
|
||||
private readonly IEnumerable<WaifuItemModel> _antiGiftSeed = new[]
|
||||
{
|
||||
new WaifuItemModel("🥀", 100, "WiltedRose", true), new WaifuItemModel("✂️", 1000, "Haircut", true),
|
||||
new WaifuItemModel("🧻", 10000, "ToiletPaper", true)
|
||||
};
|
||||
|
||||
public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub)
|
||||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
|
@ -154,51 +148,12 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
|
|||
|
||||
public void Migrate()
|
||||
{
|
||||
if (data.Version < 2)
|
||||
if (data.Version < 13)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Waifu.Items = c.Waifu.Items.Concat(_antiGiftSeed).ToList();
|
||||
c.Version = 2;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 3)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 3;
|
||||
c.VoteReward = 100;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 7)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 7;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 8)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 8;
|
||||
c.Waifu.Decay.UnclaimedDecayPercent = 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 12)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 12;
|
||||
|
||||
if (c.BetRoll.Pairs.Length == 3 && c.BetRoll.Pairs[2].WhenAbove == 66)
|
||||
{
|
||||
c.BetRoll.Pairs[2].WhenAbove = 65;
|
||||
}
|
||||
c.Version = 13;
|
||||
c.VotePlatforms = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Gambling.Common;
|
||||
using EllieBot.Modules.Gambling.Common.Connect4;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
using EllieBot.Modules.Patronage;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Services;
|
||||
|
||||
|
@ -15,7 +18,9 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly GamblingConfigService _gss;
|
||||
private readonly GamblingConfigService _gcs;
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly QuestService _quests;
|
||||
private readonly EllieRandom _rng;
|
||||
|
||||
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
|
||||
|
@ -24,12 +29,16 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
DbService db,
|
||||
DiscordSocketClient client,
|
||||
IBotCache cache,
|
||||
GamblingConfigService gss)
|
||||
GamblingConfigService gcs,
|
||||
IPatronageService ps,
|
||||
QuestService quests)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
_cache = cache;
|
||||
_gss = gss;
|
||||
_gcs = gcs;
|
||||
_ps = ps;
|
||||
_quests = quests;
|
||||
_rng = new EllieRandom();
|
||||
}
|
||||
|
||||
|
@ -53,7 +62,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
{
|
||||
try
|
||||
{
|
||||
var lifetime = _gss.Data.Currency.TransactionsLifetime;
|
||||
var lifetime = _gcs.Data.Currency.TransactionsLifetime;
|
||||
if (lifetime <= 0)
|
||||
continue;
|
||||
|
||||
|
@ -61,7 +70,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
var days = TimeSpan.FromDays(lifetime);
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.Set<CurrencyTransaction>()
|
||||
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
|
||||
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -82,7 +91,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
{
|
||||
try
|
||||
{
|
||||
var config = _gss.Data;
|
||||
var config = _gcs.Data;
|
||||
var maxDecay = config.Decay.MaxDecay;
|
||||
if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0)
|
||||
continue;
|
||||
|
@ -113,14 +122,14 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
|
||||
var decay = (double)config.Decay.Percent;
|
||||
await uow.Set<DiscordUser>()
|
||||
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
|
||||
.UpdateAsync(old => new()
|
||||
{
|
||||
CurrencyAmount =
|
||||
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
|
||||
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
|
||||
: old.CurrencyAmount - maxDecay
|
||||
});
|
||||
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
|
||||
.UpdateAsync(old => new()
|
||||
{
|
||||
CurrencyAmount =
|
||||
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
|
||||
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
|
||||
: old.CurrencyAmount - maxDecay
|
||||
});
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
|
@ -142,6 +151,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
private static TypedKey<Dictionary<ulong, long>> _timelyKey
|
||||
= new("timely:claims");
|
||||
|
||||
|
||||
public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period)
|
||||
{
|
||||
if (period == 0)
|
||||
|
@ -190,8 +200,77 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
return db.GetTable<Reminder>()
|
||||
.Any(x => x.UserId == userId
|
||||
&& x.Type == ReminderType.Timely);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveAllTimelyClaimsAsync()
|
||||
=> await _cache.RemoveAsync(_timelyKey);
|
||||
|
||||
private string N(long amount)
|
||||
=> CurrencyHelper.N(amount, CultureInfo.InvariantCulture, _gcs.Data.Currency.Sign);
|
||||
|
||||
public async Task<(long val, string msg)> GetAmountAndMessage(ulong userId, long originalAmount)
|
||||
{
|
||||
var gcsData = _gcs.Data;
|
||||
var boostGuilds = gcsData.BoostBonus.GuildIds ?? [];
|
||||
var guildUsers = await boostGuilds
|
||||
.Select(async gid =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var guild = _client.GetGuild(gid) as IGuild ?? await _client.Rest.GetGuildAsync(gid, false);
|
||||
var user = await guild.GetUserAsync(gid) ?? await _client.Rest.GetGuildUserAsync(gid, userId);
|
||||
return (guild, user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
||||
var booster = userInfo != default;
|
||||
|
||||
if (booster)
|
||||
originalAmount += gcsData.BoostBonus.BaseTimelyBonus;
|
||||
|
||||
var hasCompletedDailies = await _quests.UserCompletedDailies(userId);
|
||||
|
||||
if (hasCompletedDailies)
|
||||
originalAmount = (long)(1.5 * originalAmount);
|
||||
|
||||
var patron = await _ps.GetPatronAsync(userId);
|
||||
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
||||
|
||||
originalAmount += (long)(originalAmount * percentBonus);
|
||||
|
||||
var msg = $"**{N(originalAmount)}** base reward\n\n";
|
||||
if (boostGuilds.Count > 0)
|
||||
{
|
||||
if (booster)
|
||||
msg += $"✅ *+{N(gcsData.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
|
||||
else
|
||||
msg += $"❌ *+0 bonus for boosting {userInfo.guild}*\n";
|
||||
}
|
||||
|
||||
if (_ps.GetConfig().IsEnabled)
|
||||
{
|
||||
if (percentBonus > float.Epsilon)
|
||||
msg +=
|
||||
$"✅ *+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/elliebot) pledge! <:hart:746995901758832712>*\n";
|
||||
else
|
||||
msg += $"❌ *+0 bonus for the [Patreon](https://patreon.com/elliebot) pledge*\n";
|
||||
}
|
||||
|
||||
if (hasCompletedDailies)
|
||||
{
|
||||
msg += $"✅ *+50% bonus for completing daily quests*\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
msg += $"❌ *+0 bonus for completing daily quests*\n";
|
||||
}
|
||||
|
||||
return (originalAmount, msg);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ using LinqToDB.EntityFrameworkCore;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Unicode;
|
||||
using SixLabors.ImageSharp;
|
||||
|
@ -15,67 +16,47 @@ using Image = SixLabors.ImageSharp.Image;
|
|||
|
||||
namespace EllieBot.Modules.Gambling.Services;
|
||||
|
||||
public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
||||
public class PlantPickService(
|
||||
DbService db,
|
||||
IBotStrings strings,
|
||||
IImageCache images,
|
||||
FontProvider fonts,
|
||||
ICurrencyService cs,
|
||||
CommandHandler cmdHandler,
|
||||
DiscordSocketClient client,
|
||||
GamblingConfigService gss,
|
||||
GamblingService gs,
|
||||
QuestService quests) : IEService, IExecNoCommand, IReadyExecutor
|
||||
{
|
||||
//channelId/last generation
|
||||
public ConcurrentDictionary<ulong, long> LastGenerations { get; } = new();
|
||||
private readonly DbService _db;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly IImageCache _images;
|
||||
private readonly FontProvider _fonts;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly CommandHandler _cmdHandler;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly GamblingConfigService _gss;
|
||||
private readonly GamblingService _gs;
|
||||
|
||||
private ConcurrentHashSet<ulong> _generationChannels = [];
|
||||
|
||||
public PlantPickService(
|
||||
DbService db,
|
||||
IBotStrings strings,
|
||||
IImageCache images,
|
||||
FontProvider fonts,
|
||||
ICurrencyService cs,
|
||||
CommandHandler cmdHandler,
|
||||
DiscordSocketClient client,
|
||||
GamblingConfigService gss,
|
||||
GamblingService gs)
|
||||
{
|
||||
_db = db;
|
||||
_strings = strings;
|
||||
_images = images;
|
||||
_fonts = fonts;
|
||||
_cs = cs;
|
||||
_cmdHandler = cmdHandler;
|
||||
_client = client;
|
||||
_gss = gss;
|
||||
_gs = gs;
|
||||
}
|
||||
|
||||
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
||||
=> PotentialFlowerGeneration(msg);
|
||||
|
||||
private string GetText(ulong gid, LocStr str)
|
||||
=> _strings.GetText(str, gid);
|
||||
=> strings.GetText(str, gid);
|
||||
|
||||
public async Task<bool> ToggleCurrencyGeneration(ulong gid, ulong cid)
|
||||
{
|
||||
bool enabled;
|
||||
await using var uow = _db.GetDbContext();
|
||||
await using var uow = db.GetDbContext();
|
||||
|
||||
if (_generationChannels.Add(cid))
|
||||
{
|
||||
await uow.GetTable<GCChannelId>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
ChannelId = cid,
|
||||
GuildId = gid
|
||||
}, (x) => new()
|
||||
},
|
||||
(x) => new()
|
||||
{
|
||||
ChannelId = cid,
|
||||
GuildId = gid
|
||||
}, () => new()
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
ChannelId = cid,
|
||||
GuildId = gid
|
||||
|
@ -87,8 +68,8 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
else
|
||||
{
|
||||
await uow.GetTable<GCChannelId>()
|
||||
.Where(x => x.ChannelId == cid && x.GuildId == gid)
|
||||
.DeleteAsync();
|
||||
.Where(x => x.ChannelId == cid && x.GuildId == gid)
|
||||
.DeleteAsync();
|
||||
|
||||
_generationChannels.TryRemove(cid);
|
||||
enabled = false;
|
||||
|
@ -99,9 +80,9 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
|
||||
public async Task<IReadOnlyCollection<GCChannelId>> GetAllGeneratingChannels()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await using var uow = db.GetDbContext();
|
||||
return await uow.GetTable<GCChannelId>()
|
||||
.ToListAsyncLinqToDB();
|
||||
.ToListAsyncLinqToDB();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -111,7 +92,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
/// <returns>Stream of the currency image</returns>
|
||||
public async Task<(Stream, string)> GetRandomCurrencyImageAsync(string pass)
|
||||
{
|
||||
var curImg = await _images.GetCurrencyImageAsync();
|
||||
var curImg = await images.GetCurrencyImageAsync();
|
||||
|
||||
if (curImg is null)
|
||||
return (new MemoryStream(), null);
|
||||
|
@ -142,7 +123,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
pass = pass.TrimTo(10, true).ToLowerInvariant();
|
||||
using var img = Image.Load<Rgba32>(curImg);
|
||||
// choose font size based on the image height, so that it's visible
|
||||
var font = _fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold);
|
||||
var font = fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold);
|
||||
img.Mutate(x =>
|
||||
{
|
||||
// measure the size of the text to be drawing
|
||||
|
@ -170,13 +151,13 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
|
||||
// draw the password over the background
|
||||
x.DrawText(new RichTextOptions(font)
|
||||
{
|
||||
Origin = new(0, 0),
|
||||
TextRuns =
|
||||
{
|
||||
Origin = new(0, 0),
|
||||
TextRuns =
|
||||
[
|
||||
strikeoutRun
|
||||
]
|
||||
},
|
||||
},
|
||||
pass,
|
||||
new SolidBrush(Color.White));
|
||||
});
|
||||
|
@ -200,7 +181,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
{
|
||||
try
|
||||
{
|
||||
var config = _gss.Data;
|
||||
var config = gss.Data;
|
||||
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue.ToBinary());
|
||||
var rng = new EllieRandom();
|
||||
|
||||
|
@ -219,7 +200,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
|
||||
if (dropAmount > 0)
|
||||
{
|
||||
var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
|
||||
var prefix = cmdHandler.GetPrefix(channel.Guild.Id);
|
||||
var toSend = dropAmount == 1
|
||||
? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign))
|
||||
+ " "
|
||||
|
@ -228,7 +209,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
+ " "
|
||||
+ GetText(channel.GuildId, strs.pick_pl(prefix));
|
||||
|
||||
var pw = config.Generation.HasPassword ? _gs.GeneratePassword().ToUpperInvariant() : null;
|
||||
var pw = config.Generation.HasPassword ? gs.GeneratePassword().ToUpperInvariant() : null;
|
||||
|
||||
IUserMessage sent;
|
||||
var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
|
||||
|
@ -238,7 +219,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
|
||||
var res = await AddPlantToDatabase(channel.GuildId,
|
||||
channel.Id,
|
||||
_client.CurrentUser.Id,
|
||||
client.CurrentUser.Id,
|
||||
sent.Id,
|
||||
dropAmount,
|
||||
pw,
|
||||
|
@ -261,12 +242,12 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
public async Task<long> PickAsync(
|
||||
ulong gid,
|
||||
ITextChannel ch,
|
||||
ulong uid,
|
||||
ulong userId,
|
||||
string pass)
|
||||
{
|
||||
long amount;
|
||||
ulong[] ids;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
await using (var uow = db.GetDbContext())
|
||||
{
|
||||
// this method will sum all plants with that password,
|
||||
// remove them, and get messageids of the removed plants
|
||||
|
@ -274,8 +255,8 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
pass = pass?.Trim().TrimTo(10, true)?.ToUpperInvariant();
|
||||
// gets all plants in this channel with the same password
|
||||
var entries = await uow.GetTable<PlantedCurrency>()
|
||||
.Where(x => x.ChannelId == ch.Id && pass == x.Password)
|
||||
.DeleteWithOutputAsync();
|
||||
.Where(x => x.ChannelId == ch.Id && pass == x.Password)
|
||||
.DeleteWithOutputAsync();
|
||||
|
||||
if (!entries.Any())
|
||||
return 0;
|
||||
|
@ -285,14 +266,24 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
}
|
||||
|
||||
if (amount > 0)
|
||||
await _cs.AddAsync(uid, amount, new("currency", "collect"));
|
||||
{
|
||||
await cs.AddAsync(userId, amount, new("currency", "collect"));
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.PlantOrPick,
|
||||
new()
|
||||
{
|
||||
{ "type", "pick" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
_ = ch.DeleteMessagesAsync(ids);
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// return the amount of currency the user picked
|
||||
return amount;
|
||||
|
@ -308,8 +299,8 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
try
|
||||
{
|
||||
// get the text
|
||||
var prefix = _cmdHandler.GetPrefix(gid);
|
||||
var msgToSend = GetText(gid, strs.planted(Format.Bold(user), amount + _gss.Data.Currency.Sign));
|
||||
var prefix = cmdHandler.GetPrefix(gid);
|
||||
var msgToSend = GetText(gid, strs.planted(Format.Bold(user), amount + gss.Data.Currency.Sign));
|
||||
|
||||
if (amount > 1)
|
||||
msgToSend += " " + GetText(gid, strs.pick_pl(prefix));
|
||||
|
@ -337,7 +328,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
public async Task<bool> PlantAsync(
|
||||
ulong gid,
|
||||
ITextChannel ch,
|
||||
ulong uid,
|
||||
ulong userId,
|
||||
string user,
|
||||
long amount,
|
||||
string pass)
|
||||
|
@ -349,19 +340,20 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
return false;
|
||||
|
||||
// remove currency from the user who's planting
|
||||
if (await _cs.RemoveAsync(uid, amount, new("put/collect", "put")))
|
||||
if (await cs.RemoveAsync(userId, amount, new("put/collect", "put")))
|
||||
{
|
||||
// try to send the message with the currency image
|
||||
var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass);
|
||||
if (msgId is null)
|
||||
{
|
||||
// if it fails it will return null, if it returns null, refund
|
||||
await _cs.AddAsync(uid, amount, new("put/collect", "refund"));
|
||||
await cs.AddAsync(userId, amount, new("put/collect", "refund"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// if it doesn't fail, put the plant in the database for other people to pick
|
||||
await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass);
|
||||
await AddPlantToDatabase(gid, ch.Id, userId, msgId.Value, amount, pass);
|
||||
await quests.ReportActionAsync(userId, QuestEventType.PlantOrPick, new() { { "type", "plant" } });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -379,43 +371,42 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
string pass,
|
||||
bool auto = false)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await using var uow = db.GetDbContext();
|
||||
|
||||
PlantedCurrency[] deleted = [];
|
||||
if (!string.IsNullOrWhiteSpace(pass) && auto)
|
||||
{
|
||||
deleted = await uow.GetTable<PlantedCurrency>()
|
||||
.Where(x => x.GuildId == gid
|
||||
&& x.ChannelId == cid
|
||||
&& x.Password != null
|
||||
&& x.Password.Length == pass.Length)
|
||||
.DeleteWithOutputAsync();
|
||||
.Where(x => x.GuildId == gid
|
||||
&& x.ChannelId == cid
|
||||
&& x.Password != null
|
||||
&& x.Password.Length == pass.Length)
|
||||
.DeleteWithOutputAsync();
|
||||
}
|
||||
|
||||
var totalDeletedAmount = deleted.Length == 0 ? 0 : deleted.Sum(x => x.Amount);
|
||||
|
||||
await uow.GetTable<PlantedCurrency>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
Amount = totalDeletedAmount + amount,
|
||||
GuildId = gid,
|
||||
ChannelId = cid,
|
||||
Password = pass,
|
||||
UserId = uid,
|
||||
MessageId = mid,
|
||||
});
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
Amount = totalDeletedAmount + amount,
|
||||
GuildId = gid,
|
||||
ChannelId = cid,
|
||||
Password = pass,
|
||||
UserId = uid,
|
||||
MessageId = mid,
|
||||
});
|
||||
|
||||
return (totalDeletedAmount + amount, deleted.Select(x => x.MessageId).ToArray());
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await using var uow = db.GetDbContext();
|
||||
_generationChannels = (await uow.GetTable<GCChannelId>()
|
||||
.Select(x => x.ChannelId)
|
||||
.ToListAsyncLinqToDB())
|
||||
.Select(x => x.ChannelId)
|
||||
.ToListAsyncLinqToDB())
|
||||
.ToHashSet()
|
||||
.ToConcurrentSet();
|
||||
|
||||
}
|
||||
}
|
|
@ -332,17 +332,18 @@ public partial class Gambling
|
|||
Type = ShopEntryType.Role,
|
||||
AuthorId = ctx.User.Id,
|
||||
RoleId = role.Id,
|
||||
RoleName = role.Name
|
||||
RoleName = role.Name,
|
||||
GuildId = ctx.Guild.Id,
|
||||
};
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var entries = new IndexedCollection<ShopEntry>(await uow.Set<ShopEntry>()
|
||||
.Where(x => x.GuildId == ctx.Guild.Id)
|
||||
.Include(x => x.Items)
|
||||
.ToListAsyncEF())
|
||||
{
|
||||
entry
|
||||
};
|
||||
.ToListAsyncEF());
|
||||
|
||||
entries.Add(entry);
|
||||
uow.Add(entry);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
@ -363,7 +364,8 @@ public partial class Gambling
|
|||
Price = price,
|
||||
Type = ShopEntryType.List,
|
||||
AuthorId = ctx.User.Id,
|
||||
Items = new()
|
||||
Items = new(),
|
||||
GuildId = ctx.Guild.Id
|
||||
};
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
|
@ -440,7 +442,7 @@ public partial class Gambling
|
|||
var items = await uow.Set<ShopEntry>()
|
||||
.Where(x => x.GuildId == ctx.Guild.Id)
|
||||
.Include(x => x.Items)
|
||||
.ToListAsyncLinqToDB();
|
||||
.ToListAsyncEF();
|
||||
|
||||
var entries = new IndexedCollection<ShopEntry>(items);
|
||||
removed = entries.ElementAtOrDefault(index);
|
||||
|
|
|
@ -1,106 +1,202 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Json;
|
||||
using Grpc.Core;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using EllieBot.GrpcVotesApi;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Services;
|
||||
|
||||
public class VoteModel
|
||||
public sealed class ServerCountRewardService(
|
||||
IBotCreds creds,
|
||||
IHttpClientFactory httpFactory,
|
||||
DiscordSocketClient client,
|
||||
ShardData shardData
|
||||
)
|
||||
: IEService, IReadyExecutor
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public ulong UserId { get; set; }
|
||||
private Task dblTask = Task.CompletedTask;
|
||||
private Task discordsTask = Task.CompletedTask;
|
||||
|
||||
public Task OnReadyAsync()
|
||||
{
|
||||
if (creds.Votes is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(creds.Votes.DblApiKey))
|
||||
{
|
||||
dblTask = Task.Run(async () =>
|
||||
{
|
||||
var dblApiKey = creds.Votes.DblApiKey;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = httpFactory.CreateClient();
|
||||
httpClient.DefaultRequestHeaders.Clear();
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", dblApiKey);
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
|
||||
await httpClient.PostAsJsonAsync(
|
||||
$"https://discordbotlist.com/api/v1/bots/{608119997713350679}/stats",
|
||||
new
|
||||
{
|
||||
users = client.Guilds.Sum(x => x.MemberCount),
|
||||
shard_id = shardData.ShardId,
|
||||
guilds = client.Guilds.Count,
|
||||
voice_connections = 0
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to send server count to DBL");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromHours(12));
|
||||
}
|
||||
});
|
||||
|
||||
if (shardData.ShardId != 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(creds.Votes.DiscordsApiKey))
|
||||
{
|
||||
discordsTask = Task.Run(async () =>
|
||||
{
|
||||
var discordsApiKey = creds.Votes.DiscordsApiKey;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = httpFactory.CreateClient();
|
||||
httpClient.DefaultRequestHeaders.Clear();
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", discordsApiKey);
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type",
|
||||
"application/json");
|
||||
await httpClient.PostAsJsonAsync(
|
||||
$"https://discords.com/bots/api/bot/{client.CurrentUser.Id}/setservers",
|
||||
new
|
||||
{
|
||||
server_count = client.Guilds.Count * shardData.TotalShards,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to send server count to Discords");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromHours(12));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public class VoteRewardService : IEService, IReadyExecutor
|
||||
public class VoteRewardService(
|
||||
ShardData shardData,
|
||||
GamblingConfigService gcs,
|
||||
GamblingService gs,
|
||||
CurrencyService cs,
|
||||
DiscordSocketClient client,
|
||||
IMessageSenderService sender,
|
||||
IBotCreds creds
|
||||
) : IEService, IReadyExecutor
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCreds _creds;
|
||||
private readonly ICurrencyService _currencyService;
|
||||
private readonly GamblingConfigService _gamb;
|
||||
|
||||
public VoteRewardService(
|
||||
DiscordSocketClient client,
|
||||
IBotCreds creds,
|
||||
ICurrencyService currencyService,
|
||||
GamblingConfigService gamb)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_currencyService = currencyService;
|
||||
_gamb = gamb;
|
||||
}
|
||||
private Server? _app;
|
||||
private IMessageChannel? _voteFeedChannel;
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
if (_client.ShardId != 0)
|
||||
if (shardData.ShardId != 0)
|
||||
return;
|
||||
|
||||
using var http = new HttpClient(new HttpClientHandler
|
||||
if (creds.Votes is null || creds.Votes.Host is null || creds.Votes.Port == 0)
|
||||
return;
|
||||
|
||||
var serverCreds = ServerCredentials.Insecure;
|
||||
var ssd = VoteService.BindService(new VotesGrpcService(this));
|
||||
|
||||
_app = new()
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
ServerCertificateCustomValidationCallback = delegate { return true; }
|
||||
});
|
||||
Ports =
|
||||
{
|
||||
new(creds.Votes.Host, creds.Votes.Port, serverCreds),
|
||||
}
|
||||
};
|
||||
|
||||
while (true)
|
||||
_app.Services.Add(ssd);
|
||||
_app.Start();
|
||||
|
||||
if (gcs.Data.VoteFeedChannelId is ulong cid)
|
||||
{
|
||||
await Task.Delay(30000);
|
||||
|
||||
var topggKey = _creds.Votes?.TopggKey;
|
||||
var topggServiceUrl = _creds.Votes?.TopggServiceUrl;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(topggKey) && !string.IsNullOrWhiteSpace(topggServiceUrl))
|
||||
{
|
||||
http.DefaultRequestHeaders.Authorization = new(topggKey);
|
||||
var uri = new Uri(new(topggServiceUrl), "topgg/new");
|
||||
var res = await http.GetStringAsync(uri);
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
_gamb.Data.VoteReward,
|
||||
new("vote", "top.gg", "top.gg vote reward"));
|
||||
|
||||
Log.Information("Rewarding {Count} top.gg voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading top.gg vote rewards");
|
||||
}
|
||||
|
||||
var discordsKey = _creds.Votes?.DiscordsKey;
|
||||
var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(discordsKey) && !string.IsNullOrWhiteSpace(discordsServiceUrl))
|
||||
{
|
||||
http.DefaultRequestHeaders.Authorization = new(discordsKey);
|
||||
var res = await http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new"));
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
_gamb.Data.VoteReward,
|
||||
new("vote", "discords", "discords.com vote reward"));
|
||||
|
||||
Log.Information("Rewarding {Count} discords.com voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading discords.com vote rewards");
|
||||
}
|
||||
_voteFeedChannel = await client.GetChannelAsync(cid) as IMessageChannel;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVoiceChannel(IMessageChannel? channel)
|
||||
{
|
||||
gcs.ModifyConfig(c => { c.VoteFeedChannelId = channel?.Id; });
|
||||
_voteFeedChannel = channel;
|
||||
}
|
||||
|
||||
public async Task UserVotedAsync(ulong userId, VoteType requestType)
|
||||
{
|
||||
var gcsData = gcs.Data;
|
||||
var reward = gcsData.VoteReward;
|
||||
if (reward <= 0)
|
||||
return;
|
||||
|
||||
(reward, var msg) = await gs.GetAmountAndMessage(userId, reward);
|
||||
await cs.AddAsync(userId, reward, new("vote", requestType.ToString()));
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await client.GetUserAsync(userId);
|
||||
|
||||
await sender
|
||||
.Response(user)
|
||||
.Confirm($"You've received{N(reward)} for voting!\n\n{msg}")
|
||||
.SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to send vote confirmation message to user {UserId}", userId);
|
||||
}
|
||||
});
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (_voteFeedChannel is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await client.GetUserAsync(userId);
|
||||
await _voteFeedChannel.SendMessageAsync(
|
||||
$"**{user}** just received **{N(reward)}** for voting!",
|
||||
allowedMentions: AllowedMentions.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to send vote reward message to user {UserId}", userId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private string N(long amount)
|
||||
=> CurrencyHelper.N(amount, CultureInfo.InvariantCulture, gcs.Data.Currency.Sign);
|
||||
}
|
||||
|
||||
public sealed class VotesGrpcService(VoteRewardService vrs)
|
||||
: VoteService.VoteServiceBase, IEService
|
||||
{
|
||||
public override async Task<GrpcVoteResult> VoteReceived(GrpcVoteData request, ServerCallContext context)
|
||||
{
|
||||
await vrs.UserVotedAsync(ulong.Parse(request.UserId), request.Type);
|
||||
|
||||
return new();
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ using EllieBot.Common.ModuleBehaviors;
|
|||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Gambling.Common;
|
||||
using EllieBot.Modules.Gambling.Common.Waifu;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Services;
|
||||
|
||||
|
@ -15,23 +16,23 @@ public class WaifuService : IEService, IReadyExecutor
|
|||
private readonly ICurrencyService _cs;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly GamblingConfigService _gss;
|
||||
private readonly IBotCreds _creds;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly QuestService _quests;
|
||||
|
||||
public WaifuService(
|
||||
DbService db,
|
||||
ICurrencyService cs,
|
||||
IBotCache cache,
|
||||
GamblingConfigService gss,
|
||||
IBotCreds creds,
|
||||
DiscordSocketClient client)
|
||||
DiscordSocketClient client,
|
||||
QuestService quests)
|
||||
{
|
||||
_db = db;
|
||||
_cs = cs;
|
||||
_cache = cache;
|
||||
_gss = gss;
|
||||
_creds = creds;
|
||||
_client = client;
|
||||
_quests = quests;
|
||||
}
|
||||
|
||||
public async Task<bool> WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner)
|
||||
|
@ -411,6 +412,8 @@ public class WaifuService : IEService, IReadyExecutor
|
|||
w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
else
|
||||
w.Price += totalValue / 2;
|
||||
|
||||
await _quests.ReportActionAsync(from.Id, QuestEventType.WaifuGiftSent);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -145,7 +145,7 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
|||
if (!res.IsAllowed)
|
||||
return false;
|
||||
|
||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
|
||||
if (!await _ps.LimitHitAsync("ai", guild.OwnerId, 1))
|
||||
{
|
||||
// limit exceeded
|
||||
return false;
|
||||
|
@ -156,14 +156,6 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
|||
|
||||
if (response.TryPickT0(out var result, out var error))
|
||||
{
|
||||
// calculate the diff in case we overestimated user's usage
|
||||
var inTokens = (result.TokensIn - 2048) / 2;
|
||||
|
||||
// add the output tokens to the limit
|
||||
await _ps.LimitForceHit(LimitedFeatureName.ChatBot,
|
||||
usrMsg.Author.Id,
|
||||
(inTokens) + (result.TokensOut / 2 * 3));
|
||||
|
||||
await _sender.Response(channel)
|
||||
.Confirm(result.Text)
|
||||
.SendAsync();
|
||||
|
|
70
src/EllieBot/Modules/Games/Fish/Db/UserFishItem.cs
Normal file
70
src/EllieBot/Modules/Games/Fish/Db/UserFishItem.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using EllieBot.Db;
|
||||
using EllieBot.Modules.Games.Fish;
|
||||
|
||||
namespace EllieBot.Modules.Games.Fish.Db;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a fishing item owned by a user.
|
||||
/// </summary>
|
||||
public class UserFishItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this user fish item.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user who owns this item.
|
||||
/// </summary>
|
||||
public ulong UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the fishing item.
|
||||
/// </summary>
|
||||
public FishItemType ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the fishing item.
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the item is currently equipped by the user.
|
||||
/// </summary>
|
||||
public bool IsEquipped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of uses left for this item. Null means unlimited uses.
|
||||
/// </summary>
|
||||
public int? UsesLeft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date and time when this item expires. Null means the item doesn't expire.
|
||||
/// </summary>
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
|
||||
|
||||
public int? ExpiryFromNowInMinutes()
|
||||
{
|
||||
if (ExpiresAt is null)
|
||||
return null;
|
||||
|
||||
return (int)(ExpiresAt.Value - DateTime.UtcNow).TotalMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entity configuration for UserFishItem.
|
||||
/// </summary>
|
||||
public class UserFishItemConfiguration : IEntityTypeConfiguration<UserFishItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserFishItem> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.UserId });
|
||||
}
|
||||
}
|
|
@ -9,20 +9,4 @@ public sealed class UserFishStats
|
|||
|
||||
public ulong UserId { get; set; }
|
||||
public int Skill { get; set; }
|
||||
|
||||
public int? Pole { get; set; }
|
||||
public int? Bait { get; set; }
|
||||
}
|
||||
|
||||
// public sealed class FishingPole
|
||||
// {
|
||||
// [Key]
|
||||
// public int Id { get; set; }
|
||||
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
|
||||
// public long Price { get; set; }
|
||||
|
||||
// public string Emoji { get; set; } = string.Empty;
|
||||
|
||||
// }
|
||||
}
|
|
@ -1,276 +0,0 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using EllieBot.Modules.Games.Fish;
|
||||
using Format = Discord.Format;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public class FishCommands(
|
||||
FishService fs,
|
||||
FishConfigService fcs,
|
||||
IBotCache cache,
|
||||
CaptchaService captchaService) : EllieModule
|
||||
{
|
||||
private static readonly EllieRandom _rng = new();
|
||||
|
||||
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||
=> new($"fishingwhitelist:{userId}");
|
||||
|
||||
[Cmd]
|
||||
public async Task Fish()
|
||||
{
|
||||
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||
if (cRes.TryPickT1(out _, out _))
|
||||
{
|
||||
var password = await captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
if (password is not null)
|
||||
{
|
||||
var img = captchaService.GetPasswordImage(password);
|
||||
using var stream = await img.ToStreamAsync();
|
||||
|
||||
var toSend = Response()
|
||||
.File(stream, "timely.png");
|
||||
|
||||
#if GLOBAL_ELLIE
|
||||
if (_rng.Next(0, 8) == 0)
|
||||
toSend = toSend
|
||||
.Text("*[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.*");
|
||||
#endif
|
||||
var captcha = await toSend.SendAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// whitelist the user for 30 minutes
|
||||
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||
// reset the password
|
||||
await captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = captcha.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (fishResult.TryPickT1(out _, out var fishTask))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentWeather = fs.GetCurrentWeather();
|
||||
var currentTod = fs.GetTime();
|
||||
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||
|
||||
var msg = await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(GetText(strs.fish_waiting))
|
||||
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
|
||||
.AddField(GetText(strs.fish_weather),
|
||||
GetWeatherEmoji(currentWeather) + " " + currentWeather,
|
||||
true)
|
||||
.AddField(GetText(strs.fish_tod), GetTodEmoji(currentTod) + " " + currentTod, true))
|
||||
.SendAsync();
|
||||
|
||||
var res = await fishTask;
|
||||
if (res is null)
|
||||
{
|
||||
await Response().Error(strs.fish_nothing).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var desc = GetText(strs.fish_caught(res.Fish.Emoji + " " + Format.Bold(res.Fish.Name)));
|
||||
|
||||
if (res.IsSkillUp)
|
||||
{
|
||||
desc += "\n" + GetText(strs.fish_skill_up(res.Skill, res.MaxSkill));
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(desc)
|
||||
.AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
|
||||
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||
.WithThumbnailUrl(res.Fish.Image))
|
||||
.SendAsync();
|
||||
|
||||
await msg.DeleteAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task FishSpot()
|
||||
{
|
||||
var ws = fs.GetWeatherForPeriods(7);
|
||||
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||
var time = fs.GetTime();
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.fish_weather_duration(fs.GetWeatherPeriodDuration())))
|
||||
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot, true)
|
||||
.AddField(GetText(strs.fish_tod), GetTodEmoji(time) + " " + time, true)
|
||||
.AddField(GetText(strs.fish_weather_forecast),
|
||||
ws.Select(x => GetWeatherEmoji(x)).Join(""),
|
||||
true))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Fishlist(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
var fishes = await fs.GetAllFish();
|
||||
|
||||
var catches = await fs.GetUserCatches(ctx.User.Id);
|
||||
var (skill, maxSkill) = await fs.GetSkill(ctx.User.Id);
|
||||
|
||||
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(fishes)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((fs, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_list_title))
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var f in fs)
|
||||
{
|
||||
if (catchDict.TryGetValue(f.Id, out var c))
|
||||
{
|
||||
eb.AddField(f.Name,
|
||||
GetFishEmoji(f, c.Count)
|
||||
+ " "
|
||||
+ GetSpotEmoji(f.Spot)
|
||||
+ GetTodEmoji(f.Time)
|
||||
+ GetWeatherEmoji(f.Weather)
|
||||
+ "\n"
|
||||
+ GetStarText(c.MaxStars, f.Stars)
|
||||
+ "\n"
|
||||
+ Format.Italics(f.Fluff),
|
||||
true);
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + GetStarText(0, f.Stars), true);
|
||||
}
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private string GetFishEmoji(FishData? fish, int count)
|
||||
{
|
||||
if (fish is null)
|
||||
return "";
|
||||
|
||||
return fish.Emoji + " x" + count;
|
||||
}
|
||||
|
||||
private string GetSpotEmoji(FishingSpot? spot)
|
||||
{
|
||||
if (spot is not FishingSpot fs)
|
||||
return string.Empty;
|
||||
|
||||
var conf = fcs.Data;
|
||||
|
||||
return conf.SpotEmojis[(int)fs];
|
||||
}
|
||||
|
||||
private string GetTodEmoji(FishingTime? fishTod)
|
||||
{
|
||||
return fishTod switch
|
||||
{
|
||||
FishingTime.Night => "🌑",
|
||||
FishingTime.Dawn => "🌅",
|
||||
FishingTime.Dusk => "🌆",
|
||||
FishingTime.Day => "☀️",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private string GetWeatherEmoji(FishingWeather? w)
|
||||
=> w switch
|
||||
{
|
||||
FishingWeather.Rain => "🌧️",
|
||||
FishingWeather.Snow => "❄️",
|
||||
FishingWeather.Storm => "⛈️",
|
||||
FishingWeather.Clear => "☀️",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private string GetStarText(int resStars, int fishStars)
|
||||
{
|
||||
if (resStars == fishStars)
|
||||
{
|
||||
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
|
||||
}
|
||||
|
||||
var c = fcs.Data;
|
||||
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
|
||||
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
|
||||
|
||||
return starsp1 + starsp2;
|
||||
}
|
||||
|
||||
private string MultiplyStars(string starEmoji, int count)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
sb.Append(starEmoji);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FishingSpot
|
||||
{
|
||||
Ocean,
|
||||
River,
|
||||
Lake,
|
||||
Swamp,
|
||||
Reef
|
||||
}
|
||||
|
||||
public enum FishingTime
|
||||
{
|
||||
Night,
|
||||
Dawn,
|
||||
Day,
|
||||
Dusk
|
||||
}
|
||||
|
||||
public enum FishingWeather
|
||||
{
|
||||
Clear,
|
||||
Rain,
|
||||
Storm,
|
||||
Snow
|
||||
}
|
|
@ -7,49 +7,15 @@ namespace EllieBot.Modules.Games;
|
|||
public sealed partial class FishConfig : ICloneable<FishConfig>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 1;
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public string WeatherSeed { get; set; } = string.Empty;
|
||||
public bool RequireCaptcha { get; set; } = true;
|
||||
public List<string> StarEmojis { get; set; } = new();
|
||||
public List<string> SpotEmojis { get; set; } = new();
|
||||
public FishChance Chance { get; set; } = new FishChance();
|
||||
// public List<FishBait> Baits { get; set; } = new();
|
||||
// public List<FishingPole> Poles { get; set; } = new();
|
||||
|
||||
public List<FishData> Fish { get; set; } = new();
|
||||
public List<FishData> Trash { get; set; } = new();
|
||||
}
|
||||
|
||||
// public sealed class FishBait : ICloneable<FishBait>
|
||||
// {
|
||||
// public int Id { get; set; }
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
// public long Price { get; set; }
|
||||
// public string Emoji { get; set; } = string.Empty;
|
||||
// public int StackSize { get; set; } = 100;
|
||||
//
|
||||
// public string? OnlyWeather { get; set; }
|
||||
// public string? OnlySpot { get; set; }
|
||||
// public string? OnlyTime { get; set; }
|
||||
//
|
||||
// public double FishMulti { get; set; } = 1;
|
||||
// public double TrashMulti { get; set; } = 1;
|
||||
// public double NothingMulti { get; set; } = 1;
|
||||
//
|
||||
// public double RareFishMulti { get; set; } = 1;
|
||||
// public double RareTrashMulti { get; set; } = 1;
|
||||
//
|
||||
// public double MaxStarMulti { get; set; } = 1;
|
||||
// }
|
||||
//
|
||||
// public sealed class FishingPole : ICloneable<FishingPole>
|
||||
// {
|
||||
// public int Id { get; set; }
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
// public long Price { get; set; }
|
||||
// public string Emoji { get; set; } = string.Empty;
|
||||
// public string Img { get; set; } = string.Empty;
|
||||
//
|
||||
// public double FishMulti { get; set; } = 1;
|
||||
// public double TrashMulti { get; set; } = 1;
|
||||
// public double NothingMulti { get; set; } = 1;
|
||||
// }
|
||||
public List<FishItem> Items { get; set; } = new();
|
||||
}
|
|
@ -15,5 +15,18 @@ public sealed class FishConfigService : ConfigServiceBase<FishConfig>
|
|||
IPubSub pubSub)
|
||||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
Migrate();
|
||||
}
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
if (data.Version < 2)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 2;
|
||||
c.RequireCaptcha = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
98
src/EllieBot/Modules/Games/Fish/FishItem.cs
Normal file
98
src/EllieBot/Modules/Games/Fish/FishItem.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
namespace EllieBot.Modules.Games;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an item used in the fishing game.
|
||||
/// </summary>
|
||||
public class FishItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the item.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the fishing item (pole, bait, boat, potion).
|
||||
/// </summary>
|
||||
public FishItemType ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the item.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item Emoji
|
||||
/// </summary>
|
||||
public string Emoji { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Description of the item.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Price of the item.
|
||||
/// </summary>
|
||||
public int Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of times the item can be used. Null means unlimited uses.
|
||||
/// </summary>
|
||||
public int? Uses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the item's effect in minutes. Null means permanent effect.
|
||||
/// </summary>
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the fish catch rate.
|
||||
/// </summary>
|
||||
public double? FishMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the trash catch rate.
|
||||
/// </summary>
|
||||
public double? TrashMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the maximum star rating of caught fish.
|
||||
/// </summary>
|
||||
public double? MaxStarMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the chance of catching rare fish.
|
||||
/// </summary>
|
||||
public double? RareMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the fishing speed.
|
||||
/// </summary>
|
||||
public double? FishingSpeedMultiplier { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the types of items available in the fishing game.
|
||||
/// </summary>
|
||||
public enum FishItemType
|
||||
{
|
||||
/// <summary>
|
||||
/// Fishing pole used to catch fish.
|
||||
/// </summary>
|
||||
Pole,
|
||||
|
||||
/// <summary>
|
||||
/// Bait used to attract fish.
|
||||
/// </summary>
|
||||
Bait,
|
||||
|
||||
/// <summary>
|
||||
/// Boat used for fishing.
|
||||
/// </summary>
|
||||
Boat,
|
||||
|
||||
/// <summary>
|
||||
/// Potion that provides temporary effects.
|
||||
/// </summary>
|
||||
Potion
|
||||
}
|
240
src/EllieBot/Modules/Games/Fish/FishItemCommands.cs
Normal file
240
src/EllieBot/Modules/Games/Fish/FishItemCommands.cs
Normal file
|
@ -0,0 +1,240 @@
|
|||
using EllieBot.Modules.Games.Fish.Db;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public class FishItemCommands(FishItemService fis, ICurrencyProvider cp) : EllieModule
|
||||
{
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishShop()
|
||||
{
|
||||
var items = fis.GetItems();
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(items)
|
||||
.PageSize(9)
|
||||
.CurrentPage(0)
|
||||
.Page((pageItems, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.fish_items_title))
|
||||
.WithFooter("`.fibuy <id>` to by an item")
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var item in pageItems)
|
||||
{
|
||||
var description = GetItemDescription(item);
|
||||
eb.AddField($"{item.Id}",
|
||||
$"""
|
||||
{description}
|
||||
|
||||
""",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.AddFooter(false)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private string GetItemDescription(FishItem item, UserFishItem? userItem = null)
|
||||
{
|
||||
var multiplierInfo = GetMultiplierInfo(item);
|
||||
|
||||
var priceText = userItem is null
|
||||
? $"【 **{CurrencyHelper.N(item.Price, Culture, cp.GetCurrencySign())}** 】"
|
||||
: "";
|
||||
|
||||
return $"""
|
||||
《 **{item.Name}** 》
|
||||
{GetEmoji(item.ItemType)} `{item.ItemType.ToString().ToLower()}` {priceText}
|
||||
{item.Description}
|
||||
{GetItemNotes(item, userItem)}
|
||||
{multiplierInfo}
|
||||
""";
|
||||
}
|
||||
|
||||
private string GetItemNotes(FishItem item, UserFishItem? userItem)
|
||||
{
|
||||
var stats = new List<string>();
|
||||
|
||||
if (item.Uses.HasValue)
|
||||
stats.Add($"**Uses:** {userItem?.UsesLeft ?? item.Uses}");
|
||||
|
||||
if (item.DurationMinutes.HasValue)
|
||||
stats.Add($"**Duration:** {userItem?.ExpiryFromNowInMinutes() ?? item.DurationMinutes}m");
|
||||
|
||||
var toReturn = stats.Count > 0 ? string.Join(" | ", stats) + "\n" : "\n";
|
||||
|
||||
return "\n" + toReturn;
|
||||
}
|
||||
|
||||
public static string GetEmoji(FishItemType itemType)
|
||||
=> itemType switch
|
||||
{
|
||||
FishItemType.Pole => @"\🎣",
|
||||
FishItemType.Boat => @"\⛵",
|
||||
FishItemType.Bait => @"\🍥",
|
||||
FishItemType.Potion => @"\🍷",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private string GetMultiplierInfo(FishItem item)
|
||||
{
|
||||
var multipliers = new FishMultipliers()
|
||||
{
|
||||
FishMultiplier = item.FishMultiplier ?? 1,
|
||||
TrashMultiplier = item.TrashMultiplier ?? 1,
|
||||
RareMultiplier = item.RareMultiplier ?? 1,
|
||||
StarMultiplier = item.MaxStarMultiplier ?? 1,
|
||||
FishingSpeedMultiplier = item.FishingSpeedMultiplier ?? 1
|
||||
};
|
||||
|
||||
return GetMultiplierInfo(multipliers);
|
||||
}
|
||||
|
||||
|
||||
public static string GetMultiplierInfo(FishMultipliers item)
|
||||
{
|
||||
var multipliers = new List<string>();
|
||||
if (item.FishMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.FishMultiplier)} chance to catch fish");
|
||||
|
||||
if (item.TrashMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.TrashMultiplier)} chance to catch trash");
|
||||
|
||||
if (item.RareMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.RareMultiplier)} chance to catch rare fish");
|
||||
|
||||
if (item.StarMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.StarMultiplier)} to max star rating");
|
||||
|
||||
if (item.FishingSpeedMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.FishingSpeedMultiplier)} fishing speed");
|
||||
|
||||
return multipliers.Count > 0
|
||||
? $"{string.Join("\n", multipliers)}\n"
|
||||
: "";
|
||||
}
|
||||
|
||||
private static string AsPercent(double multiplier)
|
||||
{
|
||||
var percentage = (int)((multiplier - 1.0f) * 100);
|
||||
return percentage >= 0 ? $"**+{percentage}%**" : $"**{percentage}%**";
|
||||
}
|
||||
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishBuy(int itemId)
|
||||
{
|
||||
var res = await fis.BuyItemAsync(ctx.User.Id, itemId);
|
||||
|
||||
if (res.TryPickT1(out var err, out var eqItem))
|
||||
{
|
||||
if (err == BuyResult.InsufficientFunds)
|
||||
await Response().Error(strs.not_enough(cp.GetCurrencySign())).SendAsync();
|
||||
else
|
||||
await Response().Error(strs.fish_item_not_found).SendAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithDescription(GetText(strs.fish_buy_success))
|
||||
.AddField(eqItem.Name, GetMultiplierInfo(eqItem));
|
||||
|
||||
await Response()
|
||||
.Embed(embed)
|
||||
.Interaction(_inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder("Inventory", Guid.NewGuid().ToString(), ButtonStyle.Secondary),
|
||||
(smc) => FishInv()))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishUse(int index)
|
||||
{
|
||||
var eqItem = await fis.EquipItemAsync(ctx.User.Id, index);
|
||||
|
||||
if (eqItem is null)
|
||||
{
|
||||
await Response().Error(strs.fish_item_not_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithDescription(GetText(strs.fish_use_success))
|
||||
.AddField(eqItem.Name, GetMultiplierInfo(eqItem));
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishUnequip(FishItemType itemType)
|
||||
{
|
||||
var res = await fis.UnequipItemAsync(ctx.User.Id, itemType);
|
||||
|
||||
if (res == UnequipResult.Success)
|
||||
await Response().Confirm(strs.fish_unequip_success).SendAsync();
|
||||
else if (res == UnequipResult.NotFound)
|
||||
await Response().Error(strs.fish_item_not_found).SendAsync();
|
||||
else
|
||||
await Response().Error(strs.fish_cant_uneq_potion).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishInv()
|
||||
{
|
||||
var userItems = await fis.GetUserItemsAsync(ctx.User.Id);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(userItems)
|
||||
.PageSize(9)
|
||||
.Page((items, page) =>
|
||||
{
|
||||
page += 1;
|
||||
var eb = CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_inv_title))
|
||||
.WithFooter($"`.fiuse <num>` to use/equip an item")
|
||||
.WithOkColor();
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var (userItem, item) = items[i];
|
||||
var isEquipped = userItem.IsEquipped;
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
eb.AddField($"{(page * 9) + i + 1} | Item not found", $"ID: {userItem.Id}", true);
|
||||
continue;
|
||||
}
|
||||
|
||||
var description = GetItemDescription(item, userItem);
|
||||
|
||||
if (isEquipped)
|
||||
description = "🫴 **IN USE**\n" + description;
|
||||
|
||||
eb.AddField($"{i + 1} | {item.Name} ",
|
||||
$"""
|
||||
{description}
|
||||
""",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.AddFooter(false)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
276
src/EllieBot/Modules/Games/Fish/FishItemService.cs
Normal file
276
src/EllieBot/Modules/Games/Fish/FishItemService.cs
Normal file
|
@ -0,0 +1,276 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Modules.Games.Fish.Db;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing fish items that users can buy, equip, and use.
|
||||
/// </summary>
|
||||
public sealed class FishItemService(
|
||||
DbService db,
|
||||
ICurrencyService cs,
|
||||
FishConfigService fcs) : IEService
|
||||
{
|
||||
private IReadOnlyList<FishItem> _items
|
||||
=> fcs.Data.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available fish items.
|
||||
/// </summary>
|
||||
public IReadOnlyList<FishItem> GetItems()
|
||||
=> _items;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific fish item by ID.
|
||||
/// </summary>
|
||||
public FishItem? GetItem(int id)
|
||||
=> _items.FirstOrDefault(i => i.Id == id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items of a specific type.
|
||||
/// </summary>
|
||||
public List<FishItem> GetItemsByType(FishItemType type)
|
||||
=> _items.Where(i => i.ItemType == type).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items owned by a user.
|
||||
/// </summary>
|
||||
public async Task<List<(UserFishItem UserItem, FishItem? Item)>> GetUserItemsAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var userItems = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return userItems
|
||||
.Select(ui => (ui, GetItem(ui.ItemId)))
|
||||
.Where(x => x.Item2 != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all equipped items for a user.
|
||||
/// </summary>
|
||||
public async Task<List<(UserFishItem UserItem, FishItem Item)>> GetEquippedItemsAsync(ulong userId)
|
||||
{
|
||||
await CheckExpiredItemsAsync(userId);
|
||||
|
||||
await using var ctx = db.GetDbContext();
|
||||
var items = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.IsEquipped)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
var output = new List<(UserFishItem, FishItem)>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var fishItem = GetItem(item.ItemId);
|
||||
if (fishItem is not null)
|
||||
output.Add((item, fishItem));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Buys an item for a user.
|
||||
/// </summary>
|
||||
public async Task<OneOf.OneOf<FishItem, BuyResult>> BuyItemAsync(ulong userId, int itemId)
|
||||
{
|
||||
var item = GetItem(itemId);
|
||||
if (item is null)
|
||||
return BuyResult.NotFound;
|
||||
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var removed = await cs.RemoveAsync(userId, item.Price, new("fish_item_purchase", item.Name));
|
||||
if (!removed)
|
||||
return BuyResult.InsufficientFunds;
|
||||
|
||||
// Add item to user's inventory
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.InsertAsync(() => new UserFishItem
|
||||
{
|
||||
UserId = userId,
|
||||
ItemId = itemId,
|
||||
ItemType = item.ItemType,
|
||||
UsesLeft = item.Uses,
|
||||
IsEquipped = false,
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips an item for a user.
|
||||
/// </summary>
|
||||
public async Task<FishItem?> EquipItemAsync(ulong userId, int index)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
await using var tr = await ctx.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var userItem = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Skip(index - 1)
|
||||
.Take(1)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (userItem is null)
|
||||
return null;
|
||||
|
||||
var fishItem = GetItem(userItem.ItemId);
|
||||
|
||||
if (fishItem is null)
|
||||
return null;
|
||||
|
||||
if (userItem.ItemType == FishItemType.Potion)
|
||||
{
|
||||
var query = ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.Id == userItem.Id && !x.IsEquipped)
|
||||
.Set(x => x.IsEquipped, true);
|
||||
|
||||
if (fishItem.DurationMinutes is { } dur)
|
||||
query = query
|
||||
.Set(x => x.ExpiresAt, DateTime.UtcNow.AddMinutes(dur));
|
||||
|
||||
await query.UpdateAsync();
|
||||
await tr.CommitAsync();
|
||||
return fishItem;
|
||||
}
|
||||
|
||||
// UnEquip any currently equipped item of the same type
|
||||
// and equip current one
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.ItemType == userItem.ItemType)
|
||||
.Set(x => x.IsEquipped, x => x.Id == userItem.Id)
|
||||
.UpdateAsync();
|
||||
|
||||
await tr.CommitAsync();
|
||||
|
||||
return fishItem;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tr.RollbackAsync();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unequips an item for a user.
|
||||
/// </summary>
|
||||
public async Task<UnequipResult> UnequipItemAsync(ulong userId, FishItemType itemType)
|
||||
{
|
||||
// can't unequip potions
|
||||
if (itemType == FishItemType.Potion)
|
||||
return UnequipResult.Potion;
|
||||
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var affected = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.ItemType == itemType && x.IsEquipped)
|
||||
.Set(x => x.IsEquipped, false)
|
||||
.UpdateAsync();
|
||||
|
||||
if (affected > 0)
|
||||
return UnequipResult.Success;
|
||||
else
|
||||
return UnequipResult.NotFound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the multipliers from a user's equipped items.
|
||||
/// </summary>
|
||||
public async Task<FishMultipliers> GetUserMultipliersAsync(ulong userId)
|
||||
{
|
||||
var equippedItems = await GetEquippedItemsAsync(userId);
|
||||
|
||||
var multipliers = new FishMultipliers();
|
||||
|
||||
foreach (var (_, item) in equippedItems)
|
||||
{
|
||||
multipliers.FishMultiplier *= item.FishMultiplier ?? 1;
|
||||
multipliers.TrashMultiplier *= item.TrashMultiplier ?? 1;
|
||||
multipliers.StarMultiplier *= item.MaxStarMultiplier ?? 1;
|
||||
multipliers.RareMultiplier *= item.RareMultiplier ?? 1;
|
||||
multipliers.FishingSpeedMultiplier *= item.FishingSpeedMultiplier ?? 1;
|
||||
}
|
||||
|
||||
return multipliers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a bait item (reduces uses left) when fishing.
|
||||
/// </summary>
|
||||
public async Task<bool> UseBaitAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var updated = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x =>
|
||||
x.UserId == userId &&
|
||||
x.ItemType == FishItemType.Bait &&
|
||||
x.IsEquipped)
|
||||
.Set(x => x.UsesLeft, x => x.UsesLeft - 1)
|
||||
.UpdateWithOutputAsync((o, n) => n);
|
||||
|
||||
if (updated.Length == 0)
|
||||
return false;
|
||||
|
||||
if (updated[0].UsesLeft <= 0)
|
||||
{
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.DeleteAsync(x => x.Id == updated[0].Id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks and removes expired items.
|
||||
/// </summary>
|
||||
public async Task CheckExpiredItemsAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.ExpiresAt.HasValue && x.ExpiresAt < now)
|
||||
.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a buy operation.
|
||||
/// </summary>
|
||||
public enum BuyResult
|
||||
{
|
||||
NotFound,
|
||||
InsufficientFunds
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of an equip operation.
|
||||
/// </summary>
|
||||
public enum UnequipResult
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
Potion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains multipliers applied to fishing based on equipped items.
|
||||
/// </summary>
|
||||
public class FishMultipliers
|
||||
{
|
||||
public double FishMultiplier { get; set; } = 1.0;
|
||||
public double TrashMultiplier { get; set; } = 1.0;
|
||||
public double StarMultiplier { get; set; } = 1.0;
|
||||
public double RareMultiplier { get; set; } = 1.0;
|
||||
public double FishingSpeedMultiplier { get; set; } = 1.0;
|
||||
}
|
|
@ -7,6 +7,12 @@ public sealed class FishResult
|
|||
public bool IsSkillUp { get; set; }
|
||||
public int Skill { get; set; }
|
||||
public int MaxSkill { get; set; }
|
||||
|
||||
public bool IsMaxStar()
|
||||
=> Stars == Fish.Stars;
|
||||
|
||||
public bool IsRare()
|
||||
=> Fish.Chance <= 15;
|
||||
}
|
||||
|
||||
public readonly record struct AlreadyFishing;
|
|
@ -1,10 +1,23 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AngleSharp.Common;
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Modules.Administration;
|
||||
using EllieBot.Modules.Administration.Services;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
|
||||
namespace EllieBot.Modules.Games.Fish;
|
||||
|
||||
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : IEService
|
||||
public sealed class FishService(
|
||||
FishConfigService fcs,
|
||||
IBotCache cache,
|
||||
DbService db,
|
||||
INotifySubscriber notify,
|
||||
QuestService quests,
|
||||
FishItemService itemService
|
||||
)
|
||||
: IEService
|
||||
{
|
||||
private const double MAX_SKILL = 100;
|
||||
|
||||
|
@ -13,19 +26,24 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
private static TypedKey<bool> FishingKey(ulong userId)
|
||||
=> new($"fishing:{userId}");
|
||||
|
||||
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
|
||||
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId,
|
||||
FishMultipliers multipliers)
|
||||
{
|
||||
var duration = _rng.Next(5, 9);
|
||||
var duration = _rng.Next(3, 6) / multipliers.FishingSpeedMultiplier;
|
||||
|
||||
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
|
||||
{
|
||||
return new AlreadyFishing();
|
||||
}
|
||||
|
||||
return TryFishAsync(userId, channelId, duration);
|
||||
return TryFishAsync(userId, channelId, duration, multipliers);
|
||||
}
|
||||
|
||||
private async Task<FishResult?> TryFishAsync(ulong userId, ulong channelId, int duration)
|
||||
private async Task<FishResult?> TryFishAsync(
|
||||
ulong userId,
|
||||
ulong channelId,
|
||||
double duration,
|
||||
FishMultipliers multipliers)
|
||||
{
|
||||
var conf = fcs.Data;
|
||||
await Task.Delay(TimeSpan.FromSeconds(duration));
|
||||
|
@ -35,8 +53,8 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
var trashChanceMultiplier = Math.Clamp(((2 * MAX_SKILL) - playerSkill) / MAX_SKILL, 1, 2);
|
||||
|
||||
var nothingChance = conf.Chance.Nothing;
|
||||
var fishChance = conf.Chance.Fish * fishChanceMultiplier;
|
||||
var trashChance = conf.Chance.Trash * trashChanceMultiplier;
|
||||
var fishChance = conf.Chance.Fish * fishChanceMultiplier * multipliers.FishMultiplier;
|
||||
var trashChance = conf.Chance.Trash * trashChanceMultiplier * multipliers.TrashMultiplier;
|
||||
|
||||
// first roll whether it's fish, trash or nothing
|
||||
var totalChance = fishChance + trashChance + conf.Chance.Nothing;
|
||||
|
@ -48,13 +66,21 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
return null;
|
||||
}
|
||||
|
||||
var items = typeRoll < nothingChance + fishChance
|
||||
var isFish = typeRoll < nothingChance + fishChance;
|
||||
|
||||
var items = isFish
|
||||
? conf.Fish
|
||||
: conf.Trash;
|
||||
|
||||
var result = await FishAsyncInternal(userId, channelId, items, multipliers);
|
||||
|
||||
// use bait
|
||||
if (result is not null)
|
||||
{
|
||||
await itemService.UseBaitAsync(userId);
|
||||
}
|
||||
|
||||
var result = await FishAsyncInternal(userId, channelId, items);
|
||||
|
||||
// skill
|
||||
if (result is not null)
|
||||
{
|
||||
var isSkillUp = await TrySkillUpAsync(userId, playerSkill);
|
||||
|
@ -69,6 +95,27 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
}
|
||||
}
|
||||
|
||||
// notification system
|
||||
if (result is not null)
|
||||
{
|
||||
if (result.IsMaxStar() || result.IsRare())
|
||||
{
|
||||
await notify.NotifyAsync(new NiceCatchNotifyModel(
|
||||
userId,
|
||||
result.Fish,
|
||||
GetStarText(result.Stars, result.Fish.Stars)
|
||||
));
|
||||
}
|
||||
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.FishCaught,
|
||||
new()
|
||||
{
|
||||
{ "fish", result.Fish.Name },
|
||||
{ "type", typeRoll < nothingChance + fishChance ? "fish" : "trash" },
|
||||
{ "stars", result.Stars.ToString() }
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -85,21 +132,21 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
|
||||
var maxSkill = (int)MAX_SKILL;
|
||||
await ctx.GetTable<UserFishStats>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = 1,
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = playerSkill
|
||||
});
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = 1,
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = playerSkill
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -123,14 +170,18 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var skill = await ctx.GetTable<UserFishStats>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => x.Skill)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => x.Skill)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
|
||||
return (skill, (int)MAX_SKILL);
|
||||
}
|
||||
|
||||
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
|
||||
private async Task<FishResult?> FishAsyncInternal(
|
||||
ulong userId,
|
||||
ulong channelId,
|
||||
List<FishData> items,
|
||||
FishMultipliers multipliers)
|
||||
{
|
||||
var filteredItems = new List<FishData>();
|
||||
|
||||
|
@ -160,7 +211,20 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
filteredItems.Add(item);
|
||||
}
|
||||
|
||||
var maxSum = filteredItems.Sum(x => x.Chance * 100);
|
||||
|
||||
var maxSum = filteredItems
|
||||
.Select(x => (x.Id, x.Chance, x.Stars))
|
||||
.Select(x =>
|
||||
{
|
||||
if (x.Chance <= 15)
|
||||
return x with
|
||||
{
|
||||
Chance = x.Chance *= multipliers.RareMultiplier
|
||||
};
|
||||
|
||||
return x;
|
||||
})
|
||||
.Sum(x => { return x.Chance * 100; });
|
||||
|
||||
|
||||
var roll = _rng.NextDouble() * maxSum;
|
||||
|
@ -177,7 +241,7 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
caught = new FishResult()
|
||||
{
|
||||
Fish = i,
|
||||
Stars = GetRandomStars(i.Stars),
|
||||
Stars = GetRandomStars(i.Stars, multipliers),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
@ -188,23 +252,23 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
await using var uow = db.GetDbContext();
|
||||
|
||||
await uow.GetTable<FishCatch>()
|
||||
.InsertOrUpdateAsync(() => new FishCatch()
|
||||
{
|
||||
UserId = userId,
|
||||
FishId = caught.Fish.Id,
|
||||
MaxStars = caught.Stars,
|
||||
Count = 1
|
||||
},
|
||||
(old) => new FishCatch()
|
||||
{
|
||||
Count = old.Count + 1,
|
||||
MaxStars = Math.Max(old.MaxStars, caught.Stars),
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
FishId = caught.Fish.Id,
|
||||
UserId = userId
|
||||
});
|
||||
.InsertOrUpdateAsync(() => new FishCatch()
|
||||
{
|
||||
UserId = userId,
|
||||
FishId = caught.Fish.Id,
|
||||
MaxStars = caught.Stars,
|
||||
Count = 1
|
||||
},
|
||||
(old) => new FishCatch()
|
||||
{
|
||||
Count = old.Count + 1,
|
||||
MaxStars = Math.Max(old.MaxStars, caught.Stars),
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
FishId = caught.Fish.Id,
|
||||
UserId = userId
|
||||
});
|
||||
|
||||
return caught;
|
||||
}
|
||||
|
@ -321,25 +385,30 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
/// if maxStars == 5, returns 1 (40%) or 2 (30%) or 3 (15%) or 4 (10%) or 5 (5%)
|
||||
/// </summary>
|
||||
/// <param name="maxStars">Max Number of stars to generate</param>
|
||||
/// <param name="multipliers"></param>
|
||||
/// <returns>Random number of stars</returns>
|
||||
private int GetRandomStars(int maxStars)
|
||||
private int GetRandomStars(int maxStars, FishMultipliers multipliers)
|
||||
{
|
||||
if (maxStars == 1)
|
||||
return 1;
|
||||
|
||||
var maxStarMulti = multipliers.StarMultiplier;
|
||||
double baseChance;
|
||||
if (maxStars == 2)
|
||||
{
|
||||
// 15% chance of 1 star, 85% chance of 2 stars
|
||||
return _rng.NextDouble() < 0.85 ? 1 : 2;
|
||||
baseChance = Math.Clamp(0.15 * multipliers.StarMultiplier, 0, 1);
|
||||
return _rng.NextDouble() < (1 - baseChance) ? 1 : 2;
|
||||
}
|
||||
|
||||
if (maxStars == 3)
|
||||
{
|
||||
// 65% chance of 1 star, 30% chance of 2 stars, 5% chance of 3 stars
|
||||
baseChance = 0.05 * multipliers.StarMultiplier;
|
||||
var r = _rng.NextDouble();
|
||||
if (r < 0.65)
|
||||
if (r < (1 - baseChance - 0.3))
|
||||
return 1;
|
||||
if (r < 0.95)
|
||||
if (r < (1 - baseChance))
|
||||
return 2;
|
||||
return 3;
|
||||
}
|
||||
|
@ -349,26 +418,28 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
// this should never happen
|
||||
// 50% chance of 1 star, 25% chance of 2 stars, 18% chance of 3 stars, 7% chance of 4 stars
|
||||
var r = _rng.NextDouble();
|
||||
if (r < 0.55)
|
||||
baseChance = 0.02 * multipliers.StarMultiplier;
|
||||
if (r < (1 - baseChance - 0.45))
|
||||
return 1;
|
||||
if (r < 0.80)
|
||||
if (r < (1 - baseChance - 0.15))
|
||||
return 2;
|
||||
if (r < 0.98)
|
||||
if (r < (1 - baseChance))
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (maxStars == 5)
|
||||
{
|
||||
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 5% chance of 5 stars
|
||||
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 2% chance of 5 stars
|
||||
var r = _rng.NextDouble();
|
||||
if (r < 0.4)
|
||||
baseChance = 0.02 * multipliers.StarMultiplier;
|
||||
if (r < (1 - baseChance - 0.6))
|
||||
return 1;
|
||||
if (r < 0.7)
|
||||
if (r < (1 - baseChance - 0.3))
|
||||
return 2;
|
||||
if (r < 0.9)
|
||||
if (r < (1 - baseChance - 0.1))
|
||||
return 3;
|
||||
if (r < 0.98)
|
||||
if (r < (1 - baseChance))
|
||||
return 4;
|
||||
return 5;
|
||||
}
|
||||
|
@ -392,9 +463,62 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var catches = await ctx.GetTable<FishCatch>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => x.UserId == userId)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return catches;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<(ulong UserId, int Catches, int Unique)>> GetFishLbAsync(int page)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var result = await ctx.GetTable<FishCatch>()
|
||||
.GroupBy(x => x.UserId)
|
||||
.OrderByDescending(x => x.Count()).ThenByDescending(x => x.Sum(x => x.Count))
|
||||
.Skip(page * 10)
|
||||
.Take(10)
|
||||
.Select(x => new
|
||||
{
|
||||
UserId = x.Key,
|
||||
Catches = x.Sum(x => x.Count),
|
||||
Unique = x.Count()
|
||||
})
|
||||
.ToListAsyncLinqToDB()
|
||||
.Fmap(x => x.Map(y => (y.UserId, y.Catches, y.Unique)).ToList());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetStarText(int resStars, int fishStars)
|
||||
{
|
||||
if (resStars == fishStars)
|
||||
{
|
||||
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
|
||||
}
|
||||
|
||||
var c = fcs.Data;
|
||||
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
|
||||
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
|
||||
|
||||
return starsp1 + starsp2;
|
||||
}
|
||||
|
||||
private string MultiplyStars(string starEmoji, int count)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
sb.Append(starEmoji);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class IUserFishCatch
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
320
src/EllieBot/Modules/Games/Fish/FishingCommands.cs
Normal file
320
src/EllieBot/Modules/Games/Fish/FishingCommands.cs
Normal file
|
@ -0,0 +1,320 @@
|
|||
using EllieBot.Modules.Games.Fish;
|
||||
using EllieBot.Modules.Xp.Services;
|
||||
using Format = Discord.Format;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public class FishingCommands(
|
||||
FishService fs,
|
||||
FishItemService fis,
|
||||
FishConfigService fcs,
|
||||
IBotCache cache,
|
||||
UserService us,
|
||||
CaptchaService captchaService) : EllieModule
|
||||
{
|
||||
private static readonly EllieRandom _rng = new();
|
||||
|
||||
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||
=> new($"fishingwhitelist:{userId}");
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Fish()
|
||||
{
|
||||
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||
if (cRes.TryPickT1(out _, out _))
|
||||
{
|
||||
string? password = null;
|
||||
if (fcs.Data.RequireCaptcha)
|
||||
password = await captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
|
||||
if (password is not null)
|
||||
{
|
||||
var img = captchaService.GetPasswordImage(password);
|
||||
using var stream = await img.ToStreamAsync();
|
||||
|
||||
var toSend = Response()
|
||||
.File(stream, "timely.png")
|
||||
.Embed(CreateEmbed()
|
||||
.WithFooter("captcha: type the text from the image")
|
||||
.WithImageUrl("attachment://timely.png"));
|
||||
|
||||
#if GLOBAL_ELLIE
|
||||
if (_rng.Next(0, 8) == 0)
|
||||
toSend = toSend
|
||||
.Text("*[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.*");
|
||||
#endif
|
||||
var captcha = await toSend.SendAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// whitelist the user for 30 minutes
|
||||
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||
// reset the password
|
||||
await captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = captcha.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var multis = await fis.GetUserMultipliersAsync(ctx.User.Id);
|
||||
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id, multis);
|
||||
if (fishResult.TryPickT1(out _, out var fishTask))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentWeather = fs.GetCurrentWeather();
|
||||
var currentTod = fs.GetTime();
|
||||
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||
|
||||
var msg = await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription($"""
|
||||
{GetText(strs.fish_waiting)}
|
||||
{FishItemCommands.GetMultiplierInfo(multis)}
|
||||
""")
|
||||
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
|
||||
.AddField(GetText(strs.fish_weather),
|
||||
GetWeatherEmoji(currentWeather) + " " + currentWeather,
|
||||
true)
|
||||
.AddField(GetText(strs.fish_tod), GetTodEmoji(currentTod) + " " + currentTod, true))
|
||||
.SendAsync();
|
||||
|
||||
var res = await fishTask;
|
||||
if (res is null)
|
||||
{
|
||||
await Response().Error(strs.fish_nothing).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var desc = GetText(strs.fish_caught(res.Fish.Emoji + " " + Format.Bold(res.Fish.Name)));
|
||||
|
||||
if (res.IsSkillUp)
|
||||
{
|
||||
desc += "\n" + GetText(strs.fish_skill_up(res.Skill, res.MaxSkill));
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(desc)
|
||||
.AddField(GetText(strs.fish_quality), fs.GetStarText(res.Stars, res.Fish.Stars), true)
|
||||
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||
.WithThumbnailUrl(res.Fish.Image))
|
||||
.SendAsync();
|
||||
|
||||
await msg.DeleteAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishSpot()
|
||||
{
|
||||
var ws = fs.GetWeatherForPeriods(7);
|
||||
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||
var time = fs.GetTime();
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.fish_weather_duration(fs.GetWeatherPeriodDuration())))
|
||||
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot, true)
|
||||
.AddField(GetText(strs.fish_tod), GetTodEmoji(time) + " " + time, true)
|
||||
.AddField(GetText(strs.fish_weather_forecast),
|
||||
ws.Select(x => GetWeatherEmoji(x)).Join(""),
|
||||
true))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
var fishes = await fs.GetAllFish();
|
||||
|
||||
var catches = await fs.GetUserCatches(ctx.User.Id);
|
||||
var (skill, maxSkill) = await fs.GetSkill(ctx.User.Id);
|
||||
|
||||
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||
|
||||
var items = await fis.GetEquippedItemsAsync(ctx.User.Id);
|
||||
var desc = $"""
|
||||
🧠 {skill} / {maxSkill}
|
||||
""";
|
||||
|
||||
foreach (var itemType in Enum.GetValues<FishItemType>())
|
||||
{
|
||||
var i = items.Where(x => x.Item.ItemType == itemType).ToArray();
|
||||
|
||||
desc += " · " + FishItemCommands.GetEmoji(itemType) + " " +
|
||||
(i.Any() ? string.Join(", ", i.Select(x => x.Item.Name)) : "None");
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(fishes)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((pageFish, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithDescription(desc)
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_list_title))
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var f in pageFish)
|
||||
{
|
||||
if (catchDict.TryGetValue(f.Id, out var c))
|
||||
{
|
||||
eb.AddField(f.Name,
|
||||
GetFishEmoji(f, c.Count)
|
||||
+ " "
|
||||
+ GetSpotEmoji(f.Spot)
|
||||
+ GetTodEmoji(f.Time)
|
||||
+ GetWeatherEmoji(f.Weather)
|
||||
+ "\n"
|
||||
+ fs.GetStarText(c.MaxStars, f.Stars)
|
||||
+ "\n"
|
||||
+ Format.Italics(f.Fluff),
|
||||
true);
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + fs.GetStarText(0, f.Stars), true);
|
||||
}
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishLb(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(async p => await fs.GetFishLbAsync(p))
|
||||
.PageSize(9)
|
||||
.Page(async (items, page) =>
|
||||
{
|
||||
var users = await us.GetUsersAsync(items.Select(x => x.UserId).ToArray());
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.fish_lb_title))
|
||||
.WithOkColor();
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var data = items[i];
|
||||
var user = users.TryGetValue(data.UserId, out var ud)
|
||||
? ud.ToString()
|
||||
: data.UserId.ToString();
|
||||
|
||||
var text =
|
||||
$"""
|
||||
{GetText(strs.fish_unique(Format.Bold(data.Unique.ToString())))}
|
||||
*{GetText(strs.fish_catches(data.Catches))}*
|
||||
""";
|
||||
|
||||
eb.AddField("#" + (page * 9 + i + 1) + " | " + user,
|
||||
text,
|
||||
false);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
|
||||
private string GetFishEmoji(FishData? fish, int count)
|
||||
{
|
||||
if (fish is null)
|
||||
return "";
|
||||
|
||||
return fish.Emoji + " x" + count;
|
||||
}
|
||||
|
||||
private string GetSpotEmoji(FishingSpot? spot)
|
||||
{
|
||||
if (spot is not FishingSpot fs)
|
||||
return string.Empty;
|
||||
|
||||
var conf = fcs.Data;
|
||||
|
||||
return conf.SpotEmojis[(int)fs];
|
||||
}
|
||||
|
||||
private string GetTodEmoji(FishingTime? fishTod)
|
||||
{
|
||||
return fishTod switch
|
||||
{
|
||||
FishingTime.Night => "🌑",
|
||||
FishingTime.Dawn => "🌅",
|
||||
FishingTime.Dusk => "🌆",
|
||||
FishingTime.Day => "☀️",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private string GetWeatherEmoji(FishingWeather? w)
|
||||
=> w switch
|
||||
{
|
||||
FishingWeather.Rain => "🌧️",
|
||||
FishingWeather.Snow => "❄️",
|
||||
FishingWeather.Storm => "⛈️",
|
||||
FishingWeather.Clear => "☀️",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum FishingSpot
|
||||
{
|
||||
Ocean,
|
||||
River,
|
||||
Lake,
|
||||
Swamp,
|
||||
Reef
|
||||
}
|
||||
|
||||
public enum FishingTime
|
||||
{
|
||||
Night,
|
||||
Dawn,
|
||||
Day,
|
||||
Dusk
|
||||
}
|
||||
|
||||
public enum FishingWeather
|
||||
{
|
||||
Clear,
|
||||
Rain,
|
||||
Storm,
|
||||
Snow
|
||||
}
|
21
src/EllieBot/Modules/Games/Fish/strings.json
Normal file
21
src/EllieBot/Modules/Games/Fish/strings.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"fish_items_title": "Available Fishing Items",
|
||||
"fish_buy_success": "Item purchased successfully!",
|
||||
"fish_buy_not_found": "Item not found.",
|
||||
"fish_buy_already_owned": "You already own this item.",
|
||||
"fish_buy_insufficient_funds": "You don't have enough currency to buy this item.",
|
||||
"fish_buy_error": "An error occurred while trying to buy the item.",
|
||||
"fish_use_success": "Item equipped successfully!",
|
||||
"fish_use_not_found": "Item not found.",
|
||||
"fish_use_not_owned": "You don't own this item.",
|
||||
"fish_use_expired": "This item has expired.",
|
||||
"fish_use_no_uses": "This item has no uses left.",
|
||||
"fish_use_error": "An error occurred while trying to use the item.",
|
||||
"fish_unequip_success": "Item unequipped successfully!",
|
||||
"fish_unequip_error": "Could not unequip item.",
|
||||
"fish_inv_title": "{0}'s Fishing Inventory",
|
||||
"fish_gift_self": "You can't gift items to yourself.",
|
||||
"fish_gift_not_owned": "You don't own this item.",
|
||||
"fish_gift_equipped": "You can't gift equipped items. Unequip it first.",
|
||||
"fish_gift_success": "Item successfully gifted to {0}!"
|
||||
}
|
|
@ -10,44 +10,54 @@ public partial class Games
|
|||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Hangmanlist()
|
||||
=> await Response().Confirm(GetText(strs.hangman_types(prefix)), _service.GetHangmanTypes().Join('\n')).SendAsync();
|
||||
=> await Response().Confirm(GetText(strs.hangman_types(prefix)), _service.GetHangmanTypes().Join('\n'))
|
||||
.SendAsync();
|
||||
|
||||
private static string Draw(HangmanGame.State state)
|
||||
=> $"""
|
||||
. ┌─────┐
|
||||
.┃...............┋
|
||||
.┃...............┋
|
||||
.┃{(state.Errors > 0 ? ".............😲" : "")}
|
||||
.┃{(state.Errors > 1 ? "............./" : "")} {(state.Errors > 2 ? "|" : "")} {(state.Errors > 3 ? "\\" : "")}
|
||||
.┃{(state.Errors > 4 ? "............../" : "")} {(state.Errors > 5 ? "\\" : "")}
|
||||
/-\
|
||||
""";
|
||||
{
|
||||
var head = state.Errors >= 1 ? "O" : " ";
|
||||
var torso = state.Errors >= 2 ? "|" : " ";
|
||||
var leftArm = state.Errors >= 3 ? "/" : " ";
|
||||
var rightArm = state.Errors >= 4 ? "\\" : " ";
|
||||
var leftLeg = state.Errors >= 5 ? "/" : " ";
|
||||
var rightLeg = state.Errors >= 6 ? "\\" : " ";
|
||||
|
||||
return $"""
|
||||
```
|
||||
┌─────┐
|
||||
│ {head}
|
||||
│ {leftArm}{torso}{rightArm}
|
||||
│ {leftLeg} {rightLeg}
|
||||
─┴─
|
||||
```
|
||||
""";
|
||||
}
|
||||
|
||||
public static EmbedBuilder GetEmbed(IMessageSenderService sender, HangmanGame.State state)
|
||||
{
|
||||
if (state.Phase == HangmanGame.Phase.Running)
|
||||
{
|
||||
return sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
}
|
||||
|
||||
if (state.Phase == HangmanGame.Phase.Ended && state.Failed)
|
||||
{
|
||||
return sender.CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
.WithErrorColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
}
|
||||
|
||||
return sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Modules.Games.Services;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
|
||||
namespace EllieBot.Modules.Games.Hangman;
|
||||
|
||||
|
@ -13,6 +14,7 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
|
|||
private readonly GamesConfigService _gcs;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly IMemoryCache _cdCache;
|
||||
private readonly QuestService _quests;
|
||||
private readonly object _locker = new();
|
||||
|
||||
public HangmanService(
|
||||
|
@ -20,13 +22,15 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
|
|||
IMessageSenderService sender,
|
||||
GamesConfigService gcs,
|
||||
ICurrencyService cs,
|
||||
IMemoryCache cdCache)
|
||||
IMemoryCache cdCache,
|
||||
QuestService quests)
|
||||
{
|
||||
_source = source;
|
||||
_sender = sender;
|
||||
_gcs = gcs;
|
||||
_cs = cs;
|
||||
_cdCache = cdCache;
|
||||
_quests = quests;
|
||||
}
|
||||
|
||||
public bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? state)
|
||||
|
@ -104,6 +108,9 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
|
|||
|
||||
if (rew > 0)
|
||||
await _cs.AddAsync(msg.Author, rew, new("hangman", "win"));
|
||||
|
||||
if (state.GuessResult == HangmanGame.GuessResult.Win)
|
||||
await _quests.ReportActionAsync(msg.Author.Id, QuestEventType.GameWon, new() { { "game", "hangman" } });
|
||||
|
||||
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ using SixLabors.ImageSharp.Advanced;
|
|||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public sealed class NCanvasCommands : EllieModule
|
||||
{
|
||||
private readonly INCanvasService _service;
|
||||
|
@ -30,37 +32,61 @@ public partial class Games
|
|||
_gcs = gcs;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task NCanvas(kwum x)
|
||||
=> await NcPixel(x);
|
||||
|
||||
[Cmd]
|
||||
public async Task NCanvas()
|
||||
{
|
||||
var pixels = await _service.GetCanvas();
|
||||
var image = new Image<Rgba32>(_service.GetWidth(), _service.GetHeight());
|
||||
var w = _service.GetWidth();
|
||||
var h = _service.GetHeight();
|
||||
var image = new Image<Rgba32>(w * 2, h * 2);
|
||||
|
||||
Parallel.For(0,
|
||||
image.Height,
|
||||
h * 2,
|
||||
y =>
|
||||
{
|
||||
var pixelAccessor = image.DangerousGetPixelRowMemory(y);
|
||||
var row = pixelAccessor.Span;
|
||||
for (int x = 0; x < image.Width; x++)
|
||||
for (var x = 0; x < image.Width; x += 2)
|
||||
{
|
||||
row[x] = new Rgba32(pixels[(y * image.Width) + x]);
|
||||
var pi = pixels[(y / 2 * w) + x / 2];
|
||||
|
||||
row[x] = new Rgba32(pi);
|
||||
row[x + 1] = new Rgba32(pi);
|
||||
}
|
||||
});
|
||||
|
||||
await using var stream = await image.ToStreamAsync();
|
||||
|
||||
var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight()));
|
||||
|
||||
var rng = new EllieRandom();
|
||||
var inter = _inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder("Zoom to a Random Spot",
|
||||
Guid.NewGuid().ToString(),
|
||||
ButtonStyle.Secondary),
|
||||
(_) => NCzoom(rng.Next(0, w), rng.Next(0, h)));
|
||||
|
||||
await Response()
|
||||
.File(stream, "ncanvas.png")
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.File(stream, "ncanvas.png")
|
||||
.Interaction(inter)
|
||||
.Embed(CreateEmbed()
|
||||
.WithDescription("""
|
||||
Draw pixels on the canvas!
|
||||
`.ncz x y` to zoom to a particular coordinates
|
||||
`.ncs CODE color` to set color
|
||||
`.nc` to see the whole canvas
|
||||
""")
|
||||
.WithOkColor()
|
||||
#if GLOBAL_ELLIE
|
||||
.WithDescription("This is not available yet.")
|
||||
#endif
|
||||
.WithFooter(hint)
|
||||
.WithImageUrl("attachment://ncanvas.png"))
|
||||
.SendAsync();
|
||||
.WithFooter(hint)
|
||||
.WithImageUrl("attachment://ncanvas.png"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -81,7 +107,20 @@ public partial class Games
|
|||
|
||||
using var img = await GetZoomImage(position);
|
||||
await using var stream = await img.ToStreamAsync();
|
||||
await ctx.Channel.SendFileAsync(stream, $"zoom_{position}.png");
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithImageUrl($"attachment://zoom_{position}.png")
|
||||
.WithFooter($"`.ncs code color` to set | ex: `.ncs {position} pink`");
|
||||
|
||||
await Response()
|
||||
.Embed(eb)
|
||||
.Interaction(_inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder("See Canvas",
|
||||
Guid.NewGuid().ToString(),
|
||||
ButtonStyle.Secondary),
|
||||
async (smc) => await NCanvas()))
|
||||
.File(stream, $"zoom_{position}.png")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private async Task<Image<Rgba32>> GetZoomImage(kwum position)
|
||||
|
@ -97,11 +136,13 @@ public partial class Games
|
|||
const float fontSize = 30;
|
||||
|
||||
var posFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
|
||||
var priceFont = _fonts.Symbola.CreateFont(35, FontStyle.Regular);
|
||||
|
||||
var size = TextMeasurer.MeasureSize("wwww", new TextOptions(posFont));
|
||||
var scale = 100f / size.Width;
|
||||
if (scale < 1)
|
||||
posFont = _fonts.NotoSans.CreateFont(fontSize * scale, FontStyle.Bold);
|
||||
var outlinePen = new SolidPen(SixLabors.ImageSharp.Color.Black, 1f);
|
||||
var outlinePen = new SolidPen(SixLabors.ImageSharp.Color.Black, 0.5f);
|
||||
|
||||
Parallel.For(0,
|
||||
pixels.Length,
|
||||
|
@ -119,14 +160,26 @@ public partial class Games
|
|||
image.Mutate(x =>
|
||||
{
|
||||
x.DrawText(new RichTextOptions(posFont)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(startX + 50, startY + 50)
|
||||
},
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(startX + 50, startY + 30)
|
||||
},
|
||||
((kwum)pix.Position).ToString().PadLeft(2, '2'),
|
||||
Brushes.Solid(SixLabors.ImageSharp.Color.White),
|
||||
outlinePen);
|
||||
|
||||
x.DrawText(new RichTextOptions(priceFont)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(startX + 50, startY + 80)
|
||||
},
|
||||
// "", Brushes.Solid(SixLabors.ImageSharp.Color.White), outlinePen);
|
||||
pix.Price + "💵",
|
||||
// CurrencyHelper.N(pix.Price, Culture, _gcs.Data.Currency.Sign),
|
||||
Brushes.Solid(SixLabors.ImageSharp.Color.White),
|
||||
outlinePen);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -135,7 +188,7 @@ public partial class Games
|
|||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task NcSetPixel(kwum position, string colorHex, [Leftover] string text = "")
|
||||
public async Task NcSetPixel(kwum position, Rgba32 color, [Leftover] string text = "")
|
||||
{
|
||||
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
|
||||
{
|
||||
|
@ -143,15 +196,6 @@ public partial class Games
|
|||
return;
|
||||
}
|
||||
|
||||
if (colorHex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
colorHex = colorHex[2..];
|
||||
|
||||
if (!Rgba32.TryParseHex(colorHex, out var clr))
|
||||
{
|
||||
await Response().Error(strs.invalid_color).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var pixel = await _service.GetPixel(position);
|
||||
if (pixel is null)
|
||||
{
|
||||
|
@ -165,13 +209,13 @@ public partial class Games
|
|||
_gcs.Data.Currency.Sign))));
|
||||
|
||||
if (!await PromptUserConfirmAsync(CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription(prompt)))
|
||||
.WithPendingColor()
|
||||
.WithDescription(prompt)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _service.SetPixel(position, clr.PackedValue, text, ctx.User.Id, pixel.Price);
|
||||
var result = await _service.SetPixel(position, color.PackedValue, text, ctx.User.Id, pixel.Price);
|
||||
|
||||
if (result == SetPixelResult.NotEnoughMoney)
|
||||
{
|
||||
|
@ -193,12 +237,17 @@ public partial class Games
|
|||
await using var stream = await img.ToStreamAsync();
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
|
||||
.WithImageUrl($"attachment://zoom_{position}.png"))
|
||||
.File(stream, $"zoom_{position}.png")
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
|
||||
.WithImageUrl($"attachment://zoom_{position}.png"))
|
||||
.Interaction(_inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder("See Canvas",
|
||||
Guid.NewGuid().ToString(),
|
||||
ButtonStyle.Secondary),
|
||||
async (smc) => await NCanvas()))
|
||||
.File(stream, $"zoom_{position}.png")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -230,18 +279,18 @@ public partial class Games
|
|||
|
||||
var pos = new kwum(pixel.Position);
|
||||
await Response()
|
||||
.File(stream, $"{pixel.Position}.png")
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
|
||||
.WithTitle(GetText(strs.nc_pixel(pos)))
|
||||
.AddField(GetText(strs.nc_position),
|
||||
$"{pixel.Position % _service.GetWidth()} {pixel.Position / _service.GetWidth()}",
|
||||
true)
|
||||
.AddField(GetText(strs.price), pixel.Price.ToString(), true)
|
||||
.AddField(GetText(strs.color), "#" + new Rgba32(pixel.Color).ToHex())
|
||||
.WithImageUrl($"attachment://{pixel.Position}.png"))
|
||||
.SendAsync();
|
||||
.File(stream, $"{pixel.Position}.png")
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
|
||||
.WithTitle(GetText(strs.nc_pixel(pos)))
|
||||
.AddField(GetText(strs.nc_position),
|
||||
$"{pixel.Position % _service.GetWidth()} {pixel.Position / _service.GetWidth()}",
|
||||
true)
|
||||
.AddField(GetText(strs.price), pixel.Price.ToString(), true)
|
||||
.AddField(GetText(strs.color), "#" + new Rgba32(pixel.Color).ToHex())
|
||||
.WithImageUrl($"attachment://{pixel.Position}.png"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -264,9 +313,9 @@ public partial class Games
|
|||
}
|
||||
|
||||
if (!await PromptUserConfirmAsync(CreateEmbed()
|
||||
.WithDescription(
|
||||
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
|
||||
+ "Are you sure you want to continue?")))
|
||||
.WithDescription(
|
||||
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
|
||||
+ "Are you sure you want to continue?")))
|
||||
return;
|
||||
|
||||
using var http = _http.CreateClient();
|
||||
|
@ -294,9 +343,9 @@ public partial class Games
|
|||
await _service.ResetAsync();
|
||||
|
||||
if (!await PromptUserConfirmAsync(CreateEmbed()
|
||||
.WithDescription(
|
||||
"This will delete all pixels and reset the canvas.\n\n"
|
||||
+ "Are you sure you want to continue?")))
|
||||
.WithDescription(
|
||||
"This will delete all pixels and reset the canvas.\n\n"
|
||||
+ "Are you sure you want to continue?")))
|
||||
return;
|
||||
|
||||
await ctx.OkAsync();
|
||||
|
|
|
@ -3,9 +3,9 @@ using LinqToDB.Data;
|
|||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using SixLabors.ImageSharp.ColorSpaces;
|
||||
using SixLabors.ImageSharp.ColorSpaces.Conversion;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
|
@ -17,21 +17,24 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
private readonly IBotCache _cache;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly QuestService _quests;
|
||||
|
||||
public const int CANVAS_WIDTH = 500;
|
||||
public const int CANVAS_HEIGHT = 350;
|
||||
public const int CANVAS_WIDTH = 200;
|
||||
public const int CANVAS_HEIGHT = 100;
|
||||
public const int INITIAL_PRICE = 3;
|
||||
|
||||
public NCanvasService(
|
||||
DbService db,
|
||||
IBotCache cache,
|
||||
DiscordSocketClient client,
|
||||
ICurrencyService cs)
|
||||
ICurrencyService cs,
|
||||
QuestService quests)
|
||||
{
|
||||
_db = db;
|
||||
_cache = cache;
|
||||
_client = client;
|
||||
_cs = cs;
|
||||
_quests = quests;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
|
@ -41,9 +44,10 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
if (await uow.GetTable<NCPixel>().CountAsyncLinqToDB() > 0)
|
||||
var count = await uow.GetTable<NCPixel>().CountAsync();
|
||||
if (count == CANVAS_WIDTH * CANVAS_HEIGHT)
|
||||
return;
|
||||
|
||||
|
||||
await ResetAsync();
|
||||
}
|
||||
|
||||
|
@ -59,23 +63,20 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
}
|
||||
|
||||
await uow.GetTable<NCPixel>()
|
||||
.BulkCopyAsync(toAdd.Select(x =>
|
||||
{
|
||||
var clr = ColorSpaceConverter.ToRgb(new Hsv(((float)Random.Shared.NextDouble() * 360),
|
||||
(float)(0.5 + (Random.Shared.NextDouble() * 0.49)),
|
||||
(float)(0.4 + (Random.Shared.NextDouble() / 5 + (x % 100 * 0.2)))))
|
||||
.ToVector3();
|
||||
.BulkCopyAsync(toAdd.Select(x =>
|
||||
{
|
||||
var clr = Color.Black;
|
||||
|
||||
var packed = new Rgba32(clr).PackedValue;
|
||||
return new NCPixel()
|
||||
{
|
||||
Color = packed,
|
||||
Price = 1,
|
||||
Position = x,
|
||||
Text = "",
|
||||
OwnerId = 0
|
||||
};
|
||||
}));
|
||||
var packed = ((Rgba32)clr).PackedValue;
|
||||
return new NCPixel()
|
||||
{
|
||||
Color = packed,
|
||||
Price = 1,
|
||||
Position = x,
|
||||
Text = "",
|
||||
OwnerId = 0
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
@ -83,9 +84,9 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var colors = await uow.GetTable<NCPixel>()
|
||||
.OrderBy(x => x.Position)
|
||||
.Select(x => x.Color)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
.OrderBy(x => x.Position)
|
||||
.Select(x => x.Color)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
@ -121,15 +122,15 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var updates = await uow.GetTable<NCPixel>()
|
||||
.Where(x => x.Position == position && x.Price <= price)
|
||||
.UpdateAsync(old => new NCPixel()
|
||||
{
|
||||
Position = position,
|
||||
Color = color,
|
||||
Text = text,
|
||||
OwnerId = userId,
|
||||
Price = price + 1
|
||||
});
|
||||
.Where(x => x.Position == position && x.Price <= price)
|
||||
.UpdateAsync(old => new NCPixel()
|
||||
{
|
||||
Position = position,
|
||||
Color = color,
|
||||
Text = text,
|
||||
OwnerId = userId,
|
||||
Price = price + 1
|
||||
});
|
||||
success = updates > 0;
|
||||
}
|
||||
catch
|
||||
|
@ -140,6 +141,10 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
{
|
||||
await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel {new kwum(position)} purchase"));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _quests.ReportActionAsync(userId, QuestEventType.PixelSet);
|
||||
}
|
||||
|
||||
return success ? SetPixelResult.Success : SetPixelResult.InsufficientPayment;
|
||||
}
|
||||
|
@ -152,14 +157,14 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<NCPixel>().DeleteAsync();
|
||||
await uow.GetTable<NCPixel>()
|
||||
.BulkCopyAsync(colors.Select((x, i) => new NCPixel()
|
||||
{
|
||||
Color = x,
|
||||
Price = INITIAL_PRICE,
|
||||
Position = i,
|
||||
Text = "",
|
||||
OwnerId = 0
|
||||
}));
|
||||
.BulkCopyAsync(colors.Select((x, i) => new NCPixel()
|
||||
{
|
||||
Color = x,
|
||||
Price = INITIAL_PRICE,
|
||||
Position = i,
|
||||
Text = "",
|
||||
OwnerId = 0
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -190,12 +195,12 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService
|
|||
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.GetTable<NCPixel>()
|
||||
.Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2
|
||||
&& x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2
|
||||
&& x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2
|
||||
&& x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2)
|
||||
.OrderBy(x => x.Position)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
.Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2
|
||||
&& x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2
|
||||
&& x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2
|
||||
&& x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2)
|
||||
.OrderBy(x => x.Position)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
}
|
||||
|
||||
public int GetHeight()
|
||||
|
|
9
src/EllieBot/Modules/Games/Quests/Quest.cs
Normal file
9
src/EllieBot/Modules/Games/Quests/Quest.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public record class Quest(
|
||||
QuestIds Id,
|
||||
string Name,
|
||||
string Description,
|
||||
QuestEventType TriggerEvent,
|
||||
int RequiredAmount
|
||||
);
|
38
src/EllieBot/Modules/Games/Quests/QuestCommands.cs
Normal file
38
src/EllieBot/Modules/Games/Quests/QuestCommands.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public class QuestCommands : EllieModule<QuestService>
|
||||
{
|
||||
[Cmd]
|
||||
public async Task QuestLog()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var quests = await _service.GetUserQuestsAsync(ctx.User.Id, now);
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.quest_log));
|
||||
|
||||
var allDone = quests.All(x => x.UserQuest.IsCompleted);
|
||||
|
||||
var tmrw = now.AddDays(1).Date;
|
||||
var desc = GetText(strs.dailies_reset(TimestampTag.FromDateTime(tmrw, TimestampTagStyles.Relative)));
|
||||
if (allDone)
|
||||
desc = GetText(strs.dailies_done) + "\n" + desc;
|
||||
|
||||
embed.WithDescription(desc);
|
||||
|
||||
foreach (var res in quests)
|
||||
{
|
||||
if (res.Quest is null)
|
||||
continue;
|
||||
|
||||
embed.AddField(
|
||||
(res.UserQuest.IsCompleted ? IQuest.COMPLETED : IQuest.INCOMPLETE) + " " + res.Quest.Name,
|
||||
$"{res.Quest.Desc}\n\n" +
|
||||
res.Quest.ToString(res.UserQuest.Progress),
|
||||
true);
|
||||
}
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
}
|
15
src/EllieBot/Modules/Games/Quests/QuestEvent.cs
Normal file
15
src/EllieBot/Modules/Games/Quests/QuestEvent.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public class QuestEvent
|
||||
{
|
||||
public QuestEventType EventType { get; }
|
||||
public ulong UserId { get; }
|
||||
public Dictionary<string, string> Metadata { get; }
|
||||
|
||||
public QuestEvent(QuestEventType eventType, ulong userId, Dictionary<string, string>? metadata = null)
|
||||
{
|
||||
EventType = eventType;
|
||||
UserId = userId;
|
||||
Metadata = metadata ?? new Dictionary<string, string>();
|
||||
}
|
||||
}
|
15
src/EllieBot/Modules/Games/Quests/QuestEventType.cs
Normal file
15
src/EllieBot/Modules/Games/Quests/QuestEventType.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public enum QuestEventType
|
||||
{
|
||||
CommandUsed,
|
||||
GameWon,
|
||||
BetPlaced,
|
||||
FishCaught,
|
||||
PixelSet,
|
||||
RaceJoined,
|
||||
BankAction,
|
||||
PlantOrPick,
|
||||
Give,
|
||||
WaifuGiftSent
|
||||
}
|
16
src/EllieBot/Modules/Games/Quests/QuestIds.cs
Normal file
16
src/EllieBot/Modules/Games/Quests/QuestIds.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public enum QuestIds
|
||||
{
|
||||
HangmanWin,
|
||||
Bet,
|
||||
WaifuGift,
|
||||
CatchFish,
|
||||
SetPixels,
|
||||
JoinAnimalRace,
|
||||
BankDeposit,
|
||||
CheckBetting,
|
||||
PlantPick,
|
||||
GiveFlowers,
|
||||
WellInformed
|
||||
}
|
65
src/EllieBot/Modules/Games/Quests/QuestModels/BankerQuest.cs
Normal file
65
src/EllieBot/Modules/Games/Quests/QuestModels/BankerQuest.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class BankerQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.BankDeposit;
|
||||
|
||||
public string Name
|
||||
=> "Banker";
|
||||
|
||||
public string Desc
|
||||
=> "Perform bank actions";
|
||||
|
||||
public string ProgDesc
|
||||
=> "";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.BankAction;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 0b111;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("type", out var type))
|
||||
return oldProgress;
|
||||
|
||||
var progress = oldProgress;
|
||||
|
||||
if (type == "balance")
|
||||
progress |= 0b001;
|
||||
else if (type == "deposit")
|
||||
progress |= 0b010;
|
||||
else if (type == "withdraw")
|
||||
progress |= 0b100;
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
public string ToString(long progress)
|
||||
{
|
||||
var msg = "";
|
||||
|
||||
var emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b001) == 0b001)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " checked bank balance";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b010) == 0b010)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += "\n" + emoji + " made a deposit";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b100) == 0b100)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += "\n" + emoji + " made a withdrawal";
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class BetFlowersQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.Bet;
|
||||
|
||||
public string Name
|
||||
=> "Flower Gambler";
|
||||
|
||||
public string Desc
|
||||
=> "Bet 300 flowers";
|
||||
|
||||
public string ProgDesc
|
||||
=> "flowers bet";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.BetPlaced;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 300;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("amount", out var amountStr)
|
||||
|| !long.TryParse(amountStr, out var amount))
|
||||
return oldProgress;
|
||||
|
||||
return oldProgress + amount;
|
||||
}
|
||||
}
|
27
src/EllieBot/Modules/Games/Quests/QuestModels/BetQuest.cs
Normal file
27
src/EllieBot/Modules/Games/Quests/QuestModels/BetQuest.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class BetQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.Bet;
|
||||
|
||||
public string Name
|
||||
=> "High Roller";
|
||||
|
||||
public string Desc
|
||||
=> "Place 20 bets";
|
||||
|
||||
public string ProgDesc
|
||||
=> "bets placed";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.BetPlaced;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 20;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
return oldProgress + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class CatchFishQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.CatchFish;
|
||||
|
||||
public string Name
|
||||
=> "Fisherman";
|
||||
|
||||
public string Desc
|
||||
=> "Catch 10 fish";
|
||||
|
||||
public string ProgDesc
|
||||
=> "fish caught";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.FishCaught;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 10;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (metadata.TryGetValue("type", out var type) && type == "fish")
|
||||
return oldProgress + 1;
|
||||
|
||||
return oldProgress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class CatchQualityQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.CatchFish;
|
||||
|
||||
public string Name
|
||||
=> "Master Angler";
|
||||
|
||||
public string Desc
|
||||
=> "Catch a fish or an item rated 3 stars or above.";
|
||||
|
||||
public string ProgDesc
|
||||
=> "3+ star fish caught";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.FishCaught;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 1;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (metadata.TryGetValue("stars", out var quality)
|
||||
&& int.TryParse(quality, out var q)
|
||||
&& q >= 3)
|
||||
return oldProgress + 1;
|
||||
|
||||
return oldProgress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class CatchTrashQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.CatchFish;
|
||||
|
||||
public string Name
|
||||
=> "Environmentalist";
|
||||
|
||||
public string Desc
|
||||
=> "Catch 10 trash items while fishing";
|
||||
|
||||
public string ProgDesc
|
||||
=> "items caught";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.FishCaught;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 10;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (metadata.TryGetValue("type", out var type) && type == "trash")
|
||||
return oldProgress + 1;
|
||||
|
||||
return oldProgress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class CheckLeaderboardsQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.CheckBetting;
|
||||
|
||||
public string Name
|
||||
=> "Leaderboard Enthusiast";
|
||||
|
||||
public string Desc
|
||||
=> "Check lb, xplb, fishlb and waifulb";
|
||||
|
||||
public string ProgDesc
|
||||
=> "";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.CommandUsed;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 0b1111;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("name", out var name))
|
||||
return oldProgress;
|
||||
|
||||
var progress = oldProgress;
|
||||
|
||||
if (name == "leaderboard")
|
||||
progress |= 0b0001;
|
||||
else if (name == "xpleaderboard")
|
||||
progress |= 0b0010;
|
||||
else if (name == "waifulb")
|
||||
progress |= 0b0100;
|
||||
else if (name == "fishlb")
|
||||
progress |= 0b1000;
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
public string ToString(long progress)
|
||||
{
|
||||
var msg = "";
|
||||
|
||||
var emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b0001) == 0b0001)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " flower lb seen\n";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b0010) == 0b0010)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " xp lb seen\n";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b0100) == 0b0100)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " waifu lb seen";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b1000) == 0b1000)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += "\n" + emoji + " fish lb seen";
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class GiftWaifuQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.WaifuGift;
|
||||
|
||||
public string Name
|
||||
=> "Generous Gifter";
|
||||
|
||||
public string Desc
|
||||
=> "Gift a waifu 2 times";
|
||||
|
||||
public string ProgDesc
|
||||
=> "waifus gifted";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.WaifuGiftSent;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 2;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
return oldProgress + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class GiveFlowersQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.GiveFlowers;
|
||||
|
||||
public string Name
|
||||
=> "Sharing is Caring";
|
||||
|
||||
public string Desc
|
||||
=> "Give 20 flowers to someone";
|
||||
|
||||
public string ProgDesc
|
||||
=> "flowers given";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.Give;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 20;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("amount", out var amountStr)
|
||||
|| !long.TryParse(amountStr, out var amount))
|
||||
return oldProgress;
|
||||
|
||||
return oldProgress + amount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class HangmanWinQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.HangmanWin;
|
||||
|
||||
public string Name
|
||||
=> "Hangman Champion";
|
||||
|
||||
public string Desc
|
||||
=> "Win 2 games of Hangman";
|
||||
|
||||
public string ProgDesc
|
||||
=> "hangman games won";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.GameWon;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 2;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("game", out var value))
|
||||
return oldProgress;
|
||||
|
||||
return value == "hangman" ? oldProgress + 1 : oldProgress;
|
||||
}
|
||||
}
|
33
src/EllieBot/Modules/Games/Quests/QuestModels/IQuest.cs
Normal file
33
src/EllieBot/Modules/Games/Quests/QuestModels/IQuest.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using EllieBot.Db.Models;
|
||||
|
||||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public interface IQuest
|
||||
{
|
||||
QuestIds QuestId { get; }
|
||||
string Name { get; }
|
||||
string Desc { get; }
|
||||
string ProgDesc { get; }
|
||||
QuestEventType EventType { get; }
|
||||
long RequiredAmount { get; }
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress);
|
||||
|
||||
public virtual string ToString(long progress)
|
||||
=> GetEmoji(progress, RequiredAmount) + $" [{progress}/{RequiredAmount}] " + ProgDesc;
|
||||
|
||||
public static string GetEmoji(long progress, long requiredAmount)
|
||||
=> progress >= requiredAmount
|
||||
? COMPLETED
|
||||
: INCOMPLETE;
|
||||
|
||||
/// <summary>
|
||||
/// Completed Emoji
|
||||
/// </summary>
|
||||
public const string COMPLETED = "✅";
|
||||
|
||||
/// <summary>
|
||||
/// Incomplete Emoji
|
||||
/// </summary>
|
||||
public const string INCOMPLETE = "❌";
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class JoinAnimalRaceQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.JoinAnimalRace;
|
||||
|
||||
public string Name
|
||||
=> "Race Participant";
|
||||
|
||||
public string Desc
|
||||
=> "Join an animal race";
|
||||
|
||||
public string ProgDesc
|
||||
=> "races joined";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.RaceJoined;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 1;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
return oldProgress + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class PlantPickQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.PlantPick;
|
||||
|
||||
public string Name
|
||||
=> "Gardener";
|
||||
|
||||
public string Desc
|
||||
=> "pick and plant";
|
||||
|
||||
public string ProgDesc
|
||||
=> "";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.PlantOrPick;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 0b11;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("type", out var val))
|
||||
return oldProgress;
|
||||
|
||||
if (val == "plant")
|
||||
{
|
||||
oldProgress |= 0b10;
|
||||
return oldProgress;
|
||||
}
|
||||
|
||||
if (val == "pick")
|
||||
{
|
||||
oldProgress |= 0b01;
|
||||
return oldProgress;
|
||||
}
|
||||
|
||||
return oldProgress;
|
||||
}
|
||||
|
||||
public string ToString(long progress)
|
||||
{
|
||||
var msg = "";
|
||||
|
||||
var emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b01) == 0b01)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " picked flowers\n";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b10) == 0b10)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " planted flowers";
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class SetPixelsQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.SetPixels;
|
||||
|
||||
public string Name
|
||||
=> "Pixel Artist";
|
||||
|
||||
public string Desc
|
||||
=> "Set 3 pixels";
|
||||
|
||||
public string ProgDesc
|
||||
=> "pixels set";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.PixelSet;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 3;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
return oldProgress + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class WellInformedQuest : IQuest
|
||||
{
|
||||
public QuestIds QuestId
|
||||
=> QuestIds.WellInformed;
|
||||
|
||||
public string Name
|
||||
=> "Well Informed";
|
||||
|
||||
public string Desc
|
||||
=> "Check your flower stats";
|
||||
|
||||
public string ProgDesc
|
||||
=> "";
|
||||
|
||||
public QuestEventType EventType
|
||||
=> QuestEventType.CommandUsed;
|
||||
|
||||
public long RequiredAmount
|
||||
=> 0b111;
|
||||
|
||||
public long TryUpdateProgress(IDictionary<string, string> metadata, long oldProgress)
|
||||
{
|
||||
if (!metadata.TryGetValue("name", out var type))
|
||||
return oldProgress;
|
||||
|
||||
var progress = oldProgress;
|
||||
|
||||
if (type == "cash")
|
||||
progress |= 0b001;
|
||||
else if (type == "rakeback")
|
||||
progress |= 0b010;
|
||||
else if (type == "betstats")
|
||||
progress |= 0b100;
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
public string ToString(long progress)
|
||||
{
|
||||
var msg = "";
|
||||
|
||||
var emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b001) == 0b001)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " checked cash\n";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b010) == 0b010)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " checked rakeback\n";
|
||||
|
||||
emoji = IQuest.INCOMPLETE;
|
||||
if ((progress & 0b100) == 0b100)
|
||||
emoji = IQuest.COMPLETED;
|
||||
|
||||
msg += emoji + " checked bet stats";
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
199
src/EllieBot/Modules/Games/Quests/QuestService.cs
Normal file
199
src/EllieBot/Modules/Games/Quests/QuestService.cs
Normal file
|
@ -0,0 +1,199 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
|
||||
namespace EllieBot.Modules.Games.Quests;
|
||||
|
||||
public sealed class QuestService(
|
||||
DbService db,
|
||||
IBotCache botCache,
|
||||
IMessageSenderService sender,
|
||||
DiscordSocketClient client
|
||||
) : IEService, IExecPreCommand
|
||||
{
|
||||
private readonly IQuest[] _availableQuests =
|
||||
[
|
||||
new HangmanWinQuest(),
|
||||
new PlantPickQuest(),
|
||||
new BetQuest(),
|
||||
new BetFlowersQuest(),
|
||||
new GiftWaifuQuest(),
|
||||
new CatchFishQuest(),
|
||||
new SetPixelsQuest(),
|
||||
new JoinAnimalRaceQuest(),
|
||||
new BankerQuest(),
|
||||
new CheckLeaderboardsQuest(),
|
||||
new WellInformedQuest(),
|
||||
];
|
||||
|
||||
private const int MAX_QUESTS_PER_DAY = 3;
|
||||
|
||||
private TypedKey<bool> UserHasQuestsKey(ulong userId)
|
||||
=> new($"daily:generated:{userId}");
|
||||
|
||||
private TypedKey<bool> UserCompletedDailiesKey(ulong userId)
|
||||
=> new($"daily:completed:{userId}");
|
||||
|
||||
|
||||
public Task ReportActionAsync(
|
||||
ulong userId,
|
||||
QuestEventType eventType,
|
||||
Dictionary<string, string>? metadata = null)
|
||||
{
|
||||
// don't block any caller
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
metadata ??= new();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var alreadyDone = await botCache.GetAsync(UserCompletedDailiesKey(userId));
|
||||
if (alreadyDone.IsT0)
|
||||
return;
|
||||
|
||||
var userQuests = await GetUserQuestsAsync(userId, now);
|
||||
|
||||
foreach (var (q, uq) in userQuests)
|
||||
{
|
||||
// deleted quest
|
||||
if (q is null)
|
||||
continue;
|
||||
|
||||
// user already completed or incorrect event
|
||||
if (uq.IsCompleted || q.EventType != eventType)
|
||||
continue;
|
||||
|
||||
var newProgress = q.TryUpdateProgress(metadata, uq.Progress);
|
||||
|
||||
// user already did that part of the quest
|
||||
if (newProgress == uq.Progress)
|
||||
continue;
|
||||
|
||||
var isCompleted = newProgress >= q.RequiredAmount;
|
||||
|
||||
await using var uow = db.GetDbContext();
|
||||
await uow.GetTable<UserQuest>()
|
||||
.Where(x => x.UserId == userId && x.QuestId == q.QuestId && x.QuestNumber == uq.QuestNumber)
|
||||
.Set(x => x.Progress, newProgress)
|
||||
.Set(x => x.IsCompleted, isCompleted)
|
||||
.UpdateAsync();
|
||||
|
||||
uq.IsCompleted = isCompleted;
|
||||
|
||||
if (userQuests.All(x => x.UserQuest.IsCompleted))
|
||||
{
|
||||
var timeUntilTomorrow = now.Date.AddDays(1) - DateTime.UtcNow;
|
||||
if (!await botCache.AddAsync(
|
||||
UserCompletedDailiesKey(userId),
|
||||
true,
|
||||
expiry: timeUntilTomorrow))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var user = await client.GetUserAsync(userId);
|
||||
await sender
|
||||
.Response(user)
|
||||
.Confirm(strs.dailies_done)
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// we don't really care if the user receives it
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<(IQuest? Quest, UserQuest UserQuest)>> GetUserQuestsAsync(
|
||||
ulong userId,
|
||||
DateTime now)
|
||||
{
|
||||
var today = now.Date;
|
||||
await EnsureUserDailiesAsync(userId, today);
|
||||
|
||||
await using var uow = db.GetDbContext();
|
||||
var quests = await uow.GetTable<UserQuest>()
|
||||
.Where(x => x.UserId == userId && x.DateAssigned == today)
|
||||
.ToListAsync();
|
||||
|
||||
return quests
|
||||
.Select(x => (_availableQuests.FirstOrDefault(q => q.QuestId == x.QuestId), x))
|
||||
.Select(x => x!)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task EnsureUserDailiesAsync(ulong userId, DateTime date)
|
||||
{
|
||||
var today = date.Date;
|
||||
var timeUntilTomorrow = today.AddDays(1) - DateTime.UtcNow;
|
||||
if (!await botCache.AddAsync(UserHasQuestsKey(userId), true, expiry: timeUntilTomorrow, overwrite: false))
|
||||
return;
|
||||
|
||||
await using var uow = db.GetDbContext();
|
||||
var newQuests = GenerateDailyQuestsAsync();
|
||||
for (var i = 0; i < MAX_QUESTS_PER_DAY; i++)
|
||||
{
|
||||
await uow.GetTable<UserQuest>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
QuestNumber = i,
|
||||
DateAssigned = today,
|
||||
|
||||
IsCompleted = false,
|
||||
QuestId = newQuests[i].QuestId,
|
||||
Progress = 0,
|
||||
},
|
||||
old => new()
|
||||
{
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
QuestNumber = i,
|
||||
DateAssigned = today
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<IQuest> GenerateDailyQuestsAsync()
|
||||
{
|
||||
return _availableQuests
|
||||
.ToList()
|
||||
.Shuffle()
|
||||
.Take(MAX_QUESTS_PER_DAY)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public int Priority
|
||||
=> int.MinValue;
|
||||
|
||||
public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
|
||||
{
|
||||
var cmdName = command.Name.ToLowerInvariant();
|
||||
|
||||
await ReportActionAsync(
|
||||
context.User.Id,
|
||||
QuestEventType.CommandUsed,
|
||||
new()
|
||||
{
|
||||
{ "name", cmdName }
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> UserCompletedDailies(ulong userId)
|
||||
{
|
||||
var result = await botCache.GetAsync(UserCompletedDailiesKey(userId));
|
||||
|
||||
return result.IsT0;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue