forked from EllieBotDevs/elliebot
Merge pull request 'v6' (#2) from EllieBotDevs/elliebot:v6 into v6
Reviewed-on: #2
This commit is contained in:
commit
7dba1ebec4
164 changed files with 20692 additions and 16102 deletions
.gitignoreCHANGELOG.mdLICENSE
src
EllieBot.GrpcApiBase
EllieBot
Bot.cs
Db
EllieBot.csprojMigrations
PostgreSql
20250202124905_init.Designer.cs20250225212147_xp-excl-xp-rate.sql20250226215159_xp-rate-rework.sql20250228044141_notify-allow-origin-channel.sql20250310101121_userroles.sql20250310143026_club-banners.sql20250315193825_fs-prettyname.sql20250315225518_prot-delete-gcid.sql20250317063129_scheduled-commands.sql20250318221943_xpexclusion.sql20250319010757_livechannels.sql20250319010930_init.Designer.cs20250319010930_init.csPostgreSqlContextModelSnapshot.cs
Sqlite
20250126062816_cleanup.sql20250202124903_init.Designer.cs20250225212144_xp-excl-xp-rate.sql20250226215156_xp-rate-rework.sql20250228044138_notify-allow-origin-channel.sql20250310101118_userroles.sql20250310143023_club-banners.sql20250315193822_fs-prettyname.sql20250315225515_prot-delete-gcid.sql20250317063119_scheduled-commands.sql20250318221922_xpexclusion.sql20250319010745_livechannels.sql20250319010920_init.Designer.cs20250319010920_init.csSqliteContextModelSnapshot.cs
Modules
Administration
Mute
Notify
Protection
Self
Timezone
UserPunish
Gambling
Games
Help
Music
Patronage
Permissions/Filter
Searches
Crypto
StreamNotification
_common
Utility/Info
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -371,4 +371,10 @@ site/
|
|||
|
||||
.aider.*
|
||||
PROMPT.md
|
||||
.aider*
|
||||
.aider*
|
||||
.windsurfrules
|
||||
|
||||
## Python pip/env files
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
.venv
|
124
CHANGELOG.md
124
CHANGELOG.md
|
@ -1,6 +1,128 @@
|
|||
# Changelog
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
|
||||
*a,c,f,r,o*
|
||||
|
||||
## [6.0.9] - 19.03.2025
|
||||
|
||||
### Changed
|
||||
|
||||
- `.cinfo` now also has a member list
|
||||
|
||||
### Fixed
|
||||
|
||||
- `.antispamignore` will now properly persist through restarts
|
||||
- livechannels and scheduled commands will now be inside utility module as they should
|
||||
|
||||
## [6.0.8] - 19.03.2025
|
||||
|
||||
### Added
|
||||
|
||||
- Live channel commands
|
||||
- `.lcha` adds a channel with a template message (supports placeholders, and works on category channels too!)
|
||||
- Every 10 minutes, channel name will be updated
|
||||
- example: `.lcha #my-channel --> Members: %server.members% <--` will display the number of members in the server as a channel name, updating once every 10 minutes
|
||||
- `.lchl` lists all live channels (Up to 5)
|
||||
- `.lchd <channel or channelId>` removed a live channel
|
||||
|
||||
### Fixed
|
||||
|
||||
- `.antispamignore` fixed
|
||||
|
||||
## [6.0.7] - 19.03.2025
|
||||
|
||||
### Added
|
||||
|
||||
- Schedule commands!
|
||||
- `.scha <time> <text>` adds the command to be excuted after the specified amount of time
|
||||
- `.schd <id>` deletes the command with the specified id
|
||||
- `.schl` lists your scheduled commands
|
||||
- `.masskick` added as massban and masskill already exist
|
||||
- `.xpex` and `.xpexl` are back, as there was no way to exclude specific users or roles with .xprate
|
||||
|
||||
### Fix
|
||||
|
||||
- `.xprate` will now (as exclusion did) respect parent channel xp rates in threads
|
||||
- the xprate system will first check if a thread channel has a rate set
|
||||
- if it doesn't it will try to use the parent channel's rate
|
||||
|
||||
## [6.0.6] - 16.03.2025
|
||||
|
||||
### Added
|
||||
|
||||
- Added youtube live stream notification support for `.streamadd`
|
||||
- it only works by using an invidious instance (with a working api) from data/searches.yml
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.hangman` not receiving input sometimes
|
||||
- Fixed `.sfl` and similar toggles not working
|
||||
- Fixed `.antialt` and other protection commands not properly turning on
|
||||
- Fixed `%bot.time%` and `%bot.date%` placeholders showing wrong date.
|
||||
- No longer a timestamp
|
||||
|
||||
## [6.0.5] - 15.03.2025
|
||||
|
||||
### Added
|
||||
|
||||
- Aded a title in `.whosplaying`
|
||||
- Added a crown emoji next to commands if -v 1 or -v2 option is specified
|
||||
|
||||
### Changed
|
||||
|
||||
- `.remind` looks better
|
||||
- `.savechat` no longer owner only, up to 1000 messages - unlimited if ran by the bot owner
|
||||
|
||||
### Fixed
|
||||
|
||||
- `.ropl` fixed
|
||||
|
||||
## [6.0.4] - 14.03.2025
|
||||
|
||||
### Added
|
||||
|
||||
- `.xp` system reworked
|
||||
- Global XP has been removed in favor of server XP
|
||||
- You can now set `.xprate` for each channel in your server!
|
||||
- You can set voice, image, and text rates
|
||||
- Use `.xpratereset` to reset it back to default
|
||||
- This feature makes `.xpexclude` obsolete
|
||||
- Requirement to create a club removed
|
||||
- `.xp` card should generate faster
|
||||
- Fixed countless possible issues with xp where some users didn't gain xp, or froze, etc
|
||||
- user-role commands added!
|
||||
- `.ura <user> <role>` - assign a role to a user
|
||||
- `.url <user?>` - list assigned roles for all users or a specific user
|
||||
- `.urm` - show 'my' (your) assigned roles
|
||||
- `.urn <role> <new_name>` - set a name for your role
|
||||
- `.urc <role> <hex_color>` - set a color for your role
|
||||
- `.uri <role> <url/server_emoji>` - set an icon for your role (accepts either a server emoji or a link to an image)
|
||||
- `.notify` improved
|
||||
- Lets you specify source channel (for some events) as the message output
|
||||
- `.pload <id> --shuffle` lets you load a saved playlist in random order
|
||||
- `.lyrics <song_name>` added - find lyrics for a song (it's not always accurate)
|
||||
|
||||
- For Selfhosters
|
||||
- you have to update to latest v5 before updating to v6, otherwise migrations will fail
|
||||
- migration system was reworked
|
||||
- Xp card is now 500x245
|
||||
- xp_template.json backed up to old_xp_template.json
|
||||
- check pinned message in #dev channel to see full selfhoster announcement
|
||||
- Get bot version via --version
|
||||
|
||||
### Changed
|
||||
|
||||
- `.lopl` will queue subdirectories too now
|
||||
- Some music playlist commands have been renamed to fit with other commands
|
||||
- Removed gold/silver frames from xp card
|
||||
- `.inrole` is now showing users in alphabetical order
|
||||
- `.curtrs` are now paginated
|
||||
- pagination now lasts for 10+ minutes
|
||||
- selfhosters: Restart command default now assumes binary installation
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed several fonts
|
||||
- Xp Exclusion commands (superseded by `.xprate`)
|
||||
|
||||
## [5.3.9] - 31.01.2025
|
||||
|
||||
|
|
756
LICENSE
756
LICENSE
|
@ -1,201 +1,619 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
1. Definitions.
|
||||
Preamble
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
0. Definitions.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
1. Source Code.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
2. Basic Permissions.
|
||||
|
||||
Copyright 2025 Toastie_t0ast
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
|
@ -9,7 +9,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
|
||||
<PackageReference Include="Grpc" Version="2.46.6" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.69.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.68.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -12,7 +12,6 @@ service GrpcOther {
|
|||
rpc GetRoles(GetRolesRequest) returns (GetRolesReply);
|
||||
|
||||
rpc GetCurrencyLb(GetLbRequest) returns (CurrencyLbReply);
|
||||
rpc GetXpLb(GetLbRequest) returns (XpLbReply);
|
||||
rpc GetWaifuLb(GetLbRequest) returns (WaifuLbReply);
|
||||
|
||||
rpc GetShardStats(google.protobuf.Empty) returns (stream ShardStatsReply);
|
||||
|
@ -78,17 +77,6 @@ message GetLbRequest {
|
|||
int32 perPage = 2;
|
||||
}
|
||||
|
||||
message XpLbReply {
|
||||
repeated XpLbEntryReply entries = 1;
|
||||
}
|
||||
|
||||
message XpLbEntryReply {
|
||||
string user = 1;
|
||||
uint64 userId = 2;
|
||||
int64 totalXp = 3;
|
||||
int64 level = 4;
|
||||
}
|
||||
|
||||
message WaifuLbReply {
|
||||
repeated WaifuLbEntry entries = 1;
|
||||
}
|
||||
|
|
|
@ -10,22 +10,8 @@ service GrpcXp {
|
|||
|
||||
rpc GetXpSettings(GetXpSettingsRequest) returns (GetXpSettingsReply);
|
||||
|
||||
rpc AddExclusion(AddExclusionRequest) returns (AddExclusionReply);
|
||||
rpc DeleteExclusion(DeleteExclusionRequest) returns (DeleteExclusionReply);
|
||||
|
||||
rpc AddReward(AddRewardRequest) returns (AddRewardReply);
|
||||
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
|
||||
|
||||
rpc SetServerExclusion(SetServerExclusionRequest) returns (SetServerExclusionReply);
|
||||
}
|
||||
|
||||
message SetServerExclusionRequest {
|
||||
uint64 guildId = 1;
|
||||
bool serverExcluded = 2;
|
||||
}
|
||||
|
||||
message SetServerExclusionReply {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message GetXpLbRequest {
|
||||
|
@ -57,47 +43,19 @@ message ResetUserXpReply {
|
|||
}
|
||||
|
||||
message GetXpSettingsReply {
|
||||
repeated ExclItemReply exclusions = 1;
|
||||
repeated RewItemReply rewards = 2;
|
||||
bool serverExcluded = 3;
|
||||
}
|
||||
|
||||
message GetXpSettingsRequest {
|
||||
uint64 guildId = 1;
|
||||
}
|
||||
|
||||
message ExclItemReply {
|
||||
string type = 1;
|
||||
uint64 id = 2;
|
||||
string name = 3;
|
||||
}
|
||||
|
||||
message RewItemReply {
|
||||
int32 level = 1;
|
||||
string type = 2;
|
||||
string value = 3;
|
||||
}
|
||||
|
||||
message AddExclusionRequest {
|
||||
uint64 guildId = 1;
|
||||
string type = 2;
|
||||
uint64 id = 3;
|
||||
}
|
||||
|
||||
message AddExclusionReply {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message DeleteExclusionRequest {
|
||||
uint64 guildId = 1;
|
||||
string type = 2;
|
||||
uint64 id = 3;
|
||||
}
|
||||
|
||||
message DeleteExclusionReply {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message AddRewardRequest {
|
||||
uint64 guildId = 1;
|
||||
int32 level = 2;
|
||||
|
|
|
@ -16,7 +16,6 @@ public sealed class Bot : IBot
|
|||
|
||||
private IContainer Services { get; set; }
|
||||
|
||||
public bool IsReady { get; private set; }
|
||||
public int ShardId { get; }
|
||||
|
||||
private readonly IBotCreds _creds;
|
||||
|
@ -97,7 +96,7 @@ public sealed class Bot : IBot
|
|||
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId);
|
||||
uow.EnsureUserCreated(bot.Id, bot.Username, bot.AvatarId);
|
||||
}
|
||||
|
||||
// var svcs = new StandardKernel(new NinjectSettings()
|
||||
|
@ -262,19 +261,20 @@ public sealed class Bot : IBot
|
|||
Stopwatch.GetElapsedTime(startTime).TotalSeconds);
|
||||
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
||||
|
||||
// start handling messages received in commandhandler
|
||||
await commandHandler.StartHandling();
|
||||
|
||||
|
||||
foreach (var a in _loadedAssemblies)
|
||||
{
|
||||
await _commandService.AddModulesAsync(a, Services);
|
||||
}
|
||||
|
||||
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
|
||||
IsReady = true;
|
||||
|
||||
await EnsureBotOwnershipAsync();
|
||||
|
||||
await commandHandler.InitializeAsync();
|
||||
|
||||
_ = Task.Run(ExecuteReadySubscriptions);
|
||||
|
||||
await commandHandler.StartHandling();
|
||||
|
||||
Log.Information("Shard {ShardId} ready", Client.ShardId);
|
||||
}
|
||||
|
||||
|
|
|
@ -272,9 +272,6 @@ public abstract class EllieContext : DbContext
|
|||
du.Property(x => x.IsClubAdmin)
|
||||
.HasDefaultValue(false);
|
||||
|
||||
du.Property(x => x.NotifyOnLevelUp)
|
||||
.HasDefaultValue(XpNotificationLocation.None);
|
||||
|
||||
du.Property(x => x.TotalXp)
|
||||
.HasDefaultValue(0);
|
||||
|
||||
|
|
|
@ -94,14 +94,13 @@ public sealed class EllieDbService : DbService
|
|||
.OrderBy(x => x);
|
||||
|
||||
var lastApplied = applied.Last();
|
||||
Log.Information("Last applied migration: {LastApplied}", lastApplied);
|
||||
|
||||
// apply all migrations with names greater than the last applied
|
||||
foreach (var runnable in available)
|
||||
{
|
||||
if (string.Compare(lastApplied, runnable, StringComparison.Ordinal) < 0)
|
||||
{
|
||||
Log.Warning("Migration {MigrationName} has not been applied yet", runnable);
|
||||
Log.Warning("Applying migration {MigrationName}", runnable);
|
||||
|
||||
var query = await File.ReadAllTextAsync(GetMigrationPath(ctx.Database, runnable));
|
||||
await ctx.Database.ExecuteSqlRawAsync(query);
|
||||
|
|
|
@ -7,14 +7,23 @@ namespace EllieBot.Db;
|
|||
|
||||
public static class CurrencyTransactionExtensions
|
||||
{
|
||||
public static Task<List<CurrencyTransaction>> GetPageFor(
|
||||
public static async Task<IReadOnlyCollection<CurrencyTransaction>> GetPageFor(
|
||||
this DbSet<CurrencyTransaction> set,
|
||||
ulong userId,
|
||||
int page)
|
||||
=> set.ToLinqToDBTable()
|
||||
{
|
||||
var items = await set.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.DateAdded)
|
||||
.Skip(15 * page)
|
||||
.Take(15)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public static async Task<int> GetCountFor(this DbSet<CurrencyTransaction> set, ulong userId)
|
||||
=> await set.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId)
|
||||
.CountAsyncLinqToDB();
|
||||
}
|
|
@ -17,7 +17,6 @@ public static class DiscordUserExtensions
|
|||
this DbContext ctx,
|
||||
ulong userId,
|
||||
string username,
|
||||
string discrim,
|
||||
string avatarId)
|
||||
=> ctx.GetTable<DiscordUser>()
|
||||
.InsertOrUpdate(
|
||||
|
@ -65,11 +64,10 @@ public static class DiscordUserExtensions
|
|||
this DbContext ctx,
|
||||
ulong userId,
|
||||
string username,
|
||||
string discrim,
|
||||
string avatarId,
|
||||
Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
|
||||
{
|
||||
ctx.EnsureUserCreated(userId, username, discrim, avatarId);
|
||||
ctx.EnsureUserCreated(userId, username, avatarId);
|
||||
|
||||
IQueryable<DiscordUser> queryable = ctx.Set<DiscordUser>();
|
||||
if (includes is not null)
|
||||
|
@ -78,13 +76,6 @@ public static class DiscordUserExtensions
|
|||
}
|
||||
|
||||
|
||||
public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id)
|
||||
=> users.AsQueryable()
|
||||
.Where(x => x.TotalXp
|
||||
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
|
||||
.Count()
|
||||
+ 1;
|
||||
|
||||
public static Task<List<DiscordUser>> GetTopRichest(
|
||||
this DbSet<DiscordUser> users,
|
||||
ulong botId,
|
||||
|
|
|
@ -121,7 +121,6 @@ public static class GuildConfigExtensions
|
|||
.InsertWithOutputAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
ServerExcluded = false,
|
||||
});
|
||||
|
||||
return srs;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
namespace EllieBot.Db.Models;
|
||||
|
||||
|
||||
// FUTURE remove LastLevelUp from here and UserXpStats
|
||||
public class DiscordUser : DbEntity
|
||||
{
|
||||
public const string DEFAULT_USERNAME = "??Unknown";
|
||||
|
@ -15,7 +13,6 @@ public class DiscordUser : DbEntity
|
|||
public bool IsClubAdmin { get; set; }
|
||||
|
||||
public long TotalXp { get; set; }
|
||||
public XpNotificationLocation NotifyOnLevelUp { get; set; }
|
||||
|
||||
public long CurrencyAmount { get; set; }
|
||||
|
||||
|
|
|
@ -19,8 +19,9 @@ public class FollowedStream
|
|||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string PrettyName { get; set; } = null;
|
||||
public FType Type { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string Message { get; set; } = null;
|
||||
|
||||
protected bool Equals(FollowedStream other)
|
||||
=> ChannelId == other.ChannelId
|
||||
|
|
|
@ -8,7 +8,7 @@ public class Notify
|
|||
public int Id { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public ulong? ChannelId { get; set; }
|
||||
public NotifyType Type { get; set; }
|
||||
|
||||
[MaxLength(10_000)]
|
||||
|
@ -21,4 +21,5 @@ public enum NotifyType
|
|||
Protection = 1, Prot = 1,
|
||||
AddRoleReward = 2,
|
||||
RemoveRoleReward = 3,
|
||||
// BigWin = 4,
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using LinqToDB.Mapping;
|
||||
using DataType = LinqToDB.DataType;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
|
@ -9,11 +11,7 @@ public class AntiAltSetting
|
|||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public int GuildConfigId { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
|
||||
public TimeSpan MinAge { get; set; }
|
||||
public PunishmentAction Action { get; set; }
|
||||
public int ActionDurationMinutes { get; set; }
|
||||
|
|
|
@ -8,8 +8,6 @@ namespace EllieBot.Db.Models;
|
|||
#nullable disable
|
||||
public class AntiSpamSetting : DbEntity
|
||||
{
|
||||
public int GuildConfigId { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
|
||||
public PunishmentAction Action { get; set; }
|
||||
|
|
|
@ -9,7 +9,8 @@ public class ClubInfo : DbEntity
|
|||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
|
||||
public string BannerUrl { get; set; } = string.Empty;
|
||||
|
||||
public int Xp { get; set; } = 0;
|
||||
public int? OwnerId { get; set; }
|
||||
public DiscordUser Owner { get; set; }
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class ExcludedItem : DbEntity
|
||||
{
|
||||
public int? XpSettingsId { get; set; }
|
||||
public ulong ItemId { get; set; }
|
||||
public ExcludedItemType ItemType { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ItemId.GetHashCode() ^ ItemType.GetHashCode();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType;
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
namespace EllieBot.Db.Models;
|
||||
|
||||
public enum ExcludedItemType { Channel, Role }
|
||||
public enum XpExcludedItemType
|
||||
{
|
||||
User,
|
||||
Role
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class UserXpStats : DbEntity
|
||||
|
|
31
src/EllieBot/Db/Models/xp/XpExcludedItem.cs
Normal file
31
src/EllieBot/Db/Models/xp/XpExcludedItem.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class XpExcludedItem
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
|
||||
public XpExcludedItemType ItemType { get; set; }
|
||||
public ulong ItemId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class XpExclusionEntityConfig : IEntityTypeConfiguration<XpExcludedItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<XpExcludedItem> builder)
|
||||
{
|
||||
builder.HasIndex(x => x.GuildId);
|
||||
|
||||
builder.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.ItemType,
|
||||
x.ItemId
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,14 +1,9 @@
|
|||
#nullable disable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class XpSettings : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public bool ServerExcluded { get; set; }
|
||||
public HashSet<ExcludedItem> ExclusionList { get; set; } = new();
|
||||
|
||||
public HashSet<XpRoleReward> RoleRewards { get; set; } = new();
|
||||
public HashSet<XpCurrencyReward> CurrencyRewards { get; set; } = new();
|
||||
|
|
|
@ -15,8 +15,5 @@ public class XpSettingsEntityConfiguration : IEntityTypeConfiguration<XpSettings
|
|||
|
||||
builder.HasMany(x => x.RoleRewards)
|
||||
.WithOne();
|
||||
|
||||
builder.HasMany(x => x.ExclusionList)
|
||||
.WithOne();
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>6.0.0-alpha1</Version>
|
||||
<Version>6.0.9</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
|
@ -34,7 +34,7 @@
|
|||
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
|
||||
<PackageReference Include="Grpc" Version="2.46.6" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.69.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.68.1" PrivateAssets="All" />
|
||||
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
|||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
|
||||
<PackageReference Include="SixLabors.Fonts" Version="2.1.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
|
||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,27 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE userfishstats ADD bait integer;
|
||||
|
||||
ALTER TABLE userfishstats ADD pole integer;
|
||||
|
||||
CREATE TABLE channelxpconfig (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
channelid numeric(20,0) NOT NULL,
|
||||
xpamount integer NOT NULL,
|
||||
cooldown real NOT NULL,
|
||||
CONSTRAINT pk_channelxpconfig PRIMARY KEY (id),
|
||||
CONSTRAINT ak_channelxpconfig_guildid_channelid UNIQUE (guildid, channelid)
|
||||
);
|
||||
|
||||
CREATE TABLE guildxpconfig (
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
xpamount integer NOT NULL,
|
||||
cooldown integer NOT NULL,
|
||||
xptemplateurl text,
|
||||
CONSTRAINT pk_guildxpconfig PRIMARY KEY (guildid)
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250225212147_xp-excl-xp-rate', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,31 @@
|
|||
START TRANSACTION;
|
||||
DROP TABLE excludeditem;
|
||||
|
||||
ALTER TABLE guildxpconfig DROP CONSTRAINT pk_guildxpconfig;
|
||||
|
||||
ALTER TABLE channelxpconfig DROP CONSTRAINT ak_channelxpconfig_guildid_channelid;
|
||||
|
||||
ALTER TABLE xpsettings DROP COLUMN serverexcluded;
|
||||
|
||||
ALTER TABLE guildxpconfig ALTER COLUMN xpamount TYPE bigint;
|
||||
|
||||
ALTER TABLE guildxpconfig ALTER COLUMN cooldown TYPE real;
|
||||
|
||||
ALTER TABLE guildxpconfig ADD id integer GENERATED BY DEFAULT AS IDENTITY;
|
||||
|
||||
ALTER TABLE guildxpconfig ADD ratetype integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE channelxpconfig ALTER COLUMN xpamount TYPE bigint;
|
||||
|
||||
ALTER TABLE channelxpconfig ADD ratetype integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE guildxpconfig ADD CONSTRAINT ak_guildxpconfig_guildid_ratetype UNIQUE (guildid, ratetype);
|
||||
|
||||
ALTER TABLE guildxpconfig ADD CONSTRAINT pk_guildxpconfig PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE channelxpconfig ADD CONSTRAINT ak_channelxpconfig_guildid_channelid_ratetype UNIQUE (guildid, channelid, ratetype);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250226215159_xp-rate-rework', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,7 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE notify ALTER COLUMN channelid DROP NOT NULL;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250228044141_notify-allow-origin-channel', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,16 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE userrole (
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
userid numeric(20,0) NOT NULL,
|
||||
roleid numeric(20,0) NOT NULL,
|
||||
CONSTRAINT pk_userrole PRIMARY KEY (guildid, userid, roleid)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_userrole_guildid ON userrole (guildid);
|
||||
|
||||
CREATE INDEX ix_userrole_guildid_userid ON userrole (guildid, userid);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250310101121_userroles', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,7 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE clubs ADD bannerurl text;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250310143026_club-banners', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,9 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE discorduser DROP COLUMN notifyonlevelup;
|
||||
|
||||
ALTER TABLE followedstream ADD prettyname text;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250315193825_fs-prettyname', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,9 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE antispamsetting DROP COLUMN guildconfigid;
|
||||
|
||||
ALTER TABLE antialtsetting DROP COLUMN guildconfigid;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250315225518_prot-delete-gcid', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,23 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE scheduledcommand (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
userid numeric(20,0) NOT NULL,
|
||||
channelid numeric(20,0) NOT NULL,
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
messageid numeric(20,0) NOT NULL,
|
||||
text text NOT NULL,
|
||||
"when" timestamp without time zone NOT NULL,
|
||||
CONSTRAINT pk_scheduledcommand PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_scheduledcommand_guildid ON scheduledcommand (guildid);
|
||||
|
||||
CREATE INDEX ix_scheduledcommand_userid ON scheduledcommand (userid);
|
||||
|
||||
CREATE INDEX ix_scheduledcommand_when ON scheduledcommand ("when");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250317063129_scheduled-commands', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE xpexcludeditem (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
itemtype integer NOT NULL,
|
||||
itemid numeric(20,0) NOT NULL,
|
||||
CONSTRAINT pk_xpexcludeditem PRIMARY KEY (id),
|
||||
CONSTRAINT ak_xpexcludeditem_guildid_itemtype_itemid UNIQUE (guildid, itemtype, itemid)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_xpexcludeditem_guildid ON xpexcludeditem (guildid);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250318221943_xpexclusion', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE livechannelconfig (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
channelid numeric(20,0) NOT NULL,
|
||||
template text NOT NULL,
|
||||
CONSTRAINT pk_livechannelconfig PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_livechannelconfig_guildid ON livechannelconfig (guildid);
|
||||
|
||||
CREATE UNIQUE INDEX ix_livechannelconfig_guildid_channelid ON livechannelconfig (guildid, channelid);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250319010757_livechannels', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
4154
src/EllieBot/Migrations/PostgreSql/20250319010930_init.Designer.cs
generated
Normal file
4154
src/EllieBot/Migrations/PostgreSql/20250319010930_init.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -18,7 +18,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildconfigid = table.Column<int>(type: "integer", nullable: false),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
minage = table.Column<TimeSpan>(type: "interval", nullable: false),
|
||||
action = table.Column<int>(type: "integer", nullable: false),
|
||||
|
@ -54,7 +53,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildconfigid = table.Column<int>(type: "integer", nullable: false),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
action = table.Column<int>(type: "integer", nullable: false),
|
||||
messagethreshold = table.Column<int>(type: "integer", nullable: false),
|
||||
|
@ -187,6 +185,24 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.UniqueConstraint("ak_buttonrole_roleid_messageid", x => new { x.roleid, x.messageid });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "channelxpconfig",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ratetype = table.Column<int>(type: "integer", nullable: false),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
xpamount = table.Column<long>(type: "bigint", nullable: false),
|
||||
cooldown = table.Column<float>(type: "real", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_channelxpconfig", x => x.id);
|
||||
table.UniqueConstraint("ak_channelxpconfig_guildid_channelid_ratetype", x => new { x.guildid, x.channelid, x.ratetype });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "commandalias",
|
||||
columns: table => new
|
||||
|
@ -349,6 +365,7 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
username = table.Column<string>(type: "text", nullable: true),
|
||||
prettyname = table.Column<string>(type: "text", nullable: true),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
message = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
|
@ -487,6 +504,24 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.PrimaryKey("pk_guildfilterconfig", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "guildxpconfig",
|
||||
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),
|
||||
ratetype = table.Column<int>(type: "integer", nullable: false),
|
||||
xpamount = table.Column<long>(type: "bigint", nullable: false),
|
||||
cooldown = table.Column<float>(type: "real", nullable: false),
|
||||
xptemplateurl = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_guildxpconfig", x => x.id);
|
||||
table.UniqueConstraint("ak_guildxpconfig_guildid_ratetype", x => new { x.guildid, x.ratetype });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "honeypotchannels",
|
||||
columns: table => new
|
||||
|
@ -515,6 +550,21 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.PrimaryKey("pk_imageonlychannels", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "livechannelconfig",
|
||||
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),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
template = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_livechannelconfig", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "logsettings",
|
||||
columns: table => new
|
||||
|
@ -622,7 +672,7 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false)
|
||||
},
|
||||
|
@ -820,6 +870,24 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.UniqueConstraint("ak_sargroup_guildid_groupnumber", x => new { x.guildid, x.groupnumber });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "scheduledcommand",
|
||||
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),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
text = table.Column<string>(type: "text", nullable: false),
|
||||
when = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_scheduledcommand", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "shopentry",
|
||||
columns: table => new
|
||||
|
@ -1033,13 +1101,28 @@ 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)
|
||||
skill = table.Column<int>(type: "integer", nullable: false),
|
||||
pole = table.Column<int>(type: "integer", nullable: true),
|
||||
bait = table.Column<int>(type: "integer", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_userfishstats", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userrole",
|
||||
columns: table => new
|
||||
{
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_userrole", x => new { x.guildid, x.userid, x.roleid });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userxpstats",
|
||||
columns: table => new
|
||||
|
@ -1111,6 +1194,22 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.PrimaryKey("pk_warnings", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "xpexcludeditem",
|
||||
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),
|
||||
itemtype = table.Column<int>(type: "integer", nullable: false),
|
||||
itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_xpexcludeditem", x => x.id);
|
||||
table.UniqueConstraint("ak_xpexcludeditem_guildid_itemtype_itemid", x => new { x.guildid, x.itemtype, x.itemid });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "xpsettings",
|
||||
columns: table => new
|
||||
|
@ -1118,7 +1217,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
serverexcluded = table.Column<bool>(type: "boolean", nullable: false),
|
||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
@ -1473,27 +1571,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "excludeditem",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
xpsettingsid = table.Column<int>(type: "integer", nullable: true),
|
||||
itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
itemtype = table.Column<int>(type: "integer", nullable: false),
|
||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_excludeditem", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_excludeditem_xpsettings_xpsettingsid",
|
||||
column: x => x.xpsettingsid,
|
||||
principalTable: "xpsettings",
|
||||
principalColumn: "id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "xpcurrencyreward",
|
||||
columns: table => new
|
||||
|
@ -1572,6 +1649,7 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
name = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||
description = table.Column<string>(type: "text", nullable: true),
|
||||
imageurl = table.Column<string>(type: "text", nullable: true),
|
||||
bannerurl = table.Column<string>(type: "text", nullable: true),
|
||||
xp = table.Column<int>(type: "integer", nullable: false),
|
||||
ownerid = table.Column<int>(type: "integer", nullable: true),
|
||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
|
@ -1593,7 +1671,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
clubid = table.Column<int>(type: "integer", nullable: true),
|
||||
isclubadmin = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||
totalxp = table.Column<long>(type: "bigint", nullable: false, defaultValue: 0L),
|
||||
notifyonlevelup = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||
currencyamount = table.Column<long>(type: "bigint", nullable: false, defaultValue: 0L),
|
||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
},
|
||||
|
@ -1827,11 +1904,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table: "discorduser",
|
||||
column: "username");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_excludeditem_xpsettingsid",
|
||||
table: "excludeditem",
|
||||
column: "xpsettingsid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_feedsub_guildid_url",
|
||||
table: "feedsub",
|
||||
|
@ -1927,6 +1999,17 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
column: "channelid",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_livechannelconfig_guildid",
|
||||
table: "livechannelconfig",
|
||||
column: "guildid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_livechannelconfig_guildid_channelid",
|
||||
table: "livechannelconfig",
|
||||
columns: new[] { "guildid", "channelid" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_logsettings_guildid",
|
||||
table: "logsettings",
|
||||
|
@ -2025,6 +2108,21 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
column: "guildid",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scheduledcommand_guildid",
|
||||
table: "scheduledcommand",
|
||||
column: "guildid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scheduledcommand_userid",
|
||||
table: "scheduledcommand",
|
||||
column: "userid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_scheduledcommand_when",
|
||||
table: "scheduledcommand",
|
||||
column: "when");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_shopentry_guildid_index",
|
||||
table: "shopentry",
|
||||
|
@ -2120,6 +2218,16 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
column: "userid",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userrole_guildid",
|
||||
table: "userrole",
|
||||
column: "guildid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userrole_guildid_userid",
|
||||
table: "userrole",
|
||||
columns: new[] { "guildid", "userid" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userxpstats_guildid",
|
||||
table: "userxpstats",
|
||||
|
@ -2214,6 +2322,11 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table: "xpcurrencyreward",
|
||||
column: "xpsettingsid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_xpexcludeditem_guildid",
|
||||
table: "xpexcludeditem",
|
||||
column: "guildid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_xprolereward_xpsettingsid_level",
|
||||
table: "xprolereward",
|
||||
|
@ -2310,6 +2423,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "buttonrole");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "channelxpconfig");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "clubapplicants");
|
||||
|
||||
|
@ -2331,9 +2447,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "discordpermoverrides");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "excludeditem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "expressions");
|
||||
|
||||
|
@ -2376,6 +2489,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "guildcolors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "guildxpconfig");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "honeypotchannels");
|
||||
|
||||
|
@ -2385,6 +2501,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "imageonlychannels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "livechannelconfig");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "musicplayersettings");
|
||||
|
||||
|
@ -2436,6 +2555,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "sarautodelete");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "scheduledcommand");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "shopentryitem");
|
||||
|
||||
|
@ -2478,6 +2600,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "userfishstats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userrole");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userxpstats");
|
||||
|
||||
|
@ -2499,6 +2624,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "xpcurrencyreward");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "xpexcludeditem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "xprolereward");
|
||||
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
|||
BEGIN TRANSACTION;
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DROP INDEX "IX_XpSettings_GuildConfigId";
|
||||
|
||||
|
@ -142,7 +142,8 @@ DELETE FROM VcRoleInfo WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELE
|
|||
UPDATE VcRoleInfo
|
||||
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE GuildConfigs.Id = VcRoleInfo.GuildConfigId);
|
||||
|
||||
DELETE FROM UnroleTimer WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs);
|
||||
DELETE FROM UnroleTimer WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs)
|
||||
OR (GuildId, UserId) IN (SELECT GuildId, UserId FROM UnroleTimer GROUP BY GuildId, UserId HAVING COUNT(*) > 1);
|
||||
UPDATE UnroleTimer
|
||||
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE GuildConfigs.Id = UnroleTimer.GuildConfigId);
|
||||
|
||||
|
@ -170,7 +171,8 @@ DELETE FROM Permissions WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SEL
|
|||
UPDATE Permissions
|
||||
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE GuildConfigs.Id = Permissions.GuildConfigId);
|
||||
|
||||
DELETE FROM MutedUserId WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs);
|
||||
DELETE FROM MutedUserId WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs)
|
||||
OR (GuildId, UserId) IN (SELECT GuildId, UserId FROM MutedUserId GROUP BY GuildId, UserId HAVING COUNT(*) > 1);
|
||||
UPDATE MutedUserId
|
||||
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE GuildConfigs.Id = MutedUserId.GuildConfigId);
|
||||
|
||||
|
@ -178,7 +180,8 @@ DELETE FROM GcChannelId WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SEL
|
|||
UPDATE GcChannelId
|
||||
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE GuildConfigs.Id = GcChannelId.GuildConfigId);
|
||||
|
||||
DELETE FROM CommandCooldown WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs);
|
||||
DELETE FROM CommandCooldown WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs)
|
||||
OR (GuildId, CommandName) IN (SELECT GuildId, CommandName FROM CommandCooldown GROUP BY GuildId, CommandName HAVING COUNT(*) > 1);
|
||||
UPDATE CommandCooldown
|
||||
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE GuildConfigs.Id = CommandCooldown.GuildConfigId);
|
||||
|
||||
|
@ -760,4 +763,3 @@ INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
|||
VALUES ('20250126062816_cleanup', '8.0.8');
|
||||
|
||||
COMMIT;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,25 @@
|
|||
BEGIN TRANSACTION;
|
||||
ALTER TABLE "UserFishStats" ADD "Bait" INTEGER NULL;
|
||||
|
||||
ALTER TABLE "UserFishStats" ADD "Pole" INTEGER NULL;
|
||||
|
||||
CREATE TABLE "ChannelXpConfig" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ChannelXpConfig" PRIMARY KEY AUTOINCREMENT,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"ChannelId" INTEGER NOT NULL,
|
||||
"XpAmount" INTEGER NOT NULL,
|
||||
"Cooldown" REAL NOT NULL,
|
||||
CONSTRAINT "AK_ChannelXpConfig_GuildId_ChannelId" UNIQUE ("GuildId", "ChannelId")
|
||||
);
|
||||
|
||||
CREATE TABLE "GuildXpConfig" (
|
||||
"GuildId" INTEGER NOT NULL CONSTRAINT "PK_GuildXpConfig" PRIMARY KEY AUTOINCREMENT,
|
||||
"XpAmount" INTEGER NOT NULL,
|
||||
"Cooldown" INTEGER NOT NULL,
|
||||
"XpTemplateUrl" TEXT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250225212144_xp-excl-xp-rate', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,75 @@
|
|||
BEGIN TRANSACTION;
|
||||
DROP TABLE "ExcludedItem";
|
||||
|
||||
ALTER TABLE "GuildXpConfig" ADD "Id" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "GuildXpConfig" ADD "RateType" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "ChannelXpConfig" ADD "RateType" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE "ef_temp_GuildXpConfig" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_GuildXpConfig" PRIMARY KEY AUTOINCREMENT,
|
||||
"Cooldown" REAL NOT NULL,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"RateType" INTEGER NOT NULL,
|
||||
"XpAmount" INTEGER NOT NULL,
|
||||
"XpTemplateUrl" TEXT NULL,
|
||||
CONSTRAINT "AK_GuildXpConfig_GuildId_RateType" UNIQUE ("GuildId", "RateType")
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_GuildXpConfig" ("Id", "Cooldown", "GuildId", "RateType", "XpAmount", "XpTemplateUrl")
|
||||
SELECT "Id", "Cooldown", "GuildId", "RateType", "XpAmount", "XpTemplateUrl"
|
||||
FROM "GuildXpConfig";
|
||||
|
||||
CREATE TABLE "ef_temp_ChannelXpConfig" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ChannelXpConfig" PRIMARY KEY AUTOINCREMENT,
|
||||
"ChannelId" INTEGER NOT NULL,
|
||||
"Cooldown" REAL NOT NULL,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"RateType" INTEGER NOT NULL,
|
||||
"XpAmount" INTEGER NOT NULL,
|
||||
CONSTRAINT "AK_ChannelXpConfig_GuildId_ChannelId_RateType" UNIQUE ("GuildId", "ChannelId", "RateType")
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_ChannelXpConfig" ("Id", "ChannelId", "Cooldown", "GuildId", "RateType", "XpAmount")
|
||||
SELECT "Id", "ChannelId", "Cooldown", "GuildId", "RateType", "XpAmount"
|
||||
FROM "ChannelXpConfig";
|
||||
|
||||
CREATE TABLE "ef_temp_XpSettings" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_XpSettings" PRIMARY KEY AUTOINCREMENT,
|
||||
"DateAdded" TEXT NULL,
|
||||
"GuildId" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_XpSettings" ("Id", "DateAdded", "GuildId")
|
||||
SELECT "Id", "DateAdded", "GuildId"
|
||||
FROM "XpSettings";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "GuildXpConfig";
|
||||
|
||||
ALTER TABLE "ef_temp_GuildXpConfig" RENAME TO "GuildXpConfig";
|
||||
|
||||
DROP TABLE "ChannelXpConfig";
|
||||
|
||||
ALTER TABLE "ef_temp_ChannelXpConfig" RENAME TO "ChannelXpConfig";
|
||||
|
||||
DROP TABLE "XpSettings";
|
||||
|
||||
ALTER TABLE "ef_temp_XpSettings" RENAME TO "XpSettings";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE UNIQUE INDEX "IX_XpSettings_GuildId" ON "XpSettings" ("GuildId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250226215156_xp-rate-rework', '9.0.1');
|
|
@ -0,0 +1,29 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Notify" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_Notify" PRIMARY KEY AUTOINCREMENT,
|
||||
"ChannelId" INTEGER NULL,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"Message" TEXT NOT NULL,
|
||||
"Type" INTEGER NOT NULL,
|
||||
CONSTRAINT "AK_Notify_GuildId_Type" UNIQUE ("GuildId", "Type")
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Notify" ("Id", "ChannelId", "GuildId", "Message", "Type")
|
||||
SELECT "Id", "ChannelId", "GuildId", "Message", "Type"
|
||||
FROM "Notify";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Notify";
|
||||
|
||||
ALTER TABLE "ef_temp_Notify" RENAME TO "Notify";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250228044138_notify-allow-origin-channel', '9.0.1');
|
16
src/EllieBot/Migrations/Sqlite/20250310101118_userroles.sql
Normal file
16
src/EllieBot/Migrations/Sqlite/20250310101118_userroles.sql
Normal file
|
@ -0,0 +1,16 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "UserRole" (
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"UserId" INTEGER NOT NULL,
|
||||
"RoleId" INTEGER NOT NULL,
|
||||
CONSTRAINT "PK_UserRole" PRIMARY KEY ("GuildId", "UserId", "RoleId")
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_UserRole_GuildId" ON "UserRole" ("GuildId");
|
||||
|
||||
CREATE INDEX "IX_UserRole_GuildId_UserId" ON "UserRole" ("GuildId", "UserId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310101118_userroles', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN TRANSACTION;
|
||||
ALTER TABLE "Clubs" ADD "BannerUrl" TEXT NULL;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310143023_club-banners', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,9 @@
|
|||
BEGIN TRANSACTION;
|
||||
ALTER TABLE discorduser DROP COLUMN notifyonlevelup;
|
||||
|
||||
ALTER TABLE "FollowedStream" ADD "PrettyName" TEXT NULL;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250315193822_fs-prettyname', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,9 @@
|
|||
BEGIN TRANSACTION;
|
||||
ALTER TABLE "AntiSpamSetting" DROP COLUMN "GuildConfigId";
|
||||
|
||||
ALTER TABLE "AntiAltSetting" DROP COLUMN "GuildConfigId";
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250315225515_prot-delete-gcid', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,22 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "ScheduledCommand" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ScheduledCommand" PRIMARY KEY AUTOINCREMENT,
|
||||
"UserId" INTEGER NOT NULL,
|
||||
"ChannelId" INTEGER NOT NULL,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"MessageId" INTEGER NOT NULL,
|
||||
"Text" TEXT NOT NULL,
|
||||
"When" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_ScheduledCommand_GuildId" ON "ScheduledCommand" ("GuildId");
|
||||
|
||||
CREATE INDEX "IX_ScheduledCommand_UserId" ON "ScheduledCommand" ("UserId");
|
||||
|
||||
CREATE INDEX "IX_ScheduledCommand_When" ON "ScheduledCommand" ("When");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250317063119_scheduled-commands', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "XpExcludedItem" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_XpExcludedItem" PRIMARY KEY AUTOINCREMENT,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"ItemType" INTEGER NOT NULL,
|
||||
"ItemId" INTEGER NOT NULL,
|
||||
CONSTRAINT "AK_XpExcludedItem_GuildId_ItemType_ItemId" UNIQUE ("GuildId", "ItemType", "ItemId")
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_XpExcludedItem_GuildId" ON "XpExcludedItem" ("GuildId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250318221922_xpexclusion', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "LiveChannelConfig" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_LiveChannelConfig" PRIMARY KEY AUTOINCREMENT,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"ChannelId" INTEGER NOT NULL,
|
||||
"Template" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_LiveChannelConfig_GuildId" ON "LiveChannelConfig" ("GuildId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_LiveChannelConfig_GuildId_ChannelId" ON "LiveChannelConfig" ("GuildId", "ChannelId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250319010745_livechannels', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
3159
src/EllieBot/Migrations/Sqlite/20250319010920_init.Designer.cs
generated
Normal file
3159
src/EllieBot/Migrations/Sqlite/20250319010920_init.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
MinAge = table.Column<TimeSpan>(type: "TEXT", nullable: false),
|
||||
Action = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
|
@ -53,7 +52,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Action = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MessageThreshold = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
|
@ -186,6 +184,24 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.UniqueConstraint("AK_ButtonRole_RoleId_MessageId", x => new { x.RoleId, x.MessageId });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChannelXpConfig",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
RateType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
XpAmount = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
Cooldown = table.Column<float>(type: "REAL", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChannelXpConfig", x => x.Id);
|
||||
table.UniqueConstraint("AK_ChannelXpConfig_GuildId_ChannelId_RateType", x => new { x.GuildId, x.ChannelId, x.RateType });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CommandAlias",
|
||||
columns: table => new
|
||||
|
@ -348,6 +364,7 @@ namespace EllieBot.Migrations.Sqlite
|
|||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Username = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PrettyName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Message = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
|
@ -486,6 +503,24 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.PrimaryKey("PK_GuildFilterConfig", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GuildXpConfig",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
RateType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
XpAmount = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
Cooldown = table.Column<float>(type: "REAL", nullable: false),
|
||||
XpTemplateUrl = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GuildXpConfig", x => x.Id);
|
||||
table.UniqueConstraint("AK_GuildXpConfig_GuildId_RateType", x => new { x.GuildId, x.RateType });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "HoneyPotChannels",
|
||||
columns: table => new
|
||||
|
@ -515,6 +550,21 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.PrimaryKey("PK_ImageOnlyChannels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LiveChannelConfig",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Template = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LiveChannelConfig", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LogSettings",
|
||||
columns: table => new
|
||||
|
@ -622,7 +672,7 @@ namespace EllieBot.Migrations.Sqlite
|
|||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: true),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false)
|
||||
},
|
||||
|
@ -822,6 +872,24 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.UniqueConstraint("AK_SarGroup_GuildId_GroupNumber", x => new { x.GuildId, x.GroupNumber });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ScheduledCommand",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Text = table.Column<string>(type: "TEXT", nullable: false),
|
||||
When = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ScheduledCommand", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ShopEntry",
|
||||
columns: table => new
|
||||
|
@ -1035,13 +1103,28 @@ 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)
|
||||
Skill = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Pole = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Bait = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserFishStats", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRole",
|
||||
columns: table => new
|
||||
{
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRole", x => new { x.GuildId, x.UserId, x.RoleId });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserXpStats",
|
||||
columns: table => new
|
||||
|
@ -1113,6 +1196,22 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.PrimaryKey("PK_Warnings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "XpExcludedItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ItemType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ItemId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_XpExcludedItem", x => x.Id);
|
||||
table.UniqueConstraint("AK_XpExcludedItem_GuildId_ItemType_ItemId", x => new { x.GuildId, x.ItemType, x.ItemId });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "XpSettings",
|
||||
columns: table => new
|
||||
|
@ -1120,7 +1219,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ServerExcluded = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
@ -1475,27 +1573,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExcludedItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
XpSettingsId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
ItemId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ItemType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExcludedItem", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExcludedItem_XpSettings_XpSettingsId",
|
||||
column: x => x.XpSettingsId,
|
||||
principalTable: "XpSettings",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "XpCurrencyReward",
|
||||
columns: table => new
|
||||
|
@ -1574,6 +1651,7 @@ namespace EllieBot.Migrations.Sqlite
|
|||
Name = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ImageUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
BannerUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Xp = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
|
@ -1595,7 +1673,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
ClubId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
IsClubAdmin = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||
TotalXp = table.Column<long>(type: "INTEGER", nullable: false, defaultValue: 0L),
|
||||
NotifyOnLevelUp = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
|
||||
CurrencyAmount = table.Column<long>(type: "INTEGER", nullable: false, defaultValue: 0L),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
|
@ -1829,11 +1906,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table: "DiscordUser",
|
||||
column: "Username");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExcludedItem_XpSettingsId",
|
||||
table: "ExcludedItem",
|
||||
column: "XpSettingsId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedSub_GuildId_Url",
|
||||
table: "FeedSub",
|
||||
|
@ -1929,6 +2001,17 @@ namespace EllieBot.Migrations.Sqlite
|
|||
column: "ChannelId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LiveChannelConfig_GuildId",
|
||||
table: "LiveChannelConfig",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LiveChannelConfig_GuildId_ChannelId",
|
||||
table: "LiveChannelConfig",
|
||||
columns: new[] { "GuildId", "ChannelId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LogSettings_GuildId",
|
||||
table: "LogSettings",
|
||||
|
@ -2027,6 +2110,21 @@ namespace EllieBot.Migrations.Sqlite
|
|||
column: "GuildId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScheduledCommand_GuildId",
|
||||
table: "ScheduledCommand",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScheduledCommand_UserId",
|
||||
table: "ScheduledCommand",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScheduledCommand_When",
|
||||
table: "ScheduledCommand",
|
||||
column: "When");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ShopEntry_GuildId_Index",
|
||||
table: "ShopEntry",
|
||||
|
@ -2122,6 +2220,16 @@ namespace EllieBot.Migrations.Sqlite
|
|||
column: "UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRole_GuildId",
|
||||
table: "UserRole",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRole_GuildId_UserId",
|
||||
table: "UserRole",
|
||||
columns: new[] { "GuildId", "UserId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserXpStats_GuildId",
|
||||
table: "UserXpStats",
|
||||
|
@ -2216,6 +2324,11 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table: "XpCurrencyReward",
|
||||
column: "XpSettingsId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_XpExcludedItem_GuildId",
|
||||
table: "XpExcludedItem",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_XpRoleReward_XpSettingsId_Level",
|
||||
table: "XpRoleReward",
|
||||
|
@ -2312,6 +2425,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "ButtonRole");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChannelXpConfig");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClubApplicants");
|
||||
|
||||
|
@ -2333,9 +2449,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "DiscordPermOverrides");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExcludedItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Expressions");
|
||||
|
||||
|
@ -2378,6 +2491,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "GuildColors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GuildXpConfig");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "HoneyPotChannels");
|
||||
|
||||
|
@ -2387,6 +2503,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "ImageOnlyChannels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LiveChannelConfig");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MusicPlayerSettings");
|
||||
|
||||
|
@ -2438,6 +2557,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "SarAutoDelete");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ScheduledCommand");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ShopEntryItem");
|
||||
|
||||
|
@ -2480,6 +2602,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "UserFishStats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRole");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserXpStats");
|
||||
|
||||
|
@ -2501,6 +2626,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "XpCurrencyReward");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "XpExcludedItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "XpRoleReward");
|
||||
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
|||
#nullable disable
|
||||
using System.Net;
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
|
@ -15,7 +16,12 @@ public enum MuteType
|
|||
|
||||
public class MuteService : IEService, IReadyExecutor
|
||||
{
|
||||
public enum TimerType { Mute, Ban, AddRole }
|
||||
public enum TimerType
|
||||
{
|
||||
Mute,
|
||||
Ban,
|
||||
AddRole
|
||||
}
|
||||
|
||||
private static readonly OverwritePermissions _denyOverwrite = new(addReactions: PermValue.Deny,
|
||||
sendMessages: PermValue.Deny,
|
||||
|
@ -57,12 +63,12 @@ public class MuteService : IEService, IReadyExecutor
|
|||
return;
|
||||
|
||||
_ = Task.Run(() => _sender.Response(user)
|
||||
.Embed(_sender.CreateEmbed(user?.GuildId)
|
||||
.WithDescription($"You've been muted in {user.Guild} server")
|
||||
.AddField("Mute Type", type.ToString())
|
||||
.AddField("Moderator", mod.ToString())
|
||||
.AddField("Reason", reason))
|
||||
.SendAsync());
|
||||
.Embed(_sender.CreateEmbed(user?.GuildId)
|
||||
.WithDescription($"You've been muted in {user.Guild} server")
|
||||
.AddField("Mute Type", type.ToString())
|
||||
.AddField("Moderator", mod.ToString())
|
||||
.AddField("Reason", reason))
|
||||
.SendAsync());
|
||||
}
|
||||
|
||||
private void OnUserUnmuted(
|
||||
|
@ -75,12 +81,12 @@ public class MuteService : IEService, IReadyExecutor
|
|||
return;
|
||||
|
||||
_ = Task.Run(() => _sender.Response(user)
|
||||
.Embed(_sender.CreateEmbed(user.GuildId)
|
||||
.WithDescription($"You've been unmuted in {user.Guild} server")
|
||||
.AddField("Unmute Type", type.ToString())
|
||||
.AddField("Moderator", mod.ToString())
|
||||
.AddField("Reason", reason))
|
||||
.SendAsync());
|
||||
.Embed(_sender.CreateEmbed(user.GuildId)
|
||||
.WithDescription($"You've been unmuted in {user.Guild} server")
|
||||
.AddField("Unmute Type", type.ToString())
|
||||
.AddField("Moderator", mod.ToString())
|
||||
.AddField("Reason", reason))
|
||||
.SendAsync());
|
||||
}
|
||||
|
||||
private Task Client_UserJoined(IGuildUser usr)
|
||||
|
@ -105,8 +111,8 @@ public class MuteService : IEService, IReadyExecutor
|
|||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var config = uow.GetTable<GuildConfig>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.FirstOrDefault();
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.FirstOrDefault();
|
||||
config.MuteRoleName = name;
|
||||
_guildMuteRoles.AddOrUpdate(guildId, name, (_, _) => name);
|
||||
await uow.SaveChangesAsync();
|
||||
|
@ -121,8 +127,12 @@ public class MuteService : IEService, IReadyExecutor
|
|||
if (type == MuteType.All)
|
||||
{
|
||||
try
|
||||
{ await usr.ModifyAsync(x => x.Mute = true); }
|
||||
catch { }
|
||||
{
|
||||
await usr.ModifyAsync(x => x.Mute = true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var muteRole = await GetMuteRole(usr.Guild);
|
||||
if (!usr.RoleIds.Contains(muteRole.Id))
|
||||
|
@ -131,19 +141,19 @@ public class MuteService : IEService, IReadyExecutor
|
|||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
await uow.GetTable<MutedUserId>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = usr.GuildId,
|
||||
UserId = usr.Id
|
||||
},
|
||||
(_) => new()
|
||||
{
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = usr.GuildId,
|
||||
UserId = usr.Id
|
||||
});
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = usr.GuildId,
|
||||
UserId = usr.Id
|
||||
},
|
||||
(_) => new()
|
||||
{
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = usr.GuildId,
|
||||
UserId = usr.Id
|
||||
});
|
||||
|
||||
if (_mutedUsers.TryGetValue(usr.Guild.Id, out var muted))
|
||||
muted.Add(usr.Id);
|
||||
|
@ -160,7 +170,9 @@ public class MuteService : IEService, IReadyExecutor
|
|||
await usr.ModifyAsync(x => x.Mute = true);
|
||||
UserMuted(usr, mod, MuteType.Voice, reason);
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
else if (type == MuteType.Chat)
|
||||
{
|
||||
|
@ -183,12 +195,12 @@ public class MuteService : IEService, IReadyExecutor
|
|||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
await uow.GetTable<MutedUserId>()
|
||||
.Where(x => x.GuildId == guildId && x.UserId == usrId)
|
||||
.DeleteAsync();
|
||||
.Where(x => x.GuildId == guildId && x.UserId == usrId)
|
||||
.DeleteAsync();
|
||||
|
||||
await uow.GetTable<UnmuteTimer>()
|
||||
.Where(x => x.GuildId == guildId && x.UserId == usrId)
|
||||
.DeleteAsync();
|
||||
.Where(x => x.GuildId == guildId && x.UserId == usrId)
|
||||
.DeleteAsync();
|
||||
|
||||
if (_mutedUsers.TryGetValue(guildId, out var muted))
|
||||
muted.TryRemove(usrId);
|
||||
|
@ -197,11 +209,17 @@ public class MuteService : IEService, IReadyExecutor
|
|||
if (usr is not null)
|
||||
{
|
||||
try
|
||||
{ await usr.ModifyAsync(x => x.Mute = false); }
|
||||
catch { }
|
||||
{
|
||||
await usr.ModifyAsync(x => x.Mute = false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{ await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); }
|
||||
{
|
||||
await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild));
|
||||
}
|
||||
catch
|
||||
{
|
||||
/*ignore*/
|
||||
|
@ -231,26 +249,29 @@ public class MuteService : IEService, IReadyExecutor
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(guild);
|
||||
|
||||
const string defaultMuteRoleName = "nadeko-mute";
|
||||
const string defaultMuteRoleName = "ellie-mute";
|
||||
|
||||
var muteRoleName = _guildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName);
|
||||
|
||||
var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName);
|
||||
if (muteRole is null)
|
||||
//if it doesn't exist, create it
|
||||
{
|
||||
try
|
||||
{ muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false); }
|
||||
catch
|
||||
{
|
||||
//if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one
|
||||
muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName)
|
||||
?? await guild.CreateRoleAsync(defaultMuteRoleName, isMentionable: false);
|
||||
muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to create mute role for guild {GuildId}", guild.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var toOverwrite in await guild.GetTextChannelsAsync())
|
||||
{
|
||||
if (toOverwrite is IThreadChannel)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id
|
||||
|
@ -261,9 +282,10 @@ public class MuteService : IEService, IReadyExecutor
|
|||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions)
|
||||
{
|
||||
// ignored
|
||||
Log.Error(ex, "Error in Initializing mute role in guild {GuildId}: {Message}", guild.Id, ex.Message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,12 +304,12 @@ public class MuteService : IEService, IReadyExecutor
|
|||
{
|
||||
var unmuteAt = DateTime.UtcNow + after;
|
||||
await uow.GetTable<UnmuteTimer>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
GuildId = user.GuildId,
|
||||
UserId = user.Id,
|
||||
UnmuteAt = unmuteAt
|
||||
});
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
GuildId = user.GuildId,
|
||||
UserId = user.Id,
|
||||
UnmuteAt = unmuteAt
|
||||
});
|
||||
}
|
||||
|
||||
StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer
|
||||
|
@ -305,12 +327,12 @@ public class MuteService : IEService, IReadyExecutor
|
|||
{
|
||||
var unbanAt = DateTime.UtcNow + after;
|
||||
await uow.GetTable<UnbanTimer>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
GuildId = guild.Id,
|
||||
UserId = userId,
|
||||
UnbanAt = unbanAt
|
||||
});
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
GuildId = guild.Id,
|
||||
UserId = userId,
|
||||
UnbanAt = unbanAt
|
||||
});
|
||||
}
|
||||
|
||||
StartUn_Timer(guild.Id, userId, after, TimerType.Ban); // start the timer
|
||||
|
@ -415,83 +437,80 @@ public class MuteService : IEService, IReadyExecutor
|
|||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var configs = await uow.Set<GuildConfig>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
_guildMuteRoles = configs.Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName))
|
||||
.ToDictionary(c => c.GuildId, c => c.MuteRoleName)
|
||||
.ToConcurrent();
|
||||
.ToDictionary(c => c.GuildId, c => c.MuteRoleName)
|
||||
.ToConcurrent();
|
||||
|
||||
_mutedUsers = await uow.GetTable<MutedUserId>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB()
|
||||
.Fmap(x => x.GroupBy(x => x.GuildId)
|
||||
.ToDictionary(g => g.Key, g => new ConcurrentHashSet<ulong>(g.Select(x => x.UserId)))
|
||||
.ToConcurrent());
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB()
|
||||
.Fmap(x => x.GroupBy(x => x.GuildId)
|
||||
.ToDictionary(g => g.Key, g => new ConcurrentHashSet<ulong>(g.Select(x => x.UserId)))
|
||||
.ToConcurrent());
|
||||
|
||||
var max = TimeSpan.FromDays(49);
|
||||
|
||||
var unmuteTimers = await uow.GetTable<UnmuteTimer>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
var unbanTimers = await uow.GetTable<UnbanTimer>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
var unroleTimers = await uow.GetTable<UnroleTimer>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
foreach (var conf in configs)
|
||||
foreach (var x in unmuteTimers)
|
||||
{
|
||||
foreach (var x in unmuteTimers)
|
||||
TimeSpan after;
|
||||
if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||
{
|
||||
TimeSpan after;
|
||||
if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||
{
|
||||
after = TimeSpan.FromMinutes(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unmute = x.UnmuteAt - DateTime.UtcNow;
|
||||
after = unmute > max ? max : unmute;
|
||||
}
|
||||
|
||||
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute);
|
||||
after = TimeSpan.FromMinutes(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unmute = x.UnmuteAt - DateTime.UtcNow;
|
||||
after = unmute > max ? max : unmute;
|
||||
}
|
||||
|
||||
foreach (var x in unbanTimers)
|
||||
{
|
||||
TimeSpan after;
|
||||
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||
{
|
||||
after = TimeSpan.FromMinutes(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unban = x.UnbanAt - DateTime.UtcNow;
|
||||
after = unban > max ? max : unban;
|
||||
}
|
||||
StartUn_Timer(x.GuildId, x.UserId, after, TimerType.Mute);
|
||||
}
|
||||
|
||||
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban);
|
||||
foreach (var x in unbanTimers)
|
||||
{
|
||||
TimeSpan after;
|
||||
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||
{
|
||||
after = TimeSpan.FromMinutes(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unban = x.UnbanAt - DateTime.UtcNow;
|
||||
after = unban > max ? max : unban;
|
||||
}
|
||||
|
||||
foreach (var x in unroleTimers)
|
||||
{
|
||||
TimeSpan after;
|
||||
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||
{
|
||||
after = TimeSpan.FromMinutes(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unban = x.UnbanAt - DateTime.UtcNow;
|
||||
after = unban > max ? max : unban;
|
||||
}
|
||||
StartUn_Timer(x.GuildId, x.UserId, after, TimerType.Ban);
|
||||
}
|
||||
|
||||
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId);
|
||||
foreach (var x in unroleTimers)
|
||||
{
|
||||
TimeSpan after;
|
||||
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||
{
|
||||
after = TimeSpan.FromMinutes(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unban = x.UnbanAt - DateTime.UtcNow;
|
||||
after = unban > max ? max : unban;
|
||||
}
|
||||
|
||||
StartUn_Timer(x.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId);
|
||||
}
|
||||
|
||||
_client.UserJoined += Client_UserJoined;
|
||||
|
|
|
@ -3,15 +3,25 @@
|
|||
namespace EllieBot.Modules.Administration;
|
||||
|
||||
public interface INotifyModel<T>
|
||||
where T: struct, INotifyModel<T>
|
||||
where T : struct, INotifyModel<T>
|
||||
{
|
||||
static abstract string KeyName { get; }
|
||||
static abstract NotifyType NotifyType { get; }
|
||||
static abstract IReadOnlyList<NotifyModelPlaceholderData<T>> GetReplacements();
|
||||
|
||||
static virtual bool SupportsOriginTarget
|
||||
=> false;
|
||||
|
||||
public virtual bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool TryGetChannelId(out ulong channelId)
|
||||
{
|
||||
channelId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,4 +13,7 @@ public interface INotifySubscriber
|
|||
NotifyModelData GetRegisteredModel(NotifyType nType);
|
||||
}
|
||||
|
||||
public readonly record struct NotifyModelData(NotifyType Type, IReadOnlyList<string> Replacements);
|
||||
public readonly record struct NotifyModelData(
|
||||
NotifyType Type,
|
||||
bool SupportsOriginTarget,
|
||||
IReadOnlyList<string> Replacements);
|
|
@ -3,7 +3,12 @@ using EllieBot.Modules.Administration;
|
|||
|
||||
namespace EllieBot.Modules.Xp.Services;
|
||||
|
||||
public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel<AddRoleRewardNotifyModel>
|
||||
public record struct AddRoleRewardNotifyModel(
|
||||
ulong GuildId,
|
||||
ulong RoleId,
|
||||
ulong UserId,
|
||||
long Level)
|
||||
: INotifyModel<AddRoleRewardNotifyModel>
|
||||
{
|
||||
public static string KeyName
|
||||
=> "notify.reward.addrole";
|
||||
|
@ -18,9 +23,9 @@ public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong
|
|||
public static IReadOnlyList<NotifyModelPlaceholderData<AddRoleRewardNotifyModel>> GetReplacements()
|
||||
=>
|
||||
[
|
||||
new(PH_LEVEL, static (data, g) => data.Level.ToString() ),
|
||||
new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() ),
|
||||
new(PH_ROLE, static (data, g) => g.GetRole(data.RoleId)?.ToString() ?? data.RoleId.ToString() )
|
||||
new(PH_LEVEL, static (data, g) => data.Level.ToString()),
|
||||
new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString()),
|
||||
new(PH_ROLE, static (data, g) => g.GetRole(data.RoleId)?.ToString() ?? data.RoleId.ToString())
|
||||
];
|
||||
|
||||
public bool TryGetUserId(out ulong userId)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// using System.Globalization;
|
||||
// using EllieBot.Db.Models;
|
||||
// using EllieBot.Modules.Administration;
|
||||
//
|
||||
// namespace EllieBot.Modules.Gambling;
|
||||
//
|
||||
// public readonly record struct BigWinNotifyModel(
|
||||
// string GuildName,
|
||||
// ulong ChannelId,
|
||||
// ulong UserId,
|
||||
// string Amount)
|
||||
// : INotifyModel<BigWinNotifyModel>
|
||||
// {
|
||||
// public const string PH_USER = "user";
|
||||
// public const string PH_GUILD = "server";
|
||||
// public const string PH_AMOUNT = "amount";
|
||||
//
|
||||
// public static string KeyName
|
||||
// => "notify.bigwin";
|
||||
//
|
||||
// public static NotifyType NotifyType
|
||||
// => NotifyType.BigWin;
|
||||
//
|
||||
// public static bool SupportsOriginTarget
|
||||
// => true;
|
||||
//
|
||||
// public static IReadOnlyList<NotifyModelPlaceholderData<BigWinNotifyModel>> GetReplacements()
|
||||
// =>
|
||||
// [
|
||||
// new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString()),
|
||||
// new(PH_AMOUNT, static (data, g) => data.Amount),
|
||||
// new(PH_GUILD, static (data, g) => data.GuildName)
|
||||
// ];
|
||||
//
|
||||
// public bool TryGetChannelId(out ulong channelId)
|
||||
// {
|
||||
// channelId = ChannelId;
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// public bool TryGetUserId(out ulong userId)
|
||||
// {
|
||||
// userId = UserId;
|
||||
// return true;
|
||||
// }
|
||||
// }
|
|
@ -4,7 +4,7 @@ namespace EllieBot.Modules.Administration;
|
|||
|
||||
public readonly record struct LevelUpNotifyModel(
|
||||
ulong GuildId,
|
||||
ulong ChannelId,
|
||||
ulong? ChannelId,
|
||||
ulong UserId,
|
||||
long Level) : INotifyModel<LevelUpNotifyModel>
|
||||
{
|
||||
|
@ -21,17 +21,32 @@ public readonly record struct LevelUpNotifyModel(
|
|||
{
|
||||
return
|
||||
[
|
||||
new(PH_LEVEL, static (data, g) => data.Level.ToString() ),
|
||||
new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() )
|
||||
new(PH_LEVEL, static (data, g) => data.Level.ToString()),
|
||||
new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString())
|
||||
];
|
||||
}
|
||||
|
||||
public static bool SupportsOriginTarget
|
||||
=> true;
|
||||
|
||||
public readonly bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = GuildId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly bool TryGetChannelId(out ulong channelId)
|
||||
{
|
||||
if (ChannelId is ulong cid)
|
||||
{
|
||||
channelId = cid;
|
||||
return true;
|
||||
}
|
||||
|
||||
channelId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public readonly bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = UserId;
|
||||
|
|
|
@ -12,23 +12,23 @@ public partial class Administration
|
|||
public async Task Notify()
|
||||
{
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(Enum.GetValues<NotifyType>().DistinctBy(x => (int)x).ToList())
|
||||
.PageSize(5)
|
||||
.Page((items, page) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_available));
|
||||
.Paginated()
|
||||
.Items(Enum.GetValues<NotifyType>().DistinctBy(x => (int)x).ToList())
|
||||
.PageSize(5)
|
||||
.Page((items, page) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_available));
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
eb.AddField(item.ToString(), GetText(GetDescription(item)), false);
|
||||
}
|
||||
foreach (var item in items)
|
||||
{
|
||||
eb.AddField(item.ToString(), GetText(GetDescription(item)), false);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private LocStr GetDescription(NotifyType item)
|
||||
|
@ -43,36 +43,56 @@ public partial class Administration
|
|||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task Notify(NotifyType nType, [Leftover] string? message = null)
|
||||
public async Task Notify(NotifyType nType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
// show msg
|
||||
var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType);
|
||||
if (conf is null)
|
||||
{
|
||||
// show msg
|
||||
var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType);
|
||||
if (conf is null)
|
||||
{
|
||||
await Response().Confirm(strs.notify_msg_not_set).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_msg))
|
||||
.WithDescription(conf.Message.TrimTo(2048))
|
||||
.AddField(GetText(strs.notify_type), conf.Type.ToString(), true)
|
||||
.AddField(GetText(strs.channel),
|
||||
$"""
|
||||
<#{conf.ChannelId}>
|
||||
`{conf.ChannelId}`
|
||||
""",
|
||||
true);
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
await Response().Confirm(strs.notify_msg_not_set).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message);
|
||||
await Response().Confirm(strs.notify_on($"<#{ctx.Channel.Id}>", Format.Bold(nType.ToString()))).SendAsync();
|
||||
var outChannel = conf.ChannelId is null
|
||||
? """
|
||||
from which the event originated
|
||||
`origin`
|
||||
"""
|
||||
: $"""
|
||||
<#{conf.ChannelId}>
|
||||
`{conf.ChannelId}`
|
||||
""";
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_msg))
|
||||
.WithDescription(conf.Message.TrimTo(2048))
|
||||
.AddField(GetText(strs.notify_type), conf.Type.ToString(), true)
|
||||
.AddField(GetText(strs.channel),
|
||||
outChannel,
|
||||
true);
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task Notify(NotifyType nType, [Leftover] string message)
|
||||
=> await NotifyInternalAsync(nType, null, message);
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task Notify(NotifyType nType, IMessageChannel channel, [Leftover] string message)
|
||||
=> await NotifyInternalAsync(nType, channel, message);
|
||||
|
||||
private async Task NotifyInternalAsync(NotifyType nType, IMessageChannel? channel, [Leftover] string message)
|
||||
{
|
||||
var result = await _service.EnableAsync(ctx.Guild.Id, channel?.Id, nType, message);
|
||||
|
||||
var outChannel = channel is null ? "origin" : $"<#{channel.Id}>";
|
||||
await Response()
|
||||
.Confirm(strs.notify_on(outChannel, Format.Bold(nType.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -82,13 +102,12 @@ public partial class Administration
|
|||
var data = _service.GetRegisteredModel(nType);
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_placeholders(nType.ToString().ToLower())));
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_placeholders(nType.ToString().ToLower())));
|
||||
|
||||
eb.WithDescription(data.Replacements.Join("\n---\n", x => $"`%event.{x}%`"));
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -115,8 +134,8 @@ public partial class Administration
|
|||
sb.AppendLine(GetText(strs.notify_none));
|
||||
|
||||
await Response()
|
||||
.Confirm(GetText(strs.notify_list), text: sb.ToString())
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.notify_list), text: sb.ToString())
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
|
|
@ -4,6 +4,7 @@ using EllieBot.Common.ModuleBehaviors;
|
|||
using EllieBot.Db.Models;
|
||||
using EllieBot.Generators;
|
||||
using EllieBot.Modules.Administration.Services;
|
||||
using EllieBot.Modules.Gambling;
|
||||
using EllieBot.Modules.Xp.Services;
|
||||
|
||||
namespace EllieBot.Modules.Administration;
|
||||
|
@ -16,6 +17,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
private readonly IBotCreds _creds;
|
||||
private readonly IReplacementService _repSvc;
|
||||
private readonly IPubSub _pubSub;
|
||||
|
||||
private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new();
|
||||
|
||||
public NotifyService(
|
||||
|
@ -36,12 +38,10 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
|
||||
private void RegisterModels()
|
||||
{
|
||||
|
||||
RegisterModel<LevelUpNotifyModel>();
|
||||
RegisterModel<ProtectionNotifyModel>();
|
||||
RegisterModel<AddRoleRewardNotifyModel>();
|
||||
RegisterModel<RemoveRoleRewardNotifyModel>();
|
||||
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
|
@ -84,7 +84,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}",
|
||||
"Unknown error occurred while trying to trigger {NotifyEvent} for {NotifyModel}",
|
||||
T.KeyName,
|
||||
data);
|
||||
}
|
||||
|
@ -93,36 +93,39 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
private async Task OnEvent<T>(T model)
|
||||
where T : struct, INotifyModel<T>
|
||||
{
|
||||
if (_events.TryGetValue(T.NotifyType, out var subs))
|
||||
if (!_events.TryGetValue(T.NotifyType, out var subs))
|
||||
return;
|
||||
|
||||
// make sure the event is consumed
|
||||
// only in the guild it was meant for
|
||||
if (model.TryGetGuildId(out var gid))
|
||||
{
|
||||
if (model.TryGetGuildId(out var gid))
|
||||
{
|
||||
if (!subs.TryGetValue(gid, out var conf))
|
||||
return;
|
||||
|
||||
await HandleNotifyEvent(conf, model);
|
||||
if (!subs.TryGetValue(gid, out var conf))
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var key in subs.Keys.ToArray())
|
||||
await HandleNotifyEvent(conf, model);
|
||||
return;
|
||||
}
|
||||
|
||||
// todo optimize this
|
||||
foreach (var key in subs.Keys)
|
||||
{
|
||||
if (subs.TryGetValue(key, out var notif))
|
||||
{
|
||||
if (subs.TryGetValue(key, out var notif))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await HandleNotifyEvent(notif, model);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
|
||||
T.NotifyType,
|
||||
key,
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
await Task.Delay(500);
|
||||
await HandleNotifyEvent(notif, model);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
|
||||
T.NotifyType,
|
||||
key,
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,9 +134,27 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
where T : struct, INotifyModel<T>
|
||||
{
|
||||
var guild = _client.GetGuild(conf.GuildId);
|
||||
var channel = guild?.GetTextChannel(conf.ChannelId);
|
||||
|
||||
if (guild is null || channel is null)
|
||||
// bot probably left the guild, cleanup?
|
||||
if (guild is null)
|
||||
return;
|
||||
|
||||
IMessageChannel? channel;
|
||||
// if notify channel is specified for this event, send the event to that channel
|
||||
if (conf.ChannelId is ulong confCid)
|
||||
{
|
||||
channel = guild.GetTextChannel(confCid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise get the origin channel of the event
|
||||
if (!model.TryGetChannelId(out var cid))
|
||||
return;
|
||||
|
||||
channel = guild.GetChannel(cid) as IMessageChannel;
|
||||
}
|
||||
|
||||
if (channel is null)
|
||||
return;
|
||||
|
||||
IUser? user = null;
|
||||
|
@ -165,14 +186,24 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
.SendAsync();
|
||||
}
|
||||
|
||||
private static string GetPhToken(string name) => $"%event.{name}%";
|
||||
private static string GetPhToken(string name)
|
||||
=> $"%event.{name}%";
|
||||
|
||||
public async Task EnableAsync(
|
||||
public async Task<bool> EnableAsync(
|
||||
ulong guildId,
|
||||
ulong channelId,
|
||||
ulong? channelId,
|
||||
NotifyType nType,
|
||||
string message)
|
||||
{
|
||||
// check if the notify type model supports null channel
|
||||
if (channelId is null)
|
||||
{
|
||||
var model = GetRegisteredModel(nType);
|
||||
if (!model.SupportsOriginTarget)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<Notify>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
|
@ -201,6 +232,8 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
Type = nType,
|
||||
Message = message
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task DisableAsync(ulong guildId, NotifyType nType)
|
||||
|
@ -244,11 +277,15 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
|
|||
|
||||
// messed up big time, it was supposed to be fully extensible, but it's stored as an enum in the database already...
|
||||
private readonly ConcurrentDictionary<NotifyType, NotifyModelData> _models = new();
|
||||
|
||||
public void RegisterModel<T>() where T : struct, INotifyModel<T>
|
||||
{
|
||||
var data = new NotifyModelData(T.NotifyType, T.GetReplacements().Map(x => x.Name));
|
||||
var data = new NotifyModelData(T.NotifyType,
|
||||
T.SupportsOriginTarget,
|
||||
T.GetReplacements().Map(x => x.Name));
|
||||
_models[T.NotifyType] = data;
|
||||
}
|
||||
|
||||
public NotifyModelData GetRegisteredModel(NotifyType nType) => _models[nType];
|
||||
}
|
||||
public NotifyModelData GetRegisteredModel(NotifyType nType)
|
||||
=> _models[nType];
|
||||
}
|
|
@ -143,9 +143,9 @@ public partial class Administration
|
|||
return;
|
||||
|
||||
await Response()
|
||||
.Confirm(GetText(strs.prot_enable("Anti-Raid")),
|
||||
$"{ctx.User.Mention} {GetAntiRaidString(stats)}")
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.prot_enable("Anti-Raid")),
|
||||
$"{ctx.User.Mention} {GetAntiRaidString(stats)}")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -213,9 +213,9 @@ public partial class Administration
|
|||
var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id);
|
||||
|
||||
await Response()
|
||||
.Confirm(GetText(strs.prot_enable("Anti-Spam")),
|
||||
$"{ctx.User.Mention} {GetAntiSpamString(stats)}")
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.prot_enable("Anti-Spam")),
|
||||
$"{ctx.User.Mention} {GetAntiSpamString(stats)}")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace EllieBot.Modules.Administration.Services;
|
|||
|
||||
public class ProtectionService : IReadyExecutor, IEService
|
||||
{
|
||||
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate
|
||||
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = static delegate
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
@ -301,7 +301,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
{
|
||||
if (_antiRaidGuilds.TryRemove(guildId, out _))
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<AntiRaidSetting>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.DeleteAsync();
|
||||
|
@ -316,7 +316,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
{
|
||||
if (_antiSpamGuilds.TryRemove(guildId, out _))
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<AntiSpamSetting>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.DeleteAsync();
|
||||
|
@ -335,7 +335,9 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
ulong? roleId)
|
||||
{
|
||||
var g = _client.GetGuild(guildId);
|
||||
await _mute.GetMuteRole(g);
|
||||
|
||||
if (action == PunishmentAction.Mute)
|
||||
await _mute.GetMuteRole(g);
|
||||
|
||||
if (!IsDurationAllowed(action))
|
||||
punishDurationMinutes = 0;
|
||||
|
@ -391,7 +393,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
};
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var spam = await uow.GetTable<AntiSpamSetting>()
|
||||
var spam = await uow.Set<AntiSpamSetting>()
|
||||
.Include(x => x.IgnoredChannels)
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.FirstOrDefaultAsyncEF();
|
||||
|
@ -405,6 +407,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
|
||||
temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache
|
||||
|
||||
spam.IgnoredChannels.Add(obj);
|
||||
added = true;
|
||||
}
|
||||
else
|
||||
|
@ -502,9 +505,10 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var configs = await uow.GetTable<AntiAltSetting>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
var gids = _client.GetGuildIds();
|
||||
var configs = await uow.Set<AntiAltSetting>()
|
||||
.Where(x => gids.Contains(x.GuildId))
|
||||
.ToListAsyncEF();
|
||||
|
||||
foreach (var config in configs)
|
||||
_antiAltGuilds[config.GuildId] = new(config);
|
||||
|
@ -522,6 +526,7 @@ public class ProtectionService : IReadyExecutor, IEService
|
|||
}
|
||||
|
||||
var spamConfigs = await uow.GetTable<AntiSpamSetting>()
|
||||
.AsNoTracking()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
|
|
|
@ -84,7 +84,8 @@ public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IE
|
|||
{
|
||||
Name = game,
|
||||
Link = null,
|
||||
Type = type
|
||||
Type = type,
|
||||
Disable = true
|
||||
});
|
||||
|
||||
public Task SetStreamAsync(string name, string link)
|
||||
|
@ -93,7 +94,8 @@ public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IE
|
|||
{
|
||||
Name = name,
|
||||
Link = link,
|
||||
Type = ActivityType.Streaming
|
||||
Type = ActivityType.Streaming,
|
||||
Disable = true
|
||||
});
|
||||
|
||||
private sealed class ActivityPubData
|
||||
|
@ -101,6 +103,7 @@ public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IE
|
|||
public string Name { get; init; }
|
||||
public string Link { get; init; }
|
||||
public ActivityType? Type { get; init; }
|
||||
public bool Disable { get; init; }
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
|
@ -108,7 +111,7 @@ public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IE
|
|||
await _pubSub.Sub(_activitySetKey,
|
||||
async data =>
|
||||
{
|
||||
if (_client.ShardId == 0)
|
||||
if (_client.ShardId == 0 && data.Disable)
|
||||
{
|
||||
DisableRotatePlaying();
|
||||
}
|
||||
|
@ -156,11 +159,19 @@ public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IE
|
|||
: rotatingStatuses[index++];
|
||||
|
||||
var statusText = await _rep.ReplaceAsync(playingStatus.Status, new(client: _client));
|
||||
await SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
|
||||
|
||||
await _pubSub.Pub(_activitySetKey,
|
||||
new()
|
||||
{
|
||||
Name = statusText,
|
||||
Link = null,
|
||||
Type = (ActivityType)playingStatus.Type,
|
||||
Disable = false
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
|
||||
Log.Error(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,8 +85,7 @@ public sealed class GuildTimezoneService : ITimezoneService, IReadyExecutor, IES
|
|||
to = GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
|
||||
}
|
||||
|
||||
return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
|
||||
+ to.StandardName.GetInitials();
|
||||
return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToShortTimeString();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -44,12 +44,12 @@ public partial class Administration
|
|||
try
|
||||
{
|
||||
await _sender.Response(user)
|
||||
.Embed(CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
|
||||
.AddField(GetText(strs.moderator), ctx.User.ToString())
|
||||
.AddField(GetText(strs.reason), reason ?? "-"))
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
|
||||
.AddField(GetText(strs.moderator), ctx.User.ToString())
|
||||
.AddField(GetText(strs.reason), reason ?? "-"))
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -65,8 +65,8 @@ public partial class Administration
|
|||
{
|
||||
Log.Warning(ex, "Exception occured while warning a user");
|
||||
var errorEmbed = CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText(strs.cant_apply_punishment));
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText(strs.cant_apply_punishment));
|
||||
|
||||
if (dmFailed)
|
||||
errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||
|
@ -177,45 +177,45 @@ public partial class Administration
|
|||
var allWarnings = _service.UserWarnings(ctx.Guild.Id, userId);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allWarnings)
|
||||
.PageSize(9)
|
||||
.CurrentPage(inputPage)
|
||||
.Page((warnings, page) =>
|
||||
{
|
||||
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
|
||||
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
|
||||
.Paginated()
|
||||
.Items(allWarnings)
|
||||
.PageSize(9)
|
||||
.CurrentPage(inputPage)
|
||||
.Page((warnings, page) =>
|
||||
{
|
||||
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
|
||||
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
|
||||
|
||||
if (!warnings.Any())
|
||||
embed.WithDescription(GetText(strs.warnings_none));
|
||||
else
|
||||
{
|
||||
var descText = GetText(strs.warn_count(
|
||||
Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()),
|
||||
Format.Bold(warnings.Sum(x => x.Weight).ToString())));
|
||||
if (!warnings.Any())
|
||||
embed.WithDescription(GetText(strs.warnings_none));
|
||||
else
|
||||
{
|
||||
var descText = GetText(strs.warn_count(
|
||||
Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()),
|
||||
Format.Bold(warnings.Sum(x => x.Weight).ToString())));
|
||||
|
||||
embed.WithDescription(descText);
|
||||
embed.WithDescription(descText);
|
||||
|
||||
var i = page * 9;
|
||||
foreach (var w in warnings)
|
||||
{
|
||||
i++;
|
||||
var name = GetText(strs.warned_on_by(w.DateAdded?.ToString("dd.MM.yyy"),
|
||||
w.DateAdded?.ToString("HH:mm"),
|
||||
w.Moderator));
|
||||
var i = page * 9;
|
||||
foreach (var w in warnings)
|
||||
{
|
||||
i++;
|
||||
var name = GetText(strs.warned_on_by(w.DateAdded?.ToString("dd.MM.yyy"),
|
||||
w.DateAdded?.ToString("HH:mm"),
|
||||
w.Moderator));
|
||||
|
||||
if (w.Forgiven)
|
||||
name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
|
||||
if (w.Forgiven)
|
||||
name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
|
||||
|
||||
|
||||
embed.AddField($"#`{i}` " + name,
|
||||
Format.Code(GetText(strs.warn_weight(w.Weight))) + '\n' + w.Reason.TrimTo(1000));
|
||||
}
|
||||
}
|
||||
embed.AddField($"#`{i}` " + name,
|
||||
Format.Code(GetText(strs.warn_weight(w.Weight))) + '\n' + w.Reason.TrimTo(1000));
|
||||
}
|
||||
}
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -228,29 +228,29 @@ public partial class Administration
|
|||
var allWarnings = _service.WarnlogAll(ctx.Guild.Id);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allWarnings)
|
||||
.PageSize(15)
|
||||
.CurrentPage(page)
|
||||
.Page((warnings, _) =>
|
||||
{
|
||||
var ws = warnings
|
||||
.Select(x =>
|
||||
{
|
||||
var all = x.Count();
|
||||
var forgiven = x.Count(y => y.Forgiven);
|
||||
var total = all - forgiven;
|
||||
var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key);
|
||||
return (usr?.ToString() ?? x.Key.ToString())
|
||||
+ $" | {total} ({all} - {forgiven})";
|
||||
});
|
||||
.Paginated()
|
||||
.Items(allWarnings)
|
||||
.PageSize(15)
|
||||
.CurrentPage(page)
|
||||
.Page((warnings, _) =>
|
||||
{
|
||||
var ws = warnings
|
||||
.Select(x =>
|
||||
{
|
||||
var all = x.Count();
|
||||
var forgiven = x.Count(y => y.Forgiven);
|
||||
var total = all - forgiven;
|
||||
var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key);
|
||||
return (usr?.ToString() ?? x.Key.ToString())
|
||||
+ $" | {total} ({all} - {forgiven})";
|
||||
});
|
||||
|
||||
return CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.warnings_list))
|
||||
.WithDescription(string.Join("\n", ws));
|
||||
})
|
||||
.SendAsync();
|
||||
return CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.warnings_list))
|
||||
.WithDescription(string.Join("\n", ws));
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -332,17 +332,17 @@ public partial class Administration
|
|||
if (timespan is null)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString()),
|
||||
Format.Bold(timespan.Input)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString()),
|
||||
Format.Bold(timespan.Input)))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,17 +368,17 @@ public partial class Administration
|
|||
if (timespan is null)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString()),
|
||||
Format.Bold(timespan.Input)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString()),
|
||||
Format.Bold(timespan.Input)))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -458,13 +458,13 @@ public partial class Administration
|
|||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||
await _mute.TimedBan(ctx.Guild, userId, timespan.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
|
||||
.AddField("ID", userId.ToString(), true)
|
||||
.AddField(GetText(strs.duration),
|
||||
timespan.Time.ToPrettyStringHm(),
|
||||
true);
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
|
||||
.AddField("ID", userId.ToString(), true)
|
||||
.AddField(GetText(strs.duration),
|
||||
timespan.Time.ToPrettyStringHm(),
|
||||
true);
|
||||
|
||||
if (dmFailed)
|
||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||
|
@ -486,11 +486,11 @@ public partial class Administration
|
|||
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField("ID", userId.ToString(), true))
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField("ID", userId.ToString(), true))
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
await Ban(user, msg);
|
||||
|
@ -524,10 +524,10 @@ public partial class Administration
|
|||
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
|
||||
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
|
||||
if (dmFailed)
|
||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||
|
@ -704,9 +704,9 @@ public partial class Administration
|
|||
try
|
||||
{
|
||||
await Response()
|
||||
.Channel(await user.CreateDMChannelAsync())
|
||||
.Error(strs.sbdm(Format.Bold(ctx.Guild.Name), msg))
|
||||
.SendAsync();
|
||||
.Channel(await user.CreateDMChannelAsync())
|
||||
.Error(strs.sbdm(Format.Bold(ctx.Guild.Name), msg))
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -716,14 +716,18 @@ public partial class Administration
|
|||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||
await ctx.Guild.AddBanAsync(user, banPrune, ("Softban | " + ctx.User + " | " + msg).TrimTo(512));
|
||||
try
|
||||
{ await ctx.Guild.RemoveBanAsync(user); }
|
||||
catch { await ctx.Guild.RemoveBanAsync(user); }
|
||||
{
|
||||
await ctx.Guild.RemoveBanAsync(user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("☣ " + GetText(strs.sb_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
.WithOkColor()
|
||||
.WithTitle("☣ " + GetText(strs.sb_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
|
||||
if (dmFailed)
|
||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||
|
@ -763,9 +767,9 @@ public partial class Administration
|
|||
try
|
||||
{
|
||||
await Response()
|
||||
.Channel(await user.CreateDMChannelAsync())
|
||||
.Error(GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg)))
|
||||
.SendAsync();
|
||||
.Channel(await user.CreateDMChannelAsync())
|
||||
.Error(GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg)))
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -775,10 +779,10 @@ public partial class Administration
|
|||
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
|
||||
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.kicked_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.kicked_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
|
||||
if (dmFailed)
|
||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||
|
@ -807,10 +811,10 @@ public partial class Administration
|
|||
{
|
||||
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
|
||||
await _sender.Response(user)
|
||||
.Embed(CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription(dmMessage))
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription(dmMessage))
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -820,10 +824,10 @@ public partial class Administration
|
|||
await user.SetTimeOutAsync(timespan.Time);
|
||||
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("⏳ " + GetText(strs.timedout_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
.WithOkColor()
|
||||
.WithTitle("⏳ " + GetText(strs.timedout_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
.AddField("ID", user.Id.ToString(), true);
|
||||
|
||||
if (dmFailed)
|
||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||
|
@ -831,21 +835,38 @@ public partial class Administration
|
|||
await Response().Embed(toSend).SendAsync();
|
||||
}
|
||||
|
||||
public enum PunishType
|
||||
{
|
||||
Kick,
|
||||
Ban
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.KickMembers)]
|
||||
[BotPerm(GuildPerm.KickMembers)]
|
||||
[Ratelimit(30)]
|
||||
public async Task MassKick(params string[] users)
|
||||
=> await MassPunish(PunishType.Ban, users);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
[BotPerm(GuildPerm.BanMembers)]
|
||||
[Ratelimit(30)]
|
||||
public async Task MassBan(params string[] userStrings)
|
||||
public async Task MassBan(params string[] users)
|
||||
=> await MassPunish(PunishType.Ban, users);
|
||||
|
||||
private async Task MassPunish(PunishType type, params string[] users)
|
||||
{
|
||||
if (userStrings.Length == 0)
|
||||
if (users.Length == 0)
|
||||
return;
|
||||
|
||||
var missing = new List<string>();
|
||||
var banning = new HashSet<IUser>();
|
||||
var punishing = new HashSet<IUser>();
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
foreach (var userStr in userStrings)
|
||||
foreach (var userStr in users)
|
||||
{
|
||||
if (ulong.TryParse(userStr, out var userId))
|
||||
{
|
||||
|
@ -870,7 +891,7 @@ public partial class Administration
|
|||
if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
|
||||
return;
|
||||
|
||||
banning.Add(user);
|
||||
punishing.Add(user);
|
||||
}
|
||||
else
|
||||
missing.Add(userStr);
|
||||
|
@ -881,33 +902,48 @@ public partial class Administration
|
|||
missStr = "-";
|
||||
|
||||
var toSend = CreateEmbed()
|
||||
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
|
||||
.AddField(GetText(strs.invalid(missing.Count)), missStr)
|
||||
.WithPendingColor();
|
||||
.WithDescription(GetText(strs.mass_ban_in_progress(punishing.Count)))
|
||||
.AddField(GetText(strs.invalid(missing.Count)), missStr)
|
||||
.WithPendingColor();
|
||||
|
||||
var banningMessage = await Response().Embed(toSend).SendAsync();
|
||||
|
||||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||
foreach (var toBan in banning)
|
||||
|
||||
foreach (var toPunish in punishing)
|
||||
{
|
||||
try
|
||||
if (type == PunishType.Kick)
|
||||
{
|
||||
await ctx.Guild.AddBanAsync(toBan.Id, banPrune, $"{ctx.User} | Massban");
|
||||
try
|
||||
{
|
||||
if (toPunish is IGuildUser u)
|
||||
await u.KickAsync($"{ctx.User} | Masskick");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error kicking {User} user in {GuildId} server", toPunish.Id, ctx.Guild.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Log.Warning(ex, "Error banning {User} user in {GuildId} server", toBan.Id, ctx.Guild.Id);
|
||||
try
|
||||
{
|
||||
await ctx.Guild.AddBanAsync(toPunish.Id, banPrune, $"{ctx.User} | Massban");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error banning {User} user in {GuildId} server", toPunish.Id, ctx.Guild.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
|
||||
.WithDescription(
|
||||
GetText(strs.mass_ban_completed(
|
||||
banning.Count())))
|
||||
.AddField(GetText(strs.invalid(missing.Count)),
|
||||
missStr)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
.WithDescription(
|
||||
GetText(strs.mass_ban_completed(
|
||||
punishing.Count())))
|
||||
.AddField(GetText(strs.invalid(missing.Count)),
|
||||
missStr)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -928,33 +964,33 @@ public partial class Administration
|
|||
|
||||
//send a message but don't wait for it
|
||||
var banningMessageTask = Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithDescription(
|
||||
GetText(strs.mass_kill_in_progress(bans.Count())))
|
||||
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||
.WithPendingColor())
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithDescription(
|
||||
GetText(strs.mass_kill_in_progress(bans.Count())))
|
||||
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||
.WithPendingColor())
|
||||
.SendAsync();
|
||||
|
||||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||
//do the banning
|
||||
await Task.WhenAll(bans.Where(x => x.Id.HasValue)
|
||||
.Select(x => ctx.Guild.AddBanAsync(x.Id.Value,
|
||||
banPrune,
|
||||
x.Reason,
|
||||
new()
|
||||
{
|
||||
RetryMode = RetryMode.AlwaysRetry
|
||||
})));
|
||||
.Select(x => ctx.Guild.AddBanAsync(x.Id.Value,
|
||||
banPrune,
|
||||
x.Reason,
|
||||
new()
|
||||
{
|
||||
RetryMode = RetryMode.AlwaysRetry
|
||||
})));
|
||||
|
||||
//wait for the message and edit it
|
||||
var banningMessage = await banningMessageTask;
|
||||
|
||||
await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
|
||||
.WithDescription(
|
||||
GetText(strs.mass_kill_completed(bans.Count())))
|
||||
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
.WithDescription(
|
||||
GetText(strs.mass_kill_completed(bans.Count())))
|
||||
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
}
|
||||
|
||||
public class WarnExpireOptions : IEllieCommandOptions
|
||||
|
|
|
@ -15,13 +15,6 @@ using EllieBot.Modules.Gambling.Rps;
|
|||
using EllieBot.Common.TypeReaders;
|
||||
using EllieBot.Modules.Games;
|
||||
using EllieBot.Modules.Patronage;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Unicode;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
namespace EllieBot.Modules.Gambling;
|
||||
|
||||
|
@ -218,20 +211,20 @@ 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();
|
||||
.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;
|
||||
|
@ -296,8 +289,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,10 +309,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
||||
await Response()
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -337,10 +330,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
||||
await Response()
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -373,41 +366,54 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
return;
|
||||
}
|
||||
|
||||
List<CurrencyTransaction> trs;
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.transactions(
|
||||
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
||||
?? $"{userId}")))
|
||||
.WithOkColor();
|
||||
|
||||
int count;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
|
||||
count = await uow.Set<CurrencyTransaction>()
|
||||
.GetCountFor(userId);
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.transactions(
|
||||
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
||||
?? $"{userId}")))
|
||||
.WithOkColor();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var tr in trs)
|
||||
{
|
||||
var change = tr.Amount >= 0 ? "🔵" : "🔴";
|
||||
var kwumId = new kwum(tr.Id).ToString();
|
||||
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
|
||||
|
||||
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
|
||||
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
|
||||
if (transactionString is not null)
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(async (curPage) =>
|
||||
{
|
||||
sb.AppendLine(transactionString);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tr.Note))
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<CurrencyTransaction>()
|
||||
.GetPageFor(userId, curPage);
|
||||
})
|
||||
.PageSize(15)
|
||||
.TotalElements(count)
|
||||
.Page((trs, _) =>
|
||||
{
|
||||
sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}");
|
||||
}
|
||||
}
|
||||
var sb = new StringBuilder();
|
||||
foreach (var tr in trs)
|
||||
{
|
||||
var change = tr.Amount >= 0 ? "🔵" : "🔴";
|
||||
var kwumId = new kwum(tr.Id).ToString();
|
||||
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
|
||||
|
||||
embed.WithDescription(sb.ToString());
|
||||
embed.WithFooter(GetText(strs.page(page + 1)));
|
||||
await Response().Embed(embed).SendAsync();
|
||||
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
|
||||
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
|
||||
if (transactionString is not null)
|
||||
{
|
||||
sb.AppendLine(transactionString);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tr.Note))
|
||||
{
|
||||
sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}");
|
||||
}
|
||||
}
|
||||
|
||||
embed.WithDescription(sb.ToString());
|
||||
return Task.FromResult(embed);
|
||||
}).SendAsync();
|
||||
}
|
||||
|
||||
private static string GetFormattedCurtrDate(CurrencyTransaction ct)
|
||||
|
@ -420,9 +426,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var tr = await uow.Set<CurrencyTransaction>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (tr is null)
|
||||
{
|
||||
|
@ -483,9 +489,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
|
||||
|
||||
await N(balance)
|
||||
.Pipe(strs.bank_balance)
|
||||
.Pipe(GetText)
|
||||
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
||||
.Pipe(strs.bank_balance)
|
||||
.Pipe(GetText)
|
||||
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
||||
}
|
||||
|
||||
private EllieInteractionBase CreateCashInteraction()
|
||||
|
@ -507,13 +513,13 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
: null;
|
||||
|
||||
await Response()
|
||||
.Confirm(
|
||||
user.ToString()
|
||||
.Pipe(Format.Bold)
|
||||
.With(cur)
|
||||
.Pipe(strs.has))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
.Confirm(
|
||||
user.ToString()
|
||||
.Pipe(Format.Bold)
|
||||
.With(cur)
|
||||
.Pipe(strs.has))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -594,10 +600,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.mass_award(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.mass_award(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -613,10 +619,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
new("take", ctx.User.ToString()!, null, ctx.User.Id));
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.mass_take(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.mass_take(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -639,8 +645,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -662,8 +668,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -695,12 +701,12 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(Format.Bold(str))
|
||||
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), true)
|
||||
.WithOkColor();
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(Format.Bold(str))
|
||||
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), true)
|
||||
.WithOkColor();
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
|
@ -741,11 +747,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var cleanRichest = await uow.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId.In(users))
|
||||
.OrderByDescending(x => x.CurrencyAmount)
|
||||
.Skip(curPage * perPage)
|
||||
.Take(perPage)
|
||||
.ToListAsync();
|
||||
.Where(x => x.UserId.In(users))
|
||||
.OrderByDescending(x => x.CurrencyAmount)
|
||||
.Skip(curPage * perPage)
|
||||
.Take(perPage)
|
||||
.ToListAsync();
|
||||
|
||||
return cleanRichest;
|
||||
}
|
||||
|
@ -757,34 +763,34 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(GetTopRichest)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((toSend, curPage) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
||||
.Paginated()
|
||||
.PageItems(GetTopRichest)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((toSend, curPage) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
||||
|
||||
if (!toSend.Any())
|
||||
{
|
||||
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
||||
return Task.FromResult(embed);
|
||||
}
|
||||
if (!toSend.Any())
|
||||
{
|
||||
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
||||
return Task.FromResult(embed);
|
||||
}
|
||||
|
||||
for (var i = 0; i < toSend.Count; i++)
|
||||
{
|
||||
var x = toSend[i];
|
||||
var usrStr = x.ToString().TrimTo(20, true);
|
||||
for (var i = 0; i < toSend.Count; i++)
|
||||
{
|
||||
var x = toSend[i];
|
||||
var usrStr = x.ToString().TrimTo(20, true);
|
||||
|
||||
var j = i;
|
||||
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
||||
}
|
||||
var j = i;
|
||||
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
||||
}
|
||||
|
||||
return Task.FromResult(embed);
|
||||
})
|
||||
.SendAsync();
|
||||
return Task.FromResult(embed);
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
public enum InputRpsPick : byte
|
||||
|
@ -895,11 +901,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(sb.ToString())
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
|
||||
.WithAuthor(ctx.User);
|
||||
.WithOkColor()
|
||||
.WithDescription(sb.ToString())
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
|
||||
.WithAuthor(ctx.User);
|
||||
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
|
@ -924,8 +930,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
public async Task BetTest()
|
||||
{
|
||||
var values = Enum.GetValues<GambleTestTarget>()
|
||||
.Select(x => $"`{x}`")
|
||||
.Join(", ");
|
||||
.Select(x => $"`{x}`")
|
||||
.Join(", ");
|
||||
|
||||
await Response().Confirm(GetText(strs.available_tests), values).SendAsync();
|
||||
}
|
||||
|
@ -998,10 +1004,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
sb.AppendLine($"Longest lose streak: `{maxL}`");
|
||||
|
||||
await Response()
|
||||
.Confirm(GetText(strs.test_results_for(target)),
|
||||
sb.ToString(),
|
||||
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.test_results_for(target)),
|
||||
sb.ToString(),
|
||||
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private EllieInteractionBase CreateRakebackInteraction()
|
||||
|
@ -1032,16 +1038,16 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
if (rb < 1)
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.rakeback_none)
|
||||
.SendAsync();
|
||||
.Error(strs.rakeback_none)
|
||||
.SendAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var inter = CreateRakebackInteraction();
|
||||
await Response()
|
||||
.Pending(strs.rakeback_available(N(rb)))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
.Pending(strs.rakeback_available(N(rb)))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
|
@ -25,13 +25,11 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
private readonly FontProvider _fonts;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly CommandHandler _cmdHandler;
|
||||
private readonly EllieRandom _rng;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly GamblingConfigService _gss;
|
||||
private readonly GamblingService _gs;
|
||||
|
||||
private ConcurrentHashSet<ulong> _generationChannels;
|
||||
private readonly SemaphoreSlim _pickLock = new(1, 1);
|
||||
private ConcurrentHashSet<ulong> _generationChannels = [];
|
||||
|
||||
public PlantPickService(
|
||||
DbService db,
|
||||
|
@ -50,13 +48,9 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
_fonts = fonts;
|
||||
_cs = cs;
|
||||
_cmdHandler = cmdHandler;
|
||||
_rng = new();
|
||||
_client = client;
|
||||
_gss = gss;
|
||||
_gs = gs;
|
||||
|
||||
using var uow = db.GetDbContext();
|
||||
var guildIds = client.Guilds.Select(x => x.Id).ToList();
|
||||
}
|
||||
|
||||
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
||||
|
@ -416,7 +410,6 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor
|
|||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
_generationChannels = (await uow.GetTable<GCChannelId>()
|
||||
.Select(x => x.ChannelId)
|
||||
|
|
|
@ -180,17 +180,17 @@ public partial class Gambling
|
|||
|
||||
Color fontColor = Config.Slots.CurrencyFontColor;
|
||||
|
||||
bgImage.Mutate<Rgba32>(x => x.DrawText(new RichTextOptions(_fonts.DottyFont.CreateFont(65))
|
||||
bgImage.Mutate<Rgba32>(x => x.DrawText(new RichTextOptions(_fonts.NotoSans.CreateFont(35))
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
WrappingLength = 140,
|
||||
Origin = new(298, 100)
|
||||
Origin = new(295, 100)
|
||||
},
|
||||
((long)result.Won).ToString(),
|
||||
fontColor));
|
||||
|
||||
var bottomFont = _fonts.DottyFont.CreateFont(50);
|
||||
var bottomFont = _fonts.NotoSans.CreateFont(50);
|
||||
|
||||
bgImage.Mutate(x => x.DrawText(new RichTextOptions(bottomFont)
|
||||
{
|
||||
|
@ -206,7 +206,7 @@ public partial class Gambling
|
|||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(393, 480)
|
||||
Origin = new(393, 479)
|
||||
},
|
||||
ownedAmount.ToString(),
|
||||
fontColor));
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace EllieBot.Modules.Games.Services;
|
|||
|
||||
public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
||||
{
|
||||
private ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> _chatterBotGuilds;
|
||||
private ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> _chatterBotGuilds = [];
|
||||
|
||||
public int Priority
|
||||
=> 1;
|
||||
|
@ -165,8 +165,8 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
|||
(inTokens) + (result.TokensOut / 2 * 3));
|
||||
|
||||
await _sender.Response(channel)
|
||||
.Confirm(result.Text)
|
||||
.SendAsync();
|
||||
.Confirm(result.Text)
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -204,12 +204,12 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
|||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.Set<GuildConfig>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateAsync((gc) => new GuildConfig()
|
||||
{
|
||||
CleverbotEnabled = false
|
||||
});
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateAsync((gc) => new GuildConfig()
|
||||
{
|
||||
CleverbotEnabled = false
|
||||
});
|
||||
await uow.SaveChangesAsync();
|
||||
return false;
|
||||
}
|
||||
|
@ -219,12 +219,12 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
|||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
await uow.Set<GuildConfig>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateAsync((gc) => new GuildConfig()
|
||||
{
|
||||
CleverbotEnabled = true
|
||||
});
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateAsync((gc) => new GuildConfig()
|
||||
{
|
||||
CleverbotEnabled = true
|
||||
});
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
@ -235,12 +235,13 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
|
|||
public async Task OnReadyAsync()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
_chatterBotGuilds = uow.GuildConfigs
|
||||
.AsNoTracking()
|
||||
.Where(gc => gc.CleverbotEnabled)
|
||||
.AsEnumerable()
|
||||
.ToDictionary(gc => gc.GuildId,
|
||||
_ => new Lazy<IChatterBotSession>(() => CreateSession(), true))
|
||||
.ToConcurrent();
|
||||
_chatterBotGuilds = await uow.GuildConfigs
|
||||
.AsNoTracking()
|
||||
.Where(gc => gc.CleverbotEnabled)
|
||||
.ToListAsyncLinqToDB()
|
||||
.Fmap(x => x
|
||||
.ToDictionary(gc => gc.GuildId,
|
||||
_ => new Lazy<IChatterBotSession>(() => CreateSession(), true))
|
||||
.ToConcurrent());
|
||||
}
|
||||
}
|
|
@ -35,7 +35,6 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
|
|||
if (!_source.GetTerm(category, out var term))
|
||||
return false;
|
||||
|
||||
|
||||
var game = new HangmanGame(term);
|
||||
lock (_locker)
|
||||
{
|
||||
|
@ -52,11 +51,8 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
|
|||
|
||||
public ValueTask<bool> StopHangman(ulong channelId)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
if (_hangmanGames.TryRemove(channelId, out _))
|
||||
return new(true);
|
||||
}
|
||||
if (_hangmanGames.TryRemove(channelId, out _))
|
||||
return new(true);
|
||||
|
||||
return new(false);
|
||||
}
|
||||
|
@ -66,48 +62,50 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand
|
|||
|
||||
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
||||
{
|
||||
if (_hangmanGames.ContainsKey(msg.Channel.Id))
|
||||
if (!_hangmanGames.ContainsKey(msg.Channel.Id))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(msg.Content))
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(msg.Content))
|
||||
return;
|
||||
|
||||
if (_cdCache.TryGetValue("hangman:" + msg.Author.Id, out _))
|
||||
return;
|
||||
|
||||
HangmanGame.State state;
|
||||
long rew = 0;
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game))
|
||||
return;
|
||||
|
||||
if (_cdCache.TryGetValue(msg.Author.Id, out _))
|
||||
state = game.Guess(msg.Content.ToLowerInvariant());
|
||||
|
||||
if (state.GuessResult == HangmanGame.GuessResult.NoAction)
|
||||
return;
|
||||
|
||||
HangmanGame.State state;
|
||||
long rew = 0;
|
||||
lock (_locker)
|
||||
if (state.GuessResult is HangmanGame.GuessResult.Incorrect or HangmanGame.GuessResult.AlreadyTried)
|
||||
{
|
||||
if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game))
|
||||
return;
|
||||
|
||||
state = game.Guess(msg.Content.ToLowerInvariant());
|
||||
|
||||
if (state.GuessResult == HangmanGame.GuessResult.NoAction)
|
||||
return;
|
||||
|
||||
if (state.GuessResult is HangmanGame.GuessResult.Incorrect or HangmanGame.GuessResult.AlreadyTried)
|
||||
{
|
||||
_cdCache.Set(msg.Author.Id,
|
||||
string.Empty,
|
||||
new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
|
||||
if (state.Phase == HangmanGame.Phase.Ended)
|
||||
{
|
||||
if (_hangmanGames.TryRemove(msg.Channel.Id, out _))
|
||||
rew = _gcs.Data.Hangman.CurrencyReward;
|
||||
}
|
||||
_cdCache.Set("hangman:" + msg.Author.Id,
|
||||
string.Empty,
|
||||
new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
|
||||
if (rew > 0)
|
||||
await _cs.AddAsync(msg.Author, rew, new("hangman", "win"));
|
||||
|
||||
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
|
||||
if (state.Phase == HangmanGame.Phase.Ended)
|
||||
{
|
||||
if (_hangmanGames.TryRemove(msg.Channel.Id, out _))
|
||||
rew = _gcs.Data.Hangman.CurrencyReward;
|
||||
}
|
||||
}
|
||||
|
||||
if (rew > 0)
|
||||
await _cs.AddAsync(msg.Author, rew, new("hangman", "win"));
|
||||
|
||||
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
|
||||
}
|
||||
|
||||
private Task<IUserMessage> SendState(
|
||||
|
|
62
src/EllieBot/Modules/Help/CommandListGenerator.cs
Normal file
62
src/EllieBot/Modules/Help/CommandListGenerator.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Ellie.Common.Marmalade;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EllieBot.Modules.Help;
|
||||
|
||||
public sealed partial class CommandListGenerator(
|
||||
CommandService cmds,
|
||||
IMarmaladeLoaderService marmalades,
|
||||
IBotStrings strings
|
||||
) : IEService, IReadyExecutor
|
||||
{
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
await Task.Delay(10_000);
|
||||
|
||||
#if DEBUG
|
||||
await GenerateCommandListAsync(".", CultureInfo.InvariantCulture);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async Task<Stream> GenerateCommandListAsync(string prefix, CultureInfo culture)
|
||||
{
|
||||
// order commands by top level name
|
||||
// and make a dictionary of <ModuleName, Array<JsonCommandData>>
|
||||
var cmdData = cmds.Commands.GroupBy(x => x.Module.GetTopLevelModule().Name)
|
||||
.OrderBy(x => x.Key)
|
||||
.ToDictionary(x => x.Key,
|
||||
x => x.DistinctBy(c => c.Aliases.First())
|
||||
.Select(com =>
|
||||
{
|
||||
List<string> optHelpStr = null;
|
||||
|
||||
var opt = CommandsUtilityService.GetEllieOptionType(com.Attributes);
|
||||
if (opt is not null)
|
||||
optHelpStr = CommandsUtilityService.GetCommandOptionHelpList(opt);
|
||||
|
||||
return new CommandJsonObject
|
||||
{
|
||||
Aliases = com.Aliases.Select(alias => prefix + alias).ToArray(),
|
||||
Description = com.RealSummary(strings, marmalades, culture, prefix),
|
||||
Usage = com.RealRemarksArr(strings, marmalades, culture, prefix),
|
||||
Submodule = com.Module.Name,
|
||||
Module = com.Module.GetTopLevelModule().Name,
|
||||
Options = optHelpStr,
|
||||
Requirements = CommandsUtilityService.GetCommandRequirements(com)
|
||||
};
|
||||
})
|
||||
.ToList());
|
||||
|
||||
var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented);
|
||||
|
||||
// send the indented file to chat
|
||||
var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData));
|
||||
await File.WriteAllTextAsync("data/commandlist.json", readableData);
|
||||
|
||||
return rDataStream;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
#nullable disable
|
||||
using EllieBot.Modules.Help.Common;
|
||||
using EllieBot.Modules.Help.Services;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using Ellie.Common.Marmalade;
|
||||
|
||||
namespace EllieBot.Modules.Help;
|
||||
|
@ -22,6 +20,7 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
|
||||
private readonly AsyncLazy<ulong> _lazyClientId;
|
||||
private readonly IMarmaladeLoaderService _marmalades;
|
||||
private readonly CommandListGenerator _cmdListGen;
|
||||
|
||||
public Help(
|
||||
ICommandsUtilityService _cus,
|
||||
|
@ -31,7 +30,8 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
IServiceProvider services,
|
||||
DiscordSocketClient client,
|
||||
IBotStrings strings,
|
||||
IMarmaladeLoaderService marmalades)
|
||||
IMarmaladeLoaderService marmalades,
|
||||
CommandListGenerator cmdListGen)
|
||||
{
|
||||
this._cus = _cus;
|
||||
_cmds = cmds;
|
||||
|
@ -41,6 +41,7 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
_client = client;
|
||||
_strings = strings;
|
||||
_marmalades = marmalades;
|
||||
_cmdListGen = cmdListGen;
|
||||
|
||||
_lazyClientId = new(async () => (await _client.GetApplicationInfoAsync()).Id);
|
||||
}
|
||||
|
@ -53,10 +54,10 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
|
||||
var clientId = await _lazyClientId.Value;
|
||||
var repCtx = new ReplacementContext(Context)
|
||||
.WithOverride("{0}", () => clientId.ToString())
|
||||
.WithOverride("{1}", () => prefix)
|
||||
.WithOverride("%prefix%", () => prefix)
|
||||
.WithOverride("%bot.prefix%", () => prefix);
|
||||
.WithOverride("{0}", () => clientId.ToString())
|
||||
.WithOverride("{1}", () => prefix)
|
||||
.WithOverride("%prefix%", () => prefix)
|
||||
.WithOverride("%bot.prefix%", () => prefix);
|
||||
|
||||
var text = SmartText.CreateFrom(botSettings.HelpText);
|
||||
return await repSvc.ReplaceAsync(text, repCtx);
|
||||
|
@ -87,8 +88,8 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
}
|
||||
|
||||
var menu = new SelectMenuBuilder()
|
||||
.WithPlaceholder("Select a module to see its commands")
|
||||
.WithCustomId("cmds:modules_select");
|
||||
.WithPlaceholder("Select a module to see its commands")
|
||||
.WithCustomId("cmds:modules_select");
|
||||
|
||||
foreach (var m in topLevelModules)
|
||||
menu.AddOption(m.Name, m.Name, GetModuleEmoji(m.Name));
|
||||
|
@ -106,33 +107,33 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
});
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(topLevelModules)
|
||||
.PageSize(12)
|
||||
.CurrentPage(page)
|
||||
.Interaction(inter)
|
||||
.AddFooter(false)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.list_of_modules));
|
||||
.Paginated()
|
||||
.Items(topLevelModules)
|
||||
.PageSize(12)
|
||||
.CurrentPage(page)
|
||||
.Interaction(inter)
|
||||
.AddFooter(false)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.list_of_modules));
|
||||
|
||||
if (!items.Any())
|
||||
{
|
||||
embed = embed.WithOkColor().WithDescription(GetText(strs.module_page_empty));
|
||||
return embed;
|
||||
}
|
||||
if (!items.Any())
|
||||
{
|
||||
embed = embed.WithOkColor().WithDescription(GetText(strs.module_page_empty));
|
||||
return embed;
|
||||
}
|
||||
|
||||
items
|
||||
.ToList()
|
||||
.ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}",
|
||||
GetModuleDescription(module.Name)
|
||||
+ "\n"
|
||||
+ Format.Code(GetText(strs.module_footer(prefix, module.Name.ToLowerInvariant()))),
|
||||
true));
|
||||
items
|
||||
.ToList()
|
||||
.ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}",
|
||||
GetModuleDescription(module.Name)
|
||||
+ "\n"
|
||||
+ Format.Code(GetText(strs.module_footer(prefix, module.Name.ToLowerInvariant()))),
|
||||
true));
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private string GetModuleDescription(string moduleName)
|
||||
|
@ -142,11 +143,11 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
if (key.Key == strs.module_description_missing.Key)
|
||||
{
|
||||
var desc = _marmalades
|
||||
.GetLoadedMarmalades(Culture)
|
||||
.FirstOrDefault(m => m.Canaries
|
||||
.Any(x => x.Name.Equals(moduleName,
|
||||
StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.Description;
|
||||
.GetLoadedMarmalades(Culture)
|
||||
.FirstOrDefault(m => m.Canaries
|
||||
.Any(x => x.Name.Equals(moduleName,
|
||||
StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.Description;
|
||||
|
||||
if (desc is not null)
|
||||
return desc;
|
||||
|
@ -238,18 +239,18 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
var allowed = new List<CommandInfo>();
|
||||
|
||||
var mdls = _cmds.Commands
|
||||
.Where(c => c.Module.GetTopLevelModule()
|
||||
.Name
|
||||
.StartsWith(module, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
.Where(c => c.Module.GetTopLevelModule()
|
||||
.Name
|
||||
.StartsWith(module, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
if (mdls.Length == 0)
|
||||
{
|
||||
var group = _cmds.Modules
|
||||
.Where(x => x.Parent is not null)
|
||||
.FirstOrDefault(x => string.Equals(x.Name.Replace("Commands", ""),
|
||||
module,
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
.Where(x => x.Parent is not null)
|
||||
.FirstOrDefault(x => string.Equals(x.Name.Replace("Commands", ""),
|
||||
module,
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (group is not null)
|
||||
{
|
||||
|
@ -272,8 +273,8 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
|
||||
|
||||
var cmds = allowed.OrderBy(c => c.Aliases[0])
|
||||
.DistinctBy(x => x.Aliases[0])
|
||||
.ToList();
|
||||
.DistinctBy(x => x.Aliases[0])
|
||||
.ToList();
|
||||
|
||||
|
||||
// check preconditions for all commands, but only if it's not 'all'
|
||||
|
@ -284,12 +285,12 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
succ =
|
||||
[
|
||||
..(await cmds.Select(async x =>
|
||||
{
|
||||
var pre = await x.CheckPreconditionsAsync(Context, _services);
|
||||
return (Cmd: x, Succ: pre.IsSuccess);
|
||||
})
|
||||
.WhenAll()).Where(x => x.Succ)
|
||||
.Select(x => x.Cmd)
|
||||
{
|
||||
var pre = await x.CheckPreconditionsAsync(Context, _services);
|
||||
return (Cmd: x, Succ: pre.IsSuccess);
|
||||
})
|
||||
.WhenAll()).Where(x => x.Succ)
|
||||
.Select(x => x.Cmd)
|
||||
];
|
||||
|
||||
if (opts.View == CommandsOptions.ViewType.Hide)
|
||||
|
@ -298,8 +299,8 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
}
|
||||
|
||||
var cmdsWithGroup = cmds.GroupBy(c => c.Module.GetGroupName())
|
||||
.OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
|
||||
.ToList();
|
||||
.OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
|
||||
.ToList();
|
||||
|
||||
if (cmdsWithGroup.Count == 0)
|
||||
{
|
||||
|
@ -311,8 +312,8 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
}
|
||||
|
||||
var sb = new SelectMenuBuilder()
|
||||
.WithCustomId("cmds:submodule_select")
|
||||
.WithPlaceholder("Select a submodule to see detailed commands");
|
||||
.WithCustomId("cmds:submodule_select")
|
||||
.WithPlaceholder("Select a submodule to see detailed commands");
|
||||
|
||||
var groups = cmdsWithGroup.ToArray();
|
||||
var embed = CreateEmbed().WithOkColor();
|
||||
|
@ -322,17 +323,29 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
var transformed = g
|
||||
.Select(x =>
|
||||
{
|
||||
var prepend = string.Empty;
|
||||
|
||||
var isOwnerOnly = x.Preconditions.Any(x => x is OwnerOnlyAttribute);
|
||||
if (isOwnerOnly)
|
||||
prepend = " \\👑";
|
||||
|
||||
//if cross is specified, and the command doesn't satisfy the requirements, cross it out
|
||||
if (opts.View == CommandsOptions.ViewType.Cross)
|
||||
{
|
||||
return $"{(succ.Contains(x) ? "✅" : "❌")} {prefix + x.Aliases[0]}";
|
||||
var outp = $"{(succ.Contains(x) ? "✅" : "❌")} {prefix + x.Aliases[0]}";
|
||||
|
||||
return prepend + outp;
|
||||
}
|
||||
|
||||
var output = prefix + x.Aliases[0];
|
||||
|
||||
if (x.Aliases.Count == 1)
|
||||
return prefix + x.Aliases[0];
|
||||
if (x.Aliases.Count > 1)
|
||||
output += " | " + prefix + x.Aliases[1];
|
||||
|
||||
return prefix + x.Aliases[0] + " | " + prefix + x.Aliases[1];
|
||||
if (opts.View == CommandsOptions.ViewType.All)
|
||||
return prepend + output;
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
embed.AddField(g.Key, "" + string.Join("\n", transformed) + "", true);
|
||||
|
@ -347,7 +360,9 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
{
|
||||
var groupName = smc.Data.Values.FirstOrDefault();
|
||||
var mdl = _cmds.Modules.FirstOrDefault(x
|
||||
=> string.Equals(x.Name.Replace("Commands", ""), groupName, StringComparison.InvariantCultureIgnoreCase));
|
||||
=> string.Equals(x.Name.Replace("Commands", ""),
|
||||
groupName,
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
await smc.DeferAsync();
|
||||
await Group(mdl);
|
||||
}
|
||||
|
@ -359,8 +374,8 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
private async Task Group(ModuleInfo group)
|
||||
{
|
||||
var menu = new SelectMenuBuilder()
|
||||
.WithCustomId("cmds:group_select")
|
||||
.WithPlaceholder("Select a command to see its details");
|
||||
.WithCustomId("cmds:group_select")
|
||||
.WithPlaceholder("Select a command to see its details");
|
||||
|
||||
foreach (var cmd in group.Commands.DistinctBy(x => x.Aliases[0]))
|
||||
{
|
||||
|
@ -377,30 +392,30 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
});
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(group.Commands.DistinctBy(x => x.Aliases[0]).ToArray())
|
||||
.PageSize(25)
|
||||
.Interaction(inter)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.cmd_group_commands(group.Name)))
|
||||
.WithOkColor();
|
||||
.Paginated()
|
||||
.Items(group.Commands.DistinctBy(x => x.Aliases[0]).ToArray())
|
||||
.PageSize(25)
|
||||
.Interaction(inter)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.cmd_group_commands(group.Name)))
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var cmd in items)
|
||||
{
|
||||
string cmdName;
|
||||
if (cmd.Aliases.Count > 1)
|
||||
cmdName = Format.Code(prefix + cmd.Aliases[0]) + " | " + Format.Code(prefix + cmd.Aliases[1]);
|
||||
else
|
||||
cmdName = Format.Code(prefix + cmd.Aliases.First());
|
||||
foreach (var cmd in items)
|
||||
{
|
||||
string cmdName;
|
||||
if (cmd.Aliases.Count > 1)
|
||||
cmdName = Format.Code(prefix + cmd.Aliases[0]) + " | " + Format.Code(prefix + cmd.Aliases[1]);
|
||||
else
|
||||
cmdName = Format.Code(prefix + cmd.Aliases.First());
|
||||
|
||||
eb.AddField(cmdName, cmd.RealSummary(_strings, _marmalades, Culture, prefix));
|
||||
}
|
||||
eb.AddField(cmdName, cmd.RealSummary(_strings, _marmalades, Culture, prefix));
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -419,10 +434,10 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
fail = fail.Substring(prefix.Length);
|
||||
|
||||
var group = _cmds.Modules
|
||||
.SelectMany(x => x.Submodules)
|
||||
.FirstOrDefault(x => string.Equals(x.Group,
|
||||
fail,
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
.SelectMany(x => x.Submodules)
|
||||
.FirstOrDefault(x => string.Equals(x.Group,
|
||||
fail,
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (group is not null)
|
||||
{
|
||||
|
@ -474,55 +489,24 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
{
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
// order commands by top level module name
|
||||
// and make a dictionary of <ModuleName, Array<JsonCommandData>>
|
||||
var cmdData = _cmds.Commands.GroupBy(x => x.Module.GetTopLevelModule().Name)
|
||||
.OrderBy(x => x.Key)
|
||||
.ToDictionary(x => x.Key,
|
||||
x => x.DistinctBy(c => c.Aliases.First())
|
||||
.Select(com =>
|
||||
{
|
||||
List<string> optHelpStr = null;
|
||||
|
||||
var opt = CommandsUtilityService.GetEllieOptionType(com.Attributes);
|
||||
if (opt is not null)
|
||||
optHelpStr = CommandsUtilityService.GetCommandOptionHelpList(opt);
|
||||
|
||||
return new CommandJsonObject
|
||||
{
|
||||
Aliases = com.Aliases.Select(alias => prefix + alias).ToArray(),
|
||||
Description = com.RealSummary(_strings, _marmalades, Culture, prefix),
|
||||
Usage = com.RealRemarksArr(_strings, _marmalades, Culture, prefix),
|
||||
Submodule = com.Module.Name,
|
||||
Module = com.Module.GetTopLevelModule().Name,
|
||||
Options = optHelpStr,
|
||||
Requirements = CommandsUtilityService.GetCommandRequirements(com)
|
||||
};
|
||||
})
|
||||
.ToList());
|
||||
|
||||
var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented);
|
||||
|
||||
// send the indented file to chat
|
||||
await using var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData));
|
||||
await File.WriteAllTextAsync("data/commandlist.json", readableData);
|
||||
await using var rDataStream = await _cmdListGen.GenerateCommandListAsync(prefix, Culture);
|
||||
await ctx.Channel.SendFileAsync(rDataStream, "cmds.json", GetText(strs.commandlist_regen));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Guide()
|
||||
=> await Response()
|
||||
.Confirm(strs.guide("https://commands.elliebot.net",
|
||||
"https://docs.elliebot.net"))
|
||||
.SendAsync();
|
||||
.Confirm(strs.guide("https://commands.elliebot.net",
|
||||
"https://docs.elliebot.net"))
|
||||
.SendAsync();
|
||||
|
||||
[Cmd]
|
||||
[OnlyPublicBot]
|
||||
public async Task Donate()
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("Thank you for considering to donate to the EllieBot project!");
|
||||
.WithOkColor()
|
||||
.WithTitle("Thank you for considering to donate to the EllieBot project!");
|
||||
|
||||
eb
|
||||
.WithDescription("""
|
||||
|
@ -554,9 +538,9 @@ public sealed partial class Help : EllieModule<HelpService>
|
|||
try
|
||||
{
|
||||
await Response()
|
||||
.Channel(await ctx.User.CreateDMChannelAsync())
|
||||
.Embed(eb)
|
||||
.SendAsync();
|
||||
.Channel(await ctx.User.CreateDMChannelAsync())
|
||||
.Embed(eb)
|
||||
.SendAsync();
|
||||
|
||||
_ = ctx.OkAsync();
|
||||
}
|
||||
|
|
|
@ -7,13 +7,24 @@ namespace EllieBot.Modules.Music;
|
|||
[NoPublicBot]
|
||||
public sealed partial class Music : EllieModule<IMusicService>
|
||||
{
|
||||
public enum All { All = -1 }
|
||||
public enum All
|
||||
{
|
||||
All = -1
|
||||
}
|
||||
|
||||
public enum InputRepeatType
|
||||
{
|
||||
N = 0, No = 0, None = 0,
|
||||
T = 1, Track = 1, S = 1, Song = 1,
|
||||
Q = 2, Queue = 2, Playlist = 2, Pl = 2
|
||||
N = 0,
|
||||
No = 0,
|
||||
None = 0,
|
||||
T = 1,
|
||||
Track = 1,
|
||||
S = 1,
|
||||
Song = 1,
|
||||
Q = 2,
|
||||
Queue = 2,
|
||||
Playlist = 2,
|
||||
Pl = 2
|
||||
}
|
||||
|
||||
public const string MUSIC_ICON_URL = "https://i.imgur.com/nhKS3PT.png";
|
||||
|
@ -22,9 +33,13 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
|
||||
private readonly ILogCommandService _logService;
|
||||
private readonly ILyricsService _lyricsService;
|
||||
|
||||
public Music(ILogCommandService logService)
|
||||
=> _logService = logService;
|
||||
public Music(ILogCommandService logService, ILyricsService lyricsService)
|
||||
{
|
||||
_logService = logService;
|
||||
_lyricsService = lyricsService;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateAsync()
|
||||
{
|
||||
|
@ -110,10 +125,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
try
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||
.WithFooter(trackInfo.Platform.ToString());
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||
.WithFooter(trackInfo.Platform.ToString());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
||||
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
||||
|
@ -301,39 +316,39 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
|
||||
desc += tracks
|
||||
.Select((v, index) =>
|
||||
{
|
||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||
if (index == currentIndex)
|
||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||
.Select((v, index) =>
|
||||
{
|
||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||
if (index == currentIndex)
|
||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||
|
||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||
})
|
||||
.Join('\n');
|
||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||
})
|
||||
.Join('\n');
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(add))
|
||||
desc = add + "\n" + desc;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(
|
||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||
MUSIC_ICON_URL)
|
||||
.WithDescription(desc)
|
||||
.WithFooter(
|
||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||
.WithOkColor();
|
||||
.WithAuthor(
|
||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||
MUSIC_ICON_URL)
|
||||
.WithDescription(desc)
|
||||
.WithFooter(
|
||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(tracks)
|
||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.Page(PrintAction)
|
||||
.SendAsync();
|
||||
.Paginated()
|
||||
.Items(tracks)
|
||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.Page(PrintAction)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
// search
|
||||
|
@ -353,15 +368,15 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
|
||||
var embeds = videos.Select((x, i) => CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithThumbnailUrl(x.Thumbnail)
|
||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||
.ToList();
|
||||
.WithOkColor()
|
||||
.WithThumbnailUrl(x.Thumbnail)
|
||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||
.ToList();
|
||||
|
||||
var msg = await Response()
|
||||
.Text(strs.queue_search_results)
|
||||
.Embeds(embeds)
|
||||
.SendAsync();
|
||||
.Text(strs.queue_search_results)
|
||||
.Embeds(embeds)
|
||||
.SendAsync();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -425,10 +440,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||
.WithDescription(track.PrettyName())
|
||||
.WithFooter(track.PrettyInfo())
|
||||
.WithErrorColor();
|
||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||
.WithDescription(track.PrettyName())
|
||||
.WithFooter(track.PrettyInfo())
|
||||
.WithErrorColor();
|
||||
|
||||
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
||||
}
|
||||
|
@ -593,11 +608,11 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(track.Title.TrimTo(65))
|
||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||
.WithOkColor();
|
||||
.WithTitle(track.Title.TrimTo(65))
|
||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||
.WithOkColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
||||
embed.WithUrl(track.Url);
|
||||
|
@ -652,19 +667,19 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
return;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||
.WithDescription(currentTrack.PrettyName())
|
||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||
.WithFooter(
|
||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||
.WithDescription(currentTrack.PrettyName())
|
||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||
.WithFooter(
|
||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task PlaylistShuffle()
|
||||
public async Task QueueShuffle()
|
||||
{
|
||||
var valid = await ValidateAsync();
|
||||
if (!valid)
|
||||
|
@ -768,4 +783,71 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
await Response().Confirm(strs.wrongsong_success(removed.Title.TrimTo(30))).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Lyrics([Leftover] string name = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
if (_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)
|
||||
&& mp.GetCurrentTrack(out _) is { } currentTrack)
|
||||
{
|
||||
name = currentTrack.Title;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = await _lyricsService.SearchTracksAsync(name);
|
||||
|
||||
if (tracks.Count == 0)
|
||||
{
|
||||
await Response().Error(strs.no_lyrics_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithFooter("type 1-5 to select");
|
||||
|
||||
for (var i = 0; i <= 5 && i < tracks.Count; i++)
|
||||
{
|
||||
var item = tracks[i];
|
||||
embed.AddField($"`{(i + 1)}`. {item.Author}", item.Title, false);
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(embed)
|
||||
.SendAsync();
|
||||
|
||||
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id, str => int.TryParse(str, out _));
|
||||
|
||||
if (input is null)
|
||||
return;
|
||||
|
||||
var index = int.Parse(input) - 1;
|
||||
if (index < 0 || index > 4)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var track = tracks[index];
|
||||
var lyrics = await _lyricsService.GetLyricsAsync(track.Id);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(lyrics))
|
||||
{
|
||||
await Response().Error(strs.no_lyrics_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(track.Author)
|
||||
.WithTitle(track.Title)
|
||||
.WithDescription(lyrics))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
#nullable disable
|
||||
using CommandLine;
|
||||
using LinqToDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Modules.Music.Services;
|
||||
using EllieBot.Db.Models;
|
||||
|
||||
|
@ -50,17 +52,17 @@ public sealed partial class Music
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(GetText(strs.playlists_page(num)), MUSIC_ICON_URL)
|
||||
.WithDescription(string.Join("\n",
|
||||
playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
|
||||
.WithOkColor();
|
||||
.WithAuthor(GetText(strs.playlists_page(num)), MUSIC_ICON_URL)
|
||||
.WithDescription(string.Join("\n",
|
||||
playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
|
||||
.WithOkColor();
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task DeletePlaylist([Leftover] int id)
|
||||
public async Task PlaylistDelete([Leftover] int id)
|
||||
{
|
||||
var success = false;
|
||||
try
|
||||
|
@ -103,26 +105,26 @@ public sealed partial class Music
|
|||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(mpl.Songs)
|
||||
.PageSize(20)
|
||||
.CurrentPage(page)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var i = 0;
|
||||
var str = string.Join("\n",
|
||||
items
|
||||
.Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
|
||||
return CreateEmbed().WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
|
||||
.WithOkColor()
|
||||
.WithDescription(str);
|
||||
})
|
||||
.SendAsync();
|
||||
.Paginated()
|
||||
.Items(mpl.Songs)
|
||||
.PageSize(20)
|
||||
.CurrentPage(page)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var i = 0;
|
||||
var str = string.Join("\n",
|
||||
items
|
||||
.Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
|
||||
return CreateEmbed().WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
|
||||
.WithOkColor()
|
||||
.WithDescription(str);
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Save([Leftover] string name)
|
||||
public async Task PlaylistSave([Leftover] string name)
|
||||
{
|
||||
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
|
||||
{
|
||||
|
@ -131,14 +133,14 @@ public sealed partial class Music
|
|||
}
|
||||
|
||||
var songs = mp.GetQueuedTracks()
|
||||
.Select(s => new PlaylistSong
|
||||
{
|
||||
Provider = s.Platform.ToString(),
|
||||
ProviderType = (MusicType)s.Platform,
|
||||
Title = s.Title,
|
||||
Query = s.Url
|
||||
})
|
||||
.ToList();
|
||||
.Select(s => new PlaylistSong
|
||||
{
|
||||
Provider = s.Platform.ToString(),
|
||||
ProviderType = (MusicType)s.Platform,
|
||||
Title = s.Title,
|
||||
Query = s.Url
|
||||
})
|
||||
.ToList();
|
||||
|
||||
MusicPlaylist playlist;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
|
@ -155,18 +157,30 @@ public sealed partial class Music
|
|||
}
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.playlist_saved))
|
||||
.AddField(GetText(strs.name), name)
|
||||
.AddField(GetText(strs.id), playlist.Id.ToString()))
|
||||
.SendAsync();
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.playlist_saved))
|
||||
.AddField(GetText(strs.name), name)
|
||||
.AddField(GetText(strs.id), playlist.Id.ToString()))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
public class PlaylistLoadOptions : IEllieCommandOptions
|
||||
{
|
||||
[Option("shuffle")]
|
||||
public bool Shuffled { get; set; } = false;
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Load([Leftover] int id)
|
||||
[EllieOptions<PlaylistLoadOptions>]
|
||||
public async Task PlaylistLoad(int id, params string[] args)
|
||||
{
|
||||
var opts = OptionsParser.ParseFrom(new PlaylistLoadOptions(), args).Item1;
|
||||
// expensive action, 1 at a time
|
||||
await _playlistLock.WaitAsync();
|
||||
try
|
||||
|
@ -201,7 +215,9 @@ public sealed partial class Music
|
|||
MusicPlaylist mpl;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
mpl = uow.Set<MusicPlaylist>().GetWithSongs(id);
|
||||
mpl = uow.Set<MusicPlaylist>()
|
||||
.AsNoTracking()
|
||||
.GetWithSongs(id);
|
||||
}
|
||||
|
||||
if (mpl is null)
|
||||
|
@ -214,14 +230,19 @@ public sealed partial class Music
|
|||
try
|
||||
{
|
||||
msg = await Response()
|
||||
.Pending(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString())))
|
||||
.SendAsync();
|
||||
.Pending(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
await mp.EnqueueManyAsync(mpl.Songs.Select(x => (x.Query, (MusicPlatform)x.ProviderType)),
|
||||
var songs = opts.Shuffled
|
||||
? mpl.Songs.Shuffle()
|
||||
: mpl.Songs;
|
||||
|
||||
await mp.EnqueueManyAsync(
|
||||
songs.Select(x => (x.Query, (MusicPlatform)x.ProviderType)),
|
||||
ctx.User.ToString());
|
||||
|
||||
if (msg is not null)
|
||||
|
|
7
src/EllieBot/Modules/Music/Services/ILyricsService.cs
Normal file
7
src/EllieBot/Modules/Music/Services/ILyricsService.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace EllieBot.Modules.Music;
|
||||
|
||||
public interface ILyricsService
|
||||
{
|
||||
public Task<IReadOnlyList<TracksItem>> SearchTracksAsync(string name);
|
||||
public Task<string> GetLyricsAsync(int trackId);
|
||||
}
|
25
src/EllieBot/Modules/Music/Services/LyricsService.cs
Normal file
25
src/EllieBot/Modules/Music/Services/LyricsService.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Musix;
|
||||
|
||||
namespace EllieBot.Modules.Music;
|
||||
|
||||
public sealed class LyricsService(HttpClient client) : ILyricsService, IEService
|
||||
{
|
||||
private readonly MusixMatchAPI _api = new(client);
|
||||
|
||||
private static string NormalizeName(string name)
|
||||
=> string.Join("-", name.Split()
|
||||
.Select(x => new string(x.Where(c => char.IsLetterOrDigit(c)).ToArray())))
|
||||
.Trim('-');
|
||||
|
||||
public async Task<IReadOnlyList<TracksItem>> SearchTracksAsync(string name)
|
||||
=> await _api.SearchTracksAsync(NormalizeName(name))
|
||||
.Fmap(x => x
|
||||
.Message
|
||||
.Body
|
||||
.TrackList
|
||||
.Map(x => new TracksItem(x.Track.ArtistName, x.Track.TrackName, x.Track.TrackId)));
|
||||
|
||||
public async Task<string> GetLyricsAsync(int trackId)
|
||||
=> await _api.GetTrackLyricsAsync(trackId)
|
||||
.Fmap(x => x.Message.Body.Lyrics.LyricsBody);
|
||||
}
|
12
src/EllieBot/Modules/Music/_common/Musix/Header.cs
Normal file
12
src/EllieBot/Modules/Music/_common/Musix/Header.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Header
|
||||
{
|
||||
[JsonPropertyName("status_code")]
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
[JsonPropertyName("execute_time")]
|
||||
public double ExecuteTime { get; set; }
|
||||
}
|
9
src/EllieBot/Modules/Music/_common/Musix/Lyrics.cs
Normal file
9
src/EllieBot/Modules/Music/_common/Musix/Lyrics.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Lyrics
|
||||
{
|
||||
[JsonPropertyName("lyrics_body")]
|
||||
public string LyricsBody { get; set; } = string.Empty;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class LyricsResponse
|
||||
{
|
||||
[JsonPropertyName("lyrics")]
|
||||
public Lyrics Lyrics { get; set; } = null!;
|
||||
}
|
12
src/EllieBot/Modules/Music/_common/Musix/Message.cs
Normal file
12
src/EllieBot/Modules/Music/_common/Musix/Message.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Message<T>
|
||||
{
|
||||
[JsonPropertyName("header")]
|
||||
public Header Header { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
public T Body { get; set; } = default!;
|
||||
}
|
141
src/EllieBot/Modules/Music/_common/Musix/MusixMatchAPI.cs
Normal file
141
src/EllieBot/Modules/Music/_common/Musix/MusixMatchAPI.cs
Normal file
|
@ -0,0 +1,141 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Musix.Models;
|
||||
|
||||
// All credit goes to https://github.com/Strvm/musicxmatch-api for the original implementation
|
||||
namespace Musix
|
||||
{
|
||||
public sealed class MusixMatchAPI
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl = "https://www.musixmatch.com/ws/1.1/";
|
||||
|
||||
private readonly string _userAgent =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public MusixMatchAPI(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent);
|
||||
_httpClient.DefaultRequestHeaders.Add("Cookie", "mxm_bab=AB");
|
||||
|
||||
_jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
_cache = new MemoryCache(new MemoryCacheOptions { });
|
||||
}
|
||||
|
||||
private async Task<string> GetLatestAppUrlAsync()
|
||||
{
|
||||
var url = "https://www.musixmatch.com/search";
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.UserAgent.ParseAdd(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||
request.Headers.Add("Cookie", "mxm_bab=AB");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var htmlContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var pattern = @"src=""([^""]*/_next/static/chunks/pages/_app-[^""]+\.js)""";
|
||||
var matches = Regex.Matches(htmlContent, pattern);
|
||||
|
||||
return matches.Count > 0
|
||||
? matches[^1].Groups[1].Value
|
||||
: throw new("_app URL not found in the HTML content.");
|
||||
}
|
||||
|
||||
private async Task<string> GetSecret()
|
||||
{
|
||||
var latestAppUrl = await GetLatestAppUrlAsync();
|
||||
var response = await _httpClient.GetAsync(latestAppUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var javascriptCode = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var pattern = @"from\(\s*""(.*?)""\s*\.split";
|
||||
var match = Regex.Match(javascriptCode, pattern);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var encodedString = match.Groups[1].Value;
|
||||
var reversedString = new string(encodedString.Reverse().ToArray());
|
||||
var decodedBytes = Convert.FromBase64String(reversedString);
|
||||
return Encoding.UTF8.GetString(decodedBytes);
|
||||
}
|
||||
|
||||
throw new Exception("Encoded string not found in the JavaScript code.");
|
||||
}
|
||||
|
||||
// It seems this is required in order to have multiword queries.
|
||||
// Spaces don't work in the original implementation either
|
||||
private string UrlEncode(string value)
|
||||
=> HttpUtility.UrlEncode(value)
|
||||
.Replace("+", "-");
|
||||
|
||||
private async Task<string> GenerateSignature(string url)
|
||||
{
|
||||
var currentDate = DateTime.Now;
|
||||
var l = currentDate.Year.ToString();
|
||||
var s = currentDate.Month.ToString("D2");
|
||||
var r = currentDate.Day.ToString("D2");
|
||||
|
||||
var message = (url + l + s + r);
|
||||
var secret = await _cache.GetOrCreateAsync("secret", async _ => await GetSecret());
|
||||
var key = Encoding.UTF8.GetBytes(secret ?? string.Empty);
|
||||
var messageBytes = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
using var hmac = new HMACSHA256(key);
|
||||
var hashBytes = hmac.ComputeHash(messageBytes);
|
||||
var signature = Convert.ToBase64String(hashBytes);
|
||||
return $"&signature={UrlEncode(signature)}&signature_protocol=sha256";
|
||||
}
|
||||
|
||||
public async Task<MusixMatchResponse<TrackSearchResponse>> SearchTracksAsync(string trackQuery, int page = 1)
|
||||
{
|
||||
var endpoint =
|
||||
$"track.search?app_id=community-app-v1.0&format=json&q={UrlEncode(trackQuery)}&f_has_lyrics=true&page_size=100&page={page}";
|
||||
var jsonResponse = await MakeRequestAsync(endpoint);
|
||||
return JsonSerializer.Deserialize<MusixMatchResponse<TrackSearchResponse>>(jsonResponse, _jsonOptions)
|
||||
?? throw new JsonException("Failed to deserialize track search response");
|
||||
}
|
||||
|
||||
public async Task<MusixMatchResponse<LyricsResponse>> GetTrackLyricsAsync(int trackId)
|
||||
{
|
||||
var endpoint = $"track.lyrics.get?app_id=community-app-v1.0&format=json&track_id={trackId}";
|
||||
var jsonResponse = await MakeRequestAsync(endpoint);
|
||||
return JsonSerializer.Deserialize<MusixMatchResponse<LyricsResponse>>(jsonResponse, _jsonOptions)
|
||||
?? throw new JsonException("Failed to deserialize lyrics response");
|
||||
}
|
||||
|
||||
private async Task<string> MakeRequestAsync(string endpoint)
|
||||
{
|
||||
var fullUrl = _baseUrl + endpoint;
|
||||
var signedUrl = fullUrl + await GenerateSignature(fullUrl);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, signedUrl);
|
||||
request.Headers.UserAgent.ParseAdd(_userAgent);
|
||||
request.Headers.Add("Cookie", "mxm_bab=AB");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Warning("Error in musix api request. Status: {ResponseStatusCode}, Content: {Content}",
|
||||
response.StatusCode,
|
||||
content);
|
||||
response.EnsureSuccessStatusCode(); // This will throw with the appropriate status code
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models
|
||||
{
|
||||
public class MusixMatchResponse<T>
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public Message<T> Message { get; set; } = null!;
|
||||
}
|
||||
}
|
23
src/EllieBot/Modules/Music/_common/Musix/Track.cs
Normal file
23
src/EllieBot/Modules/Music/_common/Musix/Track.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Track
|
||||
{
|
||||
[JsonPropertyName("track_id")]
|
||||
public int TrackId { get; set; }
|
||||
|
||||
[JsonPropertyName("track_name")]
|
||||
public string TrackName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("artist_name")]
|
||||
public string ArtistName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("album_name")]
|
||||
public string AlbumName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("track_share_url")]
|
||||
public string TrackShareUrl { get; set; } = string.Empty;
|
||||
|
||||
public override string ToString() => $"{TrackName} by {ArtistName} (Album: {AlbumName})";
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class TrackListItem
|
||||
{
|
||||
[JsonPropertyName("track")]
|
||||
public Track Track { get; set; } = null!;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class TrackSearchResponse
|
||||
{
|
||||
[JsonPropertyName("track_list")]
|
||||
public List<TrackListItem> TrackList { get; set; } = new();
|
||||
}
|
|
@ -39,7 +39,7 @@ public sealed class LocalTrackResolver : ILocalTrackResolver
|
|||
yield break;
|
||||
}
|
||||
|
||||
var files = dir.EnumerateFiles()
|
||||
var files = dir.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.Where(x =>
|
||||
{
|
||||
if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
|
||||
|
|
3
src/EllieBot/Modules/Music/_common/TracksItem.cs
Normal file
3
src/EllieBot/Modules/Music/_common/TracksItem.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
namespace EllieBot.Modules.Music;
|
||||
|
||||
public record struct TracksItem(string Author, string Title, int Id);
|
|
@ -13,6 +13,6 @@ public static class MusicPlaylistExtensions
|
|||
return playlists.AsQueryable().Skip((num - 1) * 20).Take(20).Include(pl => pl.Songs).ToList();
|
||||
}
|
||||
|
||||
public static MusicPlaylist GetWithSongs(this DbSet<MusicPlaylist> playlists, int id)
|
||||
public static MusicPlaylist GetWithSongs(this IQueryable<MusicPlaylist> playlists, int id)
|
||||
=> playlists.Include(mpl => mpl.Songs).FirstOrDefault(mpl => mpl.Id == id);
|
||||
}
|
|
@ -384,12 +384,9 @@ public sealed class PatronageService
|
|||
|
||||
return user.AmountCents switch
|
||||
{
|
||||
>= 10_000 => PatronTier.C,
|
||||
>= 5000 => PatronTier.L,
|
||||
>= 2000 => PatronTier.XX,
|
||||
>= 1000 => PatronTier.X,
|
||||
>= 500 => PatronTier.V,
|
||||
>= 100 => PatronTier.I,
|
||||
<= 200 => PatronTier.I,
|
||||
<= 1_000 => PatronTier.C,
|
||||
<= 5_000 => PatronTier.L,
|
||||
_ => PatronTier.None
|
||||
};
|
||||
}
|
||||
|
@ -402,12 +399,10 @@ public sealed class PatronageService
|
|||
public int PercentBonus(long amount)
|
||||
=> amount switch
|
||||
{
|
||||
>= 10_000 => 100,
|
||||
>= 5000 => 50,
|
||||
>= 2000 => 30,
|
||||
>= 1000 => 20,
|
||||
>= 500 => 10,
|
||||
_ => 0
|
||||
< 200 => 0,
|
||||
< 1_000 => 10,
|
||||
< 5_000 => 50,
|
||||
_ => 100
|
||||
};
|
||||
|
||||
private async Task SendWelcomeMessage(Patron patron)
|
||||
|
|
|
@ -255,8 +255,8 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
|
|||
{
|
||||
FilterInvitesChannels = conf?.FilterInvitesChannelIds.Select(x => x.ChannelId).ToArray() ?? [],
|
||||
FilterLinksChannels = conf?.FilterLinksChannelIds.Select(x => x.ChannelId).ToArray() ?? [],
|
||||
FilterInvitesEnabled = false,
|
||||
FilterLinksEnabled = false,
|
||||
FilterInvitesEnabled = conf?.FilterInvites ?? InviteFilteringServers.Contains(guildId),
|
||||
FilterLinksEnabled = conf?.FilterLinks ?? InviteFilteringServers.Contains(guildId),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -271,6 +271,7 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
|
|||
}
|
||||
else
|
||||
{
|
||||
LinkFilteringServers.TryRemove(guildId);
|
||||
fc.FilterLinks = false;
|
||||
}
|
||||
|
||||
|
@ -313,6 +314,7 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
|
|||
return true;
|
||||
}
|
||||
|
||||
InviteFilteringServers.TryRemove(guildId);
|
||||
fc.FilterInvites = false;
|
||||
await uow.SaveChangesAsync();
|
||||
return false;
|
||||
|
@ -352,6 +354,7 @@ public sealed class FilterService : IExecOnMessage, IReadyExecutor
|
|||
return true;
|
||||
}
|
||||
|
||||
WordFilteringServers.TryRemove(guildId);
|
||||
fc.FilterWords = false;
|
||||
await uow.SaveChangesAsync();
|
||||
return false;
|
||||
|
|
|
@ -34,7 +34,7 @@ public class CryptoService : IEService
|
|||
|
||||
var gElement = xml["svg"]?["g"];
|
||||
if (gElement is null)
|
||||
return Array.Empty<PointF>();
|
||||
return [];
|
||||
|
||||
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
|
||||
var cnt = 0;
|
||||
|
@ -73,7 +73,7 @@ public class CryptoService : IEService
|
|||
}
|
||||
|
||||
if (cnt == 0)
|
||||
return Array.Empty<PointF>();
|
||||
return [];
|
||||
|
||||
return points.Slice(0, cnt).ToArray();
|
||||
}
|
||||
|
|
|
@ -27,9 +27,9 @@ public partial class Searches
|
|||
|
||||
var embed = _service.GetEmbed(ctx.Guild.Id, data);
|
||||
await Response()
|
||||
.Embed(embed)
|
||||
.Text(strs.stream_tracked)
|
||||
.SendAsync();
|
||||
.Embed(embed)
|
||||
.Text(strs.stream_tracked)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -70,27 +70,27 @@ public partial class Searches
|
|||
var allStreams = await _service.GetAllStreamsAsync((SocketGuild)ctx.Guild);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allStreams)
|
||||
.PageSize(12)
|
||||
.CurrentPage(page)
|
||||
.Page((elements, cur) =>
|
||||
{
|
||||
if (elements.Count == 0)
|
||||
return CreateEmbed().WithDescription(GetText(strs.streams_none)).WithErrorColor();
|
||||
.Paginated()
|
||||
.Items(allStreams)
|
||||
.PageSize(12)
|
||||
.CurrentPage(page)
|
||||
.Page((elements, cur) =>
|
||||
{
|
||||
if (elements.Count == 0)
|
||||
return CreateEmbed().WithDescription(GetText(strs.streams_none)).WithErrorColor();
|
||||
|
||||
var eb = CreateEmbed().WithTitle(GetText(strs.streams_follow_title)).WithOkColor();
|
||||
for (var index = 0; index < elements.Count; index++)
|
||||
{
|
||||
var elem = elements[index];
|
||||
eb.AddField($"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}",
|
||||
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
|
||||
true);
|
||||
}
|
||||
var eb = CreateEmbed().WithTitle(GetText(strs.streams_follow_title)).WithOkColor();
|
||||
for (var index = 0; index < elements.Count; index++)
|
||||
{
|
||||
var elem = elements[index];
|
||||
eb.AddField($"**#{index + 1 + (12 * cur)}** {(elem.PrettyName ?? elem.Username).ToLower()}",
|
||||
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -176,10 +176,10 @@ public partial class Searches
|
|||
|
||||
if (data.IsLive)
|
||||
{
|
||||
var embed = _service.GetEmbed(ctx.Guild.Id, data, false);
|
||||
await Response()
|
||||
.Confirm(strs.streamer_online(Format.Bold(data.Name),
|
||||
Format.Bold(data.Viewers.ToString())))
|
||||
.SendAsync();
|
||||
.Embed(embed)
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
await Response().Confirm(strs.streamer_offline(data.Name)).SendAsync();
|
||||
|
|
|
@ -63,7 +63,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
|||
_repSvc = repSvc;
|
||||
_shardData = shardData;
|
||||
|
||||
_streamTracker = new(httpFactory, creds);
|
||||
_streamTracker = new(httpFactory, creds, config);
|
||||
|
||||
StreamsOnlineKey = new("streams.online");
|
||||
StreamsOfflineKey = new("streams.offline");
|
||||
|
@ -123,11 +123,11 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
|||
_shardTrackedStreams = followedStreams.GroupBy(x => new
|
||||
{
|
||||
x.Type,
|
||||
Name = x.Username.ToLower()
|
||||
Name = x.Username
|
||||
})
|
||||
.ToList()
|
||||
.ToDictionary(
|
||||
x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()),
|
||||
x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
||||
x => x.GroupBy(y => y.GuildId)
|
||||
.ToDictionary(y => y.Key,
|
||||
y => y.AsEnumerable().ToHashSet()));
|
||||
|
@ -143,7 +143,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
|||
_trackCounter = allFollowedStreams.GroupBy(x => new
|
||||
{
|
||||
x.Type,
|
||||
Name = x.Username.ToLower()
|
||||
Name = x.Username
|
||||
})
|
||||
.ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
||||
x => x.Select(fs => fs.GuildId).ToHashSet());
|
||||
|
@ -478,6 +478,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
|||
{
|
||||
Type = data.StreamType,
|
||||
Username = data.UniqueName,
|
||||
PrettyName = data.Name,
|
||||
ChannelId = channelId,
|
||||
GuildId = guildId
|
||||
};
|
||||
|
|
|
@ -5,5 +5,5 @@ namespace EllieBot.Modules.Searches.Common;
|
|||
public static class Extensions
|
||||
{
|
||||
public static StreamDataKey CreateKey(this FollowedStream fs)
|
||||
=> new(fs.Type, fs.Username.ToLower());
|
||||
=> new(fs.Type, fs.Username);
|
||||
}
|
|
@ -14,13 +14,15 @@ public class NotifChecker
|
|||
|
||||
public NotifChecker(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IBotCredsProvider credsProvider)
|
||||
IBotCredsProvider credsProvider,
|
||||
SearchesConfigService scs)
|
||||
{
|
||||
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
|
||||
{
|
||||
{ FollowedStream.FType.Twitch, new TwitchHelixProvider(httpClientFactory, credsProvider) },
|
||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
||||
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
|
||||
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) },
|
||||
{ FollowedStream.FType.Youtube, new YouTubeProvider(httpClientFactory, scs) }
|
||||
};
|
||||
_offlineBuffer = new();
|
||||
}
|
||||
|
@ -29,11 +31,11 @@ public class NotifChecker
|
|||
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
|
||||
{
|
||||
var toReturn = _streamProviders
|
||||
.SelectMany(prov => prov.Value
|
||||
.FailingStreams
|
||||
.Where(fs => DateTime.UtcNow - fs.Value > duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key)))
|
||||
.ToList();
|
||||
.SelectMany(prov => prov.Value
|
||||
.FailingStreams
|
||||
.Where(fs => DateTime.UtcNow - fs.Value > duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key)))
|
||||
.ToList();
|
||||
|
||||
if (remove)
|
||||
{
|
||||
|
@ -54,29 +56,29 @@ public class NotifChecker
|
|||
var allStreamData = GetAllData();
|
||||
|
||||
var oldStreamDataDict = allStreamData
|
||||
// group by type
|
||||
.GroupBy(entry => entry.Key.Type)
|
||||
.ToDictionary(entry => entry.Key,
|
||||
entry => entry.AsEnumerable()
|
||||
.ToDictionary(x => x.Key.Name, x => x.Value));
|
||||
// group by type
|
||||
.GroupBy(entry => entry.Key.Type)
|
||||
.ToDictionary(entry => entry.Key,
|
||||
entry => entry.AsEnumerable()
|
||||
.ToDictionary(x => x.Key.Name, x => x.Value));
|
||||
|
||||
var newStreamData = await oldStreamDataDict
|
||||
.Select(x =>
|
||||
{
|
||||
// get all stream data for the streams of this type
|
||||
if (_streamProviders.TryGetValue(x.Key,
|
||||
out var provider))
|
||||
{
|
||||
return provider.GetStreamDataAsync(x.Value
|
||||
.Select(entry => entry.Key)
|
||||
.ToList());
|
||||
}
|
||||
.Select(x =>
|
||||
{
|
||||
// get all stream data for the streams of this type
|
||||
if (_streamProviders.TryGetValue(x.Key,
|
||||
out var provider))
|
||||
{
|
||||
return provider.GetStreamDataAsync(x.Value
|
||||
.Select(entry => entry.Key)
|
||||
.ToList());
|
||||
}
|
||||
|
||||
// this means there's no provider for this stream data, (and there was before?)
|
||||
return Task.FromResult<IReadOnlyCollection<StreamData>>(
|
||||
new List<StreamData>());
|
||||
})
|
||||
.WhenAll();
|
||||
// this means there's no provider for this stream data, (and there was before?)
|
||||
return Task.FromResult<IReadOnlyCollection<StreamData>>(
|
||||
new List<StreamData>());
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var newlyOnline = new List<StreamData>();
|
||||
var newlyOffline = new List<StreamData>();
|
||||
|
@ -94,11 +96,11 @@ public class NotifChecker
|
|||
AddLastData(key, newData, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// fill with last known game in case it's empty
|
||||
if (string.IsNullOrWhiteSpace(newData.Game))
|
||||
newData.Game = oldData.Game;
|
||||
|
||||
|
||||
AddLastData(key, newData, true);
|
||||
|
||||
// if the stream is offline, we need to check if it was
|
||||
|
@ -144,6 +146,7 @@ public class NotifChecker
|
|||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
|
||||
await Task.Delay(15_000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -155,7 +158,7 @@ public class NotifChecker
|
|||
_cache[key] = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return _cache.TryAdd(key, data);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ public abstract class Provider
|
|||
/// </summary>
|
||||
/// <param name="url">Url of the stream</param>
|
||||
/// <returns><see cref="StreamData" /> of the specified stream. Null if none found</returns>
|
||||
|
||||
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
using System.Net;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml.Linq;
|
||||
using AngleSharp.Browser;
|
||||
|
||||
namespace EllieBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provider for tracking YouTube livestreams
|
||||
/// </summary>
|
||||
public sealed partial class YouTubeProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly SearchesConfigService _scs;
|
||||
private readonly EllieRandom _rng = new();
|
||||
|
||||
/// <summary>
|
||||
/// Regex to match YouTube handles
|
||||
/// </summary>
|
||||
/// <returns>Regex</returns>
|
||||
[GeneratedRegex(@"youtu(?:\.be|be\.com)\/@(?<handle>[^\/\?#]+)")]
|
||||
private static partial Regex HandleRegex();
|
||||
|
||||
// channel id regex
|
||||
[GeneratedRegex(@"youtu(?:\.be|be\.com)\/channel\/(?<channelid>[^\/\?#]+)")]
|
||||
private static partial Regex ChannelIdRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Type of the platform.
|
||||
/// </summary>
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Youtube;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="YouTubeProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpFactory">The HTTP client factory to create HTTP clients.</param>
|
||||
public YouTubeProvider(
|
||||
IHttpClientFactory httpFactory,
|
||||
SearchesConfigService scs
|
||||
)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
_scs = scs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified url is a valid YouTube url.
|
||||
/// </summary>
|
||||
/// <param name="url">Url to check</param>
|
||||
/// <returns>True if valid, otherwise false</returns>
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
{
|
||||
var success = HandleRegex().IsMatch(url)
|
||||
|| ChannelIdRegex().IsMatch(url);
|
||||
|
||||
return Task.FromResult(success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of the stream on the specified YouTube url
|
||||
/// </summary>
|
||||
/// <param name="url">Url of the stream</param>
|
||||
/// <returns><see cref="StreamData"/> of the specified stream. Null if none found</returns>
|
||||
public override async Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
{
|
||||
var match = ChannelIdRegex().Match(url);
|
||||
var channelId = string.Empty;
|
||||
if (!match.Success)
|
||||
{
|
||||
var handleMatch = HandleRegex().Match(url);
|
||||
if (!handleMatch.Success)
|
||||
return null;
|
||||
|
||||
var handle = handleMatch.Groups["handle"].Value;
|
||||
|
||||
var instances = _scs.Data.InvidiousInstances;
|
||||
|
||||
if (instances is not { Count: > 0 })
|
||||
return null;
|
||||
|
||||
var invInstance = instances[_rng.Next(0, _scs.Data.InvidiousInstances.Count)];
|
||||
|
||||
using var client = _httpFactory.CreateClient();
|
||||
client.BaseAddress = new Uri(invInstance);
|
||||
|
||||
using var response = await client.GetAsync($"/@{handle}");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
channelId = response.RequestMessage?.RequestUri?.ToString().Split("/").LastOrDefault();
|
||||
|
||||
if (channelId is null)
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
channelId = match.Groups["channelid"].Value;
|
||||
}
|
||||
|
||||
return await GetStreamDataAsync(channelId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of the specified YouTube channel
|
||||
/// </summary>
|
||||
/// <param name="channelId">Channel ID or name</param>
|
||||
/// <returns><see cref="StreamData"/> of the channel. Null if none found</returns>
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string channelId)
|
||||
{
|
||||
var instances = _scs.Data.InvidiousInstances;
|
||||
|
||||
if (instances is not { Count: > 0 })
|
||||
return null;
|
||||
|
||||
var invInstance = instances[_rng.Next(0, instances.Count)];
|
||||
var client = _httpFactory.CreateClient();
|
||||
client.BaseAddress = new Uri(invInstance);
|
||||
|
||||
var channel = await client.GetFromJsonAsync<InvidiousChannelResponse>($"/api/v1/channels/{channelId}");
|
||||
if (channel is null)
|
||||
return null;
|
||||
|
||||
var response =
|
||||
await client.GetFromJsonAsync<InvChannelStreamsResponse>($"/api/v1/channels/{channelId}/streams");
|
||||
if (response is null)
|
||||
return null;
|
||||
|
||||
var vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming && x.LengthSeconds == 0);
|
||||
var isLive = false;
|
||||
if (vid is null)
|
||||
{
|
||||
vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming);
|
||||
}
|
||||
else
|
||||
{
|
||||
isLive = true;
|
||||
}
|
||||
|
||||
if (vid is null)
|
||||
return null;
|
||||
|
||||
var avatarUrl = channel?.AuthorThumbnails?.Select(x => x.Url).LastOrDefault();
|
||||
|
||||
return new StreamData()
|
||||
{
|
||||
Game = "Livestream",
|
||||
Name = vid.Author,
|
||||
Preview = vid.Thumbnails
|
||||
.Skip(1)
|
||||
.Select(x => "https://i.ytimg.com/" + x.Url)
|
||||
.FirstOrDefault(),
|
||||
Title = vid.Title,
|
||||
Viewers = vid.ViewCount,
|
||||
AvatarUrl = avatarUrl,
|
||||
IsLive = isLive,
|
||||
StreamType = FollowedStream.FType.Youtube,
|
||||
StreamUrl = "https://youtube.com/watch?v=" + vid.VideoId,
|
||||
UniqueName = vid.AuthorId,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of all specified YouTube channels
|
||||
/// </summary>
|
||||
/// <param name="channelIds">List of channel IDs or names</param>
|
||||
/// <returns><see cref="StreamData"/> of all users, in the same order. Null for every ID not found.</returns>
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> channelIds)
|
||||
{
|
||||
var results = new List<StreamData>(channelIds.Count);
|
||||
foreach (var group in channelIds.Chunk(5))
|
||||
{
|
||||
var streamData = await Task.WhenAll(group.Select(GetStreamDataAsync));
|
||||
|
||||
foreach (var data in streamData)
|
||||
{
|
||||
if (data is not null)
|
||||
results.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InvidiousChannelResponse
|
||||
{
|
||||
[JsonPropertyName("authorId")]
|
||||
public required string AuthorId { get; init; }
|
||||
|
||||
[JsonPropertyName("authorThumbnails")]
|
||||
public required List<InvAuthorThumbnail> AuthorThumbnails { get; init; }
|
||||
|
||||
public sealed class InvAuthorThumbnail
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public required string Url { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InvChannelStreamsResponse
|
||||
{
|
||||
public required List<InvidiousStreamResponse> Videos { get; init; }
|
||||
}
|
||||
|
||||
public sealed class InvidiousStreamResponse
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public required string Title { get; init; }
|
||||
|
||||
[JsonPropertyName("videoId")]
|
||||
public required string VideoId { get; init; }
|
||||
|
||||
[JsonPropertyName("lengthSeconds")]
|
||||
public required int LengthSeconds { get; init; }
|
||||
|
||||
[JsonPropertyName("videoThumbnails")]
|
||||
public required List<InvidiousThumbnail> Thumbnails { get; init; }
|
||||
|
||||
[JsonPropertyName("author")]
|
||||
public required string Author { get; init; }
|
||||
|
||||
[JsonPropertyName("authorId")]
|
||||
public required string AuthorId { get; init; }
|
||||
|
||||
[JsonPropertyName("isUpcoming")]
|
||||
public bool IsUpcoming { get; set; }
|
||||
|
||||
[JsonPropertyName("viewCount")]
|
||||
public int ViewCount { get; set; }
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
#nullable disable
|
||||
using System.Text;
|
||||
using EllieBot.Modules.Patronage;
|
||||
|
||||
namespace EllieBot.Modules.Utility;
|
||||
|
@ -34,31 +33,35 @@ public partial class Utility
|
|||
{
|
||||
var guild = (IGuild)_client.GetGuild(guildId)
|
||||
?? await _client.Rest.GetGuildAsync(guildId);
|
||||
|
||||
|
||||
if (guild is null)
|
||||
return;
|
||||
|
||||
var ownername = await guild.GetUserAsync(guild.OwnerId);
|
||||
var textchn = (await guild.GetTextChannelsAsync()).Count;
|
||||
var voicechn = (await guild.GetVoiceChannelsAsync()).Count;
|
||||
var channels = $@"{GetText(strs.text_channels(textchn))}
|
||||
{GetText(strs.voice_channels(voicechn))}";
|
||||
var channels = $"""
|
||||
{GetText(strs.text_channels(textchn))}
|
||||
{GetText(strs.voice_channels(voicechn))}
|
||||
""";
|
||||
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(guild.Id >> 22);
|
||||
var features = guild.Features.Value.ToString();
|
||||
if (string.IsNullOrWhiteSpace(features))
|
||||
features = "-";
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(GetText(strs.server_info))
|
||||
.WithTitle(guild.Name)
|
||||
.AddField(GetText(strs.id), guild.Id.ToString(), true)
|
||||
.AddField(GetText(strs.owner), ownername.ToString(), true)
|
||||
.AddField(GetText(strs.members), (guild as SocketGuild)?.MemberCount.ToString() ?? guild.ApproximateMemberCount?.ToString() ?? "?", true)
|
||||
.AddField(GetText(strs.channels), channels, true)
|
||||
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.roles), (guild.Roles.Count - 1).ToString(), true)
|
||||
.AddField(GetText(strs.features), features)
|
||||
.WithOkColor();
|
||||
.WithAuthor(GetText(strs.server_info))
|
||||
.WithTitle(guild.Name)
|
||||
.AddField(GetText(strs.id), guild.Id.ToString(), true)
|
||||
.AddField(GetText(strs.owner), ownername.ToString(), true)
|
||||
.AddField(GetText(strs.members),
|
||||
(guild as SocketGuild)?.MemberCount.ToString() ?? guild.ApproximateMemberCount?.ToString() ?? "?",
|
||||
true)
|
||||
.AddField(GetText(strs.channels), channels, true)
|
||||
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.roles), (guild.Roles.Count - 1).ToString(), true)
|
||||
.AddField(GetText(strs.features), features)
|
||||
.WithOkColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute))
|
||||
embed.WithThumbnailUrl(guild.IconUrl);
|
||||
|
@ -81,14 +84,29 @@ public partial class Utility
|
|||
return;
|
||||
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(ch.Id >> 22);
|
||||
var usercount = (await ch.GetUsersAsync().FlattenAsync()).Count();
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(ch.Name)
|
||||
.WithDescription(ch.Topic?.SanitizeMentions(true))
|
||||
.AddField(GetText(strs.id), ch.Id.ToString(), true)
|
||||
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.users), usercount.ToString(), true)
|
||||
.WithOkColor();
|
||||
await Response().Embed(embed).SendAsync();
|
||||
|
||||
var users = await ch.GetUsersAsync(CacheMode.CacheOnly).FlattenAsync().Fmap(x => x.ToList());
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(users)
|
||||
.PageSize(20)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(ch.Name)
|
||||
.WithDescription(ch.Topic?.SanitizeMentions(true))
|
||||
.AddField(GetText(strs.id), ch.Id.ToString(), true)
|
||||
.AddField(GetText(strs.created_at),
|
||||
TimestampTag.FromDateTime(createdAt, TimestampTagStyles.ShortDate),
|
||||
true)
|
||||
.AddField(GetText(strs.users), usercount.ToString(), true)
|
||||
.AddField(GetText(strs.members), string.Join(" . ", items.Select(x => x.DisplayName)))
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -102,17 +120,17 @@ public partial class Utility
|
|||
.AddMilliseconds(role.Id >> 22);
|
||||
var usercount = role.Members.LongCount();
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(role.Name.TrimTo(128))
|
||||
.WithDescription(role.Permissions.ToList().Join(" | "))
|
||||
.AddField(GetText(strs.id), role.Id.ToString(), true)
|
||||
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.users), usercount.ToString(), true)
|
||||
.AddField(GetText(strs.color),
|
||||
$"#{role.Color.R:X2}{role.Color.G:X2}{role.Color.B:X2}",
|
||||
true)
|
||||
.AddField(GetText(strs.mentionable), role.IsMentionable.ToString(), true)
|
||||
.AddField(GetText(strs.hoisted), role.IsHoisted.ToString(), true)
|
||||
.WithOkColor();
|
||||
.WithTitle(role.Name.TrimTo(128))
|
||||
.WithDescription(role.Permissions.ToList().Join(" | "))
|
||||
.AddField(GetText(strs.id), role.Id.ToString(), true)
|
||||
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.users), usercount.ToString(), true)
|
||||
.AddField(GetText(strs.color),
|
||||
$"#{role.Color.R:X2}{role.Color.G:X2}{role.Color.B:X2}",
|
||||
true)
|
||||
.AddField(GetText(strs.mentionable), role.IsMentionable.ToString(), true)
|
||||
.AddField(GetText(strs.hoisted), role.IsHoisted.ToString(), true)
|
||||
.WithOkColor();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(role.GetIconUrl()))
|
||||
embed = embed.WithThumbnailUrl(role.GetIconUrl());
|
||||
|
@ -130,31 +148,29 @@ public partial class Utility
|
|||
return;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.AddField(GetText(strs.name), $"**{user.Username}**#{user.Discriminator}", true);
|
||||
.AddField(GetText(strs.name), $"**{user.Username}**#{user.Discriminator}", true);
|
||||
if (!string.IsNullOrWhiteSpace(user.Nickname))
|
||||
embed.AddField(GetText(strs.nickname), user.Nickname, true);
|
||||
|
||||
var joinedAt = GetJoinedAt(user);
|
||||
|
||||
embed.AddField(GetText(strs.id), user.Id.ToString(), true)
|
||||
.AddField(GetText(strs.joined_server), $"{joinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}", true)
|
||||
.AddField(GetText(strs.joined_discord), $"{user.CreatedAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.roles),
|
||||
$"**({user.RoleIds.Count - 1})** - {string.Join("\n", user.GetRoles().Take(10).Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions(true)}",
|
||||
true)
|
||||
.WithOkColor();
|
||||
.AddField(GetText(strs.joined_server), $"{joinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}", true)
|
||||
.AddField(GetText(strs.joined_discord), $"{user.CreatedAt:dd.MM.yyyy HH:mm}", true)
|
||||
.AddField(GetText(strs.roles),
|
||||
$"**({user.RoleIds.Count - 1})** - {string.Join("\n", user.GetRoles().Take(10).Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions(true)}",
|
||||
true)
|
||||
.WithOkColor();
|
||||
|
||||
var mPatron = await _ps.GetPatronAsync(user.Id);
|
||||
|
||||
if (mPatron is {} patron && patron.Tier != PatronTier.None)
|
||||
if (mPatron is { } patron && patron.Tier != PatronTier.None)
|
||||
{
|
||||
embed.WithFooter(patron.Tier switch
|
||||
embed.WithFooter((int)patron.Tier switch
|
||||
{
|
||||
PatronTier.V => "❤️❤️",
|
||||
PatronTier.X => "❤️❤️❤️",
|
||||
PatronTier.XX => "❤️❤️❤️❤️",
|
||||
PatronTier.L => "❤️❤️❤️❤️❤️",
|
||||
_ => "❤️",
|
||||
< (int)PatronTier.X => "❤️",
|
||||
< (int)PatronTier.L => "❤️❤️❤️",
|
||||
_ => "❤️❤️❤️❤️❤️",
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -166,15 +182,6 @@ public partial class Utility
|
|||
}
|
||||
|
||||
private DateTimeOffset? GetJoinedAt(IGuildUser user)
|
||||
{
|
||||
var joinedAt = user.JoinedAt;
|
||||
if (user.GuildId != 117523346618318850)
|
||||
return joinedAt;
|
||||
|
||||
if (user.Id == 351244576092192778)
|
||||
return new DateTimeOffset(2019, 12, 25, 9, 33, 0, TimeSpan.Zero);
|
||||
|
||||
return joinedAt;
|
||||
}
|
||||
=> user.JoinedAt;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue