Compare commits
66 commits
Author | SHA1 | Date | |
---|---|---|---|
ced58d17c3 | |||
d171b92989 | |||
172b1ed5e6 | |||
8f6ecff5af | |||
94aee4ad10 | |||
d910683d78 | |||
8ed26e11d3 | |||
d682909bab | |||
015724a150 | |||
7f36481987 | |||
16c64762b7 | |||
d8aa040f75 | |||
37522cf65c | |||
ea06e9f217 | |||
68e736ceb8 | |||
d3c90ab59f | |||
9aa567b276 | |||
026e5a151e | |||
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 | |||
9b8d00d184 | |||
4a5bcd46e6 | |||
2eb973ccd3 | |||
c522abe856 | |||
3a8c811798 | |||
339237b351 | |||
91d464b041 |
178 changed files with 7576 additions and 2607 deletions
.editorconfig.gitignoreCHANGELOG.mdmkdocs.yml
docs
md
assets
elliehub-1.pngelliehub-2.pngelliehub-3.pngelliehub-4.pngelliehub-5.pngelliehub-6.pngfavicon.pngpatreon.pngpaypal.png
creds-guide.mddonate.mdguides
index.mdmarmalade
snippets
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
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
|
||||
|
|
87
CHANGELOG.md
87
CHANGELOG.md
|
@ -2,6 +2,93 @@
|
|||
|
||||
*a,c,f,r,o*
|
||||
|
||||
## [6.1.2] - 03.04.2025
|
||||
|
||||
### Fixed
|
||||
- Fixed `.feed` not adding new feeds to the database
|
||||
|
||||
## [6.1.1] - 03.04.2025
|
||||
|
||||
### Added
|
||||
- Added some config options for .conf fish
|
||||
|
||||
### Fixed
|
||||
- Fixed a typo in fish shop
|
||||
- .fishlb will now compare unique fish caught, instead of total catches
|
||||
- hangman category now appears in .hangman output
|
||||
|
||||
## [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
|
||||
- `.antispamignore` fixed for the last time hopefully
|
||||
- protection commands are some of the oldest commands, and they might get overhauled in future updates
|
||||
- please report if you find any other weird issue with them
|
||||
|
||||
## [6.0.11] - 20.03.2025
|
||||
|
||||
### Changed
|
||||
- wordfilter, invitefilter and linkfilter will now properly detect forwarded messages, as forwards were used to circumvent filtering.
|
||||
|
||||
### Fixed
|
||||
- `.dmc` fixed
|
||||
- Fixed .streamremove - now showing proper youtube name when removing instead of channel id
|
||||
|
||||
## [6.0.10] - 20.03.2025
|
||||
|
||||
### Changed
|
||||
|
|
BIN
docs/md/assets/elliehub-1.png
Normal file
BIN
docs/md/assets/elliehub-1.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 52 KiB |
BIN
docs/md/assets/elliehub-2.png
Normal file
BIN
docs/md/assets/elliehub-2.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 58 KiB |
BIN
docs/md/assets/elliehub-3.png
Normal file
BIN
docs/md/assets/elliehub-3.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 56 KiB |
BIN
docs/md/assets/elliehub-4.png
Normal file
BIN
docs/md/assets/elliehub-4.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 15 KiB |
BIN
docs/md/assets/elliehub-5.png
Normal file
BIN
docs/md/assets/elliehub-5.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.2 KiB |
BIN
docs/md/assets/elliehub-6.png
Normal file
BIN
docs/md/assets/elliehub-6.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1,014 B |
BIN
docs/md/assets/favicon.png
Normal file
BIN
docs/md/assets/favicon.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 273 KiB |
BIN
docs/md/assets/patreon.png
Normal file
BIN
docs/md/assets/patreon.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 28 KiB |
BIN
docs/md/assets/paypal.png
Normal file
BIN
docs/md/assets/paypal.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 27 KiB |
32
docs/md/creds-guide.md
Normal file
32
docs/md/creds-guide.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
## Creating your own Discord bot
|
||||
|
||||
This guide will show you how to create your own discord bot, invite it to your server, and obtain the credentials needed to run it.
|
||||
|
||||
1. Go to [the Discord developer application page][Discord].
|
||||
2. Log in with your Discord account.
|
||||
3. Click **New Application**.
|
||||
4. Fill out the `Name` field however you like, accept the terms, and confirm.
|
||||
5. Go to the **Bot** tab on the left sidebar.
|
||||
6. Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
|
||||
7. **Optional:** Add bot's avatar and description.
|
||||
8. Copy your Token to `creds.yml` as shown above.
|
||||
9. Scroll down to the **`Privileged Gateway Intents`** section
|
||||
- You MUST enable the following:
|
||||
- **PRESENCE INTENT**
|
||||
- **SERVER MEMBERS INTENT**
|
||||
- **MESSAGE CONTENT INTENT**
|
||||
|
||||
### Inviting your bot to your server
|
||||
|
||||

|
||||
|
||||
- On the **General Information** tab, copy your `Application ID` from your [applications page][Discord].
|
||||
- Replace the `YOUR_CLIENT_ID_HERE` in this link:
|
||||
`https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID_HERE&scope=bot&permissions=66186303` with your `Client ID`
|
||||
- The link should now look something like this:
|
||||
`https://discord.com/oauth2/authorize?client_id=123123123123&scope=bot&permissions=66186303`
|
||||
- Access that newly created link, pick your Discord server, click `Authorize` and confirm with the captcha at the end
|
||||
- The bot should now be in your server
|
||||
|
||||
|
||||
[Discord]: https://discord.com/developers/applications/me
|
37
docs/md/donate.md
Normal file
37
docs/md/donate.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Donate
|
||||
|
||||
Ellie is an [open-source project][toastielab], and we rely on your help to develop the bot, pay hosting fees, maintain our website and more.
|
||||
Donations go a long way in helping us keep the project alive, and we appreciate every single one of them.
|
||||
|
||||
## Perks
|
||||
|
||||
Donating to us also gives you the following benefits:
|
||||
|
||||
- A hoisted **Patron** role in [Ellie Discord server][discord-server]
|
||||
- Access to exclusive **#donator-general** text and voice channels
|
||||
- **3000 candy** on the public bot per dollar donated (after fees)
|
||||
|
||||
## Patreon
|
||||
|
||||
You can set up a monthly pledge on [Patreon][patreon] and support the project's growth, and also get candy rewards for every month you donate!
|
||||
|
||||
!!! Note
|
||||
Connect your Discord account on Patreon to receive your candy automatically
|
||||
|
||||
[![img][patreon-button]][patreon]
|
||||
|
||||
## PayPal
|
||||
|
||||
You can also donate to us through [PayPal][paypal] for one-time donations using the button below, or by donating to `toastie@dragonschildstudios.com`.
|
||||
|
||||
!!! Note
|
||||
Mention your Discord username or user id in the payment note to receive candy rewards.
|
||||
|
||||
[![img][paypal-button]][paypal]
|
||||
|
||||
[toastielab]: https://toastielab.dev/EllieBotDevs/elliebot
|
||||
[discord-server]: https://discord.nadeko.bot/
|
||||
[patreon]: https://www.patreon.com/elliebot
|
||||
[patreon-button]: ./assets/patreon.png
|
||||
[paypal]: https://paypal.me/toastie_t0ast
|
||||
[paypal-button]: ./assets/paypal.png
|
235
docs/md/guides/cli-guide.md
Normal file
235
docs/md/guides/cli-guide.md
Normal file
|
@ -0,0 +1,235 @@
|
|||
# EllieBot CLI Guide (via Bash Installer)
|
||||
|
||||
### Supported Operating Systems
|
||||
|
||||
--8<-- "md/snippets/supported-platforms.md:linux"
|
||||
--8<-- "md/snippets/supported-platforms.md:macos"
|
||||
|
||||
### Prerequisites
|
||||
|
||||
macOS:
|
||||
|
||||
- [Homebrew](https://brew.sh/)
|
||||
- [Curl](#__tabbed_1_5)
|
||||
|
||||
Linux:
|
||||
|
||||
- [Curl](#__tabbed_1_1)
|
||||
|
||||
---
|
||||
|
||||
??? note "24/7 Up-time via VPS (Digital Ocean Guide)"
|
||||
--8<-- "md/guides/vps-linux-guide.md"
|
||||
|
||||
??? note "Creating a Discord Bot & Getting Credentials"
|
||||
--8<-- "md/creds-guide.md"
|
||||
|
||||
---
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
!!! failure
|
||||
This script is broken and should no be used. please use [Desktop Guide](../guides/desktop-guide.md)
|
||||
|
||||
Open Terminal (if you're on an installation with a window manager) and navigate to the location where you want to install the bot (for example `cd ~`)
|
||||
|
||||
1. First make sure that curl is installed
|
||||
|
||||
/// tab | Ubuntu | Debian | Mint
|
||||
|
||||
```bash
|
||||
sudo apt install curl
|
||||
```
|
||||
|
||||
///
|
||||
/// tab | Rocky | Alma | Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf install curl
|
||||
```
|
||||
|
||||
///
|
||||
/// tab | openSUSE
|
||||
|
||||
```bash
|
||||
sudo zypper install curl
|
||||
```
|
||||
|
||||
///
|
||||
/// tab | Arch | Artix
|
||||
|
||||
```bash
|
||||
sudo pacman -S curl
|
||||
```
|
||||
|
||||
///
|
||||
/// tab | macOS
|
||||
|
||||
```bash
|
||||
brew install curl
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
2. Download and run the **new** installer script
|
||||
``` sh
|
||||
cd ~
|
||||
curl -L -o e-install.sh https://toastielab.dev/EllieBotDevs/ellie-bash-installer/raw/branch/v6/e-install.sh
|
||||
bash e-install.sh
|
||||
```
|
||||
3. Install the bot (type `1` and press enter)
|
||||
4. Edit creds (type `3` and press enter)
|
||||
- *ALTERNATIVELY*, you can exit the installer (option `6`) and edit `ellie/creds.yml` file yourself
|
||||
5. Follow the instruction [below](#creating-your-own-discord-bot) to create your own Discord bot and obtain the credentials needed to run it.
|
||||
- After you're done, you can close nano (and save the file) by inputting, in order:
|
||||
- `CTRL` + `X`
|
||||
- `Y`
|
||||
- `Enter`
|
||||
6. Run the installer script again
|
||||
- `bash e-install.sh`
|
||||
7. Run the bot (type `3` and press enter)
|
||||
8. Done!
|
||||
|
||||
## Update Instructions
|
||||
|
||||
1. ⚠ Stop the bot ⚠
|
||||
2. Navigate to your bot's folder, we'll use home directory as an example
|
||||
- `cd ~`
|
||||
3. Simply re-install the bot with a newer version by running the installer script
|
||||
- `curl -L -o e-install.sh https://toastielab.dev/EllieBotDevs/ellie-bash-installer/raw/branch/v6/e-install.sh && bash e-install.sh`
|
||||
4. Select option 1, and select a NEWER version
|
||||
|
||||
## Running Ellie
|
||||
|
||||
There are two main methods to run EllieBot: using `tmux` (macOS and Linux) or using `systemd` with a script (Linux only).
|
||||
|
||||
/// tab | Tmux (Preferred Method)
|
||||
|
||||
Using `tmux` is the simplest method, and is therefore recommended for most users.
|
||||
|
||||
!!! warning
|
||||
Before proceeding, make sure your bot is not currently running by either running `.die` in your Discord server or exiting the process with `Ctrl+C`.
|
||||
|
||||
1. Access the directory where `e-install.sh` and `ellie` is located.
|
||||
2. Create a new tmux session: `tmux new -s ellie`
|
||||
- The above command will create a new session named **ellie**. You may replace **ellie** with any name you prefer.
|
||||
3. Run the installer: `bash e-install.sh`
|
||||
4. Start the bot by typing `3` and pressing `Enter`.
|
||||
5. Detach from the tmux session, allowing the bot to run in the background:
|
||||
- Press `Ctrl` + `B`
|
||||
- Then press `D`
|
||||
|
||||
Now check your Discord server, the bot should be online. Ellie should now be running in the background of your system.
|
||||
|
||||
To re-open the tmux session to either update, restart, or whatever, execute `tmux a -t ellie`. *(Make sure to replace "ellie" with your session name. If you didn't change it, leave it as it is.)*
|
||||
|
||||
///
|
||||
/// tab | Systemd
|
||||
|
||||
!!! note
|
||||
Systemd is only available on Linux. macOS utilizes Launchd, which is not covered in this guide. If you're on macOS, please use the `tmux` method, or use [EllieHub](desktop-guide.md) to run EllieBot.
|
||||
|
||||
This method is a bit more complex and involved, but comes with the added benefit of better error logging and control over what happens before and after the startup of Ellie.
|
||||
|
||||
1. Access the directory where `e-install.sh` and `ellie` is located.
|
||||
2. Use the following command to create a service that will be used to execute `EllieRun.bash`:
|
||||
```bash
|
||||
echo "[Unit]
|
||||
Description=EllieBot service
|
||||
After=network.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=2
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$USER
|
||||
WorkingDirectory=$PWD
|
||||
ExecStart=/bin/bash EllieRun.bash
|
||||
#ExecStart=./ellie/EllieBot
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=EllieBot
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" | sudo tee /etc/systemd/system/ellie.service
|
||||
```
|
||||
3. Make the new service available: `sudo systemctl daemon-reload`
|
||||
4. Use the following command to create a script that will be used to start Ellie:
|
||||
```bash
|
||||
cat <<EOF > EllieRun.bash
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
is_python3_installed=\$(command -v python3 &>/dev/null && echo true || echo false)
|
||||
is_yt_dlp_installed=\$(command -v yt-dlp &>/dev/null && echo true || echo false)
|
||||
|
||||
[[ \$is_python3_installed == true ]] \\
|
||||
&& echo "[INFO] python3 path: \$(which python3)" \\
|
||||
&& echo "[INFO] python3 version: \$(python3 --version)"
|
||||
[[ \$is_yt_dlp_installed == true ]] \\
|
||||
&& echo "[INFO] yt-dlp path: \$(which yt-dlp)"
|
||||
|
||||
echo "[INFO] Running EllieBot in the background with auto restart"
|
||||
if [[ \$is_yt_dlp_installed == true ]]; then
|
||||
yt-dlp -U || echo "[ERROR] Failed to update 'yt-dlp'" >&2
|
||||
fi
|
||||
|
||||
echo "[INFO] Starting EllieBot..."
|
||||
|
||||
while true; do
|
||||
if [[ -d $PWD/ellie ]]; then
|
||||
cd "$PWD/ellie" || {
|
||||
echo "[ERROR] Failed to change working directory to '$PWD/ellie'" >&2
|
||||
echo "[INFO] Exiting..."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "[WARN] '$PWD/ellie' doesn't exist" >&2
|
||||
echo "[INFO] Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./EllieBot || {
|
||||
echo "[ERROR] An error occurred when trying to start EllieBot" >&2
|
||||
echo "[INFO] Exiting..."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "[INFO] Waiting 5 seconds..."
|
||||
sleep 5
|
||||
if [[ \$is_yt_dlp_installed == true ]]; then
|
||||
yt-dlp -U || echo "[ERROR] Failed to update 'yt-dlp'" >&2
|
||||
fi
|
||||
echo "[INFO] Restarting EllieBot..."
|
||||
done
|
||||
|
||||
echo "[INFO] Stopping EllieBot..."
|
||||
EOF
|
||||
```
|
||||
|
||||
With everything set up, you can run EllieBot in one of three modes:
|
||||
|
||||
1. **Auto-Restart Mode**: EllieBot will restart automatically if you restart it via the `.die` command.
|
||||
- To enable this mode, start the service: `sudo systemctl start ellie`
|
||||
2. **Auto-Restart on Reboot Mode**: In addition to auto-restarting after `.die`, EllieBot will also start automatically on system reboot.
|
||||
- To enable this mode, run:
|
||||
```bash
|
||||
sudo systemctl enable ellie
|
||||
sudo systemctl start ellie
|
||||
```
|
||||
3. **Standard Mode**: EllieBot will stop completely when you use `.die`, without restarting automatically.
|
||||
- To switch to this mode:
|
||||
1. Stop the service: `sudo systemctl stop ellie`
|
||||
2. Edit the service file: `sudo <editor> /etc/systemd/system/ellie.service`
|
||||
3. Modify the `ExecStart` line:
|
||||
- **Comment out**: `ExecStart=/bin/bash EllieRun.bash`
|
||||
- **Uncomment**: `#ExecStart=./ellie/EllieBot`
|
||||
4. Save and exit the editor.
|
||||
5. Reload systemd: `sudo systemctl daemon-reload`
|
||||
6. Disable automatic startup: `sudo systemctl disable ellie`
|
||||
7. Start EllieBot manually: `sudo systemctl start ellie`
|
||||
|
||||
///
|
53
docs/md/guides/desktop-guide.md
Normal file
53
docs/md/guides/desktop-guide.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# EllieBot Desktop Guide (via EllieHub)
|
||||
|
||||
### Supported Operating Systems
|
||||
|
||||
--8<-- "md/snippets/supported-platforms.md"
|
||||
|
||||
---
|
||||
|
||||
??? note "Creating a Discord Bot & Getting Credentials"
|
||||
--8<-- "md/creds-guide.md"
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
1. Download and run [elliehub](https://toastielab.dev/EllieBotDevs/EllieHub/releases/latest).
|
||||
|
||||

|
||||
|
||||
2. Click the plus button to add a new bot
|
||||
|
||||

|
||||
|
||||
3. If you want to use the music module, click on the settings icon then click **`Install`** next to `ffmpeg` and `yt-dlp` near the top of the setting tab.
|
||||
4. Click on the newly created bot
|
||||
|
||||
5. Click on **Install** located above the live console (This may take a bit)
|
||||
|
||||

|
||||

|
||||
|
||||
6. When installation is finished, click on **`CREDS`** (`1`) above the **`RUN`** (`3`) button on the lower left
|
||||
- **`2`** simply opens your bot's data folder.
|
||||
7. Paste in your **BOT TOKEN** previously obtained
|
||||
|
||||
## Starting EllieBot
|
||||
|
||||
- Either click on **`RUN`** button in the updater or run the bot via its desktop shortcut.
|
||||
|
||||
## Updating EllieBot
|
||||
|
||||
!!! warning "IMPORTANT"
|
||||
|
||||
- Make sure Ellie is closed and not running
|
||||
- Run `.die` in a connected server to make sure.
|
||||
- Make sure you don't have `data` folder, bot folder, or any other bot file open in any program, as the updater will fail to replace your version
|
||||
|
||||
1. Run `elliehub` if not already running
|
||||
2. Click on your bot
|
||||
3. Click on **`Check for updates`**
|
||||
4. If updates are available, you will be able to click on the Update button
|
||||
5. Click `Update`
|
||||
6. Click `RUN` after it's done
|
72
docs/md/guides/docker-guide.md
Normal file
72
docs/md/guides/docker-guide.md
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Docker Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Docker Core Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (optional, but recommended)
|
||||
|
||||
---
|
||||
|
||||
??? note "Creating a Discord Bot & Getting Credentials"
|
||||
--8<-- "md/creds-guide.md"
|
||||
|
||||
---
|
||||
|
||||
## Installing EllieBot with Docker
|
||||
|
||||
When deploying EllieBot with Docker, you have two options: using [Docker](#__tabbed_1_1) or [Docker Compose](#__tabbed_1_2). The following sections provide step-by-step instructions for both methods.
|
||||
|
||||
/// tab | Docker
|
||||
|
||||
### Deploying EllieBot with Docker
|
||||
|
||||
1. Move to a directory where you want your Elliebot's data folder to be (data folder will keep the database and config files) and create a data folder there.
|
||||
``` sh
|
||||
cd ~ && mkdir ellie && cd ellie && mkdir data
|
||||
```
|
||||
2. Mount the newly created empty data folder as a volume while starting your docker container. Replace YOUR_TOKEN_HERE with the bot token obtained from the creds guide above.
|
||||
``` sh
|
||||
docker run -d --name ellie toastielab.dev/elliebotdevs/elliebot:v6 -e bot_token=YOUR_TOKEN_HERE -v "./data:/app/data" && docker logs -f --tail 500 ellie
|
||||
```
|
||||
3. Enjoy! 🎉
|
||||
|
||||
### Updating your bot
|
||||
|
||||
If you want to update elliebot to the latest version, all you have to do is pull the latest image and re-run.
|
||||
|
||||
1. Pull the latest image
|
||||
``` sh
|
||||
docker pull toastielab.dev/elliebotdevs/elliebot:v6
|
||||
```
|
||||
2. Re-run your bot the same way you did before
|
||||
``` sh
|
||||
docker run -d --name ellie toastielab.dev/elliebotdevs/elliebot:v6 -e bot_token=YOUR_TOKEN_HERE -v "./data:/app/data" && docker logs -f --tail 500 ellie
|
||||
```
|
||||
3. Done! 🎉
|
||||
|
||||
///
|
||||
/// tab | Docker Compose
|
||||
|
||||
1. **Choose Your Workspace:** Select a directory where you'll set up your EllieBot stack. Use your terminal to navigate to this directory. For the purpose of this guide, we'll use `/opt/stacks/ellie/` as an example, but you can choose any directory that suits your needs.
|
||||
2. **Create a Docker Compose File:** In this directory, create a Docker Compose file named `docker-compose.yml`. You can use any text editor for this task. For instance, to use the `nano` editor, type `nano docker-compose.yml`.
|
||||
3. **Configure Your Docker Compose File:** Populate your Docker Compose file with the following configuration:
|
||||
``` yml
|
||||
services:
|
||||
ellie:
|
||||
image: toastielab.dev/elliebotdevs/elliebot:v6
|
||||
container_name: ellie
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TZ: Europe/Rome # Modify this to your timezone
|
||||
bot_token: YOUR_TOKEN_HERE
|
||||
volumes:
|
||||
- /opt/stacks/ellie/data:/app/data
|
||||
networks: {}
|
||||
```
|
||||
|
||||
1. **Launch Your Bot:** Now, you're ready to run Docker Compose. Use the following command: `docker compose up -d`.
|
||||
2. **Navigate to Your Directory:** Use `cd /opt/stacks/ellie/` to go to the directory containing your Docker Compose file.
|
||||
3. **Pull the Latest Images:** Use `docker compose pull` to fetch the latest images.
|
||||
4. **Restart Your Containers:** Use `docker compose up -d` to restart the containers.
|
||||
|
||||
///
|
66
docs/md/guides/source-guide.md
Normal file
66
docs/md/guides/source-guide.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Setting Up EllieBot on Windows from source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Windows 10 or later (64-bit)
|
||||
- [.net 8 sdk](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
- If you want ellie to play music: [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required, you may install them later)
|
||||
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
|
||||
- **Optional** Any code editor, for example [Visual Studio Code](https://code.visualstudio.com/Download)
|
||||
- You'll need to at least modify creds.yml, notepad is inadequate
|
||||
|
||||
---
|
||||
|
||||
??? note "Creating a Discord Bot & Getting Credentials"
|
||||
--8<-- "md/creds-guide.md"
|
||||
|
||||
---
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and
|
||||
|
||||
1. Navigate to the location where you want to install the bot
|
||||
- for example, type `cd ~/Desktop/` and press enter
|
||||
2. `git clone https://toastielab.dev/EllieBotDevs/elliebot -b v6 --depth 1`
|
||||
3. `cd elliebot/src/EllieBot`
|
||||
4. `dotnet build -c Release`
|
||||
5. `cp data/creds_example.yml data/creds.yml`
|
||||
6. "You're done installing, you may now proceed to set up your bot's credentials by following the creds guide
|
||||
- Once done, come back here and run the last command
|
||||
6. Run the bot `dotnet EllieBot.dll`
|
||||
7. 🎉 Enjoy
|
||||
|
||||
## Update Instructions
|
||||
|
||||
Open PowerShell as described above and run the following commands:
|
||||
|
||||
1. Stop the bot
|
||||
- ⚠️ Make sure you don't have your database, credentials or any other elliebot folder open in some application, this might prevent some of the steps from executing successfully
|
||||
2. Navigate to your bot's folder, example:
|
||||
- `cd ~/Desktop/elliebot`
|
||||
3. Pull the new version, and make sure you're on the v6 branch
|
||||
- `git pull`
|
||||
- ⚠️ IF this fails, you may want to `git stash` or remove your code changes if you don't know how to resolve merge conflicts
|
||||
4. **Backup** old output in case your data is overwritten
|
||||
- `cp -r -fo output/ output-old`
|
||||
5. Build the bot again
|
||||
- `dotnet run -c Release src/EllieBot/`
|
||||
6. Copy old data, and new strings
|
||||
- `cp -r -fo .\output-old\data\ .\output\`
|
||||
7. Run the bot
|
||||
- `cd output`
|
||||
- `dotnet EllieBot.dll`
|
||||
8. 🎉 Enjoy
|
||||
|
||||
## Music Prerequisites
|
||||
|
||||
In order to use music commands, you need ffmpeg and yt-dlp installed.
|
||||
|
||||
- [ffmpeg]
|
||||
- [yt-dlp]
|
||||
- Click to download the `yt-dlp.exe` file, then move `yt-dlp.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `yt-dlp.exe` file to your elliebot's output folder.
|
||||
|
||||
[.net]: https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
[ffmpeg]: https://github.com/GyanD/codexffmpeg/releases/latest
|
||||
[yt-dlp]: https://github.com/yt-dlp/yt-dlp/releases/latest
|
54
docs/md/guides/vps-linux-guide.md
Normal file
54
docs/md/guides/vps-linux-guide.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
## Setting up Ellie on a Linux VPS (Digital Ocean Droplet)
|
||||
|
||||
If you want Ellie to play music for you 24/7 without having to hosting it on your PC and want to keep it cheap, reliable and convenient as possible, you can try Ellie on Linux Digital Ocean Droplet using the link [DigitalOcean](https://m.do.co/c/1bb1db830f41/) (by using this link, you will get **$10 credit** and also support Ellie)
|
||||
|
||||
To set up the VPS, please select the options below
|
||||
```
|
||||
These are the min requirements you must follow:
|
||||
|
||||
OS: Any between Ubuntu, Fedora, and Debian
|
||||
|
||||
Droplet Type: SHARED CPU | Basic
|
||||
|
||||
CPU options: Regular | Disk type: SSD
|
||||
6$/mo
|
||||
1 GB / 1 CPU
|
||||
25 GB SSD Disk
|
||||
1000 GB transfer
|
||||
|
||||
Note: You can select the cheapest option with 512 MB / 1 CPU but this has been a hit or miss.
|
||||
|
||||
Datacenter region: Choose one depending on where you are located.
|
||||
|
||||
Authentication: Password or SSH
|
||||
(Select SSH if you know what you are doing, otherwise choose password)
|
||||
Click create droplet
|
||||
```
|
||||
**Setting up EllieBot**
|
||||
Assuming you have followed the link above to setup an account and a Droplet with a 64-bit operational system on Digital Ocean and got the `IP address and root password (in your e-mail)` to login, it's time to get started.
|
||||
|
||||
**This section is only relevant to those who want to host Ellie on DigitalOcean. Go through this whole section before setting the bot up.**
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Download [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
|
||||
- Download [WinSCP](https://winscp.net/eng/download.php) *(optional)*
|
||||
- [Create and invite the bot](../creds-guide.md).
|
||||
|
||||
### Starting up
|
||||
|
||||
- **Open PuTTY** and paste or enter your `IP address` and then click **Open**.
|
||||
If you entered your Droplets IP address correctly, it should show **login as:** in a newly opened window.
|
||||
- Now for **login as:**, type `root` and press enter.
|
||||
- It should then ask for a password. Type the `root password` you have received in your e-mail address, then press Enter.
|
||||
|
||||
If you are running your droplet for the first time, it will most likely ask you to change your root password. To do that, copy the **password you've received by e-mail** and paste it on PuTTY.
|
||||
|
||||
- To paste, just right-click the window (it won't show any changes on the screen), then press Enter.
|
||||
- Type a **new password** somewhere, copy and paste it on PuTTY. Press Enter then paste it again.
|
||||
|
||||
**Save the new password somewhere safe.**
|
||||
|
||||
After that, your droplet should be ready for use.
|
||||
|
||||
[Setting up Ellie on a VPS (Digital Ocean)]: #setting-up-ellie-on-a-linux-vps-digital-ocean-droplet
|
40
docs/md/index.md
Normal file
40
docs/md/index.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# EllieBot Documentation
|
||||
|
||||
<!-- ![img][header] -->
|
||||
|
||||
## Inviting Ellie
|
||||
|
||||
There are two versions of Ellie, a public bot and a self-hostable bot.
|
||||
|
||||
To invite public Ellie to your server or to view its commands, click on the buttons below:
|
||||
|
||||
[:material-plus: Add Ellie to your server][invite]{ .md-button .md-button--primary }
|
||||
[:material-format-list-text: View commands][commands]{ .md-button }
|
||||
|
||||
To self-host your own Ellie, use the guides below:
|
||||
|
||||
- [:material-television-guide: Desktop guide (Windows/Linux/macOS)][desktop-guide]
|
||||
- [:material-console: CLI guide (Linux/macOS)][cli-guide]
|
||||
- [:material-docker: Docker guide][docker-guide]
|
||||
- [:material-source-branch: From source guide][from-source-guide]
|
||||
|
||||
In case you need any help, join our [Discord server][discord-server] where we may provide support.
|
||||
|
||||
## About Ellie
|
||||
|
||||
EllieBot is an [open source project][toastielab]. Any issues with the bot may be filed [here][issues].
|
||||
|
||||
If you're unsure whether something is an issue, ask in our support server first.
|
||||
|
||||
[Donations are welcome][donate], and we rely on your contributions to help keep the project alive.
|
||||
|
||||
[invite]: https://discordapp.com/oauth2/authorize?client_id=608119997713350679&scope=bot&permissions=66186303/
|
||||
[commands]: https://commands.elliebot.net/
|
||||
[desktop-guide]: ./guides/desktop-guide.md
|
||||
[cli-guide]: ./guides/cli-guide.md
|
||||
[docker-guide]: ./guides/docker-guide.md
|
||||
[from-source-guide]: ./guides/source-guide.md
|
||||
[discord-server]: https://discord.gg/etQdZxSyEH
|
||||
[toastielab]: https://toastielab.dev/EllieBotDevs/elliebot
|
||||
[issues]: https://toastielab.dev/EllieBotDevs/elliebot/issues
|
||||
[donate]: ./donate.md
|
18
docs/md/marmalade/canary-lifecycle.md
Normal file
18
docs/md/marmalade/canary-lifecycle.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Canary Lifecycle
|
||||
|
||||
*You can override several methods to hook into command handler's lifecycle.
|
||||
These methods start with `Exec*`*
|
||||
|
||||
|
||||
- `ExecOnMessageAsync` runs first right after any message was received
|
||||
- `ExecInputTransformAsync` runs after ExecOnMessageAsync and allows you to transform the message content before the bot looks for the matching command
|
||||
- `ExecPreCommandAsync` runs after a command was found but not executed, allowing you to potentially prevent command execution
|
||||
- `ExecPostCommandAsync` runs if the command was successfully executed
|
||||
- `ExecOnNoCommandAsync` runs instead of ExecPostCommandAsync if no command was found for a message
|
||||
|
||||
|
||||
*Besides that, canaries have 2 methods with which you can initialize and cleanup your canary*
|
||||
|
||||
|
||||
- `InitializeAsync` Runs when the marmalade which contains this canary is being loaded
|
||||
- `DisposeAsync` Runs when the marmalade which contains this canary is being unloaded
|
279
docs/md/marmalade/creating-a-marmalade.md
Normal file
279
docs/md/marmalade/creating-a-marmalade.md
Normal file
|
@ -0,0 +1,279 @@
|
|||
## Practice
|
||||
|
||||
This section will guide you through how to create a simple custom marmalade. You can find the entirety of this code hosted [here](https://toastielab.dev/ellie/example_marmalade)
|
||||
|
||||
#### Prerequisite
|
||||
- [.net8 sdk](https://dotnet.microsoft.com/en-us/download) installed
|
||||
- Optional: use [vscode](https://code.visualstudio.com/download) to write code
|
||||
|
||||
#### There are currently two ways of creating a marmalade and you can view both of them using the tabs below.
|
||||
|
||||
/// tab | Using our template
|
||||
|
||||
### Prerequisite
|
||||
- [git](https://git-scm.com/downloads) installed
|
||||
|
||||
### Guide
|
||||
|
||||
- Open your favorite terminal and navigate to a folder where you will keep your project .
|
||||
|
||||
- Install the ellie-marmalade template
|
||||
- `git clone https://toastielab.dev/EllieBotDevs/ellie-marmalade`
|
||||
- `cd ellie-marmalade`
|
||||
- `dotnet new install .\`
|
||||
|
||||
- Create a new folder and move into it
|
||||
- `mkdir example_marmalade `
|
||||
- `cd example_marmalade`
|
||||
|
||||
- Make a new Ellie Marmalade project
|
||||
- `dotnet new ellie-marmalade`
|
||||
- This can be any name you want you just have to specify `-n <any name you want here>` after the first part of the command
|
||||
- Here is an example `dotnet new ellie-marmalade -n my-cool-marmalade`
|
||||
- This will create a marmalade project with the name my-cool-marmalade
|
||||
|
||||
Now follow the instructions below and you should be good to go.
|
||||
///
|
||||
|
||||
/// tab | Building from scratch
|
||||
|
||||
### Guide
|
||||
|
||||
!!! info
|
||||
This requires writing a little bit of code but we will help you through it as much as we can.
|
||||
|
||||
|
||||
### Without any further issues we shall now begin
|
||||
|
||||
- Open your favorite terminal and navigate to a folder where you will keep your project .
|
||||
|
||||
- Create a new folder
|
||||
- `mkdir example_marmalade`
|
||||
- Create a new .net class library
|
||||
- `dotnet new classlib`
|
||||
- Open the current folder with your favorite editor/IDE. In this case we'll use VsCode
|
||||
- `code .`
|
||||
- Remove the `Class1.cs` file
|
||||
- Replace the contents of the `.csproj` file with the following contents
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<!-- Reduces some boilerplate in your .cs files -->
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!-- Use latest .net features -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
<EnablePreviewFeatures>true</EnablePreviewFeatures>
|
||||
<GenerateRequiresPreviewFeaturesAttribute>true</GenerateRequiresPreviewFeaturesAttribute>
|
||||
|
||||
<!-- tell .net that this library will be used as a plugin -->
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Base marmalade package. You MUST reference this in order to have a working marmalade -->
|
||||
<!-- Also, this package comes from Toastielab, which requires you to have a NuGet.Config file next to your .csproj -->
|
||||
<PackageReference Include="Ellie.Marmalade" Version="6.*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Note: If you want to use EllieBot services etc... You will have to manually clone
|
||||
the https://toastielab.dev/EllieBotDevs/elliebot repo locally and reference the EllieBot.csproj because there is no EllieBot package atm.
|
||||
It is strongly recommended that you checkout a specific tag which matches your version of ellie,
|
||||
as there could be breaking changes even between minor versions of EllieBot.
|
||||
For example if you're running EllieBot 4.1.0 locally for which you want to create a marmalade for,
|
||||
you should do "git checkout 4.1.0" in your EllieBot solution and then reference the EllieBot.csproj
|
||||
-->
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy shortcut and full strings to output (if they exist) -->
|
||||
<ItemGroup>
|
||||
<None Update="res.yml;cmds.yml;strings/**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
```
|
||||
- Create a `MyCanary.cs` file and add the following contents
|
||||
```cs
|
||||
using EllieBot.Marmalade;
|
||||
using Discord;
|
||||
|
||||
public sealed class MyCanary : Canary
|
||||
{
|
||||
[cmd]
|
||||
public async Task Hello(AnyContext ctx)
|
||||
{
|
||||
await ctx.Channel.SendMessageAsync($"Hello everyone!");
|
||||
}
|
||||
|
||||
[cmd]
|
||||
public async Task Hello(AnyContext ctx, IUser target)
|
||||
{
|
||||
await ctx.ConfirmLocalizedAsync("hello", target);
|
||||
}
|
||||
}
|
||||
```
|
||||
- Create `res.yml` and `cmds.yml` files with the following contents
|
||||
|
||||
`res.yml`
|
||||
```yml
|
||||
marmalade.description: "This is my marmalade's description"
|
||||
hello: "Hello {0}, from res.yml!"
|
||||
```
|
||||
|
||||
`cmds.yml`
|
||||
```yml
|
||||
hello:
|
||||
desc: "This is a basic hello command"
|
||||
args:
|
||||
- ""
|
||||
- "@Someone"
|
||||
```
|
||||
|
||||
- Add `NuGet.Config` file which will let you use the base Ellie.Marmalade package. This file should always look like this and you shouldn't change it
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="toastielab.dev" value="https://toastielab.dev/api/packages/ellie/nuget/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
```
|
||||
///
|
||||
|
||||
### Build it
|
||||
|
||||
- Build your Marmalade into a dll that Ellie can load. In your terminal, type:
|
||||
- `dotnet publish -o bin/marmalades/example_marmalade /p:DebugType=embedded`
|
||||
|
||||
- Done. You can now try it out in action.
|
||||
|
||||
### Try it out
|
||||
|
||||
- Copy the `bin/marmalades/example_marmalade` folder into your EllieBot's `data/marmalades/` folder. (EllieBot version 4.1.0+)
|
||||
|
||||
- Load it with `.maload example_marmalade`
|
||||
|
||||
- In the channel your bot can see, run the following commands to try it out
|
||||
- `.hello` and
|
||||
- `.hello @<someone>`
|
||||
|
||||
- Check its information with
|
||||
- `.mainfo example_marmalade`
|
||||
|
||||
- Unload it
|
||||
- `.maunload example_marmalade`
|
||||
|
||||
- 🎉 Congrats! You've just made your first marmalade! 🎉
|
||||
|
||||
## Theory
|
||||
|
||||
Marmalade system allows you to write independent marmalades (known as "modules", "cogs" or "plugins" in other software) which you can then load, unload and update at will without restarting the bot.
|
||||
|
||||
The marmalade base classes used for development are open source [here](https://toastielab.dev/EllieBotDevs/elliebot/src/branch/v5/src/Ellie.Marmalade) in case you need reference, as there is no generated documentation at the moment.
|
||||
|
||||
### Term list
|
||||
|
||||
#### Marmalade
|
||||
|
||||
- The project itself which compiles to a single `.dll` (and some optional auxiliary files), it can contain multiple [Canaries](#canary), [Services](#service), and [ParamParsers](#param-parser)
|
||||
|
||||
#### Canary
|
||||
|
||||
- A class which will be added as a single Module to EllieBot on load. It also acts as a [lifecycle handler](canary-lifecycle.md) and as a singleton service with the support for initialize and cleanup.
|
||||
- It can contain a Canary (called SubCanary) but only 1 level of nesting is supported (you can only have a canary contain a subcanary, but a subcanary can't contain any other canaries)
|
||||
- Canaries can have their own prefix
|
||||
- For example if you set this to 'test' then a command called 'cmd' will have to be invoked by using `.test cmd` instead of `.cmd`
|
||||
|
||||
#### Canary Command
|
||||
|
||||
- Acts as a normal command
|
||||
- Has context injected as a first argument which controls where the command can be executed
|
||||
- `AnyContext` the command can be executed in both DMs and Servers
|
||||
- `GuildContext` the command can only be executed in Servers
|
||||
- `DmContext` the command can only be executed in DMs
|
||||
- Support the usual features such as default values, leftover, params, etc.
|
||||
- It also supports dependency injection via `[inject]` attribute. These dependencies must come after the context and before any input parameters
|
||||
- Supports `ValueTask`, `Task`, `Task<T>` and `void` return types
|
||||
|
||||
#### Param Parser
|
||||
|
||||
- Allows custom parsing of command arguments into your own types.
|
||||
- Overriding existing parsers (for example for IGuildUser, etc...) can cause issues.
|
||||
|
||||
#### Service
|
||||
|
||||
- Usually not needed.
|
||||
- They are marked with a `[svc]` attribute, and offer a way to inject dependencies to different parts of your marmalade.
|
||||
- Transient and Singleton lifetimes are supported.
|
||||
|
||||
### Localization
|
||||
|
||||
Response and command strings can be kept in one of three different places based on whether you plan to allow support for localization
|
||||
|
||||
option 1) `res.yml` and `cmds.yml`
|
||||
|
||||
If you don't plan on having your app localized, but you just *may* in the future, you should keep your strings in the `res.yml` and `cmds.yml` file the root folder of your project, and they will be automatically copied to the output whenever you build your marmalade.
|
||||
|
||||
##### Example project folder structure:
|
||||
- uwu/
|
||||
- uwu.csproj
|
||||
- uwu.cs
|
||||
- res.yml
|
||||
- cmds.yml
|
||||
|
||||
##### Example output folder structure:
|
||||
- marmalades/uwu/
|
||||
- uwu.dll
|
||||
- res.yml
|
||||
- cmds.yml
|
||||
|
||||
option 2) `strings` folder
|
||||
|
||||
If you plan on having your app localized (or want to allow your consumers to easily add languages themselves), you should keep your response strings in the `strings/res/en-us.yml` and your command strings in `strings/cmds/en-us.yml` file. This will be your base file, and from there you can make support for additional languages, for example `strings/res/ru-ru.yml` and `strings/cmds/ru-ru.yml`
|
||||
|
||||
##### Example project folder structure:
|
||||
- uwu/
|
||||
- uwu.csproj
|
||||
- uwu.cs
|
||||
- strings/
|
||||
- res/
|
||||
- en-us.yml
|
||||
- cmds/
|
||||
- en-us.yml
|
||||
|
||||
##### Example output folder structure:
|
||||
- marmalades/uwu/
|
||||
- uwu.dll
|
||||
- strings/
|
||||
- res/
|
||||
- en-us.yml
|
||||
- cmds/
|
||||
- en-us.yml
|
||||
|
||||
option 3) In the code
|
||||
|
||||
If you don't want any auxiliary files, and you don't want to bother making new .yml files to keep your strings in, you can specify the command strings directly in the `[cmd]` attribute itself, and use non-localized methods for message sending in your commands.
|
||||
|
||||
If you update your response strings .yml file(s) while the marmalade is loaded and running, running `.stringsreload` will reload the responses without the need to reload the marmalade or restart the bot.
|
||||
|
||||
#### Bot marmalade config file
|
||||
|
||||
- Marmalade config is kept in `marmalades/marmalade.yml` file
|
||||
- At the moment this config only keeps track of which marmalades are currently loaded (they will also be always loaded at startup)
|
||||
- If a marmalade is causing issues and you're unable to unload it, you can remove it from the `loaded:` list in this config file and restart the bot. It won't be loaded next time the bot is started up
|
||||
|
||||
#### Unloadability issues
|
||||
|
||||
To make sure your marmalade can be properly unloaded/reloaded you must:
|
||||
|
||||
- Make sure that none of your types and objects are referenced by the Bot or Bot's services after the DisposeAsync is called on your Canary instances.
|
||||
|
||||
- Make sure that all of your commands execute quickly and don't have any long running tasks, as they will hold a reference to a type from your assembly
|
||||
|
||||
- If you are still having issues, you can always run `.maunload` followed by a bot restart, or if you want to find what is causing the marmalade unloadability issues, you can check the [microsoft's assembly unloadability debugging guide](https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability)
|
31
docs/md/marmalade/getting-started.md
Normal file
31
docs/md/marmalade/getting-started.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
## Getting Started
|
||||
|
||||
### What is the Marmalade system?
|
||||
|
||||
- It is a dynamic module/plugin/cog system for EllieBot introduced in **EllieBot 4.1.0**
|
||||
|
||||
- Allows developers to add custom functionality to Ellie without modifying the original code
|
||||
|
||||
- Allows for those custom features to be updated during bot runtime (if properly written), without the need for bot restart.
|
||||
|
||||
- They are added to `data/marmalades` folder and are loaded, unloaded and handled through discord commands.
|
||||
- `.maload` Loads the specified marmalade (see `.h .maload`)
|
||||
- `.maunload` Unloads the specified marmalade (see `.h .maunload`)
|
||||
- `.mainfo` Checks marmalades information (see `.h .mainfo`)
|
||||
- `.malist` Lists the available marmalades (see `.h .malist`)
|
||||
|
||||
### How to make one?
|
||||
|
||||
Marmalades are written in [C#](https://docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/) programming language, so you will need at least low-intermediate knowledge of it in order to make a useful Marmalade.
|
||||
|
||||
Follow the [creating a marmalade guide](creating-a-marmalade.md)
|
||||
|
||||
### Where to get marmalades other people made?
|
||||
|
||||
⚠ *It is EXTREMELY, and I repeat **EXTREMELY** dangerous to run marmalades of strangers or people you don't FULLY trust.* ⚠
|
||||
|
||||
⚠ *It can not only lead to your bot being stolen, but it also puts your entire computer and personal files in jeopardy.* ⚠
|
||||
|
||||
**It is strongly recommended to run only the marmalades you yourself wrote, and only on a hosted VPS or dedicated server which ONLY hosts your bot, to minimize the potential damage caused by bad actors.**
|
||||
|
||||
No easy way at the moment, except asking in the `#dev-and-modding` chat in [Ellie's Home server](https://discord.gg/etQdZxSyEH)
|
18
docs/md/snippets/supported-platforms.md
Normal file
18
docs/md/snippets/supported-platforms.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!-- TODO: These should potentially be reformated to be more readable... -->
|
||||
--8<-- [start:windows]
|
||||
- **Windows 10** or later (64-bit)
|
||||
--8<-- [end:windows]
|
||||
--8<-- [start:linux]
|
||||
- **Ubuntu**: 20.04, 22.04, 24.04
|
||||
- **Mint**: 19, 20, 21
|
||||
- **Debian**: 10, 11, 12
|
||||
- **RockyLinux**: 8, 9
|
||||
- **AlmaLinux**: 8, 9
|
||||
- **openSUSE Leap**: 15.5, 15.6
|
||||
- **openSUSE Tumbleweed**
|
||||
- **Fedora**: 38, 39, 40, 41, 42
|
||||
- **Arch** & **Artix**
|
||||
--8<-- [end:linux]
|
||||
--8<-- [start:macos]
|
||||
- **macOS 13 (Ventura)** or later
|
||||
--8<-- [end:macos]
|
80
docs/mkdocs.yml
Normal file
80
docs/mkdocs.yml
Normal file
|
@ -0,0 +1,80 @@
|
|||
site_name: EllieBot docs
|
||||
site_url: https://docs.elliebot.net
|
||||
repo_url: 'https://toastielab.dev/EllieBotDevs/elliebot'
|
||||
site_author: Toastie_t0ast
|
||||
docs_dir: 'md'
|
||||
copyright: Copyright © 2018 - 2025 Toastie_t0ast & EllieBotDevs
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: indigo
|
||||
accent: blue
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: black
|
||||
accent: blue
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to light mode
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
font:
|
||||
text: Source Sans 3
|
||||
code: Source Code Pro
|
||||
logo: assets/favicon.png
|
||||
favicon: assets/favicon.png
|
||||
icon:
|
||||
repo: material/git
|
||||
extra:
|
||||
homepage: https://elliebot.net
|
||||
plugins:
|
||||
- search
|
||||
- exclude:
|
||||
glob:
|
||||
- 'guides/vps-linux-guide.md'
|
||||
- 'snippets/supported-platforms.md'
|
||||
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
- codehilite:
|
||||
guess_lang: false
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- admonition
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.superfences
|
||||
- pymdownx.blocks.tab:
|
||||
alternate_style: true
|
||||
- pymdownx.snippets
|
||||
- pymdownx.details
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
options:
|
||||
custom_icons:
|
||||
- overrides/.icons
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Guides:
|
||||
- Desktop Guide: guides/desktop-guide.md
|
||||
- CLI Guide: guides/cli-guide.md
|
||||
- Docker Guide: guides/docker-guide.md
|
||||
- Source Guide: guides/source-guide.md
|
||||
- Commands:
|
||||
- Commands List: https://commands.elliebot.net
|
||||
- Features Explained:
|
||||
- Basic Creds: creds-guide.md
|
||||
- Marmalade System:
|
||||
- marmalade/getting-started.md
|
||||
- marmalade/creating-a-marmalade.md
|
||||
- marmalade/canary-lifecycle.md
|
||||
- Donate: donate.md
|
|
@ -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.10</Version>
|
||||
<Version>6.1.2</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
|
||||
});
|
||||
var gc = uow.GuildConfigsForId(guildId);
|
||||
gc.DeleteMessageOnCommand = !gc.DeleteMessageOnCommand;
|
||||
|
||||
if (conf.Length == 0)
|
||||
return false;
|
||||
|
||||
var val = conf[0].Inserted.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)
|
||||
|
|
|
@ -27,6 +27,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
private readonly UserPunishService _punishService;
|
||||
private readonly INotifySubscriber _notifySub;
|
||||
private readonly ShardData _shardData;
|
||||
|
||||
private readonly Channel<PunishQueueItem> _punishUserQueue =
|
||||
Channel.CreateUnbounded<PunishQueueItem>(new()
|
||||
{
|
||||
|
@ -176,10 +177,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
try
|
||||
{
|
||||
if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings)
|
||||
|| spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new()
|
||||
{
|
||||
ChannelId = channel.Id
|
||||
}))
|
||||
|| spamSettings.AntiSpamSettings.IgnoredChannels.Any(x => x.ChannelId == channel.Id))
|
||||
return;
|
||||
|
||||
var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id,
|
||||
|
@ -275,23 +273,25 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
await using var uow = _db.GetDbContext();
|
||||
|
||||
await uow.GetTable<AntiRaidSetting>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = action,
|
||||
Seconds = seconds,
|
||||
UserThreshold = userThreshold,
|
||||
PunishDuration = minutesDuration
|
||||
}, _ => new()
|
||||
{
|
||||
Action = action,
|
||||
Seconds = seconds,
|
||||
UserThreshold = userThreshold,
|
||||
PunishDuration = minutesDuration
|
||||
}, () => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = action,
|
||||
Seconds = seconds,
|
||||
UserThreshold = userThreshold,
|
||||
PunishDuration = minutesDuration
|
||||
},
|
||||
_ => new()
|
||||
{
|
||||
Action = action,
|
||||
Seconds = seconds,
|
||||
UserThreshold = userThreshold,
|
||||
PunishDuration = minutesDuration
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
|
||||
|
||||
return stats;
|
||||
|
@ -364,23 +364,25 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<AntiSpamSetting>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = stats.AntiSpamSettings.Action,
|
||||
MessageThreshold = stats.AntiSpamSettings.MessageThreshold,
|
||||
MuteTime = stats.AntiSpamSettings.MuteTime,
|
||||
RoleId = stats.AntiSpamSettings.RoleId
|
||||
}, (old) => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = stats.AntiSpamSettings.Action,
|
||||
MessageThreshold = stats.AntiSpamSettings.MessageThreshold,
|
||||
MuteTime = stats.AntiSpamSettings.MuteTime,
|
||||
RoleId = stats.AntiSpamSettings.RoleId
|
||||
}, () => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = stats.AntiSpamSettings.Action,
|
||||
MessageThreshold = stats.AntiSpamSettings.MessageThreshold,
|
||||
MuteTime = stats.AntiSpamSettings.MuteTime,
|
||||
RoleId = stats.AntiSpamSettings.RoleId
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = stats.AntiSpamSettings.Action,
|
||||
MessageThreshold = stats.AntiSpamSettings.MessageThreshold,
|
||||
MuteTime = stats.AntiSpamSettings.MuteTime,
|
||||
RoleId = stats.AntiSpamSettings.RoleId
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
@ -389,7 +391,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
{
|
||||
var obj = new AntiSpamIgnore
|
||||
{
|
||||
ChannelId = channelId
|
||||
ChannelId = channelId,
|
||||
};
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
@ -405,7 +407,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
if (spam.IgnoredChannels.All(x => x.ChannelId != channelId))
|
||||
{
|
||||
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
|
||||
temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache
|
||||
temp.AntiSpamSettings.IgnoredChannels.Add(obj);
|
||||
|
||||
spam.IgnoredChannels.Add(obj);
|
||||
added = true;
|
||||
|
@ -417,7 +419,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
uow.Set<AntiSpamIgnore>().Remove(toRemove);
|
||||
|
||||
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
|
||||
temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache
|
||||
temp.AntiSpamSettings.IgnoredChannels.RemoveAll(x => x.ChannelId == channelId);
|
||||
|
||||
added = false;
|
||||
}
|
||||
|
@ -462,22 +464,24 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
|
||||
await uow.GetTable<AntiAltSetting>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = action,
|
||||
ActionDurationMinutes = actionDurationMinutes,
|
||||
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
|
||||
RoleId = roleId
|
||||
}, _ => new()
|
||||
{
|
||||
Action = action,
|
||||
ActionDurationMinutes = actionDurationMinutes,
|
||||
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
|
||||
RoleId = roleId
|
||||
}, () => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
{
|
||||
GuildId = guildId,
|
||||
Action = action,
|
||||
ActionDurationMinutes = actionDurationMinutes,
|
||||
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
|
||||
RoleId = roleId
|
||||
},
|
||||
_ => new()
|
||||
{
|
||||
Action = action,
|
||||
ActionDurationMinutes = actionDurationMinutes,
|
||||
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
|
||||
RoleId = roleId
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
|
||||
_antiAltGuilds[guildId] = new(new()
|
||||
{
|
||||
|
@ -525,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();
|
||||
|
|
|
@ -32,10 +32,11 @@ flowchart TD
|
|||
|
||||
A[.fish] --> B[Catch Fish]
|
||||
B --> C["Show off with .fili"]
|
||||
B --> G["Show up in fishfeed"] --> E
|
||||
B --> D["Complete the set"]:::todo
|
||||
D --> E[Recognition]:::todo & F[Buff?]:::todo
|
||||
D --> E[Recognition] & F[Buff?]:::todo
|
||||
F --> B
|
||||
B --> X[Buy equipment]:::todo
|
||||
X1[Modify Odds]:::todo --> B
|
||||
B --> X[Buy equipment]
|
||||
X1[Modify Odds] --> B
|
||||
X --> X1
|
||||
```
|
||||
|
|
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,92 @@ public sealed class FishConfigService : ConfigServiceBase<FishConfig>
|
|||
IPubSub pubSub)
|
||||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
AddParsedProp("captcha",
|
||||
static (conf) => conf.RequireCaptcha,
|
||||
bool.TryParse,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("chance.nothing",
|
||||
static (conf) => conf.Chance.Nothing,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("chance.fish",
|
||||
static (conf) => conf.Chance.Fish,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("chance.trash",
|
||||
static (conf) => conf.Chance.Trash,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
if (data.Version < 11)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 11;
|
||||
if (c.Items is { Count: > 0 })
|
||||
return;
|
||||
c.Items =
|
||||
[
|
||||
new FishItem
|
||||
{
|
||||
Id = 1,
|
||||
ItemType = FishItemType.Pole,
|
||||
Name = "Wooden Rod",
|
||||
Description = "Better than catching it with bare hands.",
|
||||
Price = 1000,
|
||||
FishMultiplier = 1.2
|
||||
},
|
||||
new FishItem
|
||||
{
|
||||
Id = 11,
|
||||
ItemType = FishItemType.Pole,
|
||||
Name = "Magnet on a Stick",
|
||||
Description = "Attracts all trash, not just metal.",
|
||||
Price = 3000,
|
||||
FishMultiplier = 0.9,
|
||||
TrashMultiplier = 2
|
||||
},
|
||||
new FishItem
|
||||
{
|
||||
Id = 21,
|
||||
ItemType = FishItemType.Bait,
|
||||
Name = "Corn",
|
||||
Description = "Just some cooked corn.",
|
||||
Price = 100,
|
||||
Uses = 100,
|
||||
RareMultiplier = 1.1
|
||||
},
|
||||
new FishItem
|
||||
{
|
||||
Id = 31,
|
||||
ItemType = FishItemType.Potion,
|
||||
Name = "A Cup of Tea",
|
||||
Description = "Helps you focus.",
|
||||
Price = 12000,
|
||||
DurationMinutes = 30,
|
||||
MaxStarMultiplier = 1.1,
|
||||
FishingSpeedMultiplier = 1.01
|
||||
},
|
||||
new FishItem
|
||||
{
|
||||
Id = 41,
|
||||
ItemType = FishItemType.Boat,
|
||||
Name = "Canoe",
|
||||
Description = "Lets you fish a little faster.",
|
||||
Price = 3000,
|
||||
FishingSpeedMultiplier = 1.201,
|
||||
MaxStarMultiplier = 1.1
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
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 buy 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}!"
|
||||
}
|
|
@ -44,7 +44,7 @@ public sealed class DefaultHangmanSource : IHangmanSource
|
|||
public IReadOnlyCollection<string> GetCategories()
|
||||
=> termsDict.Keys.ToList();
|
||||
|
||||
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term)
|
||||
public bool GetTerm(string? category, [NotNullWhen(true)] out (HangmanTerm Term, string Category)? term)
|
||||
{
|
||||
if (category is null)
|
||||
{
|
||||
|
@ -54,7 +54,7 @@ public sealed class DefaultHangmanSource : IHangmanSource
|
|||
|
||||
if (termsDict.TryGetValue(category, out var terms))
|
||||
{
|
||||
term = terms[_rng.Next(0, terms.Length)];
|
||||
term = (terms[_rng.Next(0, terms.Length)], category);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,44 +10,51 @@ 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)
|
||||
{
|
||||
var eb = sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word));
|
||||
|
||||
if (state.Phase == HangmanGame.Phase.Running)
|
||||
{
|
||||
return sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
return eb
|
||||
.WithFooter(state.MissedLetters.Join(' '))
|
||||
.WithAuthor(state.Category);
|
||||
}
|
||||
|
||||
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(' '));
|
||||
return eb
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
}
|
||||
|
||||
return sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Hangman", Draw(state))
|
||||
.AddField("Guess", Format.Code(state.Word))
|
||||
.WithFooter(state.MissedLetters.Join(' '));
|
||||
return eb.WithFooter(state.MissedLetters.Join(' '));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue