Compare commits
195
Commits
rewrite-nyeki
...
pull
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0dc5a3af6 | ||
|
|
29d24e2054 | ||
|
|
ee01d2160d | ||
|
|
16cbda838e | ||
|
|
ce0cef5db7 | ||
|
|
afcfdad13d | ||
|
|
2823868460 | ||
|
|
78fcfe386d | ||
|
|
5fdea76f37 | ||
|
|
6248dc72f0 | ||
|
|
53e066f868 | ||
|
|
c9b81219e5 | ||
|
|
b0a6950005 | ||
|
|
658dbd9c8d | ||
|
|
275bfa440c | ||
|
|
943225ce1f | ||
|
|
e6b99759ce | ||
|
|
014acf55c6 | ||
|
|
769d36e9c2 | ||
|
|
12c6755897 | ||
|
|
21678239b5 | ||
|
|
d0b5f86740 | ||
|
|
70c531ddac | ||
|
|
08c4534907 | ||
|
|
6aee4f39b0 | ||
|
|
e7aa3247e5 | ||
|
|
5dd8fdcc58 | ||
|
|
a0c0c17747 | ||
|
|
2b3dc00941 | ||
|
|
1c79d4b701 | ||
|
|
384ca1f4e8 | ||
|
|
84ff48ae8c | ||
|
|
be5422345b | ||
|
|
b2297703c8 | ||
|
|
e5ecbbfa10 | ||
|
|
f9a0c8220c | ||
|
|
176280fdf4 | ||
|
|
0c3bbf9a03 | ||
|
|
08d71a5e82 | ||
|
|
86f2a6bf9c | ||
|
|
d1669003cf | ||
|
|
b442be08e2 | ||
|
|
eb057e0531 | ||
|
|
73010fec3d | ||
|
|
676fdcfd96 | ||
|
|
e5fe77c287 | ||
|
|
28d330b1b2 | ||
|
|
ec893c0cf7 | ||
|
|
16085b4bd8 | ||
|
|
ca1ce13887 | ||
|
|
e44ab51adf | ||
|
|
6d8217ed81 | ||
|
|
a75fd9f092 | ||
|
|
0f5f390d68 | ||
|
|
c1a2a6a449 | ||
|
|
82fba989a4 | ||
|
|
3f5ec4f258 | ||
|
|
39120a65fa | ||
|
|
fc9bbcc87c | ||
|
|
497d180669 | ||
|
|
bd09a55a74 | ||
|
|
14a6037101 | ||
|
|
b9dbc17b63 | ||
|
|
325b4cbc16 | ||
|
|
2a2ffd8894 | ||
|
|
134d4e8879 | ||
|
|
291c4d50b2 | ||
|
|
06faca7aa2 | ||
|
|
8e9a23b4d9 | ||
|
|
8f6d28f3b6 | ||
|
|
35f3504c00 | ||
|
|
6c72c27911 | ||
|
|
7d47a9eb35 | ||
|
|
b10484cacc | ||
|
|
7a5943a5ef | ||
|
|
8294fafeba | ||
|
|
c5d699292f | ||
|
|
679096a685 | ||
|
|
9b70714fe3 | ||
|
|
6df7fd272f | ||
|
|
d0aa3d72d5 | ||
|
|
b108d93c55 | ||
|
|
eaa38f3474 | ||
|
|
8868b18be3 | ||
|
|
55ae1c8937 | ||
|
|
5e3201e7c5 | ||
|
|
7d19e928d6 | ||
|
|
c88496a73a | ||
|
|
aa32abfa90 | ||
|
|
6c240cfece | ||
|
|
1f9f094445 | ||
|
|
e28f11d9bb | ||
|
|
408f0223a2 | ||
|
|
2b1a738379 | ||
|
|
cca77aa4a7 | ||
|
|
a92a02484c | ||
|
|
0318f002fc | ||
|
|
4563c49245 | ||
|
|
aba11cb2da | ||
|
|
ee49b0a984 | ||
|
|
3cc37cfc0c | ||
|
|
f785413696 | ||
|
|
cddda58aca | ||
|
|
0af585cf9e | ||
|
|
b6242f5a10 | ||
|
|
9799631b7c | ||
|
|
1678eedf6d | ||
|
|
a6255290b2 | ||
|
|
118dacff20 | ||
|
|
a994c86f98 | ||
|
|
23969006c2 | ||
|
|
a0f592e66f | ||
|
|
5ebbfe6458 | ||
|
|
6843865da3 | ||
|
|
f882ed7fed | ||
|
|
1149dd5bb4 | ||
|
|
54217f1d69 | ||
|
|
8ff7e3af02 | ||
|
|
040b720755 | ||
|
|
91ae88d040 | ||
|
|
31bb63a77a | ||
|
|
2f99b32aea | ||
|
|
d3e192e3c3 | ||
|
|
f98fc37191 | ||
|
|
fcb1a786be | ||
|
|
5fe3878471 | ||
|
|
d53a161716 | ||
|
|
c0b919d33c | ||
|
|
a852cd9844 | ||
|
|
36f15615b6 | ||
|
|
f909a300ad | ||
|
|
da1971060a | ||
|
|
e1c8957fc8 | ||
|
|
43ab107102 | ||
|
|
14f8df2aba | ||
|
|
a67b4f6a3d | ||
|
|
455dc27743 | ||
|
|
d39b06eb12 | ||
|
|
8eaa3f6195 | ||
|
|
f6866e1a11 | ||
|
|
700b770248 | ||
|
|
ac81e3b286 | ||
|
|
323cf7f748 | ||
|
|
281542a5c2 | ||
|
|
77182d9633 | ||
|
|
e472936c51 | ||
|
|
12948dcc15 | ||
|
|
b9410bedd7 | ||
|
|
7eb2a99b69 | ||
|
|
fb7eba11bb | ||
|
|
208e0ddbf5 | ||
|
|
deb76941ea | ||
|
|
25b96cdc2a | ||
|
|
f4f842e3c8 | ||
|
|
6dde25aa72 | ||
|
|
8e0c591b21 | ||
|
|
fe53f2e5c2 | ||
|
|
3c87fdfa69 | ||
|
|
da10b7f194 | ||
|
|
53d61cfbbb | ||
|
|
eb6dcb4604 | ||
|
|
8432cb78fa | ||
|
|
faf9d9d580 | ||
|
|
f4a282447a | ||
|
|
2b4c3d0b8b | ||
|
|
1a446ae3bf | ||
|
|
55431d580e | ||
|
|
09ed3b870e | ||
|
|
634c8eff8c | ||
|
|
8173a508ad | ||
|
|
c284ede5b4 | ||
|
|
7b3fa9905a | ||
|
|
a14ae96a47 | ||
|
|
e35b200d03 | ||
|
|
285f1d4516 | ||
|
|
2030812742 | ||
|
|
e407068a9c | ||
|
|
7270c90b62 | ||
|
|
7119730668 | ||
|
|
fb660c162b | ||
|
|
6ba65eef71 | ||
|
|
d7b8138606 | ||
|
|
66584a8239 | ||
|
|
80ed00f796 | ||
|
|
fd71345cd0 | ||
|
|
7ed2c10448 | ||
|
|
717de3eec5 | ||
|
|
09b875da97 | ||
|
|
0cdbe63c55 | ||
|
|
ed717ef94b | ||
|
|
5806493ca8 | ||
|
|
39c455f617 | ||
|
|
d04af236d2 | ||
|
|
e8a76d5e77 | ||
|
|
91d5bf73c5 |
@@ -0,0 +1,19 @@
|
|||||||
|
/target
|
||||||
|
/.venv
|
||||||
|
/.ansible
|
||||||
|
/.vscode
|
||||||
|
/k8s
|
||||||
|
/playbooks
|
||||||
|
/backup
|
||||||
|
|
||||||
|
.gitignore
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
ansible.cfg
|
||||||
|
compose.yml
|
||||||
|
inventory.ini
|
||||||
|
pyproject.toml
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
.env
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
/target
|
/target
|
||||||
|
/.venv
|
||||||
|
/.ansible
|
||||||
|
/backup
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
00-vault-pwd.txt
|
||||||
|
|
||||||
|
reset-ssh.sh
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: lint
|
||||||
|
image: rust:latest
|
||||||
|
script:
|
||||||
|
- rustup component add rustfmt clippy
|
||||||
|
- cargo fmt --check
|
||||||
|
- cargo clippy -- -D warnings
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "target/"
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: rust:latest
|
||||||
|
script:
|
||||||
|
- cargo build --release
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "target/"
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
image: rust:latest
|
||||||
|
script:
|
||||||
|
- cargo test --release
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "target/"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"ansible.python.interpreterPath": "/home/nyeki/Documents/Repositories/payphone/payphone/.venv/bin/python"
|
||||||
|
}
|
||||||
Generated
+1471
-43
File diff suppressed because it is too large
Load Diff
+31
-3
@@ -1,15 +1,23 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "payphone"
|
name = "payphone"
|
||||||
version = "4.0.0"
|
version = "4.4.57"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
default-run = "payphone"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.99"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
axum = "0.8.4"
|
axum = "0.8.4"
|
||||||
clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
||||||
|
color-thief = "0.2.2"
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
|
csv = "1.3.1"
|
||||||
|
deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
humantime = "2.2.0"
|
||||||
|
image = "0.25.6"
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
|
itertools = "0.14.0"
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
log = { version = "0.4.27", features = ["serde"] }
|
log = { version = "0.4.27", features = ["serde"] }
|
||||||
notify = "8.2.0"
|
notify = "8.2.0"
|
||||||
@@ -18,16 +26,36 @@ open = "5.3.2"
|
|||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
reqwest = { version = "0.12.23", features = [
|
||||||
|
"gzip",
|
||||||
|
"brotli",
|
||||||
|
"zstd",
|
||||||
|
"native-tls-vendored",
|
||||||
|
] }
|
||||||
|
rgb = "0.8.52"
|
||||||
scc = "2.3.4"
|
scc = "2.3.4"
|
||||||
scylla = { version = "1.3.1", features = ["time-03"] }
|
scylla = { version = "1.3.1", features = ["time-03"] }
|
||||||
|
serde_json = "1.0.143"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
time = { version = "0.3.41", features = ["formatting", "local-offset", "macros"] }
|
time = { version = "0.3.41", features = [
|
||||||
|
"formatting",
|
||||||
|
"local-offset",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
topgg = { version = "1.4.2", features = ["twilight"] }
|
||||||
tower-http = { version = "0.6.6", features = ["fs"] }
|
tower-http = { version = "0.6.6", features = ["fs"] }
|
||||||
|
twilight-cache-inmemory = { version = "0.16.0", features = [
|
||||||
|
"permission-calculator",
|
||||||
|
] }
|
||||||
twilight-gateway = "0.16.0"
|
twilight-gateway = "0.16.0"
|
||||||
twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
||||||
twilight-model = "0.16.0"
|
twilight-model = "0.16.0"
|
||||||
twilight-util = { version = "0.16.0", features = ["builder"] }
|
twilight-util = { version = "0.16.0", features = [
|
||||||
|
"builder",
|
||||||
|
"permission-calculator",
|
||||||
|
"snowflake",
|
||||||
|
] }
|
||||||
twilight-validate = "0.16.0"
|
twilight-validate = "0.16.0"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
unidecode = "0.3.0"
|
unidecode = "0.3.0"
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# --- Builder stage ---
|
||||||
|
FROM rust:1-bookworm AS builder
|
||||||
|
|
||||||
|
# Install musl target and musl-tools
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y musl-tools build-essential
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build fully static binary
|
||||||
|
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# --- Final stage ---
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
|
# Copy static binary
|
||||||
|
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/payphone /usr/local/bin/payphone
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates \
|
||||||
|
&& update-ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/payphone"]
|
||||||
|
CMD ["start"]
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
To read the documentation, run
|
To read the documentation, run
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ git clone --branch rewrite https://git.nyeki.dev/payphone/payphone.git
|
$ git clone --branch rewrite https://git.moan.dev/nyeki/payphone.git
|
||||||
$ cd payphone
|
$ cd payphone
|
||||||
$ cargo run -- docs --open
|
$ cargo run -- docs --open
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[ssh_connection]
|
||||||
|
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
|
||||||
|
|
||||||
|
[defaults]
|
||||||
|
timeout = 30
|
||||||
+66
@@ -31,6 +31,66 @@ services:
|
|||||||
scylla-net:
|
scylla-net:
|
||||||
ipv4_address: 172.20.0.4
|
ipv4_address: 172.20.0.4
|
||||||
|
|
||||||
|
redis-node-1:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-node-1
|
||||||
|
command: redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "7000:7000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-p", "7000", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
redis-net:
|
||||||
|
ipv4_address: 172.20.1.2
|
||||||
|
|
||||||
|
redis-node-2:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-node-2
|
||||||
|
command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "7001:7001"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-p", "7001", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
redis-net:
|
||||||
|
ipv4_address: 172.20.1.3
|
||||||
|
|
||||||
|
redis-node-3:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-node-3
|
||||||
|
command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "7002:7002"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-p", "7002", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
redis-net:
|
||||||
|
ipv4_address: 172.20.1.4
|
||||||
|
|
||||||
|
redis-init:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-init
|
||||||
|
command: redis-cli --cluster create 172.20.1.2:7000 172.20.1.3:7001 172.20.1.4:7002 --cluster-replicas 0 --cluster-yes
|
||||||
|
restart: no
|
||||||
|
depends_on:
|
||||||
|
redis-node-1:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-2:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-3:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- redis-net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
scylla-data-1:
|
scylla-data-1:
|
||||||
scylla-data-2:
|
scylla-data-2:
|
||||||
@@ -42,3 +102,9 @@ networks:
|
|||||||
ipam:
|
ipam:
|
||||||
config:
|
config:
|
||||||
- subnet: 172.20.0.0/24
|
- subnet: 172.20.0.0/24
|
||||||
|
|
||||||
|
redis-net:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.1.0/24
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[init]
|
||||||
|
p1 ansible_host=p1.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack1
|
||||||
|
|
||||||
|
[master]
|
||||||
|
p2 ansible_host=p2.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack2
|
||||||
|
p3 ansible_host=p3.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack3
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
p4 ansible_host=p4.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack1
|
||||||
|
p5 ansible_host=p5.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack2
|
||||||
|
p6 ansible_host=p6.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack3
|
||||||
|
|
||||||
|
[scylla]
|
||||||
|
p4
|
||||||
|
p5
|
||||||
|
p6
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: redis-config
|
||||||
|
namespace: redis
|
||||||
|
data:
|
||||||
|
update-node.sh: |
|
||||||
|
#!/bin/sh
|
||||||
|
REDIS_NODES="/data/nodes.conf"
|
||||||
|
sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/${POD_IP}/" ${REDIS_NODES}
|
||||||
|
exec "$@"
|
||||||
|
redis.conf: |+
|
||||||
|
cluster-enabled yes
|
||||||
|
cluster-require-full-coverage no
|
||||||
|
cluster-node-timeout 15000
|
||||||
|
cluster-config-file /data/nodes.conf
|
||||||
|
cluster-migration-barrier 1
|
||||||
|
save ""
|
||||||
|
appendonly no
|
||||||
|
protected-mode no
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: redis-cluster
|
||||||
|
namespace: redis
|
||||||
|
spec:
|
||||||
|
serviceName: redis-cluster
|
||||||
|
replicas: 6
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: redis-cluster
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: redis-cluster
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
name: client
|
||||||
|
- containerPort: 16379
|
||||||
|
name: gossip
|
||||||
|
command: ["/conf/update-node.sh", "redis-server", "/conf/redis.conf"]
|
||||||
|
env:
|
||||||
|
- name: POD_IP
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: status.podIP
|
||||||
|
volumeMounts:
|
||||||
|
- name: conf
|
||||||
|
mountPath: /conf
|
||||||
|
readOnly: false
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
readOnly: false
|
||||||
|
volumes:
|
||||||
|
- name: conf
|
||||||
|
configMap:
|
||||||
|
name: redis-config
|
||||||
|
defaultMode: 0755
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
storageClassName: local-path
|
||||||
|
accessModes: [ "ReadWriteOnce" ]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis-cluster
|
||||||
|
namespace: redis
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 6379
|
||||||
|
targetPort: 6379
|
||||||
|
name: client
|
||||||
|
- port: 16379
|
||||||
|
targetPort: 16379
|
||||||
|
name: gossip
|
||||||
|
selector:
|
||||||
|
app: redis-cluster
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: scylla.scylladb.com/v1alpha1
|
||||||
|
kind: NodeConfig
|
||||||
|
metadata:
|
||||||
|
name: scylladb-nodepool-1
|
||||||
|
spec:
|
||||||
|
localDiskSetup:
|
||||||
|
loopDevices:
|
||||||
|
- name: persistent-volumes
|
||||||
|
imagePath: /var/lib/persistent-volumes.img
|
||||||
|
size: 60Gi
|
||||||
|
filesystems:
|
||||||
|
- device: /dev/loops/persistent-volumes
|
||||||
|
type: xfs
|
||||||
|
mounts:
|
||||||
|
- device: /dev/loops/persistent-volumes
|
||||||
|
mountPoint: /var/lib/persistent-volumes
|
||||||
|
unsupportedOptions:
|
||||||
|
- prjquota
|
||||||
|
placement:
|
||||||
|
nodeSelector:
|
||||||
|
scylla.scylladb.com/node-type: scylla
|
||||||
|
tolerations:
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: scylladb:csi-external-provisioner
|
||||||
|
aggregationRule:
|
||||||
|
clusterRoleSelectors:
|
||||||
|
- matchLabels:
|
||||||
|
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb:aggregate-to-csi-external-provisioner
|
||||||
|
labels:
|
||||||
|
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "persistentvolumes"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "create"
|
||||||
|
- "delete"
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "persistentvolumeclaims"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "update"
|
||||||
|
- apiGroups:
|
||||||
|
- "storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "storageclasses"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "events"
|
||||||
|
verbs:
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "create"
|
||||||
|
- "update"
|
||||||
|
- "patch"
|
||||||
|
- apiGroups:
|
||||||
|
- "snapshot.storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "volumesnapshots"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- apiGroups:
|
||||||
|
- "snapshot.storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "volumesnapshotcontents"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- apiGroups:
|
||||||
|
- "storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "csinodes"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "nodes"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- apiGroups:
|
||||||
|
- "storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "csistoragecapacities"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "create"
|
||||||
|
- "update"
|
||||||
|
- "patch"
|
||||||
|
- "delete"
|
||||||
|
# The GET permissions below are needed for walking up the ownership chain
|
||||||
|
# for CSIStorageCapacity. They are sufficient for deployment via
|
||||||
|
# StatefulSet (only needs to get Pod) and Deployment (needs to get
|
||||||
|
# Pod and then ReplicaSet to find the Deployment).
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "pods"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- apiGroups:
|
||||||
|
- "apps"
|
||||||
|
resources:
|
||||||
|
- "replicasets"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb:aggregate-to-csi-external-provisioner-openshift
|
||||||
|
labels:
|
||||||
|
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- security.openshift.io
|
||||||
|
resourceNames:
|
||||||
|
- privileged
|
||||||
|
resources:
|
||||||
|
- securitycontextconstraints
|
||||||
|
verbs:
|
||||||
|
- use
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: local-csi-driver
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
kind: StorageClass
|
||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb-local-xfs
|
||||||
|
provisioner: local.csi.scylladb.com
|
||||||
|
volumeBindingMode: WaitForFirstConsumer
|
||||||
|
parameters:
|
||||||
|
csi.storage.k8s.io/fstype: xfs
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
kind: CSIDriver
|
||||||
|
metadata:
|
||||||
|
name: local.csi.scylladb.com
|
||||||
|
spec:
|
||||||
|
attachRequired: false
|
||||||
|
storageCapacity: true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
namespace: local-csi-driver
|
||||||
|
name: local-csi-driver
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb:csi-external-provisioner
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: local-csi-driver
|
||||||
|
namespace: local-csi-driver
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: scylladb:csi-external-provisioner
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
kind: DaemonSet
|
||||||
|
apiVersion: apps/v1
|
||||||
|
metadata:
|
||||||
|
namespace: local-csi-driver
|
||||||
|
name: local-csi-driver
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: local-csi-driver
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: local-csi-driver
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: local-csi-driver
|
||||||
|
spec:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/os: linux
|
||||||
|
scylla.scylladb.com/node-type: scylla
|
||||||
|
serviceAccountName: local-csi-driver
|
||||||
|
tolerations:
|
||||||
|
- operator: Exists
|
||||||
|
containers:
|
||||||
|
- name: local-csi-driver
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
image: docker.io/scylladb/local-csi-driver:0.5.0@sha256:46c1d2e9be8edf076d8dea7e82a3bb763d4bdda5adf43c794f617e9c94251079
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- --listen=/csi/csi.sock
|
||||||
|
- --node-name=$(NODE_NAME)
|
||||||
|
- --volumes-dir=/var/lib/persistent-volumes
|
||||||
|
- --v=2
|
||||||
|
env:
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
volumeMounts:
|
||||||
|
- name: kubelet-dir
|
||||||
|
mountPath: /var/lib/kubelet
|
||||||
|
mountPropagation: "Bidirectional"
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
- name: volumes-dir
|
||||||
|
mountPath: /var/lib/persistent-volumes
|
||||||
|
ports:
|
||||||
|
- name: healthz
|
||||||
|
containerPort: 9809
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: healthz
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
periodSeconds: 2
|
||||||
|
failureThreshold: 5
|
||||||
|
- name: csi-driver-registrar
|
||||||
|
image: registry.k8s.io/sig-storage/csi-node-driver-registrar@sha256:fdff3ee285341bc58033b6b2458a5d45fd90ec6922a8ba6ebdd49b0c41e2cd34
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- --csi-address=/csi/csi.sock
|
||||||
|
- --kubelet-registration-path=/var/lib/kubelet/plugins/local.csi.scylladb.com/csi.sock
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
- name: registration-dir
|
||||||
|
mountPath: /registration
|
||||||
|
- name: liveness-probe
|
||||||
|
image: registry.k8s.io/sig-storage/livenessprobe@sha256:cacee2b5c36dd59d4c7e8469c05c9e4ef53ecb2df9025fa8c10cdaf61bce62f0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- --csi-address=/csi/csi.sock
|
||||||
|
- --health-port=9809
|
||||||
|
- --v=2
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
- name: csi-provisioner
|
||||||
|
image: registry.k8s.io/sig-storage/csi-provisioner@sha256:ee3b525d5b89db99da3b8eb521d9cd90cb6e9ef0fbb651e98bb37be78d36b5b8
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- --csi-address=/csi/csi.sock
|
||||||
|
- --v=2
|
||||||
|
- --node-deployment
|
||||||
|
- --feature-gates=Topology=true
|
||||||
|
- --immediate-topology=false
|
||||||
|
- --enable-capacity
|
||||||
|
- --capacity-ownerref-level=0
|
||||||
|
- --capacity-poll-interval=30s
|
||||||
|
- --default-fstype=xfs
|
||||||
|
env:
|
||||||
|
- name: NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
volumes:
|
||||||
|
- name: kubelet-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet
|
||||||
|
type: Directory
|
||||||
|
- name: plugin-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet/plugins/local.csi.scylladb.com/
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: registration-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet/plugins_registry/
|
||||||
|
type: Directory
|
||||||
|
- name: volumes-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/persistent-volumes
|
||||||
|
type: Directory
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: scylladb-config
|
||||||
|
data:
|
||||||
|
scylla.yaml: |
|
||||||
|
authenticator: PasswordAuthenticator
|
||||||
|
authorizer: CassandraAuthorizer
|
||||||
|
---
|
||||||
|
apiVersion: scylla.scylladb.com/v1
|
||||||
|
kind: ScyllaCluster
|
||||||
|
metadata:
|
||||||
|
name: scylladb
|
||||||
|
spec:
|
||||||
|
repository: docker.io/scylladb/scylla
|
||||||
|
version: 2025.1.5
|
||||||
|
agentVersion: 3.5.1
|
||||||
|
developerMode: false
|
||||||
|
automaticOrphanedNodeCleanup: true
|
||||||
|
sysctls:
|
||||||
|
- fs.aio-max-nr=30000000
|
||||||
|
datacenter:
|
||||||
|
name: ge-dc1
|
||||||
|
racks:
|
||||||
|
- name: ge-dc1-rack1
|
||||||
|
members: 1
|
||||||
|
scyllaConfig: scylladb-config
|
||||||
|
storage:
|
||||||
|
capacity: 45Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: topology.kubernetes.io/zone
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ge-dc1-rack1
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
|
- name: ge-dc1-rack2
|
||||||
|
members: 1
|
||||||
|
scyllaConfig: scylladb-config
|
||||||
|
storage:
|
||||||
|
capacity: 45Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: topology.kubernetes.io/zone
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ge-dc1-rack2
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
|
- name: ge-dc1-rack3
|
||||||
|
members: 1
|
||||||
|
scyllaConfig: scylladb-config
|
||||||
|
storage:
|
||||||
|
capacity: 45Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: topology.kubernetes.io/zone
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ge-dc1-rack3
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager-config
|
||||||
|
namespace: scylla-manager
|
||||||
|
data:
|
||||||
|
scylla-manager.yaml: |-
|
||||||
|
http: :5080
|
||||||
|
logger:
|
||||||
|
level: info
|
||||||
|
database:
|
||||||
|
hosts:
|
||||||
|
- scylla-manager-cluster-manager-dc-manager-rack-0
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
namespace: scylla-manager
|
||||||
|
name: scylla-manager-to-scylla-pod
|
||||||
|
spec:
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/managed-by: scylla-operator
|
||||||
|
app.kubernetes.io/name: scylla
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
name: scylla-manager
|
||||||
|
namespace: scylla-manager
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 5080
|
||||||
|
- name: metrics
|
||||||
|
port: 5090
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 5090
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager
|
||||||
|
namespace: scylla-manager
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager
|
||||||
|
namespace: scylla-manager
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
spec:
|
||||||
|
serviceAccountName: scylla-manager
|
||||||
|
containers:
|
||||||
|
- name: scylla-manager
|
||||||
|
image: docker.io/scylladb/scylla-manager:3.5.1@sha256:6986ecfc8c925c3d59b65bbcb9763d62f7591a00bb30242842aada115929e816
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /usr/bin/scylla-manager
|
||||||
|
args:
|
||||||
|
- --config-file=/mnt/etc/scylla-manager/scylla-manager.yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 20Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/etc/scylla-manager
|
||||||
|
name: scylla-manager-config
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/clusters
|
||||||
|
port: 5080
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
volumes:
|
||||||
|
- configMap:
|
||||||
|
name: scylla-manager-config
|
||||||
|
name: scylla-manager-config
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: scylla.scylladb.com/v1
|
||||||
|
kind: ScyllaCluster
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager-cluster
|
||||||
|
namespace: scylla-manager
|
||||||
|
spec:
|
||||||
|
version: 2025.1.5
|
||||||
|
agentVersion: 3.5.1@sha256:d1b57d08b9949c8faad2048fdf4dc7c502dae81da856c3c6b3a77dd347d5c7fc
|
||||||
|
repository: docker.io/scylladb/scylla
|
||||||
|
agentRepository: docker.io/scylladb/scylla-manager-agent
|
||||||
|
developerMode: true
|
||||||
|
cpuset: true
|
||||||
|
sysctls:
|
||||||
|
- fs.aio-max-nr=30000000
|
||||||
|
datacenter:
|
||||||
|
name: manager-dc
|
||||||
|
racks:
|
||||||
|
- members: 1
|
||||||
|
name: manager-rack
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 1
|
||||||
|
memory: 200Mi
|
||||||
|
requests:
|
||||||
|
cpu: 1
|
||||||
|
memory: 200Mi
|
||||||
|
storage:
|
||||||
|
capacity: 5Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
---
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/01-setup.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/02-copy-k8s-files.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/03-cert-manager.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/04-prometheus-operator.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/05-scylla-operator.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/06-scylla-cluster.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/07-scylla-manager.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/08-redis.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/09-payphone.yml --vault-password-file playbooks/00-vault-pwd.txt
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
30363364303339383037643864396632306537333737383933363164663838333364323837633838
|
||||||
|
3561323032663461663834613634633734636333306638380a396436646332373330393931396530
|
||||||
|
36353439343736656432333437653764653132393364626136396230623038626330313366386263
|
||||||
|
3630663539303665340a623961656435373337363534363863353934326336653361663538616664
|
||||||
|
36313865646530346633626639393739653131313835383631613833393832396434633038343639
|
||||||
|
62656236326662343236303133306137653533633064626161636165646164646165653534633036
|
||||||
|
63623863353438616237323530383033366138656665613331666532316562336232353239323535
|
||||||
|
33326237653463663862383266373534343434383765303237613436643439313233396338376134
|
||||||
|
65303161313930383165636133616236386435663232313531633461383938326131353930353761
|
||||||
|
35303238333863666137313330336366623536613230353639666133333633353566303131393666
|
||||||
|
32313964636138343632373064366166333134343437613937303538613834623934623161626261
|
||||||
|
63303832663438356331363534316230613561613961306561376432646132663733303366393961
|
||||||
|
64373166633938633730643339646433623235306161386338316330336264623031363365376634
|
||||||
|
38306639356236383464653038663732653465626265663939643234306563346330336139373235
|
||||||
|
36616662313139356139363532633664333762623235326237636339323039373131666162613930
|
||||||
|
63336161396635646565393565373736616431643830366163356532383034396636323163623836
|
||||||
|
37613632343563363032303133333930653431343039633239306561303266393861366134643734
|
||||||
|
66326534343464363634383461326330346432613266653066643139636232656638346635343130
|
||||||
|
61383336623763353034316238313536336261616433356461666537633665636430393139663465
|
||||||
|
61643066356466646264616562343939616336343935663939313537356237303264303761376139
|
||||||
|
34623231623264633934363537643132643336313633663430643265633564646363623130396437
|
||||||
|
61383763396633663439626661333630373662616633353232356165663034336131343666643633
|
||||||
|
373138346631323465303932633962613234
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
- name: Install K3s and Apply Security Configurations on All Nodes
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Install curl
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: curl
|
||||||
|
state: present
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Download k3s install script
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: https://get.k3s.io
|
||||||
|
dest: /tmp/k3s_install.sh
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Install Python Kubernetes client
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: python3-kubernetes
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Disable UFW
|
||||||
|
ansible.builtin.command: ufw disable
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Disable password authentication for SSH
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: '^PasswordAuthentication'
|
||||||
|
line: 'PasswordAuthentication no'
|
||||||
|
state: present
|
||||||
|
backup: true
|
||||||
|
|
||||||
|
- name: Disable root password login
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: '^PermitRootLogin'
|
||||||
|
line: 'PermitRootLogin prohibit-password'
|
||||||
|
state: present
|
||||||
|
backup: true
|
||||||
|
|
||||||
|
- name: Restart SSH to apply config
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: ssh
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Install fail2ban
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: fail2ban
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Configure fail2ban for SSH
|
||||||
|
ansible.builtin.blockinfile:
|
||||||
|
path: /etc/fail2ban/jail.local
|
||||||
|
create: true
|
||||||
|
block: |
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
port = ssh
|
||||||
|
logpath = /var/log/auth.log
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 3600
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Restart fail2ban
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: fail2ban
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Set up K3s on Init Node
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Run k3s Installer
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
INSTALL_K3S_EXEC="server --tls-san {{ ansible_host }} --flannel-backend=wireguard-native --cluster-init" \
|
||||||
|
sh /tmp/k3s_install.sh
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/k3s
|
||||||
|
|
||||||
|
- name: Ensure K3s Service is Started and Enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Get Joining Token
|
||||||
|
ansible.builtin.command: cat /var/lib/rancher/k3s/server/node-token
|
||||||
|
register: k3s_token
|
||||||
|
run_once: true
|
||||||
|
tags: skip_ansible_lint
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set up K3s on Master Nodes
|
||||||
|
hosts: master
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Run k3s Installer
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
INSTALL_K3S_EXEC="server --flannel-backend=wireguard-native --tls-san {{ ansible_host }}" \
|
||||||
|
K3S_URL="https://{{ hostvars[groups['init'][0]].ansible_host }}:6443" \
|
||||||
|
K3S_TOKEN="{{ hostvars[groups['init'][0]].k3s_token.stdout }}" sh /tmp/k3s_install.sh
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/k3s
|
||||||
|
|
||||||
|
- name: Ensure k3s service is started and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Set up K3s on Worker Nodes
|
||||||
|
hosts: worker
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Run k3s Installer
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
INSTALL_K3S_EXEC="agent" K3S_URL="https://{{ hostvars[groups['init'][0]].ansible_host }}:6443" \
|
||||||
|
K3S_TOKEN="{{ hostvars[groups['init'][0]].k3s_token.stdout }}" sh /tmp/k3s_install.sh
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/k3s
|
||||||
|
|
||||||
|
- name: Ensure k3s service is started and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s-agent
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Wait for All K3s Nodes to Become Ready
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Wait until all nodes report Ready
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for=condition=Ready node --all --timeout=10m
|
||||||
|
register: wait_nodes
|
||||||
|
retries: 5
|
||||||
|
delay: 30
|
||||||
|
until: wait_nodes.rc == 0
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set Up Node Labels and Taints
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Set Rack Tags to All Nodes
|
||||||
|
loop: "{{ groups['all'] }}"
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
api_version: v1
|
||||||
|
kind: Node
|
||||||
|
name: "{{ item }}"
|
||||||
|
definition:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
topology.kubernetes.io/zone: "{{ hostvars[item].rack }}"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Set Scylla Tags to Scylla Nodes
|
||||||
|
loop: "{{ groups['scylla'] }}"
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
api_version: v1
|
||||||
|
kind: Node
|
||||||
|
name: "{{ item }}"
|
||||||
|
definition:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
scylla.scylladb.com/node-type: scylla
|
||||||
|
spec:
|
||||||
|
taints:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create Containerd Socket Symlink
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Create Containerd Symlink
|
||||||
|
ansible.builtin.command: |
|
||||||
|
ln -s /run/k3s/containerd/containerd.sock /run/containerd/containerd.sock
|
||||||
|
args:
|
||||||
|
creates: /run/containerd/containerd.sock
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
- name: Copy K8s files to node
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
k8s_files_src: "{{ playbook_dir }}/../k8s"
|
||||||
|
k8s_files_dest: /tmp/k8s
|
||||||
|
tasks:
|
||||||
|
- name: Create destination directory for K8s files
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ k8s_files_dest }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Copy K8s files to destination
|
||||||
|
ansible.posix.synchronize:
|
||||||
|
src: "{{ k8s_files_src }}/"
|
||||||
|
dest: "{{ k8s_files_dest }}/"
|
||||||
|
recursive: true
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
- name: Set up Cert-Manager
|
||||||
|
hosts: init
|
||||||
|
gather_facts: false
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
cert_manager_ns: cert-manager
|
||||||
|
cert_manager_crds:
|
||||||
|
- certificates.cert-manager.io
|
||||||
|
- issuers.cert-manager.io
|
||||||
|
cert_manager_deployments:
|
||||||
|
- cert-manager
|
||||||
|
- cert-manager-cainjector
|
||||||
|
- cert-manager-webhook
|
||||||
|
tasks:
|
||||||
|
- name: Apply Cert-Manager YAML
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "/tmp/k8s/scylla/01-cert-manager.yml"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Cert-Manager CRDs
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for condition=established --timeout=60s crd/{{ cert_manager_crds | join(' crd/') }}
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Cert-Manager deployments
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl -n {{ cert_manager_ns }} rollout status --timeout=10m deployment.apps/{{ item }}
|
||||||
|
loop: "{{ cert_manager_deployments }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: item
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Cert-Manager webhook CA secret
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for i in {1..30}; do
|
||||||
|
kubectl -n {{ cert_manager_ns }} get secret/cert-manager-webhook-ca && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
- name: Set up Prometheus operator
|
||||||
|
hosts: init
|
||||||
|
gather_facts: false
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
prometheus_crds:
|
||||||
|
- prometheuses.monitoring.coreos.com
|
||||||
|
- prometheusrules.monitoring.coreos.com
|
||||||
|
- servicemonitors.monitoring.coreos.com
|
||||||
|
tasks:
|
||||||
|
- name: Apply Prometheus operator server-side
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl apply --server-side -f /tmp/k8s/scylla/02-prometheus-operator.yml -n prometheus-operator
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Prometheus operator CRDs
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for='condition=established' crd/{{ prometheus_crds | join(' crd/') }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Prometheus operator deployment
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl -n=prometheus-operator rollout status --timeout=10m deployment.apps/prometheus-operator
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
- name: Set up Scylla Operator, NodeConfig, and the Local CSI Driver
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
scylla_crds:
|
||||||
|
- scyllaclusters.scylla.scylladb.com
|
||||||
|
- nodeconfigs.scylla.scylladb.com
|
||||||
|
- scyllaoperatorconfigs.scylla.scylladb.com
|
||||||
|
- scylladbmonitorings.scylla.scylladb.com
|
||||||
|
scylla_deployments:
|
||||||
|
- scylla-operator
|
||||||
|
- webhook-server
|
||||||
|
tasks:
|
||||||
|
- name: Apply Scylla Operator YAML
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl apply --server-side -f /tmp/k8s/scylla/03-scylla-operator.yml -n scylla-operator
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Scylla Operator CRDs
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for='condition=established' crd/{{ scylla_crds | join(' crd/') }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Scylla Operator Deployments
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl -n scylla-operator rollout status --timeout=10m deployment.apps/{{ item }}
|
||||||
|
loop: "{{ scylla_deployments }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: item
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Apply Scylla NodeConfig YAML
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "/tmp/k8s/scylla/04-scylla-node-config.yml"
|
||||||
|
namespace: scylla-operator
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Scylla NodeConfig to be applied across all nodes
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl wait --for='condition=Reconciled' --timeout=10m nodeconfigs.scylla.scylladb.com/scylladb-nodepool-1
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Find Scylla CSI Driver K8s YAMLs on Remote Host
|
||||||
|
ansible.builtin.find:
|
||||||
|
paths: /tmp/k8s/scylla
|
||||||
|
patterns: "05-*.yml"
|
||||||
|
file_type: file
|
||||||
|
register: scylla_csi_driver_files
|
||||||
|
|
||||||
|
- name: Apply all Scylla CSI Driver K8s YAMLs
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "{{ item.path }}"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
loop: "{{ scylla_csi_driver_files.files | sort(attribute='path') }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.path }}"
|
||||||
|
|
||||||
|
- name: Wait for Local CSI Driver DaemonSet
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl -n=local-csi-driver rollout status --timeout=10m daemonset.apps/local-csi-driver
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
- name: Set Up Scylla Cluster
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Create Scylla Namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: scylla
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Apply Scylla Cluster Config
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/scylla/06-scylla-cluster.yml
|
||||||
|
namespace: scylla
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Scylla Cluster Application
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl -n scylla rollout status statefulset/{{ item }} --timeout=10m
|
||||||
|
loop:
|
||||||
|
- scylladb-ge-dc1-ge-dc1-rack1
|
||||||
|
- scylladb-ge-dc1-ge-dc1-rack2
|
||||||
|
- scylladb-ge-dc1-ge-dc1-rack3
|
||||||
|
register: rollout
|
||||||
|
retries: 30
|
||||||
|
delay: 10
|
||||||
|
until: rollout.rc == 0
|
||||||
|
ignore_errors: true
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
- name: Set Up Scylla Manager
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Apply Scylla Manager YAML
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "/tmp/k8s/scylla/07-scylla-manager.yml"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Scylla Manager Deployment
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl -n=scylla-manager rollout status --timeout=10m deployment.apps/scylla-manager
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
- name: Set Up Redis Cluster
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Create Redis Config Map
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/redis/01-config-map.yml
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Create Redis Nodes
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/redis/02-redis.yml
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Redis Nodes
|
||||||
|
ansible.builtin.command:
|
||||||
|
kubectl -n=redis rollout status --timeout=10m sts/redis-cluster
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create ClusterIP Service
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/redis/03-cluster-ip.yml
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Grab Node IPs
|
||||||
|
ansible.builtin.shell:
|
||||||
|
echo $(kubectl get pods -l app=redis-cluster -n redis -o json | jq -r '.items | map(.status.podIP) | join(":6379 ")'):6379
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: redis_node_ips
|
||||||
|
changed_when: false
|
||||||
|
tags:
|
||||||
|
- skip_ansible_lint
|
||||||
|
|
||||||
|
- name: Display Node IPs
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ redis_node_ips.stdout }}"
|
||||||
|
|
||||||
|
- name: Check if Redis cluster is already active
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
set -o pipefail
|
||||||
|
kubectl exec redis-cluster-0 -n redis -- redis-cli -p 6379 cluster info | grep cluster_state
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: redis_cluster_state
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Clear Redis Nodes
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
for ip in {{ redis_node_ips.stdout }}; do
|
||||||
|
kubectl exec redis-cluster-0 -n redis -- redis-cli -p 6379 -h $ip flushall;
|
||||||
|
done
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when: "'cluster_state:ok' not in redis_cluster_state.stdout"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Activate Redis Cluster
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
kubectl exec redis-cluster-0 -n redis -- redis-cli --cluster create --cluster-replicas 1 {{ redis_node_ips.stdout }} --cluster-yes
|
||||||
|
when: "'cluster_state:ok' not in redis_cluster_state.stdout"
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
- name: Build and Push Payphone
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Get Cargo Project Metadata
|
||||||
|
ansible.builtin.command: cargo metadata --no-deps --format-version 1
|
||||||
|
register: cargo_metadata
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Parse version
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
app_version: "v{{ (cargo_metadata.stdout | from_json).packages[0].version }}"
|
||||||
|
|
||||||
|
- name: Build and Push the Docker image
|
||||||
|
community.docker.docker_image:
|
||||||
|
source: build
|
||||||
|
build:
|
||||||
|
path: "{{ playbook_dir }}/../"
|
||||||
|
name: registry.nyeki.dev/payphone/payphone
|
||||||
|
tag: "{{ app_version }}"
|
||||||
|
push: true
|
||||||
|
|
||||||
|
- name: Deploy Payphone
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
vars_files:
|
||||||
|
- 00-vault.yml
|
||||||
|
tasks:
|
||||||
|
- name: Create Payphone Namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: payphone
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Create Bot Token Secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
namespace: payphone
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: payphone-secrets
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
PAYPHONE_TOKEN: "{{ bot_token }}"
|
||||||
|
PAYPHONE_TOPGG_TOKEN: "{{ topgg_token }}"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Set Git credentials
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
kubectl create secret docker-registry payphone-git-credentials \
|
||||||
|
--docker-server=registry.nyeki.dev \
|
||||||
|
--docker-username={{ gitlab_username }} \
|
||||||
|
--docker-password={{ gitlab_pat }} \
|
||||||
|
--docker-email={{ gitlab_email }} \
|
||||||
|
-n payphone \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
- name: Apply Payphone Deployment
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
namespace: payphone
|
||||||
|
definition:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: payphone
|
||||||
|
labels:
|
||||||
|
app: payphone
|
||||||
|
spec:
|
||||||
|
serviceName: payphone
|
||||||
|
replicas: "{{ bot_shards }}"
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: payphone
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: payphone
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: payphone
|
||||||
|
image: registry.nyeki.dev/payphone/payphone:{{ hostvars['localhost'].app_version }}
|
||||||
|
imagePullPolicy: "Always"
|
||||||
|
args: [
|
||||||
|
"start",
|
||||||
|
"--shard",
|
||||||
|
"$(SHARD_ID):$(SHARD_COUNT)",
|
||||||
|
"--database-addr",
|
||||||
|
"scylladb-client.scylla",
|
||||||
|
"--database-username",
|
||||||
|
"cassandra",
|
||||||
|
"--database-password",
|
||||||
|
"cassandra",
|
||||||
|
"--redis-urls",
|
||||||
|
"redis://redis-cluster.redis",
|
||||||
|
"--bot-owner-id",
|
||||||
|
"1486270291410817114"
|
||||||
|
]
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: payphone-secrets
|
||||||
|
env:
|
||||||
|
- name: SHARD_ID
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.labels['apps.kubernetes.io/pod-index']
|
||||||
|
- name: SHARD_COUNT
|
||||||
|
value: "{{ bot_shards | string }}"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "1Gi"
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "100Mi"
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: payphone-git-credentials
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Rollout New Version
|
||||||
|
ansible.builtin.command:
|
||||||
|
kubectl set image -n payphone sts/payphone payphone=registry.nyeki.dev/payphone/payphone:{{ hostvars['localhost'].app_version }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Bump Payphone Version
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: true
|
||||||
|
tasks:
|
||||||
|
- name: Bump Version
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
cargo set-version --bump patch
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
- name: APT Update
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Update APT package index
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: yes
|
||||||
|
cache_valid_time: 3600
|
||||||
|
|
||||||
|
- name: Upgrade APT packages
|
||||||
|
ansible.builtin.apt:
|
||||||
|
upgrade: dist
|
||||||
|
autoremove: yes
|
||||||
|
autoclean: yes
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[project]
|
||||||
|
name = "payphone"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"ansible>=11.9.0",
|
||||||
|
"ansible-lint>=25.8.1",
|
||||||
|
"requests>=2.32.4",
|
||||||
|
]
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
uv run ansible-playbook -i inventory.ini playbooks/09-payphone.yml --vault-password-file playbooks/00-vault-pwd.txt
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Module"
|
||||||
+105
-4
@@ -45,21 +45,30 @@
|
|||||||
//! ```shell
|
//! ```shell
|
||||||
//! $ cargo run -- start [TOKEN]
|
//! $ cargo run -- start [TOKEN]
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
//! To run multiple shards, pass the `--shard` or `-s` argument to the `start` command with the
|
||||||
|
//! shard ID and total amount of shards as `id:total`. For example, for a single-shard bot, you'd
|
||||||
|
//! set it to `-s 0:1`. ID is zero-indexed (0 means first shard), total is one-indexed (0 means no
|
||||||
|
//! shards).
|
||||||
|
//!
|
||||||
//! # Running the Documentation Server
|
//! # Running the Documentation Server
|
||||||
//!
|
//!
|
||||||
//! Payphone has a built-in documentation server that serves the project's documentation and
|
//! Payphone has a built-in documentation server that serves the project's documentation and
|
||||||
//! refreshes it on changes. To start the documentation server, run the following command:
|
//! refreshes it on changes. To start the documentation server, run the following command:
|
||||||
//!
|
//!
|
||||||
//! ```shell
|
//! ```shell
|
||||||
//! $ cargo run -- docs --open
|
//! $ cargo run -- docs --open
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The `--open` flag is optional and will open the documentation in the default browser after
|
//! The `--open` flag is optional and will open the documentation in the default browser after
|
||||||
//! starting the server. You can also use the `--bind` argument to specify the address to bind the
|
//! starting the server. You can also use the `--bind` argument to specify the address to bind the
|
||||||
//! server to, which defaults to `0.0.0.0:3000`.
|
//! server to, which defaults to `0.0.0.0:3000`.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use twilight_gateway::ShardId;
|
||||||
|
|
||||||
/// Payphone's argument parser.
|
/// Payphone's argument parser.
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
@@ -122,6 +131,54 @@ impl Into<log::LevelFilter> for LevelFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Shard {
|
||||||
|
pub id: u32,
|
||||||
|
pub total: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shard {
|
||||||
|
fn new(id: u32, total: u32) -> Self {
|
||||||
|
Shard { id, total }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Shard {
|
||||||
|
fn default() -> Self {
|
||||||
|
Shard::new(0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Shard {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.split_once(":") {
|
||||||
|
Some((id, total)) => {
|
||||||
|
let id = id.parse().map_err(|_| "Invalid shard ID")?;
|
||||||
|
let total = total.parse().map_err(|_| "Invalid shard total")?;
|
||||||
|
|
||||||
|
Ok(Shard { id, total })
|
||||||
|
}
|
||||||
|
None => Err("Invalid shard format, expected `id:total`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Shard {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}:{}", self.id, self.total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::from_over_into)]
|
||||||
|
impl Into<ShardId> for Shard {
|
||||||
|
fn into(self) -> ShardId {
|
||||||
|
ShardId::new(self.id, self.total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The available commands for the Payphone CLI and their available arguments.
|
||||||
#[derive(clap::Subcommand, Clone)]
|
#[derive(clap::Subcommand, Clone)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Starts the bot with the provided token.
|
/// Starts the bot with the provided token.
|
||||||
@@ -139,6 +196,50 @@ pub enum Commands {
|
|||||||
default_value_t = String::from("127.0.0.1:9042"))
|
default_value_t = String::from("127.0.0.1:9042"))
|
||||||
]
|
]
|
||||||
database_addr: String,
|
database_addr: String,
|
||||||
|
|
||||||
|
/// The username to use when connecting to the database.
|
||||||
|
#[arg(long, env = "PAYPHONE_DATABASE_USERNAME", value_name = "USERNAME")]
|
||||||
|
database_username: Option<String>,
|
||||||
|
|
||||||
|
/// The password to use when connecting to the database.
|
||||||
|
#[arg(long, env = "PAYPHONE_DATABASE_PASSWORD", value_name = "PASSWORD")]
|
||||||
|
database_password: Option<String>,
|
||||||
|
|
||||||
|
/// The current shard being run.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
env = "PAYPHONE_SHARD",
|
||||||
|
value_name = "ID:TOTAL",
|
||||||
|
default_value_t = Shard::default())
|
||||||
|
]
|
||||||
|
shard: Shard,
|
||||||
|
|
||||||
|
/// The Redis URLs to connect to.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
env = "PAYPHONE_REDIS_URLS",
|
||||||
|
value_name = "URLS",
|
||||||
|
default_values = &["redis://127.0.0.1:6379"],
|
||||||
|
)]
|
||||||
|
redis_urls: Vec<String>,
|
||||||
|
|
||||||
|
/// The bot owner's ID, this user will be allowed to assign moderators.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
env = "PAYPHONE_OWNER_ID",
|
||||||
|
value_name = "ID",
|
||||||
|
default_value_t = 0
|
||||||
|
)]
|
||||||
|
bot_owner_id: u64,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
env = "PAYPHONE_TOPGG_TOKEN",
|
||||||
|
value_name = "TOKEN",
|
||||||
|
help = "The token to use for the Top.gg API, used for posting server counts and other stats. If not provided, Top.gg integration will be disabled."
|
||||||
|
)]
|
||||||
|
topgg_token: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Starts a documentation server for the project which serves `cargo doc` and refreshes on
|
/// Starts a documentation server for the project which serves `cargo doc` and refreshes on
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use color_thief::ColorFormat;
|
||||||
|
use image::{GenericImageView, ImageReader};
|
||||||
|
use reqwest::header::USER_AGENT;
|
||||||
|
use rgb::Rgb;
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
use tokio::sync::mpsc::Receiver;
|
||||||
|
|
||||||
|
use crate::constants::VERSION;
|
||||||
|
use crate::database::models::User;
|
||||||
|
use crate::discord::images::get_user_custom_avatar_url_or_none;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
pub struct AvatarColorFetchingRequest {
|
||||||
|
pub hash: String,
|
||||||
|
pub user_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvatarColorFetchingRequest {
|
||||||
|
pub fn new(hash: impl ToString, user_id: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
hash: hash.to_string(),
|
||||||
|
user_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_color_fetching_queue_listener(
|
||||||
|
mut receiver: Receiver<AvatarColorFetchingRequest>,
|
||||||
|
state: Arc<State>,
|
||||||
|
) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let semaphore = Arc::new(Semaphore::new(5));
|
||||||
|
|
||||||
|
while let Some(request) = receiver.recv().await {
|
||||||
|
let task_semaphore = semaphore.clone();
|
||||||
|
let task_state = state.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _permit = task_semaphore
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to acquire semaphore permit: {e}"))
|
||||||
|
.expect("Failed to acquire semaphore permit");
|
||||||
|
|
||||||
|
let avatar_url = get_user_custom_avatar_url_or_none(request.user_id, request.hash);
|
||||||
|
|
||||||
|
let image_bytes = reqwest::Client::new()
|
||||||
|
.get(avatar_url)
|
||||||
|
.header(USER_AGENT, format!("Payphone/v{VERSION}"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to fetch avatar color: {e}"))?
|
||||||
|
.error_for_status()?
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to read avatar image bytes: {e}"))?;
|
||||||
|
|
||||||
|
let color = tokio::task::spawn_blocking(move || {
|
||||||
|
let img = ImageReader::new(Cursor::new(image_bytes))
|
||||||
|
.with_guessed_format()?
|
||||||
|
.decode()?;
|
||||||
|
|
||||||
|
let mut pixels = Vec::new();
|
||||||
|
|
||||||
|
for (_x, _y, rgba) in img.pixels() {
|
||||||
|
pixels.extend_from_slice(&rgba.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(img);
|
||||||
|
|
||||||
|
let palette = color_thief::get_palette(&pixels, ColorFormat::Rgba, 1, 2)?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(palette.first().cloned())
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
if let Some(color) = color {
|
||||||
|
let Rgb { r, g, b } = color;
|
||||||
|
|
||||||
|
let color_int: u32 = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
|
||||||
|
|
||||||
|
let mut user = User::get_by_id(request.user_id, &task_state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to fetch user to set their new avatar color: {e}")
|
||||||
|
})?
|
||||||
|
.ok_or(anyhow::anyhow!(
|
||||||
|
"User with ID {} not found when fetching their avatar URL color",
|
||||||
|
request.user_id
|
||||||
|
))
|
||||||
|
.inspect_err(|e| log::error!("{e}"))?;
|
||||||
|
|
||||||
|
user.color = Some(color_int as i32);
|
||||||
|
|
||||||
|
user.insert(&task_state.db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
//! Constants used throughout the Payphone project.
|
//! Constants used throughout the Payphone project.
|
||||||
|
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
/// The default command prefix used by the bot for new servers.
|
/// The default command prefix used by the bot for new servers.
|
||||||
pub const DEFAULT_PREFIX: &str = "p.";
|
pub const DEFAULT_PREFIX: &str = "p.";
|
||||||
|
|
||||||
@@ -8,3 +10,6 @@ pub const COLOR_WHITE: u32 = 0xE2D9D0;
|
|||||||
|
|
||||||
/// A red color used for error messages and moderator embeds.
|
/// A red color used for error messages and moderator embeds.
|
||||||
pub const COLOR_RED: u32 = 0xFF8787;
|
pub const COLOR_RED: u32 = 0xFF8787;
|
||||||
|
|
||||||
|
/// A yellow color used for mask notifications and warnings.
|
||||||
|
pub const COLOR_YELLOW: u32 = 0xFFC868;
|
||||||
|
|||||||
@@ -60,12 +60,11 @@ CREATE TABLE IF NOT EXISTS payphone.participants (
|
|||||||
PRIMARY KEY (call_id, user_id, channel_id)
|
PRIMARY KEY (call_id, user_id, channel_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- TTL 30 days.
|
CREATE TYPE IF NOT EXISTS payphone.message (
|
||||||
CREATE TABLE IF NOT EXISTS payphone.messages (
|
id BIGINT,
|
||||||
call_id BIGINT,
|
call_id BIGINT,
|
||||||
message_id BIGINT,
|
|
||||||
author_id BIGINT,
|
|
||||||
|
|
||||||
|
author_id BIGINT,
|
||||||
author_name TEXT,
|
author_name TEXT,
|
||||||
author_avatar_url TEXT,
|
author_avatar_url TEXT,
|
||||||
author_mask ASCII,
|
author_mask ASCII,
|
||||||
@@ -77,39 +76,44 @@ CREATE TABLE IF NOT EXISTS payphone.messages (
|
|||||||
propagation_message_id BIGINT,
|
propagation_message_id BIGINT,
|
||||||
propagation_channel_id BIGINT,
|
propagation_channel_id BIGINT,
|
||||||
|
|
||||||
replying_to_id BIGINT,
|
replying_to_message_id BIGINT,
|
||||||
|
|
||||||
is_deleted BOOLEAN,
|
is_deleted BOOLEAN,
|
||||||
is_edited BOOLEAN,
|
is_edited BOOLEAN,
|
||||||
is_censored BOOLEAN,
|
is_censored BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
-- TTL 30 days.
|
||||||
|
CREATE TABLE IF NOT EXISTS payphone.messages (
|
||||||
|
call_id BIGINT,
|
||||||
|
message_id BIGINT,
|
||||||
|
author_id BIGINT,
|
||||||
|
propagation_message_id BIGINT,
|
||||||
|
|
||||||
|
data FROZEN<payphone.message>,
|
||||||
|
|
||||||
PRIMARY KEY (call_id, message_id)
|
PRIMARY KEY (call_id, message_id)
|
||||||
);
|
) WITH default_time_to_live = 2592000; -- 30 days
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.messages_by_propagation_id AS
|
||||||
|
SELECT call_id, message_id, propagation_message_id FROM payphone.messages
|
||||||
|
WHERE call_id IS NOT NULL AND message_id IS NOT NULL AND propagation_message_id IS NOT NULL
|
||||||
|
PRIMARY KEY (call_id, propagation_message_id, message_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.servers (
|
CREATE TABLE IF NOT EXISTS payphone.servers (
|
||||||
server_id BIGINT PRIMARY KEY,
|
server_id BIGINT PRIMARY KEY,
|
||||||
|
|
||||||
name TEXT,
|
name TEXT,
|
||||||
prefix TEXT,
|
prefix TEXT,
|
||||||
|
language TEXT,
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.reports (
|
CREATE TABLE IF NOT EXISTS payphone.reports (
|
||||||
report_id BIGINT,
|
report_id BIGINT,
|
||||||
|
user_id BIGINT,
|
||||||
|
|
||||||
message_author_id BIGINT,
|
message FROZEN<payphone.message>,
|
||||||
message_author_name TEXT,
|
replying_to FROZEN<payphone.message>,
|
||||||
message_author_avatar_url TEXT,
|
|
||||||
message_author_mask ASCII,
|
|
||||||
|
|
||||||
message_id BIGINT,
|
|
||||||
message_content TEXT,
|
|
||||||
message_attachments LIST<TEXT>,
|
|
||||||
message_sticker BIGINT,
|
|
||||||
|
|
||||||
message_replying_to_id BIGINT,
|
|
||||||
message_replying_to_content TEXT,
|
|
||||||
message_replying_to_attachments LIST<TEXT>,
|
|
||||||
message_replying_to_sticker BIGINT,
|
|
||||||
|
|
||||||
created_at TIMESTAMP,
|
created_at TIMESTAMP,
|
||||||
reviewed_at TIMESTAMP,
|
reviewed_at TIMESTAMP,
|
||||||
@@ -119,28 +123,18 @@ CREATE TABLE IF NOT EXISTS payphone.reports (
|
|||||||
reviewed_by_id BIGINT,
|
reviewed_by_id BIGINT,
|
||||||
resolution ASCII,
|
resolution ASCII,
|
||||||
|
|
||||||
PRIMARY KEY (message_author_id, report_id)
|
PRIMARY KEY (user_id, report_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
||||||
user_id BIGINT,
|
user_id BIGINT,
|
||||||
incident_id BIGINT,
|
incident_id BIGINT,
|
||||||
|
report_id BIGINT,
|
||||||
|
|
||||||
-- ban, warning
|
-- ban, warning
|
||||||
type ASCII,
|
type ASCII,
|
||||||
|
|
||||||
call_id BIGINT,
|
message FROZEN<payphone.message>,
|
||||||
report_id BIGINT,
|
|
||||||
|
|
||||||
message_id BIGINT,
|
|
||||||
message_content TEXT,
|
|
||||||
message_attachments LIST<TEXT>,
|
|
||||||
message_sticker BIGINT,
|
|
||||||
|
|
||||||
message_replying_to_id BIGINT,
|
|
||||||
message_replying_to_content TEXT,
|
|
||||||
message_replying_to_attachments LIST<TEXT>,
|
|
||||||
message_replying_to_sticker BIGINT,
|
|
||||||
|
|
||||||
reason TEXT,
|
reason TEXT,
|
||||||
|
|
||||||
@@ -150,7 +144,7 @@ CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
|||||||
expires_at TIMESTAMP,
|
expires_at TIMESTAMP,
|
||||||
duration ASCII,
|
duration ASCII,
|
||||||
|
|
||||||
PRIMARY KEY (user_id)
|
PRIMARY KEY (user_id, incident_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payphone.server_incidents (
|
CREATE TABLE IF NOT EXISTS payphone.server_incidents (
|
||||||
@@ -164,4 +158,4 @@ CREATE TABLE IF NOT EXISTS payphone.server_incidents (
|
|||||||
created_at TIMESTAMP,
|
created_at TIMESTAMP,
|
||||||
expires_at TIMESTAMP,
|
expires_at TIMESTAMP,
|
||||||
duration ASCII
|
duration ASCII
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE payphone.users ADD is_banned_from_leaderboard BOOLEAN;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.users_by_name AS
|
||||||
|
SELECT user_id, name FROM payphone.users
|
||||||
|
WHERE user_id IS NOT NULL AND name IS NOT NULL
|
||||||
|
PRIMARY KEY (name, user_id);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE payphone.participants ADD is_masked BOOLEAN;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.participants_by_user_id AS
|
||||||
|
SELECT user_id, call_id, channel_id FROM payphone.participants
|
||||||
|
WHERE user_id IS NOT NULL AND call_id IS NOT NULL AND channel_id IS NOT NULL
|
||||||
|
PRIMARY KEY (user_id, call_id, channel_id);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.messages_by_author_id AS
|
||||||
|
SELECT call_id, message_id, author_id FROM payphone.messages
|
||||||
|
WHERE call_id IS NOT NULL AND message_id IS NOT NULL AND author_id IS NOT NULL
|
||||||
|
PRIMARY KEY (call_id, author_id, message_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE payphone.users ADD findings SET<TEXT>;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE payphone.users ADD avatar_hash TEXT;
|
||||||
|
ALTER TABLE payphone.users ADD color INT;
|
||||||
|
ALTER TABLE payphone.users ADD is_color_set BOOLEAN;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW payphone.users_with_no_color AS
|
||||||
|
SELECT user_id, avatar_hash, is_color_set FROM payphone.users
|
||||||
|
WHERE user_id IS NOT NULL AND is_color_set IS NOT NULL
|
||||||
|
PRIMARY KEY (is_color_set, user_id);
|
||||||
+24
-11
@@ -1,10 +1,9 @@
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
|
|
||||||
use include_dir::{Dir, include_dir};
|
use include_dir::{Dir, include_dir};
|
||||||
use scylla::{
|
use scylla::client::session::Session;
|
||||||
client::{session::Session, session_builder::SessionBuilder},
|
use scylla::client::session_builder::SessionBuilder;
|
||||||
errors::ExecutionError,
|
use scylla::errors::ExecutionError;
|
||||||
};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
@@ -22,13 +21,27 @@ pub enum DatabaseError {
|
|||||||
|
|
||||||
static MIGRATIONS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/database/migrations");
|
static MIGRATIONS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/database/migrations");
|
||||||
|
|
||||||
pub async fn init(uri: impl ToString) -> Result<Session, DatabaseError> {
|
pub async fn init(
|
||||||
let session = SessionBuilder::new()
|
uri: impl ToString,
|
||||||
.known_node(uri.to_string())
|
username: Option<String>,
|
||||||
.build()
|
password: Option<String>,
|
||||||
.await
|
) -> Result<Session, DatabaseError> {
|
||||||
.inspect_err(|e| log::error!("Could not connect to the database! {e:?}"))
|
let session = if let Some((username, password)) = username.zip(password) {
|
||||||
.map_err(|_| DatabaseError::ConnectionFailed)?;
|
SessionBuilder::new()
|
||||||
|
.known_node(uri.to_string())
|
||||||
|
.user(username, password)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Could not connect to the database! {e:?}"))
|
||||||
|
.map_err(|_| DatabaseError::ConnectionFailed)?
|
||||||
|
} else {
|
||||||
|
SessionBuilder::new()
|
||||||
|
.known_node(uri.to_string())
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Could not connect to the database! {e:?}"))
|
||||||
|
.map_err(|_| DatabaseError::ConnectionFailed)?
|
||||||
|
};
|
||||||
|
|
||||||
execute_file(
|
execute_file(
|
||||||
&session,
|
&session,
|
||||||
|
|||||||
+1293
-135
File diff suppressed because it is too large
Load Diff
+36
-2
@@ -1,11 +1,35 @@
|
|||||||
|
//! Utilities for handling Discord's image hashes.
|
||||||
|
|
||||||
use twilight_model::util::ImageHash;
|
use twilight_model::util::ImageHash;
|
||||||
|
|
||||||
pub fn get_avatar_url(hash: Option<ImageHash>, user_id: u64, discriminator: u64) -> String {
|
/// Returns the URL of a user's avatar.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `hash` - The user's avatar hash (if available).
|
||||||
|
/// * `user_id` - The user's ID.
|
||||||
|
/// * `discriminator` - The user's discriminator.
|
||||||
|
/// * `guild_id` - The guild ID, **only set if the user has Nitro and a custom avatar for the
|
||||||
|
/// server. Otherwise, even in a server, this is NOT SET**.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`String`] - The URL of the user's avatar.
|
||||||
|
pub fn get_avatar_url(
|
||||||
|
hash: Option<ImageHash>,
|
||||||
|
user_id: u64,
|
||||||
|
discriminator: u64,
|
||||||
|
guild_id: Option<u64>,
|
||||||
|
) -> String {
|
||||||
if let Some(hash) = hash {
|
if let Some(hash) = hash {
|
||||||
|
if let Some(guild_id) = guild_id {
|
||||||
|
return format!(
|
||||||
|
"https://cdn.discordapp.com/guilds/{guild_id}/users/{user_id}/avatars/{hash}.webp?size=256"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
format!("https://cdn.discordapp.com/avatars/{user_id}/{hash}.webp?size=256")
|
format!("https://cdn.discordapp.com/avatars/{user_id}/{hash}.webp?size=256")
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"https://cdn.discordapp.com/embed/avatars/{}.png?size=256",
|
"https://cdn.discordapp.com/embed/avatars/{}.webp?size=256",
|
||||||
match discriminator.to_string().as_str() {
|
match discriminator.to_string().as_str() {
|
||||||
"0" => (user_id >> 22) % 6,
|
"0" => (user_id >> 22) % 6,
|
||||||
_ => discriminator % 6,
|
_ => discriminator % 6,
|
||||||
@@ -13,3 +37,13 @@ pub fn get_avatar_url(hash: Option<ImageHash>, user_id: u64, discriminator: u64)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the user's avatar URL only if it's custom (i.e. not the default avatar). It doesn't
|
||||||
|
/// take into account custom per-server avatars, i.e. only the global avatar is returned if any.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `user_id` - The user's ID.
|
||||||
|
/// * `hash` - The hex-encoded hash string, if any.
|
||||||
|
pub fn get_user_custom_avatar_url_or_none(user_id: u64, hash: String) -> String {
|
||||||
|
format!("https://cdn.discordapp.com/avatars/{user_id}/{hash}.webp?size=256")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
|
//! A set of utilities to interact with Discord not included in the main framework.
|
||||||
|
//!
|
||||||
|
//! It currently includes a markdown escaper and a utility to get image URLs from Discord's image
|
||||||
|
//! hashes.
|
||||||
|
|
||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod sanitizing;
|
pub mod sanitizing;
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
|
//! Utilities for sanitizing Discord messages (removing markdown from user input).
|
||||||
|
|
||||||
|
/// Escapes Discord markdown in a string.
|
||||||
pub trait SanitizeDiscordMarkdown {
|
pub trait SanitizeDiscordMarkdown {
|
||||||
|
/// Escapes Discord markdown in a string.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `hard` - If `false`, it'll only escape mentions and pings. Otherwise, it'll also escape
|
||||||
|
/// generic markdown like headers and code blocks.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`String`] - The sanitized string.
|
||||||
fn sanitize(&self, hard: bool) -> String;
|
fn sanitize(&self, hard: bool) -> String;
|
||||||
|
|
||||||
|
/// Sanitizes mentions in a string.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`String`] - The sanitized string.
|
||||||
|
fn sanitize_mentions(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SanitizeDiscordMarkdown for T
|
impl<T> SanitizeDiscordMarkdown for T
|
||||||
@@ -14,7 +31,8 @@ where
|
|||||||
|
|
||||||
for ch in input.chars() {
|
for ch in input.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' => {
|
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' | '[' | ']' | '/'
|
||||||
|
| '(' | ')' => {
|
||||||
escaped.push('\\');
|
escaped.push('\\');
|
||||||
escaped.push(ch);
|
escaped.push(ch);
|
||||||
}
|
}
|
||||||
@@ -28,7 +46,13 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Now block ping attempts — break mentions
|
// Now block ping attempts — break mentions
|
||||||
escaped
|
escaped.sanitize_mentions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_mentions(&self) -> String {
|
||||||
|
let input = self.to_string();
|
||||||
|
|
||||||
|
input
|
||||||
.replace("@everyone", "@\u{200B}everyone")
|
.replace("@everyone", "@\u{200B}everyone")
|
||||||
.replace("@here", "@\u{200B}here")
|
.replace("@here", "@\u{200B}here")
|
||||||
.replace("<@", "<@\u{200B}") // user mentions
|
.replace("<@", "<@\u{200B}") // user mentions
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use crate::database::models::User;
|
||||||
|
|
||||||
|
pub struct Extras {
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
+344
-172
@@ -1,192 +1,251 @@
|
|||||||
use std::{fmt::Debug, sync::Arc};
|
use std::fmt::Debug;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
use topgg::Stats;
|
||||||
|
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
|
||||||
|
use twilight_gateway::{ConfigBuilder, Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
||||||
use twilight_http::Client;
|
use twilight_http::Client;
|
||||||
|
use twilight_model::gateway::payload::outgoing::update_presence::UpdatePresencePayload;
|
||||||
|
|
||||||
use crate::{
|
use crate::framework::commands::Command;
|
||||||
framework::{
|
use crate::framework::commands::extras::ExtrasGetter;
|
||||||
commands::{Command, runner::CommandRunner},
|
use crate::framework::commands::runner::CommandRunner;
|
||||||
events::traits::{
|
use crate::framework::events::traits::{
|
||||||
OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
|
OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
|
||||||
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
|
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
|
||||||
OnServerUpdateHandler,
|
OnServerUpdateHandler,
|
||||||
},
|
|
||||||
interactions::{components::Component, registry::ComponentRegistry},
|
|
||||||
runtime::Runtime,
|
|
||||||
},
|
|
||||||
utils::Bye,
|
|
||||||
};
|
};
|
||||||
|
use crate::framework::interactions::components::Component;
|
||||||
|
use crate::framework::interactions::registry::ComponentRegistry;
|
||||||
|
use crate::framework::interactions::runner::InteractionRunner;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
use crate::framework::tasks::Task;
|
||||||
|
use crate::utils::Bye;
|
||||||
|
|
||||||
|
/// Errors occurred while attempting to run the bot.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RuntimeError {
|
pub enum RuntimeError {
|
||||||
#[error("The application's details could not be fetched")]
|
#[error("The application's details could not be fetched")]
|
||||||
ApplicationFetchingFailed,
|
ApplicationFetchingFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Getter trait for command prefixes.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait IntoPrefixes<T> {
|
pub trait PrefixGetter<T, E> {
|
||||||
|
/// Get the command prefixes for the given server and user IDs.
|
||||||
|
///
|
||||||
|
/// The getter must never fail. In case an internal process does, it must fall back to default
|
||||||
|
/// prefixes.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `runtime` - The application [`Runtime`].
|
||||||
|
/// * `server_id` - The server ID in which the command, interaction, or event is running.
|
||||||
|
/// * `user_id` - The user ID who triggered the command, interaction, or event.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Vec<String>`] - A list of command prefixes. When matching for commands, all these
|
||||||
|
/// prefixes should be checked against, as they're all valid. In case one should be picked, the
|
||||||
|
/// first one will always be the safest choice.
|
||||||
async fn get(
|
async fn get(
|
||||||
&self,
|
&self,
|
||||||
runtime: &Runtime<T>,
|
runtime: &Runtime<T, E>,
|
||||||
server_id: Option<u64>,
|
server_id: Option<u64>,
|
||||||
user_id: Option<u64>,
|
user_id: Option<u64>,
|
||||||
) -> Vec<String>;
|
) -> Vec<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T> IntoPrefixes<T> for String {
|
impl<T, E> PrefixGetter<T, E> for String {
|
||||||
async fn get(
|
async fn get(&self, _: &Runtime<T, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||||
&self,
|
|
||||||
_: &Runtime<T>,
|
|
||||||
_: Option<u64>,
|
|
||||||
_: Option<u64>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
vec![self.to_string()]
|
vec![self.to_string()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T> IntoPrefixes<T> for &str {
|
impl<T, E> PrefixGetter<T, E> for &str {
|
||||||
async fn get(
|
async fn get(&self, _: &Runtime<T, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||||
&self,
|
|
||||||
_: &Runtime<T>,
|
|
||||||
_: Option<u64>,
|
|
||||||
_: Option<u64>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
vec![self.to_string()]
|
vec![self.to_string()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Send + Sync + 'static, const N: usize, S: Send + Sync + 'static> IntoPrefixes<S> for [T; N]
|
impl<T, const N: usize, S, E> PrefixGetter<S, E> for [T; N]
|
||||||
where
|
where
|
||||||
T: ToString,
|
T: ToString + Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
async fn get(
|
async fn get(&self, _: &Runtime<S, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||||
&self,
|
|
||||||
_: &Runtime<S>,
|
|
||||||
_: Option<u64>,
|
|
||||||
_: Option<u64>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
self.iter().map(|s| s.to_string()).collect()
|
self.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<F, Fut, T> IntoPrefixes<T> for F
|
impl<F, Fut, T, E> PrefixGetter<T, E> for F
|
||||||
where
|
where
|
||||||
F: Fn() -> Fut + Send + Sync + 'static,
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = Vec<String>> + Send,
|
Fut: Future<Output = Vec<String>> + Send,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
async fn get(
|
async fn get(&self, _: &Runtime<T, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||||
&self,
|
|
||||||
_: &Runtime<T>,
|
|
||||||
_: Option<u64>,
|
|
||||||
_: Option<u64>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
(self)().await
|
(self)().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Bot<T: Send + Sync = ()> {
|
pub struct Bot<S: Send + Sync, E: Send + Sync = ()> {
|
||||||
state: Arc<T>,
|
shard: ShardId,
|
||||||
prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync>,
|
state: Arc<S>,
|
||||||
|
presence: Option<UpdatePresencePayload>,
|
||||||
|
prefixes: Arc<dyn PrefixGetter<S, E> + Send + Sync>,
|
||||||
intents: Intents,
|
intents: Intents,
|
||||||
commands: Vec<Arc<dyn Command<T>>>,
|
extras: Arc<dyn ExtrasGetter<S, Extras = E>>,
|
||||||
components: Vec<Arc<dyn Component<T>>>,
|
cached_resource_types: Vec<ResourceType>,
|
||||||
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T>>>,
|
commands: Vec<Arc<dyn Command<S, E>>>,
|
||||||
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T>>>,
|
components: Vec<Arc<dyn Component<S, E>>>,
|
||||||
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T>>>,
|
tasks: Vec<Arc<dyn Task<S, E>>>,
|
||||||
on_message_handlers: Vec<Arc<dyn OnMessageHandler<T>>>,
|
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<S, E>>>,
|
||||||
on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<T>>>,
|
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<S, E>>>,
|
||||||
on_message_delete_handlers: Vec<Arc<dyn OnMessageDeleteHandler<T>>>,
|
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<S, E>>>,
|
||||||
on_server_create_handlers: Vec<Arc<dyn OnServerCreateHandler<T>>>,
|
on_message_handlers: Vec<Arc<dyn OnMessageHandler<S, E>>>,
|
||||||
on_server_update_handlers: Vec<Arc<dyn OnServerUpdateHandler<T>>>,
|
on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<S, E>>>,
|
||||||
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<T>>>,
|
on_message_delete_handlers: Vec<Arc<dyn OnMessageDeleteHandler<S, E>>>,
|
||||||
|
on_server_create_handlers: Vec<Arc<dyn OnServerCreateHandler<S, E>>>,
|
||||||
|
on_server_update_handlers: Vec<Arc<dyn OnServerUpdateHandler<S, E>>>,
|
||||||
|
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<S, E>>>,
|
||||||
|
topgg_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static + Send + Sync> Bot<T> {
|
impl<T, E> Bot<T, E>
|
||||||
pub fn new(state: T, prefixes: impl IntoPrefixes<T> + Send + Sync + 'static) -> Self {
|
where
|
||||||
|
T: 'static + Send + Sync,
|
||||||
|
E: 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
state: Arc<T>,
|
||||||
|
prefixes: impl PrefixGetter<T, E> + Send + Sync + 'static,
|
||||||
|
extras: impl ExtrasGetter<T, Extras = E> + 'static,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: Arc::new(state),
|
shard: ShardId::new(0, 1),
|
||||||
|
state,
|
||||||
|
presence: None,
|
||||||
prefixes: Arc::new(prefixes),
|
prefixes: Arc::new(prefixes),
|
||||||
intents: Intents::empty(),
|
intents: Intents::empty(),
|
||||||
|
extras: Arc::new(extras),
|
||||||
|
cached_resource_types: Vec::new(),
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
components: Vec::new(),
|
components: Vec::new(),
|
||||||
|
tasks: Vec::new(),
|
||||||
on_ready_handlers: Vec::new(),
|
on_ready_handlers: Vec::new(),
|
||||||
on_reconnect_handlers: Vec::new(),
|
on_reconnect_handlers: Vec::new(),
|
||||||
on_interaction_handlers: Vec::new(),
|
on_interaction_handlers: vec![Arc::new(InteractionRunner)],
|
||||||
on_message_handlers: vec![Arc::new(CommandRunner)],
|
on_message_handlers: vec![Arc::new(CommandRunner)],
|
||||||
on_message_edit_handlers: Vec::new(),
|
on_message_edit_handlers: Vec::new(),
|
||||||
on_message_delete_handlers: Vec::new(),
|
on_message_delete_handlers: Vec::new(),
|
||||||
on_server_create_handlers: Vec::new(),
|
on_server_create_handlers: Vec::new(),
|
||||||
on_server_update_handlers: Vec::new(),
|
on_server_update_handlers: Vec::new(),
|
||||||
on_server_delete_handlers: Vec::new(),
|
on_server_delete_handlers: Vec::new(),
|
||||||
|
topgg_token: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn shard(mut self, shard: ShardId) -> Self {
|
||||||
|
self.shard = shard;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn intent(mut self, intent: Intents) -> Self {
|
pub fn intent(mut self, intent: Intents) -> Self {
|
||||||
self.intents.insert(intent);
|
self.intents.insert(intent);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn command(mut self, command: impl Command<T> + 'static) -> Self {
|
pub fn presence(mut self, presence: UpdatePresencePayload) -> Self {
|
||||||
|
self.presence = Some(presence);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command(mut self, command: impl Command<T, E> + 'static) -> Self {
|
||||||
self.commands.push(Arc::new(command));
|
self.commands.push(Arc::new(command));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn component(mut self, component: impl Component<T> + 'static) -> Self {
|
pub fn component(mut self, component: impl Component<T, E> + 'static) -> Self {
|
||||||
self.components.push(Arc::new(component));
|
self.components.push(Arc::new(component));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_ready(mut self, handler: impl OnReadyHandler<T> + 'static) -> Self {
|
#[allow(dead_code)]
|
||||||
|
pub fn task(mut self, task: impl Task<T, E> + 'static) -> Self {
|
||||||
|
self.tasks.push(Arc::new(task));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_resource_type(mut self, resource_type: ResourceType) -> Self {
|
||||||
|
if !self.cached_resource_types.contains(&resource_type) {
|
||||||
|
self.cached_resource_types.push(resource_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_ready(mut self, handler: impl OnReadyHandler<T, E> + 'static) -> Self {
|
||||||
self.on_ready_handlers.push(Arc::new(handler));
|
self.on_ready_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_reconnect(mut self, handler: impl OnReconnectHandler<T> + 'static) -> Self {
|
pub fn on_reconnect(mut self, handler: impl OnReconnectHandler<T, E> + 'static) -> Self {
|
||||||
self.on_reconnect_handlers.push(Arc::new(handler));
|
self.on_reconnect_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_interaction(mut self, handler: impl OnInteractionHandler<T> + 'static) -> Self {
|
#[allow(dead_code)]
|
||||||
|
pub fn on_interaction(mut self, handler: impl OnInteractionHandler<T, E> + 'static) -> Self {
|
||||||
self.on_interaction_handlers.push(Arc::new(handler));
|
self.on_interaction_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_message(mut self, handler: impl OnMessageHandler<T> + 'static) -> Self {
|
pub fn on_message(mut self, handler: impl OnMessageHandler<T, E> + 'static) -> Self {
|
||||||
self.on_message_handlers.push(Arc::new(handler));
|
self.on_message_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_message_edit(mut self, handler: impl OnMessageEditHandler<T> + 'static) -> Self {
|
#[allow(dead_code)]
|
||||||
|
pub fn on_message_edit(mut self, handler: impl OnMessageEditHandler<T, E> + 'static) -> Self {
|
||||||
self.on_message_edit_handlers.push(Arc::new(handler));
|
self.on_message_edit_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_message_delete(mut self, handler: impl OnMessageDeleteHandler<T> + 'static) -> Self {
|
#[allow(dead_code)]
|
||||||
|
pub fn on_message_delete(
|
||||||
|
mut self,
|
||||||
|
handler: impl OnMessageDeleteHandler<T, E> + 'static,
|
||||||
|
) -> Self {
|
||||||
self.on_message_delete_handlers.push(Arc::new(handler));
|
self.on_message_delete_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_server_create(mut self, handler: impl OnServerCreateHandler<T> + 'static) -> Self {
|
pub fn on_server_create(mut self, handler: impl OnServerCreateHandler<T, E> + 'static) -> Self {
|
||||||
self.on_server_create_handlers.push(Arc::new(handler));
|
self.on_server_create_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_server_update(mut self, handler: impl OnServerUpdateHandler<T> + 'static) -> Self {
|
pub fn on_server_update(mut self, handler: impl OnServerUpdateHandler<T, E> + 'static) -> Self {
|
||||||
self.on_server_update_handlers.push(Arc::new(handler));
|
self.on_server_update_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_server_delete(mut self, handler: impl OnServerDeleteHandler<T> + 'static) -> Self {
|
pub fn on_server_delete(mut self, handler: impl OnServerDeleteHandler<T, E> + 'static) -> Self {
|
||||||
self.on_server_delete_handlers.push(Arc::new(handler));
|
self.on_server_delete_handlers.push(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn topgg_token(mut self, token: impl ToString) -> Self {
|
||||||
|
self.topgg_token = Some(token.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run(self, token: &str) -> Result<(), RuntimeError> {
|
pub async fn run(self, token: &str) -> Result<(), RuntimeError> {
|
||||||
let http = Client::new(token.to_string());
|
let http = Client::new(token.to_string());
|
||||||
let app = http
|
let app = http
|
||||||
@@ -198,6 +257,60 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
|||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
|
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
|
||||||
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?;
|
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?;
|
||||||
|
|
||||||
|
if let Some(topgg_token) = &self.topgg_token
|
||||||
|
&& self.shard.number() == 0
|
||||||
|
{
|
||||||
|
let topgg_token = topgg_token.clone();
|
||||||
|
let http = Client::new(token.to_string());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let topgg_client = topgg::Client::new(topgg_token);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let app = match http
|
||||||
|
.current_user_application()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RuntimeError::ApplicationFetchingFailed)
|
||||||
|
{
|
||||||
|
Ok(v) => {
|
||||||
|
match v
|
||||||
|
.model()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RuntimeError::ApplicationFetchingFailed)
|
||||||
|
{
|
||||||
|
Ok(app) => app,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"Could not parse application data, skipping stats post to Top.gg! {err:?}"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"Could not fetch application data, skipping stats post to Top.gg! {err:?}"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
topgg_client
|
||||||
|
.post_stats(Stats::from_count(
|
||||||
|
app.approximate_guild_count.unwrap() as usize,
|
||||||
|
Some(self.shard.total() as usize),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.inspect_err(|err| {
|
||||||
|
log::warn!("Could not report bot stats to Top.gg: {err}")
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(120)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let registry = ComponentRegistry::new();
|
let registry = ComponentRegistry::new();
|
||||||
|
|
||||||
for component in &self.components {
|
for component in &self.components {
|
||||||
@@ -211,6 +324,18 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
|||||||
|
|
||||||
let bot = Arc::new(self);
|
let bot = Arc::new(self);
|
||||||
|
|
||||||
|
let mut resource_types = ResourceType::empty();
|
||||||
|
|
||||||
|
for resource_type in &bot.cached_resource_types {
|
||||||
|
resource_types.insert(*resource_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = DefaultInMemoryCache::builder()
|
||||||
|
.resource_types(resource_types)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let (gateway_tx, mut gateway_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
let runtime = Arc::new(Runtime {
|
let runtime = Arc::new(Runtime {
|
||||||
http,
|
http,
|
||||||
app,
|
app,
|
||||||
@@ -218,112 +343,159 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
|||||||
state: bot.state.clone(),
|
state: bot.state.clone(),
|
||||||
prefixes: bot.prefixes.clone(),
|
prefixes: bot.prefixes.clone(),
|
||||||
commands: bot.commands.clone(),
|
commands: bot.commands.clone(),
|
||||||
|
cache,
|
||||||
|
gateway_tx,
|
||||||
|
shard_number: bot.shard.number(),
|
||||||
|
shard_total: bot.shard.total(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut gateway = Shard::new(ShardId::new(0, 1), token.to_string(), bot.intents);
|
for task in bot.tasks.clone() {
|
||||||
|
let task_rt = runtime.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
task.run(&task_rt)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("An error occurred while running task {task:?}! {e:?}")
|
||||||
|
})
|
||||||
|
.bye();
|
||||||
|
|
||||||
|
tokio::time::sleep(task.delay()).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut config = ConfigBuilder::new(token.to_string(), bot.intents);
|
||||||
|
|
||||||
|
if let Some(presence) = &bot.presence {
|
||||||
|
config = config.presence(presence.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = config.build();
|
||||||
|
|
||||||
|
let mut gateway = Shard::with_config(bot.shard, config);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await {
|
tokio::select! {
|
||||||
let task_rt = runtime.clone();
|
Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()) => {
|
||||||
let task_bot = bot.clone();
|
let task_rt = runtime.clone();
|
||||||
|
let task_bot = bot.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
match event {
|
task_rt.cache.update(&event);
|
||||||
Event::Ready(event) => {
|
|
||||||
for handler in &task_bot.on_ready_handlers {
|
|
||||||
handler
|
|
||||||
.on_ready(&event, task_rt.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on ready for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::GatewayReconnect => {
|
|
||||||
for handler in &task_bot.on_reconnect_handlers {
|
|
||||||
handler
|
|
||||||
.on_reconnect(task_rt.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on reconnect for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::InteractionCreate(interaction) => {
|
|
||||||
for handler in &task_bot.on_interaction_handlers {
|
|
||||||
handler
|
|
||||||
.on_interaction(&interaction, task_rt.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on interaction for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::MessageCreate(message) => {
|
|
||||||
for handler in &task_bot.on_message_handlers {
|
|
||||||
log::trace!("Running {handler:?}");
|
|
||||||
|
|
||||||
handler
|
match event {
|
||||||
.on_message(&message, task_rt.clone())
|
Event::Ready(event) => {
|
||||||
.await
|
for handler in &task_bot.on_ready_handlers {
|
||||||
.inspect_err(
|
handler
|
||||||
|e| log::error!("An error occurred while handling on message for {handler:?}! {e:?}")
|
.on_ready(&event, task_rt.clone())
|
||||||
)
|
.await
|
||||||
.bye();
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on ready for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GatewayReconnect => {
|
||||||
|
for handler in &task_bot.on_reconnect_handlers {
|
||||||
|
handler
|
||||||
|
.on_reconnect(task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on reconnect for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::InteractionCreate(interaction) => {
|
||||||
|
for handler in &task_bot.on_interaction_handlers {
|
||||||
|
handler
|
||||||
|
.on_interaction(&interaction, task_rt.clone(), task_bot.extras.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on interaction for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MessageCreate(message) => {
|
||||||
|
for handler in &task_bot.on_message_handlers {
|
||||||
|
log::trace!("Running {handler:?}");
|
||||||
|
|
||||||
|
handler
|
||||||
|
.on_message(&message, task_rt.clone(), task_bot.extras.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on message for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MessageUpdate(message) => {
|
||||||
|
for handler in &task_bot.on_message_edit_handlers {
|
||||||
|
handler
|
||||||
|
.on_message_edit(&message, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on message edit for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MessageDelete(message) => {
|
||||||
|
for handler in &task_bot.on_message_delete_handlers {
|
||||||
|
handler
|
||||||
|
.on_message_delete(&message, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on message delete for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GuildCreate(guild) => {
|
||||||
|
for handler in &task_bot.on_server_create_handlers {
|
||||||
|
handler
|
||||||
|
.on_server_create(&guild, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on server create for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GuildUpdate(guild) => {
|
||||||
|
for handler in &task_bot.on_server_update_handlers {
|
||||||
|
handler
|
||||||
|
.on_server_update(&guild, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on server update for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::GuildDelete(guild) => {
|
||||||
|
for handler in &task_bot.on_server_delete_handlers {
|
||||||
|
handler
|
||||||
|
.on_server_delete(&guild, task_rt.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(
|
||||||
|
|e| log::error!("An error occurred while handling on server delete for {handler:?}! {e:?}")
|
||||||
|
)
|
||||||
|
.bye();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::trace!("Unhandled event {:?} received", event.kind());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::MessageUpdate(message) => {
|
});
|
||||||
for handler in &task_bot.on_message_edit_handlers {
|
}
|
||||||
handler
|
Some(json) = gateway_rx.recv() => {
|
||||||
.on_message_edit(&message, task_rt.clone())
|
gateway.command(&json);
|
||||||
.await
|
}
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on message edit for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::MessageDelete(message) => {
|
|
||||||
for handler in &task_bot.on_message_delete_handlers {
|
|
||||||
handler
|
|
||||||
.on_message_delete(&message, task_rt.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on message delete for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::GuildCreate(guild) => {
|
|
||||||
for handler in &task_bot.on_server_create_handlers {
|
|
||||||
handler
|
|
||||||
.on_server_create(&guild, task_rt.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on server create for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::GuildDelete(guild) => {
|
|
||||||
for handler in &task_bot.on_server_delete_handlers {
|
|
||||||
handler
|
|
||||||
.on_server_delete(&guild, task_rt.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(
|
|
||||||
|e| log::error!("An error occurred while handling on server delete for {handler:?}! {e:?}")
|
|
||||||
)
|
|
||||||
.bye();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::trace!("Unhandled event {:?} received", event.kind());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::{
|
use std::fmt;
|
||||||
fmt,
|
use std::ops::{Deref, DerefMut};
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::database::models::DatabaseError;
|
||||||
|
use crate::framework::commands::context::CommandContext;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ArgumentParsingError {
|
pub enum ArgumentParsingError {
|
||||||
#[error("This argument was missing in the arguments array")]
|
#[error("This argument was missing in the arguments array")]
|
||||||
@@ -12,14 +13,23 @@ pub enum ArgumentParsingError {
|
|||||||
|
|
||||||
#[error("An argument was there but it could not be parsed into the specified value")]
|
#[error("An argument was there but it could not be parsed into the specified value")]
|
||||||
InvalidArgument,
|
InvalidArgument,
|
||||||
|
|
||||||
|
#[error("A database operation failed")]
|
||||||
|
DatabaseFailed(#[from] DatabaseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FromArguments: Sized {
|
pub trait FromArguments<S, E>: Sized {
|
||||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError>;
|
async fn from_args<'a>(
|
||||||
|
args: &'a str,
|
||||||
|
ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromArguments for String {
|
impl<S, E> FromArguments<S, E> for String {
|
||||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
async fn from_args<'a>(
|
||||||
|
args: &'a str,
|
||||||
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
if !args.is_empty() {
|
if !args.is_empty() {
|
||||||
match args.split_once(" ") {
|
match args.split_once(" ") {
|
||||||
Some((arg, rest)) => Ok((arg.to_string(), rest)),
|
Some((arg, rest)) => Ok((arg.to_string(), rest)),
|
||||||
@@ -31,10 +41,17 @@ impl FromArguments for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consumes the entire remaining string as a single argument. If no string is remaining, it
|
||||||
|
/// returns an empty string.
|
||||||
|
///
|
||||||
|
/// Parsing this argument type never fails.
|
||||||
pub struct GreedyString(pub String);
|
pub struct GreedyString(pub String);
|
||||||
|
|
||||||
impl FromArguments for GreedyString {
|
impl<S, E> FromArguments<S, E> for GreedyString {
|
||||||
fn from_args<'a>(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
async fn from_args<'a>(
|
||||||
|
args: &'a str,
|
||||||
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
let value = GreedyString(args.to_string());
|
let value = GreedyString(args.to_string());
|
||||||
|
|
||||||
Ok((value, ""))
|
Ok((value, ""))
|
||||||
@@ -83,9 +100,12 @@ impl fmt::Display for NeedyString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromArguments for NeedyString {
|
impl<S, E> FromArguments<S, E> for NeedyString {
|
||||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
async fn from_args<'a>(
|
||||||
if !args.is_empty() {
|
args: &'a str,
|
||||||
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
|
if !args.trim().is_empty() {
|
||||||
match args.split_once(" ") {
|
match args.split_once(" ") {
|
||||||
Some((arg, rest)) => Ok((Self(arg.to_string()), rest)),
|
Some((arg, rest)) => Ok((Self(arg.to_string()), rest)),
|
||||||
None => Ok((Self(args.to_string()), "")),
|
None => Ok((Self(args.to_string()), "")),
|
||||||
@@ -118,18 +138,24 @@ impl fmt::Display for NeedyGreedyString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromArguments for NeedyGreedyString {
|
impl<S, E> FromArguments<S, E> for NeedyGreedyString {
|
||||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
async fn from_args<'a>(
|
||||||
if !args.is_empty() {
|
args: &'a str,
|
||||||
Ok((Self(args.to_string()), ""))
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
|
if !args.trim().is_empty() {
|
||||||
|
Ok((Self(args.trim().to_string()), ""))
|
||||||
} else {
|
} else {
|
||||||
Err(ArgumentParsingError::MissingArgument)
|
Err(ArgumentParsingError::MissingArgument)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromArguments for bool {
|
impl<S, E> FromArguments<S, E> for bool {
|
||||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
async fn from_args<'a>(
|
||||||
|
args: &'a str,
|
||||||
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
let arg: &str;
|
let arg: &str;
|
||||||
let rest: &str;
|
let rest: &str;
|
||||||
|
|
||||||
@@ -144,7 +170,7 @@ impl FromArguments for bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match arg {
|
match arg.to_lowercase().as_str() {
|
||||||
"y" | "yes" | "on" | "true" | "t" | "1" => Ok((true, rest)),
|
"y" | "yes" | "on" | "true" | "t" | "1" => Ok((true, rest)),
|
||||||
"n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)),
|
"n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)),
|
||||||
_ => Err(ArgumentParsingError::InvalidArgument),
|
_ => Err(ArgumentParsingError::InvalidArgument),
|
||||||
@@ -152,73 +178,98 @@ impl FromArguments for bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> FromArguments for Option<T>
|
impl<T, S, E> FromArguments<S, E> for Option<T>
|
||||||
where
|
where
|
||||||
T: FromArguments,
|
T: FromArguments<S, E>,
|
||||||
{
|
{
|
||||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
async fn from_args<'a>(
|
||||||
match T::from_args(args) {
|
args: &'a str,
|
||||||
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
|
match T::from_args(args, _ctx).await {
|
||||||
Ok((result, rest)) => Ok((Some(result), rest)),
|
Ok((result, rest)) => Ok((Some(result), rest)),
|
||||||
Err(_) => Ok((None, args)),
|
Err(_) => Ok((None, args)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ParseArguments: Sized {
|
pub struct Arg<T>(pub T);
|
||||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParseArguments for () {
|
impl<T> Deref for Arg<T> {
|
||||||
fn parse(_: &str) -> Result<Self, ArgumentParsingError> {
|
type Target = T;
|
||||||
Ok(())
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A> ParseArguments for A
|
impl<T> DerefMut for Arg<T> {
|
||||||
where
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
A: FromArguments,
|
&mut self.0
|
||||||
{
|
|
||||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
|
||||||
let (a, _) = A::from_args(args)?;
|
|
||||||
|
|
||||||
Ok(a)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A> ParseArguments for (A,)
|
pub trait ParseArguments<S, E>: Sized {
|
||||||
|
async fn parse(args: &str, ctx: &CommandContext<S, E>) -> Result<Self, ArgumentParsingError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, S, E> ParseArguments<S, E> for Arg<T>
|
||||||
where
|
where
|
||||||
A: FromArguments,
|
T: FromArguments<S, E>,
|
||||||
{
|
{
|
||||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
async fn parse(
|
||||||
let (a, _) = A::from_args(args)?;
|
args: &str,
|
||||||
|
ctx: &CommandContext<'_, S, E>,
|
||||||
|
) -> Result<Self, ArgumentParsingError> {
|
||||||
|
let (value, _) = T::from_args(args, ctx).await?;
|
||||||
|
|
||||||
|
Ok(Arg(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, S, E> ParseArguments<S, E> for (A,)
|
||||||
|
where
|
||||||
|
A: FromArguments<S, E>,
|
||||||
|
{
|
||||||
|
async fn parse(
|
||||||
|
args: &str,
|
||||||
|
ctx: &CommandContext<'_, S, E>,
|
||||||
|
) -> Result<Self, ArgumentParsingError> {
|
||||||
|
let (a, _) = A::from_args(args, ctx).await?;
|
||||||
|
|
||||||
Ok((a,))
|
Ok((a,))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A, B> ParseArguments for (A, B)
|
impl<A, B, S, E> ParseArguments<S, E> for (A, B)
|
||||||
where
|
where
|
||||||
A: FromArguments,
|
A: FromArguments<S, E>,
|
||||||
B: FromArguments,
|
B: FromArguments<S, E>,
|
||||||
{
|
{
|
||||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
async fn parse(
|
||||||
let (a, args) = A::from_args(args)?;
|
args: &str,
|
||||||
let (b, _) = B::from_args(args)?;
|
ctx: &CommandContext<'_, S, E>,
|
||||||
|
) -> Result<Self, ArgumentParsingError> {
|
||||||
|
let (a, args) = A::from_args(args, ctx).await?;
|
||||||
|
let (b, _) = B::from_args(args, ctx).await?;
|
||||||
|
|
||||||
Ok((a, b))
|
Ok((a, b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A, B, C> ParseArguments for (A, B, C)
|
impl<A, B, C, S, E> ParseArguments<S, E> for (A, B, C)
|
||||||
where
|
where
|
||||||
A: FromArguments,
|
A: FromArguments<S, E>,
|
||||||
B: FromArguments,
|
B: FromArguments<S, E>,
|
||||||
C: FromArguments,
|
C: FromArguments<S, E>,
|
||||||
{
|
{
|
||||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
async fn parse(
|
||||||
let (a, args) = A::from_args(args)?;
|
args: &str,
|
||||||
let (b, args) = B::from_args(args)?;
|
ctx: &CommandContext<'_, S, E>,
|
||||||
let (c, _) = C::from_args(args)?;
|
) -> Result<Self, ArgumentParsingError> {
|
||||||
|
let (a, args) = A::from_args(args, ctx).await?;
|
||||||
|
let (b, args) = B::from_args(args, ctx).await?;
|
||||||
|
let (c, _) = C::from_args(args, ctx).await?;
|
||||||
|
|
||||||
Ok((a, b, c))
|
Ok((a, b, c))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,97 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twilight_http::{Error, Response};
|
||||||
|
use twilight_model::channel::Message;
|
||||||
|
use twilight_model::channel::message::Component;
|
||||||
use twilight_model::gateway::payload::incoming::MessageCreate;
|
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||||
|
|
||||||
use crate::framework::{commands::messages::MessageSender, runtime::Runtime};
|
use crate::framework::commands::messages::MessageSender;
|
||||||
|
use crate::framework::interactions::components::ActionRow;
|
||||||
|
use crate::framework::messages::MessageBuilder;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
pub struct CommandContext<'a, T> {
|
pub struct CommandContext<'a, T, E> {
|
||||||
pub event: &'a MessageCreate,
|
pub event: &'a MessageCreate,
|
||||||
pub rt: Arc<Runtime<T>>,
|
pub rt: Arc<Runtime<T, E>>,
|
||||||
pub prefix: &'a str,
|
pub prefix: &'a str,
|
||||||
|
pub extras: E,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> CommandContext<'a, T>
|
impl<'a, T, E> CommandContext<'a, T, E>
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(event: &'a MessageCreate, rt: Arc<Runtime<T>>, prefix: &'a str) -> Self {
|
pub fn new(
|
||||||
Self { event, rt, prefix }
|
event: &'a MessageCreate,
|
||||||
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
prefix: &'a str,
|
||||||
|
extras: E,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
event,
|
||||||
|
rt,
|
||||||
|
prefix,
|
||||||
|
extras,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self) -> MessageSender<T> {
|
pub fn send(&self) -> MessageSender<T, E> {
|
||||||
MessageSender::new(self.event.channel_id, self.rt.clone())
|
MessageSender::new(self.event.channel_id, self.rt.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_from_builder(
|
||||||
|
&self,
|
||||||
|
builder: MessageBuilder<T, E>,
|
||||||
|
) -> Result<Response<Message>, Error> {
|
||||||
|
let mut sender = self.rt.http.create_message(self.event.channel_id);
|
||||||
|
|
||||||
|
if let Some(content) = &builder.content {
|
||||||
|
sender = sender.content(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current_row = ActionRow::new(&[]);
|
||||||
|
let mut current_index = 0;
|
||||||
|
|
||||||
|
let mut rows: Vec<Component> = vec![];
|
||||||
|
let mut custom_ids: Vec<String> = vec![];
|
||||||
|
|
||||||
|
for (component, index) in builder.components {
|
||||||
|
if index != current_index {
|
||||||
|
if !current_row.is_empty() {
|
||||||
|
rows.push(current_row.build());
|
||||||
|
current_row = ActionRow::new(&[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let custom_id = component.custom_id();
|
||||||
|
|
||||||
|
if let Some(custom_id) = &custom_id {
|
||||||
|
custom_ids.push(custom_id.clone());
|
||||||
|
|
||||||
|
self.rt.registry.register(custom_id, component.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
current_row.add(custom_id.clone(), component.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current_row.is_empty() {
|
||||||
|
rows.push(current_row.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
sender = sender.components(&rows);
|
||||||
|
sender = sender.embeds(&builder.embeds);
|
||||||
|
|
||||||
|
let result = sender.await;
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
for custom_id in custom_ids {
|
||||||
|
self.rt.registry.unregister(custom_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use twilight_model::user::User;
|
||||||
|
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ExtrasGetter<S>: Send + Sync {
|
||||||
|
type Extras;
|
||||||
|
|
||||||
|
async fn get_extra(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
rt: &Runtime<S, Self::Extras>,
|
||||||
|
) -> Result<Self::Extras, Box<dyn Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DefaultExtraGetter;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> ExtrasGetter<S> for DefaultExtraGetter {
|
||||||
|
type Extras = ();
|
||||||
|
|
||||||
|
async fn get_extra(
|
||||||
|
&self,
|
||||||
|
_user: &User,
|
||||||
|
_rt: &Runtime<S, Self::Extras>,
|
||||||
|
) -> Result<Self::Extras, Box<dyn Error>> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,44 @@
|
|||||||
use std::{
|
use std::future::{Future, IntoFuture};
|
||||||
future::{Future, IntoFuture},
|
use std::pin::Pin;
|
||||||
pin::Pin,
|
use std::sync::Arc;
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use twilight_http::{Error, Response, response::marker::EmptyBody};
|
use twilight_http::response::marker::EmptyBody;
|
||||||
use twilight_model::{
|
use twilight_http::{Error, Response};
|
||||||
channel::{
|
use twilight_model::channel::Message;
|
||||||
Message,
|
use twilight_model::channel::message::{Component as TwilightComponent, Embed};
|
||||||
message::{Component as TwilightComponent, Embed},
|
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
|
||||||
},
|
use twilight_model::id::Id;
|
||||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
use twilight_model::id::marker::{ChannelMarker, InteractionMarker, MessageMarker};
|
||||||
id::{
|
|
||||||
Id,
|
|
||||||
marker::{ChannelMarker, InteractionMarker},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use twilight_util::builder::InteractionResponseDataBuilder;
|
use twilight_util::builder::InteractionResponseDataBuilder;
|
||||||
|
|
||||||
use crate::framework::{
|
use crate::framework::interactions::components::{ActionRow, Component};
|
||||||
interactions::components::{ActionRow, Component},
|
use crate::framework::runtime::Runtime;
|
||||||
runtime::Runtime,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct MessageSender<T> {
|
pub struct MessageSender<T, E> {
|
||||||
channel_id: Id<ChannelMarker>,
|
channel_id: Id<ChannelMarker>,
|
||||||
|
|
||||||
rt: Arc<Runtime<T>>,
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
embeds: Vec<Embed>,
|
embeds: Vec<Embed>,
|
||||||
|
reply_to: Option<Id<MessageMarker>>,
|
||||||
|
|
||||||
// Row index, component
|
// Row index, component
|
||||||
components: Vec<(u8, Arc<dyn Component<T>>)>,
|
components: Vec<(u8, Arc<dyn Component<T, E>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MessageSender<T>
|
impl<T, E> MessageSender<T, E>
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(channel_id: Id<ChannelMarker>, rt: Arc<Runtime<T>>) -> Self {
|
pub fn new(channel_id: Id<ChannelMarker>, rt: Arc<Runtime<T, E>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
channel_id,
|
channel_id,
|
||||||
rt,
|
rt,
|
||||||
content: None,
|
content: None,
|
||||||
components: vec![],
|
components: vec![],
|
||||||
|
reply_to: None,
|
||||||
embeds: vec![],
|
embeds: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,12 +55,18 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn component(mut self, component: impl Component<T> + 'static, row: u8) -> Self {
|
pub fn component(mut self, component: impl Component<T, E> + 'static, row: u8) -> Self {
|
||||||
self.components.push((row, Arc::new(component)));
|
self.components.push((row, Arc::new(component)));
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reply_to(mut self, message_id: Id<MessageMarker>) -> Self {
|
||||||
|
self.reply_to = Some(message_id);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send(self) -> Result<Response<Message>, Error> {
|
pub async fn send(self) -> Result<Response<Message>, Error> {
|
||||||
let mut builder = self.rt.http.create_message(self.channel_id);
|
let mut builder = self.rt.http.create_message(self.channel_id);
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ where
|
|||||||
|
|
||||||
for (index, component) in self.components {
|
for (index, component) in self.components {
|
||||||
if index != current_index {
|
if index != current_index {
|
||||||
if current_row.len() > 0 {
|
if !current_row.is_empty() {
|
||||||
rows.push(current_row.build());
|
rows.push(current_row.build());
|
||||||
current_row = ActionRow::new(&[]);
|
current_row = ActionRow::new(&[]);
|
||||||
}
|
}
|
||||||
@@ -101,13 +101,17 @@ where
|
|||||||
current_row.add(custom_id.clone(), component.clone());
|
current_row.add(custom_id.clone(), component.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_row.len() > 0 {
|
if !current_row.is_empty() {
|
||||||
rows.push(current_row.build());
|
rows.push(current_row.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = builder.components(&rows);
|
builder = builder.components(&rows);
|
||||||
builder = builder.embeds(&self.embeds);
|
builder = builder.embeds(&self.embeds);
|
||||||
|
|
||||||
|
if let Some(reply_to) = self.reply_to {
|
||||||
|
builder = builder.reply(reply_to);
|
||||||
|
}
|
||||||
|
|
||||||
let result = builder.await;
|
let result = builder.await;
|
||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
@@ -120,7 +124,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> IntoFuture for MessageSender<T> {
|
impl<T: Send + Sync + 'static, E: Send + Sync + 'static> IntoFuture for MessageSender<T, E> {
|
||||||
type Output = Result<Response<Message>, Error>;
|
type Output = Result<Response<Message>, Error>;
|
||||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
||||||
|
|
||||||
@@ -129,27 +133,28 @@ impl<T: Send + Sync + 'static> IntoFuture for MessageSender<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OriginalMessageUpdater<T> {
|
pub struct OriginalMessageUpdater<T, E> {
|
||||||
interaction_id: Id<InteractionMarker>,
|
interaction_id: Id<InteractionMarker>,
|
||||||
interaction_token: String,
|
interaction_token: String,
|
||||||
|
|
||||||
rt: Arc<Runtime<T>>,
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
embeds: Vec<Embed>,
|
embeds: Vec<Embed>,
|
||||||
|
|
||||||
// Row index, component
|
// Row index, component
|
||||||
components: Vec<(u8, Arc<dyn Component<T>>)>,
|
components: Vec<(u8, Arc<dyn Component<T, E>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> OriginalMessageUpdater<T>
|
impl<T, E> OriginalMessageUpdater<T, E>
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
interaction_id: Id<InteractionMarker>,
|
interaction_id: Id<InteractionMarker>,
|
||||||
interaction_token: String,
|
interaction_token: String,
|
||||||
rt: Arc<Runtime<T>>,
|
rt: Arc<Runtime<T, E>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
interaction_id,
|
interaction_id,
|
||||||
@@ -161,6 +166,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn content(mut self, content: impl ToString) -> Self {
|
pub fn content(mut self, content: impl ToString) -> Self {
|
||||||
self.content = Some(content.to_string());
|
self.content = Some(content.to_string());
|
||||||
|
|
||||||
@@ -173,7 +179,7 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn component(mut self, component: impl Component<T> + 'static, row: u8) -> Self {
|
pub fn component(mut self, component: impl Component<T, E> + 'static, row: u8) -> Self {
|
||||||
self.components.push((row, Arc::new(component)));
|
self.components.push((row, Arc::new(component)));
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -194,7 +200,7 @@ where
|
|||||||
|
|
||||||
for (index, component) in self.components {
|
for (index, component) in self.components {
|
||||||
if index != current_index {
|
if index != current_index {
|
||||||
if current_row.len() > 0 {
|
if !current_row.is_empty() {
|
||||||
rows.push(current_row.build());
|
rows.push(current_row.build());
|
||||||
current_row = ActionRow::new(&[]);
|
current_row = ActionRow::new(&[]);
|
||||||
}
|
}
|
||||||
@@ -213,7 +219,7 @@ where
|
|||||||
current_row.add(custom_id.clone(), component.clone());
|
current_row.add(custom_id.clone(), component.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_row.len() > 0 {
|
if !current_row.is_empty() {
|
||||||
rows.push(current_row.build());
|
rows.push(current_row.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +250,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> IntoFuture for OriginalMessageUpdater<T> {
|
impl<T: Send + Sync + 'static, E: Send + Sync + 'static> IntoFuture
|
||||||
|
for OriginalMessageUpdater<T, E>
|
||||||
|
{
|
||||||
type Output = Result<Response<EmptyBody>, Error>;
|
type Output = Result<Response<EmptyBody>, Error>;
|
||||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
||||||
|
|
||||||
|
|||||||
+161
-17
@@ -1,40 +1,130 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use deadpool_redis::cluster::PoolError;
|
||||||
|
use deadpool_redis::redis::RedisError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use twilight_http::response::DeserializeBodyError;
|
||||||
|
|
||||||
use crate::framework::commands::{arguments::Argument, context::CommandContext};
|
use crate::database::models::{DatabaseError, DatabaseOrDiscordError};
|
||||||
|
use crate::framework::commands::arguments::Argument;
|
||||||
|
use crate::framework::commands::context::CommandContext;
|
||||||
|
use crate::framework::commands::permissions::Permission;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
pub mod arguments;
|
pub mod arguments;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
pub mod extras;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
pub mod permissions;
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
|
|
||||||
|
/// An error occurred during command execution.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CommandError {
|
pub enum CommandError {
|
||||||
#[error("A message was attempted to be sent but it failed")]
|
|
||||||
MessageSendingFailed,
|
|
||||||
|
|
||||||
#[error("A database query failed")]
|
#[error("A database query failed")]
|
||||||
DatabaseQueryFailed,
|
DatabaseFailed(#[from] DatabaseError),
|
||||||
|
|
||||||
#[error("A database operation failed due to an inconsistent state")]
|
#[error("A database operation failed due to an inconsistent state")]
|
||||||
DatabaseInconsistentState,
|
DatabaseInconsistentState,
|
||||||
|
|
||||||
|
#[error("The command was used in DMs, but it can only be used in servers")]
|
||||||
|
ServerCommandRunInDMs,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DiscordAPIFailed(#[from] twilight_http::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DiscordDeserializationFailed(#[from] DeserializeBodyError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
RedisPoolFailed(#[from] PoolError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
RedisFailed(#[from] RedisError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DatabaseOrDiscordError(#[from] DatabaseOrDiscordError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a bot command.
|
||||||
|
///
|
||||||
|
/// Only the [`Command::name()`] and the [`Command::execute()`] methods must be implemented. The
|
||||||
|
/// rest have default implementations. They can all be overriden to tweak the command's behavior
|
||||||
|
/// and display in the help command.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Command<T>: Send + Sync + Debug {
|
pub trait Command<T, E>: Send + Sync + Debug {
|
||||||
|
/// The command's name.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`&'static str`] - The command's name.
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// The command's aliases.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`&'static [&'static str]`] - The command's aliases.
|
||||||
fn aliases(&self) -> &'static [&'static str] {
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The command's description, shown in the help command.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`&'static str`] - The command's description.
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
"A Payphone command."
|
"A Payphone command."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The command's arguments, shown in the help command.
|
||||||
|
///
|
||||||
|
/// This does not affect parsing in any way, as that's done inside [`Command::execute()`]. This
|
||||||
|
/// is for display in the help command only.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`&'static [Argument]`] - The command's arguments.
|
||||||
fn arguments(&self) -> &'static [Argument] {
|
fn arguments(&self) -> &'static [Argument] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Permissions that will be checked for before running the command.
|
||||||
|
///
|
||||||
|
/// If any of the permissions fail, the command will not be run and the permission's
|
||||||
|
/// [`Permission::on_handle_denied()`] will be called.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Vec<Box<dyn Permission<T, E> + Send + Sync>>`] - A vector of the command's permissions.
|
||||||
|
fn permissions(&self) -> Vec<Box<dyn Permission<T, E> + Send + Sync + 'static>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the command should be shown in the help command.
|
||||||
|
///
|
||||||
|
/// This is useful for commands that are not meant to be used by users directly, like internal
|
||||||
|
/// commands or easter egg commands.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`bool`] - Whether the command should be shown in the help command.
|
||||||
|
fn show_in_help(&self, _rt: &Runtime<T, E>, _extras: &E) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the command only runs when the message starts with the command prefix.
|
||||||
|
///
|
||||||
|
/// When this is false, the command will be run when the command is found on any part of the
|
||||||
|
/// message. This is useful for about-group commands, like:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// User: You can donate $6 at p.d to get unbanned.
|
||||||
|
/// Bot: https://ko-fi.com/payphonedev
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`bool`] - Whether the command only runs when the message starts with the command prefix.
|
||||||
|
fn only_runs_if_startswith(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the command name + all its aliases.
|
/// Returns the command name + all its aliases.
|
||||||
///
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
@@ -45,32 +135,86 @@ pub trait Command<T>: Send + Sync + Debug {
|
|||||||
all
|
all
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, ctx: CommandContext<'_, T>, args: &str) -> Result<(), CommandError>;
|
/// Executes the command.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `ctx` - The command context, containing information about the command invocation and
|
||||||
|
/// helpers for responding.
|
||||||
|
/// * `args` - The raw arguments passed to the command, without any parsing. This is the part
|
||||||
|
/// of the message after the command name.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Result<(), CommandError>`] - Ok if the command executed successfully, or an error if
|
||||||
|
/// something went wrong during execution.
|
||||||
|
async fn execute(&self, ctx: CommandContext<'_, T, E>, args: &str) -> Result<(), CommandError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The parts of a command found in a message, without processing.
|
||||||
|
///
|
||||||
|
/// This is not guaranteed to point to a valid command, as it only finds a prefix in a message and
|
||||||
|
/// parses it without checking for command existance nor argument validity.
|
||||||
pub struct CommandParts<'a> {
|
pub struct CommandParts<'a> {
|
||||||
|
/// The name of the command.
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
|
|
||||||
|
/// The arguments passed to the command, raw as a string.
|
||||||
pub args: &'a str,
|
pub args: &'a str,
|
||||||
|
|
||||||
|
/// The index in the start-trimmed message where the command was found.
|
||||||
|
///
|
||||||
|
/// For example, given the prefix is `p.`:
|
||||||
|
/// ```text
|
||||||
|
/// Donate $6 at p.d to get unbanned.
|
||||||
|
/// ^
|
||||||
|
/// index = 13
|
||||||
|
/// ```
|
||||||
|
pub start_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommandParts<'a> {
|
impl<'a> CommandParts<'a> {
|
||||||
fn new(name: &'a str, args: &'a str) -> Self {
|
/// Creates a new [`CommandParts`] struct.
|
||||||
Self { name, args }
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `name` - The name of the command.
|
||||||
|
/// * `args` - The arguments passed to the command, raw as a string.
|
||||||
|
/// * `start_index` - The index in the start-trimmed message where the command was found.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`CommandParts`] - The created struct.
|
||||||
|
fn new(name: &'a str, args: &'a str, start_index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
start_index,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a message content to get the command name and arguments.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `prefix` - The prefix to look for in the message.
|
||||||
|
/// * `message` - The message content to parse.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Option<CommandParts>`] - The parsed command parts, or None if the prefix was not found in the
|
||||||
|
/// message.
|
||||||
pub fn get_command_parts<'a>(prefix: &str, message: &'a str) -> Option<CommandParts<'a>> {
|
pub fn get_command_parts<'a>(prefix: &str, message: &'a str) -> Option<CommandParts<'a>> {
|
||||||
let message_trimmed_start = message.trim_ascii_start();
|
let message_trimmed_start = message.trim_ascii_start();
|
||||||
|
let message_trimmed_start_lowercase = message_trimmed_start.to_ascii_lowercase();
|
||||||
|
|
||||||
if message_trimmed_start
|
let prefix_lowercase = prefix.to_ascii_lowercase();
|
||||||
.to_ascii_lowercase()
|
|
||||||
.starts_with(&prefix.to_ascii_lowercase())
|
|
||||||
{
|
|
||||||
let message = message_trimmed_start.split_at(prefix.len()).1;
|
|
||||||
|
|
||||||
match message.split_once(" ") {
|
if let Some(index) = message_trimmed_start_lowercase.find(&prefix_lowercase) {
|
||||||
Some((name, args)) => Some(CommandParts::new(name, args)),
|
let command_text = &message_trimmed_start[index + prefix.len()..];
|
||||||
None => Some(CommandParts::new(message, "")),
|
|
||||||
|
match command_text.trim_ascii_start().split_once(" ") {
|
||||||
|
Some((name, args)) => Some(CommandParts::new(name, args, index)),
|
||||||
|
None => Some(CommandParts::new(
|
||||||
|
command_text.trim_ascii_start(),
|
||||||
|
"",
|
||||||
|
index,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::framework::commands::CommandError;
|
||||||
|
use crate::framework::commands::context::CommandContext;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Permission<S: Send + Sync + 'static, E>: Debug {
|
||||||
|
async fn check(&self, ctx: &CommandContext<S, E>) -> Result<bool, CommandError>;
|
||||||
|
|
||||||
|
async fn on_handle_denied(&self, _ctx: &CommandContext<S, E>) -> Result<(), CommandError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,18 +3,27 @@ use std::sync::Arc;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use twilight_model::gateway::payload::incoming::MessageCreate;
|
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||||
|
|
||||||
use crate::framework::{
|
use crate::framework::commands::context::CommandContext;
|
||||||
commands::{context::CommandContext, get_command_parts},
|
use crate::framework::commands::extras::ExtrasGetter;
|
||||||
events::traits::{EventResult, OnMessageHandler},
|
use crate::framework::commands::get_command_parts;
|
||||||
runtime::Runtime,
|
use crate::framework::events::traits::{EventResult, OnMessageHandler};
|
||||||
};
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CommandRunner;
|
pub struct CommandRunner;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Send + Sync + 'static> OnMessageHandler<T> for CommandRunner {
|
impl<T, E> OnMessageHandler<T, E> for CommandRunner
|
||||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<T>>) -> EventResult {
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn on_message(
|
||||||
|
&self,
|
||||||
|
event: &MessageCreate,
|
||||||
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||||
|
) -> EventResult {
|
||||||
if event.author.bot || event.author.system.unwrap_or(false) {
|
if event.author.bot || event.author.system.unwrap_or(false) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -23,17 +32,37 @@ impl<T: Send + Sync + 'static> OnMessageHandler<T> for CommandRunner {
|
|||||||
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
for prefix in prefixes {
|
for prefix in &prefixes {
|
||||||
if let Some(parts) = get_command_parts(&prefix, &event.content) {
|
if let Some(parts) = get_command_parts(prefix, &event.content) {
|
||||||
let command = rt.commands.iter().find(|cmd| {
|
let command = rt.commands.iter().find(|cmd| {
|
||||||
cmd.identifiers()
|
cmd.identifiers()
|
||||||
.contains(&parts.name.to_ascii_lowercase().as_str())
|
.contains(&parts.name.to_ascii_lowercase().as_str())
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(command) = command {
|
if let Some(command) = command {
|
||||||
command
|
if command.only_runs_if_startswith() && parts.start_index != 0 {
|
||||||
.execute(CommandContext::new(event, rt.clone(), &prefix), parts.args)
|
continue;
|
||||||
.await?;
|
}
|
||||||
|
|
||||||
|
let extras = extras
|
||||||
|
.get_extra(&event.author, &rt)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get extras: {e}"))?;
|
||||||
|
|
||||||
|
let ctx = CommandContext::new(event, rt.clone(), &prefixes[0], extras);
|
||||||
|
|
||||||
|
for permission in command.permissions() {
|
||||||
|
if !permission
|
||||||
|
.check(&ctx)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Permission check failed: {e}"))?
|
||||||
|
{
|
||||||
|
permission.on_handle_denied(&ctx).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command.execute(ctx, parts.args).await?;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::{error::Error, fmt::Debug, sync::Arc};
|
use std::error::Error;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use twilight_model::gateway::payload::incoming::{
|
use twilight_model::gateway::payload::incoming::{
|
||||||
@@ -6,78 +8,99 @@ use twilight_model::gateway::payload::incoming::{
|
|||||||
MessageUpdate, Ready,
|
MessageUpdate, Ready,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::framework::commands::extras::ExtrasGetter;
|
||||||
use crate::framework::runtime::Runtime;
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
pub type EventResult = Result<(), Box<dyn Error>>;
|
pub type EventResult = Result<(), Box<dyn Error>>;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnReadyHandler<T>: Send + Sync + Debug
|
pub trait OnReadyHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_ready(&self, event: &Ready, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_ready(&self, event: &Ready, rt: Arc<Runtime<T, E>>) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnReconnectHandler<T>: Send + Sync + Debug
|
pub trait OnReconnectHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_reconnect(&self, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_reconnect(&self, rt: Arc<Runtime<T, E>>) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnInteractionHandler<T>: Send + Sync + Debug
|
pub trait OnInteractionHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_interaction(
|
||||||
|
&self,
|
||||||
|
event: &InteractionCreate,
|
||||||
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||||
|
) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnMessageHandler<T>: Send + Sync + Debug
|
pub trait OnMessageHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_message(
|
||||||
|
&self,
|
||||||
|
event: &MessageCreate,
|
||||||
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||||
|
) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnMessageEditHandler<T>: Send + Sync + Debug
|
pub trait OnMessageEditHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_message_edit(&self, event: &MessageUpdate, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_message_edit(&self, event: &MessageUpdate, rt: Arc<Runtime<T, E>>) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnMessageDeleteHandler<T>: Send + Sync + Debug
|
pub trait OnMessageDeleteHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_message_delete(&self, event: &MessageDelete, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_message_delete(&self, event: &MessageDelete, rt: Arc<Runtime<T, E>>)
|
||||||
|
-> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnServerCreateHandler<T>: Send + Sync + Debug
|
pub trait OnServerCreateHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_server_create(&self, event: &GuildCreate, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_server_create(&self, event: &GuildCreate, rt: Arc<Runtime<T, E>>) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnServerUpdateHandler<T>: Send + Sync + Debug
|
pub trait OnServerUpdateHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_server_update(&self, event: &GuildUpdate, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_server_update(&self, event: &GuildUpdate, rt: Arc<Runtime<T, E>>) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OnServerDeleteHandler<T>: Send + Sync + Debug
|
pub trait OnServerDeleteHandler<T, E>: Send + Sync + Debug
|
||||||
where
|
where
|
||||||
T: Send + Sync,
|
T: Send + Sync,
|
||||||
|
E: Send + Sync,
|
||||||
{
|
{
|
||||||
async fn on_server_delete(&self, event: &GuildDelete, rt: Arc<Runtime<T>>) -> EventResult;
|
async fn on_server_delete(&self, event: &GuildDelete, rt: Arc<Runtime<T, E>>) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
use std::{fmt::Debug, sync::Arc};
|
use std::fmt::Debug;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use twilight_model::channel::message::{
|
use twilight_model::channel::message::Component as TwilightComponent;
|
||||||
Component as TwilightComponent,
|
use twilight_model::channel::message::component::{
|
||||||
component::{ActionRow as TwilightActionRow, Button as TwilightButton, ButtonStyle},
|
ActionRow as TwilightActionRow, Button as TwilightButton, ButtonStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::framework::interactions::{InteractionError, context::InteractionContext};
|
use crate::framework::interactions::InteractionError;
|
||||||
|
use crate::framework::interactions::context::InteractionContext;
|
||||||
|
|
||||||
type ComponentWithID<T> = (Option<String>, Arc<dyn Component<T>>);
|
type ComponentWithID<T, E> = (Option<String>, Arc<dyn Component<T, E>>);
|
||||||
|
|
||||||
pub struct ActionRow<T> {
|
pub struct ActionRow<T, E> {
|
||||||
components: Vec<ComponentWithID<T>>,
|
components: Vec<ComponentWithID<T, E>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ActionRow<T> {
|
impl<T, E> ActionRow<T, E> {
|
||||||
pub fn new(components: &[ComponentWithID<T>]) -> Self {
|
pub fn new(components: &[ComponentWithID<T, E>]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
components: components.to_vec(),
|
components: components.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, id: Option<String>, component: Arc<dyn Component<T>>) {
|
pub fn add(&mut self, id: Option<String>, component: Arc<dyn Component<T, E>>) {
|
||||||
self.components.push((id, component));
|
self.components.push((id, component));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.components.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.components.is_empty()
|
self.components.is_empty()
|
||||||
}
|
}
|
||||||
@@ -45,19 +43,21 @@ impl<T> ActionRow<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Component<T>: Send + Sync + Debug {
|
pub trait Component<T, E>: Send + Sync + Debug {
|
||||||
fn custom_id(&self) -> Option<String> {
|
fn custom_id(&self) -> Option<String> {
|
||||||
Some(String::from("component"))
|
Some(String::from("component"))
|
||||||
}
|
}
|
||||||
fn build(&self, id: Option<String>) -> TwilightComponent;
|
fn build(&self, id: Option<String>) -> TwilightComponent;
|
||||||
|
|
||||||
async fn execute(&self, ctx: InteractionContext<T>) -> Result<(), InteractionError>;
|
async fn execute(&self, ctx: InteractionContext<T, E>) -> Result<(), InteractionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T, S: Send + Sync + 'static> Component<S> for T
|
impl<T, S, E> Component<S, E> for T
|
||||||
where
|
where
|
||||||
T: Button<S>,
|
T: Button<S, E>,
|
||||||
|
S: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
fn custom_id(&self) -> Option<String> {
|
fn custom_id(&self) -> Option<String> {
|
||||||
Button::id(self)
|
Button::id(self)
|
||||||
@@ -66,7 +66,7 @@ where
|
|||||||
fn build(&self, id: Option<String>) -> TwilightComponent {
|
fn build(&self, id: Option<String>) -> TwilightComponent {
|
||||||
TwilightComponent::Button(TwilightButton {
|
TwilightComponent::Button(TwilightButton {
|
||||||
custom_id: id,
|
custom_id: id,
|
||||||
disabled: false,
|
disabled: self.disabled(),
|
||||||
emoji: None,
|
emoji: None,
|
||||||
label: self.label().map(|i| i.to_string()),
|
label: self.label().map(|i| i.to_string()),
|
||||||
style: if self.url().is_none() {
|
style: if self.url().is_none() {
|
||||||
@@ -79,13 +79,17 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, ctx: InteractionContext<S>) -> Result<(), InteractionError> {
|
async fn execute(&self, ctx: InteractionContext<S, E>) -> Result<(), InteractionError> {
|
||||||
self.on_press(ctx).await
|
self.on_press(ctx).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Button<S: Send + Sync + 'static>: Send + Sync + Debug {
|
pub trait Button<S, E>: Send + Sync + Debug
|
||||||
|
where
|
||||||
|
S: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
|
{
|
||||||
fn id(&self) -> Option<String> {
|
fn id(&self) -> Option<String> {
|
||||||
if self.url().is_none() {
|
if self.url().is_none() {
|
||||||
Some(String::from("Button"))
|
Some(String::from("Button"))
|
||||||
@@ -93,17 +97,24 @@ pub trait Button<S: Send + Sync + 'static>: Send + Sync + Debug {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style(&self) -> ButtonStyle {
|
fn style(&self) -> ButtonStyle {
|
||||||
ButtonStyle::Secondary
|
ButtonStyle::Secondary
|
||||||
}
|
}
|
||||||
fn label(&self) -> Option<&'static str> {
|
|
||||||
None
|
fn label(&self) -> Option<String> {
|
||||||
}
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_press(&self, ctx: InteractionContext<S>) -> Result<(), InteractionError> {
|
fn url(&self) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disabled(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_press(&self, ctx: InteractionContext<S, E>) -> Result<(), InteractionError> {
|
||||||
ctx.defer().await
|
ctx.defer().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use twilight_model::{
|
use twilight_http::response::marker::EmptyBody;
|
||||||
gateway::payload::incoming::InteractionCreate,
|
use twilight_http::{Error, Response};
|
||||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||||
id::{Id, marker::InteractionMarker},
|
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
|
||||||
};
|
use twilight_util::builder::InteractionResponseDataBuilder;
|
||||||
|
|
||||||
use crate::framework::{
|
use crate::framework::interactions::InteractionError;
|
||||||
commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime,
|
use crate::framework::interactions::components::ActionRow;
|
||||||
};
|
use crate::framework::messages::MessageEditBuilder;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
pub struct InteractionContext<T> {
|
pub struct InteractionContext<T, E> {
|
||||||
pub event: InteractionCreate,
|
pub event: InteractionCreate,
|
||||||
pub rt: Arc<Runtime<T>>,
|
pub rt: Arc<Runtime<T, E>>,
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
|
pub extras: E,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> InteractionContext<T> {
|
impl<T, E> InteractionContext<T, E>
|
||||||
pub fn new(event: InteractionCreate, rt: Arc<Runtime<T>>, prefix: String) -> Self {
|
where
|
||||||
Self { event, rt, prefix }
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
event: InteractionCreate,
|
||||||
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
prefix: String,
|
||||||
|
extras: E,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
event,
|
||||||
|
rt,
|
||||||
|
prefix,
|
||||||
|
extras,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn defer(&self) -> Result<(), InteractionError> {
|
pub async fn defer(&self) -> Result<(), InteractionError> {
|
||||||
@@ -33,54 +49,85 @@ impl<T> InteractionContext<T> {
|
|||||||
kind: InteractionResponseType::DeferredUpdateMessage,
|
kind: InteractionResponseType::DeferredUpdateMessage,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| InteractionError::DeferringFailed)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn think(&self) -> Result<(), InteractionError> {
|
pub async fn edit_original_message(
|
||||||
self.rt
|
&self,
|
||||||
|
builder: MessageEditBuilder<T, E>,
|
||||||
|
) -> Result<Response<EmptyBody>, Error> {
|
||||||
|
let mut sender = InteractionResponseDataBuilder::new();
|
||||||
|
|
||||||
|
if let Some(content) = &builder.content {
|
||||||
|
if let Some(content) = content {
|
||||||
|
sender = sender.content(content);
|
||||||
|
} else {
|
||||||
|
sender = sender.content("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current_row = ActionRow::new(&[]);
|
||||||
|
let mut current_index = 0;
|
||||||
|
|
||||||
|
let mut custom_ids: Vec<String> = vec![];
|
||||||
|
|
||||||
|
if let Some(components) = builder.components {
|
||||||
|
let mut rows = vec![];
|
||||||
|
|
||||||
|
for (component, index) in components {
|
||||||
|
if index != current_index {
|
||||||
|
if !current_row.is_empty() {
|
||||||
|
rows.push(current_row.build());
|
||||||
|
current_row = ActionRow::new(&[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let custom_id = component.custom_id();
|
||||||
|
|
||||||
|
if let Some(custom_id) = &custom_id {
|
||||||
|
custom_ids.push(custom_id.clone());
|
||||||
|
|
||||||
|
self.rt.registry.register(custom_id, component.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
current_row.add(custom_id.clone(), component.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current_row.is_empty() {
|
||||||
|
rows.push(current_row.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
sender = sender.components(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(embeds) = builder.embeds {
|
||||||
|
sender = sender.embeds(embeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.rt
|
||||||
.http
|
.http
|
||||||
.interaction(self.rt.app.id)
|
.interaction(self.rt.app.id)
|
||||||
.create_response(
|
.create_response(
|
||||||
self.event.id,
|
self.event.id,
|
||||||
&self.event.token,
|
&self.event.token,
|
||||||
&InteractionResponse {
|
&InteractionResponse {
|
||||||
data: None,
|
data: Some(sender.build()),
|
||||||
kind: InteractionResponseType::DeferredChannelMessageWithSource,
|
kind: InteractionResponseType::UpdateMessage,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.map_err(|_| InteractionError::DeferringFailed)?;
|
|
||||||
|
|
||||||
Ok(())
|
if result.is_err() {
|
||||||
}
|
for custom_id in custom_ids {
|
||||||
|
self.rt.registry.unregister(custom_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn original_message(&self) -> Option<OriginalMessage<T>> {
|
result
|
||||||
self.event.message.as_ref().map(|_| OriginalMessage {
|
|
||||||
interaction_id: self.event.id,
|
|
||||||
interaction_token: self.event.token.clone(),
|
|
||||||
rt: self.rt.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OriginalMessage<T> {
|
|
||||||
interaction_id: Id<InteractionMarker>,
|
|
||||||
interaction_token: String,
|
|
||||||
rt: Arc<Runtime<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> OriginalMessage<T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
pub fn edit(&self) -> OriginalMessageUpdater<T> {
|
|
||||||
OriginalMessageUpdater::new(
|
|
||||||
self.interaction_id,
|
|
||||||
self.interaction_token.clone(),
|
|
||||||
self.rt.clone(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::database::models::DatabaseError;
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
@@ -12,9 +14,6 @@ pub enum InteractionError {
|
|||||||
)]
|
)]
|
||||||
InteractionNotFound,
|
InteractionNotFound,
|
||||||
|
|
||||||
#[error("The interaction was attempted to be deferred, but it failed to be done")]
|
|
||||||
DeferringFailed,
|
|
||||||
|
|
||||||
#[error("Discord responded with an interaction type Payphone does not use")]
|
#[error("Discord responded with an interaction type Payphone does not use")]
|
||||||
InteractionTypeUnsent,
|
InteractionTypeUnsent,
|
||||||
|
|
||||||
@@ -23,4 +22,13 @@ pub enum InteractionError {
|
|||||||
|
|
||||||
#[error("No prefixes were returned for the event")]
|
#[error("No prefixes were returned for the event")]
|
||||||
PrefixesVectorEmpty,
|
PrefixesVectorEmpty,
|
||||||
|
|
||||||
|
#[error("The extras getter failed to retrieve extras")]
|
||||||
|
ExtrasGetterFailed,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DatabaseError(#[from] DatabaseError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DiscordAPIError(#[from] twilight_http::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use scc::HashMap;
|
use scc::HashMap;
|
||||||
|
use twilight_model::application::interaction::InteractionData;
|
||||||
use twilight_model::{
|
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||||
application::interaction::InteractionData, gateway::payload::incoming::InteractionCreate,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::framework::{
|
|
||||||
interactions::{InteractionError, context::InteractionContext},
|
|
||||||
runtime::Runtime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::components::Component;
|
use super::components::Component;
|
||||||
|
use crate::framework::commands::extras::ExtrasGetter;
|
||||||
|
use crate::framework::interactions::InteractionError;
|
||||||
|
use crate::framework::interactions::context::InteractionContext;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ComponentRegistry<T> {
|
pub struct ComponentRegistry<T, E> {
|
||||||
inner: HashMap<String, Arc<dyn Component<T>>>,
|
inner: HashMap<String, Arc<dyn Component<T, E>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ComponentRegistry<T>
|
impl<T, E> ComponentRegistry<T, E>
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -28,7 +26,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(&self, id: impl ToString, component: Arc<dyn Component<T>>) {
|
pub fn register(&self, id: impl ToString, component: Arc<dyn Component<T, E>>) {
|
||||||
self.inner.upsert(id.to_string(), component);
|
self.inner.upsert(id.to_string(), component);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,12 +37,16 @@ where
|
|||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
&self,
|
&self,
|
||||||
event: &InteractionCreate,
|
event: &InteractionCreate,
|
||||||
rt: Arc<Runtime<T>>,
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||||
) -> Result<(), InteractionError> {
|
) -> Result<(), InteractionError> {
|
||||||
if let Some(data) = &event.data {
|
if let Some(data) = &event.data {
|
||||||
match data {
|
match data {
|
||||||
InteractionData::MessageComponent(data) => {
|
InteractionData::MessageComponent(data) => {
|
||||||
if let Some(component) = self.inner.get_async(&data.custom_id).await {
|
if let Some(guarded_component) = self.inner.get(&data.custom_id) {
|
||||||
|
let component = guarded_component.clone();
|
||||||
|
drop(guarded_component);
|
||||||
|
|
||||||
let prefixes = rt
|
let prefixes = rt
|
||||||
.get_prefixes(
|
.get_prefixes(
|
||||||
event.guild_id.map(|id| id.get()),
|
event.guild_id.map(|id| id.get()),
|
||||||
@@ -55,13 +57,27 @@ where
|
|||||||
.first()
|
.first()
|
||||||
.ok_or(InteractionError::PrefixesVectorEmpty)?;
|
.ok_or(InteractionError::PrefixesVectorEmpty)?;
|
||||||
|
|
||||||
|
let extras = extras
|
||||||
|
.get_extra(
|
||||||
|
event.author().expect("Component interaction had no author"),
|
||||||
|
&rt,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to get extras for component interaction: {e:?}")
|
||||||
|
})
|
||||||
|
.map_err(|_| InteractionError::ExtrasGetterFailed)?;
|
||||||
|
|
||||||
component
|
component
|
||||||
.execute(InteractionContext::new(
|
.execute(InteractionContext::new(
|
||||||
event.clone(),
|
event.clone(),
|
||||||
rt.clone(),
|
rt.clone(),
|
||||||
prefix.to_string(),
|
prefix.to_string(),
|
||||||
|
extras,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
.inspect(|_| log::debug!("Executed component interaction"))
|
||||||
|
.inspect_err(|e| log::error!("Interaction execution failed: {e:?}"))
|
||||||
} else {
|
} else {
|
||||||
Err(InteractionError::InteractionNotFound)
|
Err(InteractionError::InteractionNotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,25 @@ use std::sync::Arc;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||||
|
|
||||||
use crate::{
|
use crate::framework::commands::extras::ExtrasGetter;
|
||||||
framework::{
|
use crate::framework::events::traits::{EventResult, OnInteractionHandler};
|
||||||
events::traits::{EventResult, OnInteractionHandler},
|
use crate::framework::runtime::Runtime;
|
||||||
runtime::Runtime,
|
|
||||||
},
|
|
||||||
state::State,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InteractionRunner;
|
pub struct InteractionRunner;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnInteractionHandler<State> for InteractionRunner {
|
impl<T, E> OnInteractionHandler<T, E> for InteractionRunner
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
E: Send + Sync + 'static,
|
||||||
|
{
|
||||||
async fn on_interaction(
|
async fn on_interaction(
|
||||||
&self,
|
&self,
|
||||||
event: &InteractionCreate,
|
event: &InteractionCreate,
|
||||||
rt: Arc<Runtime<State>>,
|
rt: Arc<Runtime<T, E>>,
|
||||||
|
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||||
) -> EventResult {
|
) -> EventResult {
|
||||||
Ok(rt.registry.execute(event, rt.clone()).await?)
|
Ok(rt.registry.execute(event, rt.clone(), extras).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twilight_model::channel::message::Embed;
|
||||||
|
|
||||||
|
use crate::framework::interactions::components::Component;
|
||||||
|
|
||||||
|
type ComponentWithRowIndex<S, E> = (Arc<dyn Component<S, E>>, usize);
|
||||||
|
|
||||||
|
pub struct MessageBuilder<S, E> {
|
||||||
|
pub content: Option<String>,
|
||||||
|
pub embeds: Vec<Embed>,
|
||||||
|
pub components: Vec<ComponentWithRowIndex<S, E>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> MessageBuilder<S, E> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
content: None,
|
||||||
|
embeds: Vec::new(),
|
||||||
|
components: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(mut self, content: impl Into<String>) -> Self {
|
||||||
|
self.content = Some(content.into());
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn embed(mut self, embed: Embed) -> Self {
|
||||||
|
self.embeds.push(embed);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn component(mut self, component: impl Component<S, E> + 'static, row: usize) -> Self {
|
||||||
|
self.components.push((Arc::new(component), row));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageEditBuilder<S, E> {
|
||||||
|
pub content: Option<Option<String>>,
|
||||||
|
pub embeds: Option<Vec<Embed>>,
|
||||||
|
pub components: Option<Vec<ComponentWithRowIndex<S, E>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> MessageEditBuilder<S, E> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
content: None,
|
||||||
|
embeds: None,
|
||||||
|
components: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(mut self, content: impl Into<String>) -> Self {
|
||||||
|
self.content = Some(Some(content.into()));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn embed(mut self, embed: Embed) -> Self {
|
||||||
|
if self.embeds.is_none() {
|
||||||
|
self.embeds = Some(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.embeds.as_mut().unwrap().push(embed);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn component(mut self, component: impl Component<S, E> + 'static, row: usize) -> Self {
|
||||||
|
if self.components.is_none() {
|
||||||
|
self.components = Some(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.components
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push((Arc::new(component), row));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> From<MessageBuilder<S, E>> for MessageEditBuilder<S, E> {
|
||||||
|
fn from(builder: MessageBuilder<S, E>) -> Self {
|
||||||
|
Self {
|
||||||
|
content: builder.content.map(Some),
|
||||||
|
embeds: Some(builder.embeds),
|
||||||
|
components: Some(builder.components),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-41
@@ -1,35 +1,35 @@
|
|||||||
//! A framework for building Discord bots, built with Twilight.
|
//! A framework for building Discord bots, built with Twilight.
|
||||||
//!
|
//!
|
||||||
//! Since Twilight is a low-level library, it can be hard to build a bot with it. Payphone's
|
//! Since Twilight is a low-level library, it can be hard to build a bot with it. Payphone's
|
||||||
//! framework provides a high-level interface for building Discord bots, with a focus on modularity
|
//! framework provides a high-level interface for building Discord bots, with a focus on modularity
|
||||||
//! and ease of use. The framework is designed to be extensible, allowing you to add new features
|
//! and ease of use. The framework is designed to be extensible, allowing you to add new features
|
||||||
//! and commands without modifying the core codebase. It also provides a set of utilities for
|
//! and commands without modifying the core codebase. It also provides a set of utilities for
|
||||||
//! common tasks, such as sending messages, handling events, and managing state.
|
//! common tasks, such as sending messages, handling events, and managing state.
|
||||||
//!
|
//!
|
||||||
//! # Creating a Bot
|
//! # Creating a Bot
|
||||||
//!
|
//!
|
||||||
//! Creating a bot with Payphone is as simple as creating a new instance of the `Bot` struct and
|
//! Creating a bot with Payphone is as simple as creating a new instance of the `Bot` struct and
|
||||||
//! running it. The `Bot` struct is the main entry point for the framework and provides methods for
|
//! running it. The `Bot` struct is the main entry point for the framework and provides methods for
|
||||||
//! running the bot, handling events, and managing state.
|
//! running the bot, handling events, and managing state.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use crate::framework::bot::Bot;
|
//! use crate::framework::bot::Bot;
|
||||||
//! use crate::state::State;
|
//! use crate::state::State;
|
||||||
//!
|
//!
|
||||||
//! // Your app's state.
|
//! // Your app's state.
|
||||||
//! let state = State::new();
|
//! let state = State::new();
|
||||||
//!
|
//!
|
||||||
//! // Create a new bot instance with the state and a list of prefixes.
|
//! // Create a new bot instance with the state and a list of prefixes.
|
||||||
//! let bot: Bot<State> = Bot::new(state, ["p."]);
|
//! let bot: Bot<State> = Bot::new(state, ["p."]);
|
||||||
//!
|
//!
|
||||||
//! bot.run(&token).await.unwrap();
|
//! bot.run(&token).await.unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ### Prefixes
|
//! ### Prefixes
|
||||||
//!
|
//!
|
||||||
//! The framework supports multiple prefixes per command and allows you to set them dynamically
|
//! The framework supports multiple prefixes per command and allows you to set them dynamically
|
||||||
//! based on server and user IDs.
|
//! based on server and user IDs.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! async fn get_prefixes(
|
//! async fn get_prefixes(
|
||||||
//! rt: &Runtime<State>,
|
//! rt: &Runtime<State>,
|
||||||
@@ -37,15 +37,15 @@
|
|||||||
//! user_id: Option<u64>
|
//! user_id: Option<u64>
|
||||||
//! ) -> Vec<String> {
|
//! ) -> Vec<String> {
|
||||||
//! // The runtime contains your state at [`Runtime::state`].
|
//! // The runtime contains your state at [`Runtime::state`].
|
||||||
//!
|
//!
|
||||||
//! vec!["p.".to_string()]
|
//! vec!["p.".to_string()]
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! let bot: Bot<State> = Bot::new(state, get_prefixes);
|
//! let bot: Bot<State> = Bot::new(state, get_prefixes);
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Otherwise, you can just pass a static list of prefixes as shown in the previous example.
|
//! Otherwise, you can just pass a static list of prefixes as shown in the previous example.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! // All of these are valid ways to create a bot with a prefix.
|
//! // All of these are valid ways to create a bot with a prefix.
|
||||||
//! let bot: Bot<State> = Bot::new(state, ["p."]);
|
//! let bot: Bot<State> = Bot::new(state, ["p."]);
|
||||||
@@ -53,77 +53,76 @@
|
|||||||
//! let bot: Bot<State> = Bot::new(state, "p.".to_string());
|
//! let bot: Bot<State> = Bot::new(state, "p.".to_string());
|
||||||
//! let bot: Bot<State> = Bot::new(state, ["p.".to_string()]);
|
//! let bot: Bot<State> = Bot::new(state, ["p.".to_string()]);
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Setting no prefix will make the bot raise errors when receiving messages.
|
//! Setting no prefix will make the bot raise errors when receiving messages.
|
||||||
//!
|
//!
|
||||||
//! ## Adding Commands
|
//! ## Adding Commands
|
||||||
//!
|
//!
|
||||||
//! Creating a command is quite simple. You just need to implement the `Command` trait and register
|
//! Creating a command is quite simple. You just need to implement the `Command` trait and register
|
||||||
//! it with the bot.
|
//! it with the bot.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use crate::framework::commands::{Command, CommandError, context::CommandContext};
|
//! use crate::framework::commands::{Command, CommandError, context::CommandContext};
|
||||||
//!
|
//!
|
||||||
//! pub struct MyCommand;
|
//! pub struct MyCommand;
|
||||||
//!
|
//!
|
||||||
//! #[async_trait::async_trait]
|
//! #[async_trait::async_trait]
|
||||||
//! impl Command<State> for MyCommand {
|
//! impl Command<State> for MyCommand {
|
||||||
//! fn name(&self) -> &'static str {
|
//! fn name(&self) -> &'static str {
|
||||||
//! "my_command"
|
//! "my_command"
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! async fn execute(&self, ctx: CommandContext<'_, T>, _args: &str) -> Result<(), CommandError> {
|
//! async fn execute(&self, ctx: CommandContext<'_, T>, _args: &str) -> Result<(), CommandError> {
|
||||||
//! // Your command logic here.
|
//! // Your command logic here.
|
||||||
//! ctx.send()
|
//! ctx.send()
|
||||||
//! .content("Hello, world!")
|
//! .content("Hello, world!")
|
||||||
//! .await
|
//! .await?;
|
||||||
//! .map_err(|_| CommandError::MessageSendingFailed)?;
|
//!
|
||||||
//!
|
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Then you just need to register the command with the bot:
|
//! Then you just need to register the command with the bot:
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! let bot = Bot::new(state, ["p."]).command(MyCommand);
|
//! let bot = Bot::new(state, ["p."]).command(MyCommand);
|
||||||
//!
|
//!
|
||||||
//! bot.run(&token).await.unwrap();
|
//! bot.run(&token).await.unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Log into your Discord account and invite the bot to your server. Then you can test the command
|
//! Log into your Discord account and invite the bot to your server. Then you can test the command
|
||||||
//! by sending `p.my_command` in a channel the bot has access to.
|
//! by sending `p.my_command` in a channel the bot has access to.
|
||||||
//!
|
//!
|
||||||
//! ### Configuring Commands Further
|
//! ### Configuring Commands Further
|
||||||
//!
|
//!
|
||||||
//! Commands can be configured with additional metadata, such as aliases, descriptions, and
|
//! Commands can be configured with additional metadata, such as aliases, descriptions, and
|
||||||
//! arguments. The following is a full example of a command with all these features:
|
//! arguments. The following is a full example of a command with all these features:
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use crate::framework::commands::{
|
//! use crate::framework::commands::{
|
||||||
//! Command, CommandError, context::CommandContext, arguments::NeedyGreedyString
|
//! Command, CommandError, context::CommandContext, arguments::NeedyGreedyString
|
||||||
//! };
|
//! };
|
||||||
//! use crate::state::State;
|
//! use crate::state::State;
|
||||||
//!
|
//!
|
||||||
//! pub struct HelloCommand;
|
//! pub struct HelloCommand;
|
||||||
//!
|
//!
|
||||||
//! #[async_trait::async_trait]
|
//! #[async_trait::async_trait]
|
||||||
//! impl Command<State> for HelloCommand {
|
//! impl Command<State> for HelloCommand {
|
||||||
//! fn name(&self) -> &'static str {
|
//! fn name(&self) -> &'static str {
|
||||||
//! "hello"
|
//! "hello"
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! /// A list of alternative names for the command.
|
//! /// A list of alternative names for the command.
|
||||||
//! fn aliases(&self) -> &'static [&'static str] {
|
//! fn aliases(&self) -> &'static [&'static str] {
|
||||||
//! &["hola", "hey", "hi", "konnichiwa"]
|
//! &["hola", "hey", "hi", "konnichiwa"]
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! /// A description of the command and its functionality.
|
//! /// A description of the command and its functionality.
|
||||||
//! fn description(&self) -> &'static str {
|
//! fn description(&self) -> &'static str {
|
||||||
//! "This command responds with a greeting."
|
//! "This command responds with a greeting."
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! /// A list of arguments the command accepts. These are solely for documentation purposes,
|
//! /// A list of arguments the command accepts. These are solely for documentation purposes,
|
||||||
//! /// they won't affect the parsing you do in the command's `execute` method.
|
//! /// they won't affect the parsing you do in the command's `execute` method.
|
||||||
//! fn arguments(&self) -> &'static [Argument] {
|
//! fn arguments(&self) -> &'static [Argument] {
|
||||||
@@ -135,7 +134,7 @@
|
|||||||
//! },
|
//! },
|
||||||
//! ]
|
//! ]
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! async fn execute(
|
//! async fn execute(
|
||||||
//! &self,
|
//! &self,
|
||||||
//! ctx: CommandContext<'_, T>,
|
//! ctx: CommandContext<'_, T>,
|
||||||
@@ -146,12 +145,11 @@
|
|||||||
//! args
|
//! args
|
||||||
//! )
|
//! )
|
||||||
//! .unwrap_or(NeedyGreedyString("???".to_string()));
|
//! .unwrap_or(NeedyGreedyString("???".to_string()));
|
||||||
//!
|
//!
|
||||||
//! ctx.send()
|
//! ctx.send()
|
||||||
//! .content(format!("Hello, {}!", name.0))
|
//! .content(format!("Hello, {}!", name.0))
|
||||||
//! .await
|
//! .await?;
|
||||||
//! .map_err(|_| CommandError::MessageSendingFailed)?;
|
//!
|
||||||
//!
|
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
@@ -161,4 +159,6 @@ pub mod bot;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod interactions;
|
pub mod interactions;
|
||||||
|
pub mod messages;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
|
pub mod tasks;
|
||||||
|
|||||||
@@ -1,27 +1,88 @@
|
|||||||
|
//! Runtime app state of the shard.
|
||||||
|
//!
|
||||||
|
//! The [`Runtime`] is an abstraction over the application's state and provides access to various
|
||||||
|
//! components, such as the HTTP client, application information, and command registry. This was
|
||||||
|
//! done because there's a lot of data the application depends on to execute, and passing all those
|
||||||
|
//! values around individually was cumbersome.
|
||||||
|
//!
|
||||||
|
//! The [`Runtime`] includes the following data:
|
||||||
|
//!
|
||||||
|
//! - The HTTP API client.
|
||||||
|
//! - The application's details, including the user profile.
|
||||||
|
//! - The component registry.
|
||||||
|
//! - The prefix getter.
|
||||||
|
//! - The list of available commands.
|
||||||
|
//! - The app state.
|
||||||
|
//!
|
||||||
|
//! The [`Runtime`] is always passed around as [`Arc<Runtime>`], so it can be easily shared between
|
||||||
|
//! different parts of the application.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use twilight_cache_inmemory::InMemoryCache;
|
||||||
use twilight_http::Client;
|
use twilight_http::Client;
|
||||||
|
use twilight_model::gateway::payload::outgoing::UpdatePresence;
|
||||||
use twilight_model::oauth::Application;
|
use twilight_model::oauth::Application;
|
||||||
|
|
||||||
use crate::framework::{
|
use crate::framework::bot::PrefixGetter;
|
||||||
bot::IntoPrefixes, commands::Command, interactions::registry::ComponentRegistry,
|
use crate::framework::commands::Command;
|
||||||
};
|
use crate::framework::interactions::registry::ComponentRegistry;
|
||||||
|
|
||||||
pub struct Runtime<T> {
|
/// The application's runtime tools.
|
||||||
|
pub struct Runtime<T, E> {
|
||||||
|
/// The HTTP client used for making requests to the HTTP API.
|
||||||
pub http: Client,
|
pub http: Client,
|
||||||
|
|
||||||
|
/// Details about the application.
|
||||||
pub app: Application,
|
pub app: Application,
|
||||||
pub registry: ComponentRegistry<T>,
|
|
||||||
pub prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync + 'static>,
|
/// The component registry.
|
||||||
pub commands: Vec<Arc<dyn Command<T>>>,
|
pub registry: ComponentRegistry<T, E>,
|
||||||
|
|
||||||
|
/// The command prefixes getter.
|
||||||
|
pub prefixes: Arc<dyn PrefixGetter<T, E> + Send + Sync + 'static>,
|
||||||
|
|
||||||
|
/// The commands available in the application.
|
||||||
|
pub commands: Vec<Arc<dyn Command<T, E>>>,
|
||||||
|
|
||||||
|
/// The application state.
|
||||||
pub state: Arc<T>,
|
pub state: Arc<T>,
|
||||||
|
|
||||||
|
pub cache: InMemoryCache,
|
||||||
|
|
||||||
|
/// Used to send messages through the gateway.
|
||||||
|
pub gateway_tx: UnboundedSender<UpdatePresence>,
|
||||||
|
|
||||||
|
pub shard_number: u32,
|
||||||
|
pub shard_total: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Runtime<T> {
|
impl<T, E> Runtime<T, E> {
|
||||||
|
/// Get the command prefixes set for the server and user ID.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `server_id` - The ID of the server in which the command, interaction, or event is
|
||||||
|
/// running. When this is [`None`] it means the prefix should be returned for the user's DM
|
||||||
|
/// channel.
|
||||||
|
/// * `user_id` - The user who triggered the event.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Vec<String>`] - A list of prefixes, all which should be taken into account. It's safe to
|
||||||
|
/// default to the first item of the list when a single prefix should be used (e.g. to display
|
||||||
|
/// it to the user).
|
||||||
pub async fn get_prefixes(&self, server_id: Option<u64>, user_id: Option<u64>) -> Vec<String> {
|
pub async fn get_prefixes(&self, server_id: Option<u64>, user_id: Option<u64>) -> Vec<String> {
|
||||||
self.prefixes.clone().get(self, server_id, user_id).await
|
self.prefixes.clone().get(self, server_id, user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_command(&self, name: &str) -> Option<Arc<dyn Command<T>>> {
|
/// Get a command by its name or alias.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `name` - The command's name or alias, without prefix.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`Option<Arc<dyn Command<T>>>`] - The command if found, otherwise `None`.
|
||||||
|
pub fn get_command(&self, name: &str) -> Option<Arc<dyn Command<T, E>>> {
|
||||||
self.commands
|
self.commands
|
||||||
.iter()
|
.iter()
|
||||||
.find(|cmd| {
|
.find(|cmd| {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Task<S, E>: Send + Sync + Debug {
|
||||||
|
fn delay(&self) -> Duration;
|
||||||
|
|
||||||
|
async fn run(&self, rt: &Runtime<S, E>) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
+100
-25
@@ -1,25 +1,24 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::{
|
use crate::extras::Extras;
|
||||||
framework::commands::{
|
use crate::framework::commands::arguments::{Arg, Argument, NeedyString, ParseArguments};
|
||||||
Command, CommandError,
|
use crate::framework::commands::context::CommandContext;
|
||||||
arguments::{Argument, NeedyString, ParseArguments},
|
use crate::framework::commands::{Command, CommandError};
|
||||||
context::CommandContext,
|
use crate::framework::runtime::Runtime;
|
||||||
},
|
use crate::groups::about::components::{
|
||||||
groups::about::{
|
HelpDonateLinkButton, HelpGoBackButton, HelpSupportLinkButton, HelpWebsiteLinkButton,
|
||||||
components::{
|
|
||||||
HelpDonateLinkButton, HelpGoBackButton, HelpSupportLinkButton, HelpWebsiteLinkButton,
|
|
||||||
},
|
|
||||||
embeds::{get_all_command_help_embed, get_command_help_embed, get_command_not_found_embed},
|
|
||||||
},
|
|
||||||
state::State,
|
|
||||||
};
|
};
|
||||||
|
use crate::groups::about::embeds::{
|
||||||
|
get_all_command_help_embed, get_command_help_embed, get_command_not_found_embed,
|
||||||
|
};
|
||||||
|
use crate::groups::calls::components::CodeOfConductButton;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HelpCommand;
|
pub struct HelpCommand;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Command<State> for HelpCommand {
|
impl Command<State, Extras> for HelpCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"help"
|
"help"
|
||||||
}
|
}
|
||||||
@@ -38,38 +37,114 @@ impl Command<State> for HelpCommand {
|
|||||||
|
|
||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
ctx: CommandContext<'_, State>,
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
args: &str,
|
args: &str,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
let command_name: Option<NeedyString> = ParseArguments::parse(args).unwrap();
|
let command_name = Arg::<Option<NeedyString>>::parse(args, &ctx).await.unwrap();
|
||||||
|
|
||||||
if let Some(command_name) = command_name {
|
if let Some(command_name) = command_name.0 {
|
||||||
if let Some(command) = ctx.rt.get_command(&command_name) {
|
if let Some(command) = ctx.rt.get_command(&command_name) {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.embed(get_command_help_embed(&command, &ctx))
|
.embed(get_command_help_embed(&command, &ctx))
|
||||||
.component(HelpGoBackButton, 1)
|
.component(HelpGoBackButton, 1)
|
||||||
.component(HelpSupportLinkButton, 1)
|
.component(HelpSupportLinkButton, 1)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("Failed to send message! {e}"))
|
.inspect_err(|e| log::error!("Failed to send message! {e}"))?;
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
} else {
|
} else {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.embed(get_command_not_found_embed(ctx.rt, command_name.0))
|
.embed(get_command_not_found_embed(&ctx.rt, command_name.0))
|
||||||
.component(HelpGoBackButton, 1)
|
.component(HelpGoBackButton, 1)
|
||||||
.component(HelpSupportLinkButton, 1)
|
.component(HelpSupportLinkButton, 1)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.embed(get_all_command_help_embed(ctx.prefix, ctx.rt))
|
.embed(get_all_command_help_embed(ctx.prefix, &ctx.rt, &ctx.extras))
|
||||||
.component(HelpWebsiteLinkButton, 1)
|
.component(HelpWebsiteLinkButton, 1)
|
||||||
.component(HelpSupportLinkButton, 1)
|
.component(HelpSupportLinkButton, 1)
|
||||||
.component(HelpDonateLinkButton, 1)
|
.component(HelpDonateLinkButton, 1)
|
||||||
.await
|
.component(CodeOfConductButton, 1)
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CodeOfConductCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State, Extras> for CodeOfConductCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"codeofconduct"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["co"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Shows a link to Payphone's Code of Conduct."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_in_help(&self, _rt: &Runtime<State, Extras>, _extras: &Extras) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn only_runs_if_startswith(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
|
_args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
ctx.send()
|
||||||
|
.content("<https://payphone.lat/code-of-conduct>")
|
||||||
|
.reply_to(ctx.event.id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DonateCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State, Extras> for DonateCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"donate"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["d"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Shows a link to Payphone's Ko-Fi page."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_in_help(&self, _rt: &Runtime<State, Extras>, _extras: &Extras) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn only_runs_if_startswith(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
|
_args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
ctx.send()
|
||||||
|
.reply_to(ctx.event.id)
|
||||||
|
.content("<https://ko-fi.com/payphonedev>")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,40 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::{
|
use crate::extras::Extras;
|
||||||
framework::interactions::{InteractionError, components::Button, context::InteractionContext},
|
use crate::framework::interactions::InteractionError;
|
||||||
groups::about::embeds::get_all_command_help_embed,
|
use crate::framework::interactions::components::Button;
|
||||||
state::State,
|
use crate::framework::interactions::context::InteractionContext;
|
||||||
};
|
use crate::framework::messages::MessageEditBuilder;
|
||||||
|
use crate::groups::about::embeds::get_all_command_help_embed;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HelpGoBackButton;
|
pub struct HelpGoBackButton;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Button<State> for HelpGoBackButton {
|
impl Button<State, Extras> for HelpGoBackButton {
|
||||||
fn label(&self) -> Option<&'static str> {
|
fn id(&self) -> Option<String> {
|
||||||
Some("← Go back to commands list")
|
Some("help:go-back".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_press(&self, ctx: InteractionContext<State>) -> Result<(), InteractionError> {
|
fn label(&self) -> Option<String> {
|
||||||
|
Some("← Go back to commands list".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_press(
|
||||||
|
&self,
|
||||||
|
ctx: InteractionContext<State, Extras>,
|
||||||
|
) -> Result<(), InteractionError> {
|
||||||
ctx
|
ctx
|
||||||
.original_message()
|
.edit_original_message(
|
||||||
.expect(
|
MessageEditBuilder::new().embed(
|
||||||
"Buttons always have the original message in the interaction context, this one didn't (got None)"
|
get_all_command_help_embed(&ctx.prefix, &ctx.rt, &ctx.extras)
|
||||||
|
)
|
||||||
|
.component(HelpWebsiteLinkButton, 1)
|
||||||
|
.component(HelpSupportLinkButton, 1)
|
||||||
|
.component(HelpDonateLinkButton, 1)
|
||||||
|
.component(CodeOfConductLinkButton, 1)
|
||||||
)
|
)
|
||||||
.edit()
|
|
||||||
.embed(get_all_command_help_embed(&ctx.prefix, ctx.rt))
|
|
||||||
.component(HelpWebsiteLinkButton, 1)
|
|
||||||
.component(HelpSupportLinkButton, 1)
|
|
||||||
.component(HelpDonateLinkButton, 1)
|
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
||||||
.map_err(
|
.map_err(
|
||||||
@@ -40,13 +49,13 @@ impl Button<State> for HelpGoBackButton {
|
|||||||
pub struct HelpSupportLinkButton;
|
pub struct HelpSupportLinkButton;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Button<State> for HelpSupportLinkButton {
|
impl Button<State, Extras> for HelpSupportLinkButton {
|
||||||
fn label(&self) -> Option<&'static str> {
|
fn label(&self) -> Option<String> {
|
||||||
Some("Get help")
|
Some("Get help".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
fn url(&self) -> Option<String> {
|
||||||
Some("https://discord.gg/9daSsrfTmE")
|
Some("https://discord.gg/9daSsrfTmE".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +63,13 @@ impl Button<State> for HelpSupportLinkButton {
|
|||||||
pub struct HelpWebsiteLinkButton;
|
pub struct HelpWebsiteLinkButton;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Button<State> for HelpWebsiteLinkButton {
|
impl Button<State, Extras> for HelpWebsiteLinkButton {
|
||||||
fn label(&self) -> Option<&'static str> {
|
fn label(&self) -> Option<String> {
|
||||||
Some("Website")
|
Some("Website".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
fn url(&self) -> Option<String> {
|
||||||
Some("https://payphone.lat/")
|
Some("https://payphone.lat/".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +77,26 @@ impl Button<State> for HelpWebsiteLinkButton {
|
|||||||
pub struct HelpDonateLinkButton;
|
pub struct HelpDonateLinkButton;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Button<State> for HelpDonateLinkButton {
|
impl Button<State, Extras> for HelpDonateLinkButton {
|
||||||
fn label(&self) -> Option<&'static str> {
|
fn label(&self) -> Option<String> {
|
||||||
Some("Donate")
|
Some("Donate".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url(&self) -> Option<&'static str> {
|
fn url(&self) -> Option<String> {
|
||||||
Some("https://ko-fi.com/payphonedev")
|
Some("https://ko-fi.com/payphonedev".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CodeOfConductLinkButton;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Button<State, Extras> for CodeOfConductLinkButton {
|
||||||
|
fn label(&self) -> Option<String> {
|
||||||
|
Some("Code of Conduct".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<String> {
|
||||||
|
Some("https://payphone.lat/code-of-conduct".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-18
@@ -5,20 +5,19 @@ use twilight_util::builder::embed::{
|
|||||||
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::constants::{COLOR_RED, COLOR_WHITE, VERSION};
|
||||||
constants::{COLOR_RED, COLOR_WHITE},
|
use crate::discord::images;
|
||||||
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||||
framework::{
|
use crate::extras::Extras;
|
||||||
commands::{Command, context::CommandContext},
|
use crate::framework::commands::Command;
|
||||||
runtime::Runtime,
|
use crate::framework::commands::context::CommandContext;
|
||||||
},
|
use crate::framework::runtime::Runtime;
|
||||||
state::State,
|
use crate::state::State;
|
||||||
utils::ToTitleCase,
|
use crate::utils::ToTitleCase;
|
||||||
};
|
|
||||||
|
|
||||||
pub(super) fn get_command_help_embed(
|
pub(super) fn get_command_help_embed(
|
||||||
command: &Arc<dyn Command<State>>,
|
command: &Arc<dyn Command<State, Extras>>,
|
||||||
ctx: &CommandContext<'_, State>,
|
ctx: &CommandContext<'_, State, Extras>,
|
||||||
) -> Embed {
|
) -> Embed {
|
||||||
let name = format!("{}{}", ctx.prefix, command.name());
|
let name = format!("{}{}", ctx.prefix, command.name());
|
||||||
let arguments = command
|
let arguments = command
|
||||||
@@ -42,6 +41,7 @@ pub(super) fn get_command_help_embed(
|
|||||||
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
||||||
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -52,6 +52,7 @@ pub(super) fn get_command_help_embed(
|
|||||||
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
ctx.rt.app.bot.as_ref().unwrap().avatar,
|
||||||
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
ctx.rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -79,12 +80,17 @@ pub(super) fn get_command_help_embed(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn get_all_command_help_embed<T>(prefix: &str, rt: Arc<Runtime<T>>) -> Embed {
|
pub(super) fn get_all_command_help_embed(
|
||||||
|
prefix: &str,
|
||||||
|
rt: &Runtime<State, Extras>,
|
||||||
|
extras: &Extras,
|
||||||
|
) -> Embed {
|
||||||
EmbedBuilder::new()
|
EmbedBuilder::new()
|
||||||
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
||||||
rt.app.bot.as_ref().unwrap().avatar,
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
rt.app.bot.as_ref().unwrap().id.get(),
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
))
|
))
|
||||||
.unwrap()))
|
.unwrap()))
|
||||||
.title("Pick up the phone and start making friends!")
|
.title("Pick up the phone and start making friends!")
|
||||||
@@ -93,31 +99,35 @@ pub(super) fn get_all_command_help_embed<T>(prefix: &str, rt: Arc<Runtime<T>>) -
|
|||||||
rt.app.bot.as_ref().unwrap().avatar,
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
rt.app.bot.as_ref().unwrap().id.get(),
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
.color(COLOR_WHITE)
|
.color(COLOR_WHITE)
|
||||||
.field(EmbedFieldBuilder::new("Commands", get_commands_table(rt)).build())
|
.field(EmbedFieldBuilder::new("Commands", get_commands_table(rt, extras)).build())
|
||||||
.footer(EmbedFooterBuilder::new(format!("v{} - Use {}help <command> to get more information about a specific command.", env!("CARGO_PKG_VERSION"), prefix)))
|
.footer(EmbedFooterBuilder::new(format!("v{VERSION}-{} - Use {}help <command> to get more information about a specific command.", rt.shard_number + 1, prefix)))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn get_commands_table<T>(rt: Arc<Runtime<T>>) -> String {
|
pub(super) fn get_commands_table<T>(rt: &Runtime<T, Extras>, extras: &Extras) -> String {
|
||||||
let mut table = String::new();
|
let mut table = String::new();
|
||||||
|
|
||||||
for command in rt.commands.iter() {
|
for command in rt.commands.iter() {
|
||||||
table.push_str(&format!("{:<12}", command.name()));
|
if command.show_in_help(rt, extras) {
|
||||||
|
table.push_str(&format!("{:<13}", command.name()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
format!("```\n{table}\n```")
|
format!("```\n{table}\n```")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn get_command_not_found_embed<T>(rt: Arc<Runtime<T>>, name: String) -> Embed {
|
pub(super) fn get_command_not_found_embed<T>(rt: &Runtime<T, Extras>, name: String) -> Embed {
|
||||||
EmbedBuilder::new()
|
EmbedBuilder::new()
|
||||||
.color(COLOR_RED)
|
.color(COLOR_RED)
|
||||||
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
.author(EmbedAuthorBuilder::new(rt.app.name.clone()).icon_url(ImageSource::url(images::get_avatar_url(
|
||||||
rt.app.bot.as_ref().unwrap().avatar,
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
rt.app.bot.as_ref().unwrap().id.get(),
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
))
|
))
|
||||||
.unwrap()))
|
.unwrap()))
|
||||||
.title("That's not a command")
|
.title("That's not a command")
|
||||||
@@ -126,6 +136,7 @@ pub(super) fn get_command_not_found_embed<T>(rt: Arc<Runtime<T>>, name: String)
|
|||||||
rt.app.bot.as_ref().unwrap().avatar,
|
rt.app.bot.as_ref().unwrap().avatar,
|
||||||
rt.app.bot.as_ref().unwrap().id.get(),
|
rt.app.bot.as_ref().unwrap().id.get(),
|
||||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod embeds;
|
pub mod embeds;
|
||||||
|
pub mod tasks;
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
use twilight_model::gateway::payload::outgoing::UpdatePresence;
|
||||||
|
use twilight_model::gateway::presence::{Activity, ActivityType, Status};
|
||||||
|
|
||||||
|
use crate::extras::Extras;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
use crate::framework::tasks::Task;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UpdatePresenceTask;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Task<State, Extras> for UpdatePresenceTask {
|
||||||
|
fn delay(&self) -> std::time::Duration {
|
||||||
|
std::time::Duration::from_secs(120)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, rt: &Runtime<State, Extras>) -> anyhow::Result<()> {
|
||||||
|
let presences = vec![
|
||||||
|
"Billie Jean is not my lover",
|
||||||
|
"Baby vamo' a buscar una excusa para vernos",
|
||||||
|
"Vi que estaba sola y que queria bellaquear",
|
||||||
|
"Esto es real gangsta love",
|
||||||
|
"Dice que está acompañada, pero sé que en el fondo ella se encuentra sola",
|
||||||
|
"Conmigo todo eso ignora, bailando se van las horas",
|
||||||
|
"Me dice 'I wanna know you more', pero no logro entenderte",
|
||||||
|
"Tiene una amiga que le traduce, ella sola se lo introduce",
|
||||||
|
"Y aunque intentara olvidarte, mi boca no lo haría",
|
||||||
|
"Qué más da si besarte es tan fácil",
|
||||||
|
"Quería olvidarte, no lo logré, pero casi",
|
||||||
|
"Tú no me dejas en paz, de mi mente no te vas",
|
||||||
|
"No me busque' en Instagram, mami, búscame en casa",
|
||||||
|
"Dime dónde tú está', que yo por ti cojo un vuelo, y a Yonaguni le llego",
|
||||||
|
"Masivo, we run the world (da-ddy)",
|
||||||
|
"Vamos cógele el ritmo, cintura, rodilla al piso",
|
||||||
|
"Oh-eh-oh, mano arriba, no la dejes caer",
|
||||||
|
"Y esto está como, como pa, como pa, como pa rumbiar, pa pasarla caliente, con toda mi gente",
|
||||||
|
"Nos favorece el sol, tu bronceado y el calor",
|
||||||
|
"Porque hoy hace caloooor, hace un re caloooor",
|
||||||
|
"Tan bonita como tu, tan bonita y sola",
|
||||||
|
"Te he visto antes, te pareces al amor de mi vida",
|
||||||
|
"Tú está' como pa' mete-, pa' meterte el cora'",
|
||||||
|
"Whine up, whine up, whine up, mami baila, baila-a",
|
||||||
|
"Esto es pa que baile'-e'-e', mueve la cintura y dale pa'l suelo",
|
||||||
|
"Para todo' eso' amore que, que fueron obligados a ser separados",
|
||||||
|
"Dime ¿Cómo le explico a mi destino que ya no estás ahí?",
|
||||||
|
"Eres la niña de mis ojos, tú, eres todo lo que quiero yo",
|
||||||
|
"Las tentacione' así como tú merecen pecado' como yo",
|
||||||
|
"Ay, tienes la magia, tú sí tienes una vainita que a mí me encanta, me enloquece",
|
||||||
|
"Ay tú no me dices que sí, que sí que sí que sí",
|
||||||
|
"Si tú me dices ahorita que me quieres a tu la'o que lindo sería",
|
||||||
|
"La gente me dice que ya es muy obvio que ya se me nota",
|
||||||
|
"Si tú supieras que por ti me muero, que yo te quiero",
|
||||||
|
"Si tú supieras lo que te esperado, que yo te amo",
|
||||||
|
"Y me dijeron que te vieron sola ¿Por qué estás sola?",
|
||||||
|
"Ella está recién soltera, de hace poquito",
|
||||||
|
"El pelo se lo suelta, el vestido cortito",
|
||||||
|
"Si yo no te escribo, tú no me escribe'",
|
||||||
|
"Negra, rara vez te vi bien tando con aquel",
|
||||||
|
"Te estoy viendo de hace un tiempo, 'toy esperando el momento, ma', no paro de pensarte, ¿qué le voy a hacer?",
|
||||||
|
"Antes que te vayas dame un beso",
|
||||||
|
"C'mon (Oh oh oh oh oh oh), Daddy (Oh oh oh oh oh oh)",
|
||||||
|
"Y el día está soleao, te quiero de mi lao",
|
||||||
|
"Baby, te vo'a dar una vuelta por la playita",
|
||||||
|
"Aquella noche que volviste, llorando me convenciste",
|
||||||
|
"Malo, malo, pero malo",
|
||||||
|
"Goin' deep in with me for no reason",
|
||||||
|
"It ain't no problem, it's cool to do it in the night",
|
||||||
|
"You give me your love, but it's borrowed",
|
||||||
|
"Y no colme la paciencia si no quieres tu demencia",
|
||||||
|
"Tengo su criptonita, y para sus mujeres les tengo una vitamina",
|
||||||
|
"Sólo somos tú y yo",
|
||||||
|
"Llegó el tiempo de los dos",
|
||||||
|
"De verdad tienes lo que me gusta a mí",
|
||||||
|
"Al ritmo y al movimiento, le ponemos sentimiento",
|
||||||
|
"Tú eres hielo, yo soy fuego, vamo' a ver quién se va a quemar",
|
||||||
|
"Bailando así pidiendo más y más calor",
|
||||||
|
"Rumba pa' que tú abuses, BUM!",
|
||||||
|
"Nueva temporada ya empezó, dale play",
|
||||||
|
"Sonido del bajo, Bizarrap, suena el bass",
|
||||||
|
"Si me preguntan, a nadie yo le debo",
|
||||||
|
"Ahora estoy buscando algo más, una razón para volverme a enamorar (puede que seas tú)",
|
||||||
|
"Quiero una chica, quiero una gyal",
|
||||||
|
"Que-que-que no diga que no, que no, que no, que no, que no, que",
|
||||||
|
"Me monté en un barco, he cruzado el mar, he subido el río",
|
||||||
|
"Le digo hola, ella me dice goodbye",
|
||||||
|
"Si yo le salgo por la izquierda, se va pa' la derecha",
|
||||||
|
"No se lo que le pasa, conmigo ella no quiere bailar",
|
||||||
|
"Yo quería decirle que me encanta",
|
||||||
|
"Una varita diseño su cuerpo",
|
||||||
|
"Y la verdad, si está mas buena, explota",
|
||||||
|
"Baby yo llevo tiempo deseandote aquí",
|
||||||
|
"Tiraré la casa por la ventana",
|
||||||
|
"Y no te importa cuantos caigan por error",
|
||||||
|
"Presiento que tu siempre vas y vienes",
|
||||||
|
"Presiento que serás de esos errores que estoy dispuesto a cometer",
|
||||||
|
"Podré vivir sin escuchar tu voz",
|
||||||
|
"Bebé, me enteré que con ella peleaste",
|
||||||
|
"Y a estas alturas no importa si estás con él",
|
||||||
|
"La luna llena alumbrandote",
|
||||||
|
"Cuando te vi, supe que iba a pasar",
|
||||||
|
"Que me gustas mucho con bastante",
|
||||||
|
"En la radio, tu song favorito",
|
||||||
|
"Como mango y limón, saborearte",
|
||||||
|
"Tú eres la dulce fruta que mi paladar añora",
|
||||||
|
"Esa cara de ternura sé que esconde una atrevida",
|
||||||
|
"Teresa es una asesina, que cuando camina la ven y dicen oooh",
|
||||||
|
"Y no le importa si el novio le corta, se corta una botella y se va para el club",
|
||||||
|
"Essa novinha é terrorista, é especialista",
|
||||||
|
"Olha o que ela faz num baile funk com as amiga",
|
||||||
|
"Cara de doncella, pero mas bestia que bella",
|
||||||
|
"Si me avisa en cinco estoy, porque sabe como soy",
|
||||||
|
"Su gato no le da, entonces me llama y voy",
|
||||||
|
"Se te nota que quieres de mi boca",
|
||||||
|
"Si hay rumba no quiero mas na'",
|
||||||
|
"Yo lo que vine es a pasarla bien",
|
||||||
|
"Ella no e' tuya, te vendió sueño, ey, dice que no tiene dueño",
|
||||||
|
"Te está haciendo lucir mal, manito; yo que tú, ruedo",
|
||||||
|
"Los besos no se dan, se roban",
|
||||||
|
"Yo tengo tu amor, I got your love",
|
||||||
|
"Como ríe la vida si tus ojos negros me quieren mirar",
|
||||||
|
"El día que me quieras la rosa que engalana se vestirá de fiesta con su mejor color",
|
||||||
|
"Luciérnagas curiosas que verán que eres mi consuelo",
|
||||||
|
"DANZA KUDUROOOOOO",
|
||||||
|
"La mano arriba, cintura sola, da media vuelta, danza kuduro",
|
||||||
|
"¿Quién puede domar la fuerza del mal que se mete por tus vena'?",
|
||||||
|
"¿Quién puede parar eso que al bailar descontrola tu cadera?",
|
||||||
|
"Hey DJ, póngale la música que le gusta",
|
||||||
|
"Y cómo hago con mi mente pa' decirle que los momentos que tuvimos se quedan en la nada",
|
||||||
|
"No necesito quejas, sino contención",
|
||||||
|
"Cómo hago pa' mentirme diciéndome que no quiero verte aunque extrañe tu cara",
|
||||||
|
"Y no puedo sacarte de mi mente, y no quiero hablar con otra gente",
|
||||||
|
"Y si tengo que escoger, me quedo, me quedo contigo",
|
||||||
|
"Y si yo vuelvo a San Juan, yo bailo, yo bailo contigo",
|
||||||
|
"Y si te tengo que olvidar, tú sabe' que no va a pasar",
|
||||||
|
"Contigo desde Alaska hasta Buenos Aires",
|
||||||
|
"There on the subway, I nearly had a breakdown",
|
||||||
|
"Tú no fuiste la primera, tampoco serás la última",
|
||||||
|
"He giving me that good shit, that make me not quit",
|
||||||
|
"Oh, he give it to me everyday, everyday, everyday",
|
||||||
|
"You just want attention, you don't want my heart",
|
||||||
|
"Maybe you just hate the thought of me with someone new",
|
||||||
|
"Y la vi, que va a ser, una hermosa lucifer",
|
||||||
|
"Y es que este amor es azul como el mar azul",
|
||||||
|
"Como de tu mirada nació mi ilusión",
|
||||||
|
"Fue una mañana que yo te encontré, cuando la brisa besaba tu dulce piel",
|
||||||
|
"Tus ojos tristes que al ver adoré, la noche que yo te amé, azul",
|
||||||
|
"Hoy miro al cielo y en ti puedo ver, la estrella que siempre soñé (azul)",
|
||||||
|
"Eres la niña que siempre busqué, azul",
|
||||||
|
"Es tu inocencia que quiero entender",
|
||||||
|
"Y con la lluvia pintada de azul, por siempre serás solo tú",
|
||||||
|
"Azul como el lucero de nuestra pasión",
|
||||||
|
"I heard the music through the walls",
|
||||||
|
"I felt you here before I even saw your face",
|
||||||
|
"Though we can't get higher",
|
||||||
|
"But we keep going up, going up, going up, going up, going up",
|
||||||
|
"They could describe everything with one single word",
|
||||||
|
"Eres lo primero que yo pienso cuando alguien me dice 'pide un deseo'",
|
||||||
|
"Tengamos algo sin mentiras",
|
||||||
|
"Y ya sé que estás aburrida de promesas incumplidas",
|
||||||
|
"Quiero que tu seas mi princesa",
|
||||||
|
"I wanna let my hair down while I'm staring down your face",
|
||||||
|
"Que lindo que te queda ese look bronceado",
|
||||||
|
"Si tú me bailas así yo me muero de amor",
|
||||||
|
"Boy, don't make me choose",
|
||||||
|
"We can share one seat",
|
||||||
|
"I think you wanna uh",
|
||||||
|
"I think you know what this is",
|
||||||
|
"Pretty blue streetlights",
|
||||||
|
"In a room full of people I look at you",
|
||||||
|
"Somos dos desconocidos que se quieren conocer muy bien",
|
||||||
|
"Frequentemos nuestros cuerpos para ver que tal nos va",
|
||||||
|
"Nena, yo quiero con usted ser caballero",
|
||||||
|
"Deja que pase, pues que pase, lo que tenga que pasar",
|
||||||
|
"Todo casual, nada ideal, medio eventual, casi ilegal",
|
||||||
|
"Mami, long time sin saber de ti",
|
||||||
|
"Un brindis, vasitos arriba, y tú te pones loquita, mamita",
|
||||||
|
"Estoy ansioso por estar solos tú y yo",
|
||||||
|
"Good girl, I know you want it",
|
||||||
|
"I know you want it, you're a good girl (oh yeah)",
|
||||||
|
"Baby, it's in your nature (meow)",
|
||||||
|
"The way you grab me, must wanna get nasty",
|
||||||
|
"What do they make dreams for?",
|
||||||
|
"Ella sale por deporte, por la' noches",
|
||||||
|
"Sé muy dentro que yo sin ti estoy mejor",
|
||||||
|
"Oye, escucha lo que tengo que decir",
|
||||||
|
"Que sea así nomá y en no má' de seis tomas",
|
||||||
|
"Ya no me seduce la que se produce toda",
|
||||||
|
"Creo que me aburre lo que hoy produce la moda",
|
||||||
|
"Tu sonrisa rota me trastorna todavía",
|
||||||
|
"Vuelvo por necesidad de algo real y recíproco",
|
||||||
|
"Yo lo sentí en un segundo, me enamoré de primero",
|
||||||
|
"Tú te quedaste callada, y yo te dije te quiero",
|
||||||
|
"Imaginando de esos besos tuyos, porque no tengo de esos besos tuyos",
|
||||||
|
"Y mientras me enamoraba dijiste que había un tercero",
|
||||||
|
"Y aunque sé bien que si no hay otra voy a buscarte en otra boca",
|
||||||
|
"Como te extraño nena, no sabés",
|
||||||
|
"Me siento fuerte y débil a la vez",
|
||||||
|
"Quizá eso sea el amor",
|
||||||
|
"Dímelo si el cabrón no vale la pena y lo borramos de la escena",
|
||||||
|
"Confiesalo",
|
||||||
|
"Seeing you tonight, it's a bad idea, right?",
|
||||||
|
"You know what would be really fucking cool on this? Sabrina",
|
||||||
|
"She was covered in leather and gold",
|
||||||
|
"Little Miss Perfect sitting at the train stop",
|
||||||
|
"She's, somewhere, somewhere, somewhere in Brooklyn",
|
||||||
|
"Before I got her name, along came a train",
|
||||||
|
"Next stop, Brooklyn",
|
||||||
|
"Y si te invito a una copa, y me acerco a tu boca",
|
||||||
|
"Y si te robo un besito, a ver, ¿te enojas conmigo?",
|
||||||
|
"Una aventura es más divertida si huele a peligro",
|
||||||
|
"That's when the taxi man turned on the radio and a Jay-Z song was on",
|
||||||
|
"I'm nodding my head like, yeah, moving my hips like, yeah",
|
||||||
|
"Yeah, it's a party in the U.S.A.",
|
||||||
|
"Just shoot for the stars if it feels right",
|
||||||
|
"And aim for my heart if you feel like it",
|
||||||
|
"You say I'm a kid, my ego is big, I don't give a shit",
|
||||||
|
"Cuando se apaga la luz",
|
||||||
|
"Annie, are you ok?",
|
||||||
|
"Will you tell us that you're ok?",
|
||||||
|
"Life's like a road that you travel on",
|
||||||
|
"Life is a highway, I wanna ride it all night long",
|
||||||
|
"I wanna drive it all night long (all night long)",
|
||||||
|
"Tu cuerpo y el mío, llenando el vacío, subiendo y bajando",
|
||||||
|
"Yo quiero estar contigo, vivir contigo, bailar contigo, tener contigo una noche loca",
|
||||||
|
"But let me stop you there, oh, before you speak",
|
||||||
|
"Nah to the ah to the, no, no, no",
|
||||||
|
"My name is no, my sign is no, my number is no",
|
||||||
|
"If that boy ain't giving up, lick your lips and swing your hips",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut rng = ChaCha20Rng::from_os_rng();
|
||||||
|
let index = rng.random_range(0..presences.len());
|
||||||
|
|
||||||
|
rt.gateway_tx.send(UpdatePresence::new(
|
||||||
|
vec![Activity {
|
||||||
|
application_id: None,
|
||||||
|
assets: None,
|
||||||
|
buttons: vec![],
|
||||||
|
created_at: None,
|
||||||
|
details: None,
|
||||||
|
emoji: None,
|
||||||
|
flags: None,
|
||||||
|
id: None,
|
||||||
|
instance: None,
|
||||||
|
kind: ActivityType::Listening,
|
||||||
|
name: format!(
|
||||||
|
"p.help - {} - {}/{}",
|
||||||
|
presences[index], rt.shard_number, rt.shard_total
|
||||||
|
),
|
||||||
|
party: None,
|
||||||
|
secrets: None,
|
||||||
|
state: None,
|
||||||
|
timestamps: None,
|
||||||
|
url: None,
|
||||||
|
}],
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
Status::Online,
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+474
-50
@@ -1,13 +1,27 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use deadpool_redis::redis::AsyncTypedCommands;
|
||||||
use scc::HashSet;
|
use scc::HashSet;
|
||||||
|
use time::UtcDateTime;
|
||||||
use twilight_model::id::Id;
|
use twilight_model::id::Id;
|
||||||
|
|
||||||
use crate::{
|
use crate::database::models::{Call, Channel, Participant};
|
||||||
database::models::{Call, Channel},
|
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||||
framework::commands::{Command, CommandError, context::CommandContext},
|
use crate::extras::Extras;
|
||||||
state::State,
|
use crate::framework::commands::arguments::{Arg, Argument, ParseArguments};
|
||||||
utils::Bye,
|
use crate::framework::commands::context::CommandContext;
|
||||||
|
use crate::framework::commands::permissions::Permission;
|
||||||
|
use crate::framework::commands::{Command, CommandError};
|
||||||
|
use crate::groups::calls::masks::{get_mask_display_name, get_mask_emoji};
|
||||||
|
use crate::groups::calls::permissions::{
|
||||||
|
IsCallParticipantPermission, NonNewAccountPermission, NotBannedPermission,
|
||||||
};
|
};
|
||||||
|
use crate::groups::calls::strings::{
|
||||||
|
get_string_call_answered, get_string_call_hangup_other_party,
|
||||||
|
get_string_call_hangup_this_party, get_string_call_no_answer, get_string_call_searching,
|
||||||
|
get_string_tips,
|
||||||
|
};
|
||||||
|
use crate::state::State;
|
||||||
|
use crate::utils::Bye;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct CallCommand {
|
pub struct CallCommand {
|
||||||
@@ -27,7 +41,7 @@ impl CallCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Command<State> for CallCommand {
|
impl Command<State, Extras> for CallCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"call"
|
"call"
|
||||||
}
|
}
|
||||||
@@ -36,9 +50,16 @@ impl Command<State> for CallCommand {
|
|||||||
&["c"]
|
&["c"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
||||||
|
vec![
|
||||||
|
Box::new(NotBannedPermission),
|
||||||
|
Box::new(NonNewAccountPermission),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
ctx: CommandContext<'_, State>,
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
_args: &str,
|
_args: &str,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
let channel = Channel::get_or_create_by_id(
|
let channel = Channel::get_or_create_by_id(
|
||||||
@@ -49,8 +70,7 @@ impl Command<State> for CallCommand {
|
|||||||
.await
|
.await
|
||||||
.inspect_err(|e| {
|
.inspect_err(|e| {
|
||||||
log::error!("Failed to get or create channel: {e:?}");
|
log::error!("Failed to get or create channel: {e:?}");
|
||||||
})
|
})?;
|
||||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
|
||||||
|
|
||||||
if channel.is_searching || self.channel_lock.contains(&ctx.event.channel_id.get()) {
|
if channel.is_searching || self.channel_lock.contains(&ctx.event.channel_id.get()) {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
@@ -58,8 +78,7 @@ impl Command<State> for CallCommand {
|
|||||||
"☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.",
|
"☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.",
|
||||||
ctx.prefix
|
ctx.prefix
|
||||||
))
|
))
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -68,41 +87,213 @@ impl Command<State> for CallCommand {
|
|||||||
ctx.send()
|
ctx.send()
|
||||||
.content(format!("☎️ You are already on a call! Use `{}hangup` to hang up the phone, or `{}skip` to skip to the next call.", ctx.prefix, ctx.prefix))
|
.content(format!("☎️ You are already on a call! Use `{}hangup` to hang up the phone, or `{}skip` to skip to the next call.", ctx.prefix, ctx.prefix))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.channel_lock
|
self.channel_lock.insert(ctx.event.channel_id.get()).bye();
|
||||||
.insert_async(ctx.event.channel_id.get())
|
|
||||||
.await
|
|
||||||
.bye();
|
|
||||||
|
|
||||||
let call = Call::start(&channel, &ctx.rt.state.db)
|
let call = Call::start(&channel, &ctx.rt.state.db)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| {
|
.inspect_err(|e| {
|
||||||
log::error!("Failed to start call: {e:?}");
|
log::error!("Failed to start call: {e:?}");
|
||||||
self.channel_lock.remove(&ctx.event.channel_id.get());
|
self.channel_lock.remove(&ctx.event.channel_id.get());
|
||||||
})
|
})?;
|
||||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
|
||||||
|
|
||||||
self.channel_lock
|
self.channel_lock.remove(&ctx.event.channel_id.get());
|
||||||
.remove_async(&ctx.event.channel_id.get())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(call) = call {
|
if let Some(call) = call {
|
||||||
ctx.send().content("☎️ Call connected. Say hi!").await.bye();
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"**TIP:** {}\n☎️ {}\n☎️ {}",
|
||||||
|
get_string_tips(ctx.prefix),
|
||||||
|
get_string_call_searching(ctx.prefix),
|
||||||
|
get_string_call_answered(ctx.prefix)
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.bye();
|
||||||
|
|
||||||
ctx.rt
|
ctx.rt
|
||||||
.http
|
.http
|
||||||
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||||
.content("☎️ Call connected. Say hi!")
|
.content(&format!("☎️ {}", get_string_call_answered(ctx.prefix)))
|
||||||
.await
|
.await
|
||||||
.bye();
|
.bye();
|
||||||
|
|
||||||
|
let mut redis = ctx.rt.state.redis.get().await?;
|
||||||
|
|
||||||
|
let timestamp = UtcDateTime::now().unix_timestamp();
|
||||||
|
|
||||||
|
redis
|
||||||
|
.zadd(
|
||||||
|
"active-calls",
|
||||||
|
format!("{}:{}", call.id, channel.id),
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
redis
|
||||||
|
.zadd(
|
||||||
|
"active-calls",
|
||||||
|
format!("{}:{}", call.id, call.other_party_channel_id(channel.id)),
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.content("☎️ Searching for a call...")
|
.content(format!(
|
||||||
|
"**TIP:** {}\n☎️ {}",
|
||||||
|
get_string_tips(ctx.prefix),
|
||||||
|
get_string_call_searching(ctx.prefix)
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SkipCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State, Extras> for SkipCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"skip"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["s", "next"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
||||||
|
vec![
|
||||||
|
Box::new(NonNewAccountPermission),
|
||||||
|
Box::new(NotBannedPermission),
|
||||||
|
Box::new(IsCallParticipantPermission),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
|
_args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let channel = Channel::get_or_create_by_id(
|
||||||
|
ctx.event.channel_id.get(),
|
||||||
|
&ctx.rt.state.db,
|
||||||
|
&ctx.rt.http,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to get or create channel: {e:?}");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if channel.call_id.is_none() {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.is_searching {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"☎️ You're already searching for a call! Use `{}hangup` to cancel the search.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let call = Call::get_by_id(channel.call_id.unwrap(), &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))?
|
||||||
|
.ok_or(CommandError::DatabaseInconsistentState)?;
|
||||||
|
|
||||||
|
call.hangup(&ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?;
|
||||||
|
|
||||||
|
let other_channel_id = call.other_party_channel_id(ctx.event.channel_id.get());
|
||||||
|
|
||||||
|
ctx.rt
|
||||||
|
.http
|
||||||
|
.create_message(Id::new(other_channel_id))
|
||||||
|
.content(&format!(
|
||||||
|
"☎️ {}",
|
||||||
|
get_string_call_hangup_other_party(ctx.prefix)
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to send message to other party: {e:?}");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut redis = ctx.rt.state.redis.get().await?;
|
||||||
|
|
||||||
|
let channel_keys = [
|
||||||
|
format!("{}:{}", call.id, call.channel_ids[0]),
|
||||||
|
format!("{}:{}", call.id, call.channel_ids[1]),
|
||||||
|
];
|
||||||
|
|
||||||
|
redis
|
||||||
|
.zrem("active-calls", &channel_keys)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}"))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let call = Call::start(&channel, &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to start (skipped) call: {e:?}");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(call) = call {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"**TIP:** {}\n☎️ Skipped! Searching for a call...\n☎️ {}",
|
||||||
|
get_string_tips(ctx.prefix),
|
||||||
|
get_string_call_answered(ctx.prefix)
|
||||||
|
))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
.bye();
|
||||||
|
ctx.rt
|
||||||
|
.http
|
||||||
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||||
|
.content(&format!("☎️ {}", get_string_call_answered(ctx.prefix)))
|
||||||
|
.await
|
||||||
|
.bye();
|
||||||
|
|
||||||
|
let mut redis = ctx.rt.state.redis.get().await?;
|
||||||
|
|
||||||
|
let timestamp = UtcDateTime::now().unix_timestamp();
|
||||||
|
|
||||||
|
redis
|
||||||
|
.zadd(
|
||||||
|
"active-calls",
|
||||||
|
format!("{}:{}", call.id, channel.id),
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
redis
|
||||||
|
.zadd(
|
||||||
|
"active-calls",
|
||||||
|
format!("{}:{}", call.id, call.other_party_channel_id(channel.id)),
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"**TIP:** {}\n☎️ Skipped! Searching for a call...",
|
||||||
|
get_string_tips(ctx.prefix)
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -113,7 +304,7 @@ impl Command<State> for CallCommand {
|
|||||||
pub struct HangupCommand;
|
pub struct HangupCommand;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Command<State> for HangupCommand {
|
impl Command<State, Extras> for HangupCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"hangup"
|
"hangup"
|
||||||
}
|
}
|
||||||
@@ -122,23 +313,29 @@ impl Command<State> for HangupCommand {
|
|||||||
&["h", "hang"]
|
&["h", "hang"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
||||||
|
vec![
|
||||||
|
Box::new(NonNewAccountPermission),
|
||||||
|
Box::new(NotBannedPermission),
|
||||||
|
Box::new(IsCallParticipantPermission),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
ctx: CommandContext<'_, State>,
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
_args: &str,
|
_args: &str,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
let Some(channel) = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db)
|
let Some(mut channel) = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))
|
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))?
|
||||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
|
||||||
else {
|
else {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.content(format!(
|
.content(format!(
|
||||||
"❌ You're not on a call! Use `{}call` to start one.",
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
ctx.prefix
|
ctx.prefix
|
||||||
))
|
))
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
@@ -149,52 +346,73 @@ impl Command<State> for HangupCommand {
|
|||||||
.await
|
.await
|
||||||
.inspect_err(|e| {
|
.inspect_err(|e| {
|
||||||
log::error!("Failed to cancel search: {e:?}");
|
log::error!("Failed to cancel search: {e:?}");
|
||||||
})
|
})?;
|
||||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
|
||||||
|
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.content("☎️ The phone was hung up.")
|
.content(format!("☎️ {}", get_string_call_no_answer(ctx.prefix)))
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(call_id) = channel.call_id {
|
if let Some(call_id) = channel.call_id {
|
||||||
|
let mut redis = ctx.rt.state.redis.get().await?;
|
||||||
|
|
||||||
let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db)
|
let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))
|
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))?
|
||||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
|
||||||
else {
|
else {
|
||||||
ctx.send()
|
ctx.send().content("❌ The call could not be found.\n-# This is not supposed to happen. If the issue persists, contact a moderator in the official server.").await?;
|
||||||
.content("❌ Call not found.")
|
|
||||||
|
channel.call_id = None;
|
||||||
|
channel.upsert(&ctx.rt.state.db).await.bye();
|
||||||
|
|
||||||
|
redis
|
||||||
|
.zrem("active-calls", format!("{}:{}", call_id, channel.id))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
.bye();
|
||||||
|
|
||||||
return Err(CommandError::DatabaseInconsistentState);
|
return Err(CommandError::DatabaseInconsistentState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let channel_keys = [
|
||||||
|
format!("{call_id}:{}", call.channel_ids[0]),
|
||||||
|
format!("{call_id}:{}", call.channel_ids[1]),
|
||||||
|
];
|
||||||
|
|
||||||
|
redis
|
||||||
|
.zrem("active-calls", &channel_keys)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}"))
|
||||||
|
.ok();
|
||||||
|
|
||||||
call.hangup(&ctx.rt.state.db)
|
call.hangup(&ctx.rt.state.db)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))
|
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?;
|
||||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
|
||||||
|
|
||||||
ctx.send().content("☎️ You hung up the phone.").await.bye();
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"☎️ {}",
|
||||||
|
get_string_call_hangup_this_party(ctx.prefix)
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.bye();
|
||||||
|
|
||||||
ctx.rt
|
ctx.rt
|
||||||
.http
|
.http
|
||||||
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||||
.content("☎️ The other party hung up the phone.")
|
.content(&format!(
|
||||||
.await
|
"☎️ {}",
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
get_string_call_hangup_other_party(ctx.prefix)
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
ctx.send()
|
ctx.send()
|
||||||
.content(format!(
|
.content(format!(
|
||||||
"❌ You're not on a call! Use `{}call` to start one.",
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
ctx.prefix
|
ctx.prefix
|
||||||
))
|
))
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -202,3 +420,209 @@ impl Command<State> for HangupCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FriendRequestCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State, Extras> for FriendRequestCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"friendreq"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["f", "fr", "friendrequest"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Shows your username to the other party and asks them to send you a friend request on Discord."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
||||||
|
vec![
|
||||||
|
Box::new(NonNewAccountPermission),
|
||||||
|
Box::new(NotBannedPermission),
|
||||||
|
Box::new(IsCallParticipantPermission),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
|
_args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let channel = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))?;
|
||||||
|
|
||||||
|
let Some(channel) = channel else {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let call = if let Some(call_id) = channel.call_id {
|
||||||
|
Call::get_by_id(call_id, &ctx.rt.state.db)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))?
|
||||||
|
} else {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"❌ You're not on a call! Use `{}call` to start one.",
|
||||||
|
ctx.prefix
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(call) = call else {
|
||||||
|
ctx.send()
|
||||||
|
.content("❌ Oops! We could not find that call.")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Err(CommandError::DatabaseInconsistentState);
|
||||||
|
};
|
||||||
|
|
||||||
|
let other_channel_id = call.other_party_channel_id(channel.id);
|
||||||
|
|
||||||
|
let Some(participant) = Participant::get_for_user_and_channel_in_call(
|
||||||
|
ctx.event.author.id.get(),
|
||||||
|
ctx.event.channel_id.get(),
|
||||||
|
call.id,
|
||||||
|
&ctx.rt.state.db,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
ctx.send()
|
||||||
|
.content("❌ You haven't even said hi! Who are you trying to add?")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx
|
||||||
|
.rt
|
||||||
|
.http
|
||||||
|
.create_message(
|
||||||
|
Id::new(other_channel_id)
|
||||||
|
)
|
||||||
|
.content(
|
||||||
|
&format!(
|
||||||
|
"📬 **{}** wants you to send them a friend request on Discord! Their username is `{}`.",
|
||||||
|
participant.display_name().sanitize(false),
|
||||||
|
ctx.event.author.name.sanitize(false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
?;
|
||||||
|
|
||||||
|
ctx.send()
|
||||||
|
.content("📬 Asked the other party to send you a friend request!")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MaskCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command<State, Extras> for MaskCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"mask"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aliases(&self) -> &'static [&'static str] {
|
||||||
|
&["m"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Hides your profile from the other party by replacing your profile with a random animal. This command's preferences persist across calls.\n\nNote: Using this command without `on/off` will display your current mask status, without making any changes to it."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arguments(&self) -> &'static [Argument] {
|
||||||
|
&[Argument {
|
||||||
|
name: "on/off",
|
||||||
|
is_optional: true,
|
||||||
|
is_greedy: false,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
mut ctx: CommandContext<'_, State, Extras>,
|
||||||
|
args: &str,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let Arg(toggle) = match Arg::<Option<bool>>::parse(args, &ctx).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
ctx.send()
|
||||||
|
.content(format!(
|
||||||
|
"❓ Are you sure that's correct? Use `{}mask on` and `{}mask off`, or just `{}mask` to toggle it.",
|
||||||
|
ctx.prefix, ctx.prefix, ctx.prefix
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mask_verb: &'static str;
|
||||||
|
|
||||||
|
// If toggle is Some, update user mask preference
|
||||||
|
if let Some(toggle) = toggle {
|
||||||
|
ctx.extras.user.is_masked = toggle;
|
||||||
|
ctx.extras.user.insert(&ctx.rt.state.db).await?;
|
||||||
|
|
||||||
|
mask_verb = "was";
|
||||||
|
} else {
|
||||||
|
mask_verb = "is";
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask_status = if ctx.extras.user.is_masked {
|
||||||
|
"on"
|
||||||
|
} else {
|
||||||
|
"off"
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db).await?;
|
||||||
|
let mut mask_message = format!(
|
||||||
|
"🎭 Your mask {mask_verb} set to `{}`. You can toggle your mask on or off using `{}mask on` and `{}mask off`.",
|
||||||
|
mask_status, ctx.prefix, ctx.prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(channel) = channel
|
||||||
|
&& let Some(call_id) = channel.call_id
|
||||||
|
&& let Some(mut participant) = Participant::get_for_user_and_channel_in_call(
|
||||||
|
ctx.extras.user.id,
|
||||||
|
ctx.event.channel_id.get(),
|
||||||
|
call_id,
|
||||||
|
&ctx.rt.state.db,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
participant.is_masked = ctx.extras.user.is_masked;
|
||||||
|
participant.upsert(&ctx.rt.state.db).await?;
|
||||||
|
|
||||||
|
if ctx.extras.user.is_masked {
|
||||||
|
mask_message = format!(
|
||||||
|
"🎭 Your mask {mask_verb} set to `on`. You're currently being displayed as **{} {}**. You can toggle your mask on or off using `{}mask on` and `{}mask off`.",
|
||||||
|
get_mask_emoji(&participant.user_mask).expect("User's mask did not exist"),
|
||||||
|
get_mask_display_name(&participant.user_mask),
|
||||||
|
ctx.prefix,
|
||||||
|
ctx.prefix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.send().content(mask_message).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::extras::Extras;
|
||||||
|
use crate::framework::interactions::components::Button;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CodeOfConductButton;
|
||||||
|
|
||||||
|
impl Button<State, Extras> for CodeOfConductButton {
|
||||||
|
fn label(&self) -> Option<String> {
|
||||||
|
Some("Code of Conduct".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<String> {
|
||||||
|
Some("https://payphone.lat/code-of-conduct".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NeedHelpButton;
|
||||||
|
|
||||||
|
impl Button<State, Extras> for NeedHelpButton {
|
||||||
|
fn label(&self) -> Option<String> {
|
||||||
|
Some("Need Help?".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<String> {
|
||||||
|
Some("https://discord.gg/9daSsrfTmE".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twilight_model::channel::message::Embed;
|
||||||
|
use twilight_model::oauth::Application;
|
||||||
|
use twilight_util::builder::embed::{
|
||||||
|
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::constants::{COLOR_RED, COLOR_YELLOW};
|
||||||
|
use crate::database::models::Message;
|
||||||
|
use crate::discord::images;
|
||||||
|
use crate::groups::calls::moderation::Rule;
|
||||||
|
use crate::utils::ellipse;
|
||||||
|
|
||||||
|
pub fn first_warning_embed(
|
||||||
|
app: &Application,
|
||||||
|
message: &Message,
|
||||||
|
rules_broken: Vec<Arc<dyn Rule>>,
|
||||||
|
) -> Embed {
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(
|
||||||
|
EmbedAuthorBuilder::new(app.name.clone())
|
||||||
|
.icon_url(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
).unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.title("Warning")
|
||||||
|
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. Read the code of conduct to prevent getting banned.")
|
||||||
|
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||||
|
.field(EmbedFieldBuilder::new(
|
||||||
|
"What rules did you break?",
|
||||||
|
rules_broken
|
||||||
|
.iter()
|
||||||
|
.map(|rule| format!("- {}", rule.name()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
))
|
||||||
|
.color(COLOR_YELLOW)
|
||||||
|
.thumbnail(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
.footer(EmbedFooterBuilder::new("This is a warning because it's your first offense. Breaking the rules again will result in a ban."))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn repeat_warning_embed(
|
||||||
|
app: &Application,
|
||||||
|
message: &Message,
|
||||||
|
rules_broken: Vec<Arc<dyn Rule>>,
|
||||||
|
) -> Embed {
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(
|
||||||
|
EmbedAuthorBuilder::new(app.name.clone())
|
||||||
|
.icon_url(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
).unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.title("Long Time no See!")
|
||||||
|
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. It's not your first offense, yet you've gone for a long time with a good record so we're making this a warning instead. In case you've forgotten the rules, consider this a reminder to re-read them.")
|
||||||
|
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||||
|
.field(EmbedFieldBuilder::new(
|
||||||
|
"What rules did you break?",
|
||||||
|
rules_broken
|
||||||
|
.iter()
|
||||||
|
.map(|rule| format!("- {}", rule.name()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
))
|
||||||
|
.color(COLOR_YELLOW)
|
||||||
|
.thumbnail(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
.footer(EmbedFooterBuilder::new("Breaking the rules again will result in a ban."))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_ban_embed(
|
||||||
|
app: &Application,
|
||||||
|
message: &Message,
|
||||||
|
rules_broken: Vec<Arc<dyn Rule>>,
|
||||||
|
) -> Embed {
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(
|
||||||
|
EmbedAuthorBuilder::new(app.name.clone())
|
||||||
|
.icon_url(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
).unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.title("Banned")
|
||||||
|
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. This is your first offense, and as such, you are being issued a ban. This ban will last for 24 hours, next one will be permanent.")
|
||||||
|
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||||
|
.field(EmbedFieldBuilder::new(
|
||||||
|
"What rules did you break?",
|
||||||
|
rules_broken
|
||||||
|
.iter()
|
||||||
|
.map(|rule| format!("- {}", rule.name()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
))
|
||||||
|
.color(COLOR_RED)
|
||||||
|
.thumbnail(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
.footer(EmbedFooterBuilder::new("Breaking the rules again will result in being permanently banned."))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn final_ban_embed(
|
||||||
|
app: &Application,
|
||||||
|
message: &Message,
|
||||||
|
rules_broken: Vec<Arc<dyn Rule>>,
|
||||||
|
) -> Embed {
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(
|
||||||
|
EmbedAuthorBuilder::new(app.name.clone())
|
||||||
|
.icon_url(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
).unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.title("Permanently Banned")
|
||||||
|
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. This is your final offense, and as such, you are being issued a permanent ban.")
|
||||||
|
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||||
|
.field(EmbedFieldBuilder::new(
|
||||||
|
"What rules did you break?",
|
||||||
|
rules_broken
|
||||||
|
.iter()
|
||||||
|
.map(|rule| format!("- {}", rule.name()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
))
|
||||||
|
.color(COLOR_RED)
|
||||||
|
.thumbnail(
|
||||||
|
ImageSource::url(
|
||||||
|
images::get_avatar_url(
|
||||||
|
app.icon,
|
||||||
|
app.bot.as_ref().unwrap().id.get(),
|
||||||
|
app.bot.as_ref().unwrap().discriminator as u64,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
).unwrap()
|
||||||
|
)
|
||||||
|
.footer(EmbedFooterBuilder::new("You broke the rules 3 times."))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
+569
-60
@@ -1,30 +1,120 @@
|
|||||||
|
use std::error::Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
use deadpool_redis::redis::AsyncTypedCommands;
|
||||||
use twilight_validate::request::webhook_username;
|
use itertools::Itertools;
|
||||||
|
use time::{Duration, UtcDateTime};
|
||||||
use crate::{
|
use twilight_http::api_error::ApiError;
|
||||||
database::models::{Call, Channel},
|
use twilight_http::error::ErrorType;
|
||||||
discord::images,
|
use twilight_model::gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate};
|
||||||
framework::{
|
use twilight_model::id::Id;
|
||||||
events::traits::{EventResult, OnMessageHandler},
|
use twilight_util::builder::embed::{
|
||||||
runtime::Runtime,
|
EmbedAuthorBuilder, EmbedBuilder, EmbedFooterBuilder, ImageSource,
|
||||||
},
|
|
||||||
groups::calls::moderation::escape_message,
|
|
||||||
state::State,
|
|
||||||
};
|
};
|
||||||
|
use twilight_util::snowflake::Snowflake;
|
||||||
|
|
||||||
|
use crate::constants::{COLOR_WHITE, COLOR_YELLOW};
|
||||||
|
use crate::database::models::{Call, Channel, Message, Participant, User, UserIncident};
|
||||||
|
use crate::discord::images;
|
||||||
|
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||||
|
use crate::extras::Extras;
|
||||||
|
use crate::framework::commands::extras::ExtrasGetter;
|
||||||
|
use crate::framework::events::traits::{
|
||||||
|
EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
|
||||||
|
};
|
||||||
|
use crate::framework::interactions::components::ActionRow;
|
||||||
|
use crate::framework::runtime::Runtime;
|
||||||
|
use crate::groups::calls::components::{CodeOfConductButton, NeedHelpButton};
|
||||||
|
use crate::groups::calls::masks::{get_mask_avatar_url, get_mask_display_name};
|
||||||
|
use crate::groups::calls::moderation::{Action, Rule, escape_message, moderate_message};
|
||||||
|
use crate::groups::calls::{embeds, moderation};
|
||||||
|
use crate::state::State;
|
||||||
|
use crate::utils::{ellipse, i64_to_u64};
|
||||||
|
|
||||||
|
async fn penalize_user(
|
||||||
|
user: User,
|
||||||
|
message: Message,
|
||||||
|
broken_rules: Vec<Arc<dyn Rule>>,
|
||||||
|
rt: &Runtime<State, Extras>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let reason = format!(
|
||||||
|
"You broke the following rules during a call:\n{}",
|
||||||
|
broken_rules
|
||||||
|
.iter()
|
||||||
|
.map(|rule| format!("- {}", rule.name()))
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_incident = user
|
||||||
|
.incident(
|
||||||
|
reason.clone(),
|
||||||
|
Some(message.data.clone()),
|
||||||
|
None,
|
||||||
|
&rt.state.db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let dm_channel = rt
|
||||||
|
.http
|
||||||
|
.create_private_channel(Id::new(user.id))
|
||||||
|
.await?
|
||||||
|
.model()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let embed = if new_incident.is_first_incident {
|
||||||
|
embeds::first_warning_embed(&rt.app, &message, broken_rules)
|
||||||
|
} else if new_incident.counting_incidents == 0 {
|
||||||
|
embeds::repeat_warning_embed(&rt.app, &message, broken_rules)
|
||||||
|
} else if new_incident.counting_incidents == 1 {
|
||||||
|
embeds::first_ban_embed(&rt.app, &message, broken_rules)
|
||||||
|
} else {
|
||||||
|
embeds::final_ban_embed(&rt.app, &message, broken_rules)
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.http
|
||||||
|
.create_message(dm_channel.id)
|
||||||
|
.embeds(&[embed])
|
||||||
|
.components(&[ActionRow::new(&[
|
||||||
|
(None, Arc::new(CodeOfConductButton)),
|
||||||
|
(None, Arc::new(NeedHelpButton)),
|
||||||
|
])
|
||||||
|
.build()])
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MessageRedirector;
|
pub struct CallMessageHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMessageHandler<State> for MessageRedirector {
|
impl OnMessageHandler<State, Extras> for CallMessageHandler {
|
||||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
async fn on_message(
|
||||||
|
&self,
|
||||||
|
event: &MessageCreate,
|
||||||
|
rt: Arc<Runtime<State, Extras>>,
|
||||||
|
_extras: Arc<dyn ExtrasGetter<State, Extras = Extras>>,
|
||||||
|
) -> EventResult {
|
||||||
if event.author.bot || event.author.system.unwrap_or(false) {
|
if event.author.bot || event.author.system.unwrap_or(false) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if UtcDateTime::from_unix_timestamp(event.author.id.timestamp() / 1000).unwrap()
|
||||||
|
> UtcDateTime::now() - Duration::days(30)
|
||||||
|
&& event.author.id.get() != rt.state.bot_owner_id
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.content.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(guild_id) = event.guild_id else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
let prefixes = rt
|
let prefixes = rt
|
||||||
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||||
.await;
|
.await;
|
||||||
@@ -75,7 +165,44 @@ impl OnMessageHandler<State> for MessageRedirector {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(opposite_channel) = Channel::get_by_id(
|
let mut user = User::create_or_update(
|
||||||
|
event.author.clone(),
|
||||||
|
&rt.state.db,
|
||||||
|
&rt.state.color_queue_sender,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let user_incidents = UserIncident::get_for_user(event.author.id.get(), &rt.state.db)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|incident| incident.is_active())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if user_incidents > 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.content.to_ascii_lowercase().starts_with("x ") {
|
||||||
|
if !user.findings.contains("feature:hide-message") {
|
||||||
|
user.findings.insert("feature:hide-message".into());
|
||||||
|
user.insert(&rt.state.db).await?;
|
||||||
|
|
||||||
|
rt
|
||||||
|
.http
|
||||||
|
.create_message(
|
||||||
|
event.channel_id
|
||||||
|
)
|
||||||
|
.content(
|
||||||
|
"✨ You found a hidden feature! Messages starting with `x` followed by a space will not be sent to the other party (nor moderated, i.e. you won't get banned for what you say in them)."
|
||||||
|
)
|
||||||
|
.reply(event.id)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(mut opposite_channel) = Channel::get_by_id(
|
||||||
call.other_party_channel_id(event.channel_id.get()),
|
call.other_party_channel_id(event.channel_id.get()),
|
||||||
&rt.state.db,
|
&rt.state.db,
|
||||||
)
|
)
|
||||||
@@ -89,69 +216,451 @@ impl OnMessageHandler<State> for MessageRedirector {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if opposite_channel.call_id != Some(call.id) {
|
||||||
|
log::error!(
|
||||||
|
"Channel {} is listed as the opposite channel of channel {} in call {}, but its call ID is {}.",
|
||||||
|
opposite_channel.id,
|
||||||
|
channel.id,
|
||||||
|
call.id,
|
||||||
|
opposite_channel
|
||||||
|
.call_id
|
||||||
|
.map_or("None".into(), |i| i.to_string())
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let Some((webhook_id, webhook_token)) = opposite_channel
|
let Some((webhook_id, webhook_token)) = opposite_channel
|
||||||
.webhook_id
|
.webhook_id
|
||||||
.zip(opposite_channel.webhook_token)
|
.zip(opposite_channel.webhook_token.clone())
|
||||||
else {
|
else {
|
||||||
// Handle DM calls in the future.
|
// Handle DM calls in the future.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let profile = get_display_profile(event);
|
let (content, rules) = escape_message(event, user.is_moderator);
|
||||||
|
|
||||||
rt.http
|
let message: Message;
|
||||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
|
||||||
.allowed_mentions(None)
|
let replying_to = if let Some(reference) = &event.reference {
|
||||||
.username(&profile.name)
|
if let Some(replying_to_message_id) = reference.message_id {
|
||||||
.avatar_url(&profile.avatar_url)
|
Message::get_by_call_and_message_id(
|
||||||
.content(&escape_message(event))
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| {
|
|
||||||
log::error!(
|
|
||||||
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
|
||||||
call.id,
|
call.id,
|
||||||
opposite_channel.id
|
replying_to_message_id.get(),
|
||||||
|
&rt.state.db,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let replying_to_id = replying_to.as_ref().map(|i| i.message_id);
|
||||||
|
|
||||||
|
let participant = Participant::get_or_create(
|
||||||
|
user.id,
|
||||||
|
event.channel_id.get(),
|
||||||
|
call.id,
|
||||||
|
event
|
||||||
|
.member
|
||||||
|
.clone()
|
||||||
|
.and_then(|member| member.nick)
|
||||||
|
.or(event.author.global_name.clone())
|
||||||
|
.unwrap_or(event.author.name.clone()),
|
||||||
|
images::get_avatar_url(
|
||||||
|
event
|
||||||
|
.member
|
||||||
|
.clone()
|
||||||
|
.and_then(|inner| inner.avatar)
|
||||||
|
.or(event.author.avatar),
|
||||||
|
event.author.id.get(),
|
||||||
|
event.author.discriminator as u64,
|
||||||
|
event
|
||||||
|
.member
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|member| member.avatar)
|
||||||
|
.map(|_| guild_id.get()),
|
||||||
|
),
|
||||||
|
user.is_masked,
|
||||||
|
&rt.state.db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if rules
|
||||||
|
.iter()
|
||||||
|
.filter(|rule| !rule.ignore_for_moderators() || !user.is_moderator)
|
||||||
|
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||||
|
{
|
||||||
|
rt.http
|
||||||
|
.create_message(event.channel_id)
|
||||||
|
.content(
|
||||||
|
"🚩 Your message has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
rt.http
|
||||||
|
.create_message(Id::new(opposite_channel.id))
|
||||||
|
.content(&format!(
|
||||||
|
"🚩 **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.",
|
||||||
|
participant.display_name().sanitize(false)
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
message = Message::build()
|
||||||
|
.call_id(call.id)
|
||||||
|
.message(event.0.clone())
|
||||||
|
.participant(participant)
|
||||||
|
.is_censored(!rules.is_empty())
|
||||||
|
.replying_to_message_id(replying_to_id)
|
||||||
|
.build()
|
||||||
|
.expect(
|
||||||
|
"The message builder was missing either the call ID, the message, or the profile",
|
||||||
);
|
);
|
||||||
})?;
|
} else {
|
||||||
|
let mut embeds = vec![];
|
||||||
|
|
||||||
|
if let Some(replying_to) = replying_to
|
||||||
|
&& let Some((propagation_channel_id, propagation_message_id)) = replying_to
|
||||||
|
.data
|
||||||
|
.propagation_channel_id
|
||||||
|
.zip(replying_to.data.propagation_message_id)
|
||||||
|
{
|
||||||
|
let (display_name, avatar_url) = if let Some(mask) = &replying_to.data.author_mask {
|
||||||
|
(
|
||||||
|
if i64_to_u64(propagation_channel_id) != event.channel_id.get() {
|
||||||
|
get_mask_display_name(mask)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
get_mask_display_name(mask),
|
||||||
|
moderation::moderate_message(&replying_to.data.author_name, false)
|
||||||
|
.0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
get_mask_avatar_url(mask).expect("Failed to get mask avatar URL"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
moderation::moderate_message(&replying_to.data.author_name, false).0,
|
||||||
|
replying_to.data.author_avatar_url.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replied-to links are always
|
||||||
|
// .../{other_party_server_id}/{other_party_channel_id}/{other_party_message_id}
|
||||||
|
//
|
||||||
|
// When the message being replied to was sent by the other party, the
|
||||||
|
// `other_party_message_id` is the original message ID, given it was sent from
|
||||||
|
// their channel so the original is in their channel.
|
||||||
|
//
|
||||||
|
// When the message being replied to was sent by this party, the
|
||||||
|
// `other_party_message_id` is the propagation message ID given the other party
|
||||||
|
// will have seen the propagated message instead.
|
||||||
|
let message_link = if i64_to_u64(propagation_channel_id) == channel.id {
|
||||||
|
// if the message replied-to was sent by the other party
|
||||||
|
//
|
||||||
|
// replied_to.message_id -> other party's channel id
|
||||||
|
// replied_to.propagation_channel_id -> this channel's ID
|
||||||
|
// replied_to.propagation_message_id -> this channel's message ID
|
||||||
|
format!(
|
||||||
|
"https://discord.com/channels/{}/{}/{}",
|
||||||
|
opposite_channel.server_id, opposite_channel.id, replying_to.message_id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// if the message replied-to was sent by this party
|
||||||
|
//
|
||||||
|
// replied_to.message_id -> this channel's message ID
|
||||||
|
// replied_to.propagation_channel_id -> other party's channel id
|
||||||
|
// replied_to.propagation_message_id -> other party's message ID
|
||||||
|
format!(
|
||||||
|
"https://discord.com/channels/{}/{}/{}",
|
||||||
|
opposite_channel.server_id, propagation_channel_id, propagation_message_id
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let replying_to_user = User::get_by_id(replying_to.author_id, &rt.state.db).await?;
|
||||||
|
|
||||||
|
embeds.push(
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.author(
|
||||||
|
EmbedAuthorBuilder::new(format!("Replying to {display_name}",))
|
||||||
|
.icon_url(ImageSource::url(avatar_url).unwrap())
|
||||||
|
.url(message_link),
|
||||||
|
)
|
||||||
|
.color(if replying_to.data.author_mask.is_some() {
|
||||||
|
COLOR_WHITE
|
||||||
|
} else {
|
||||||
|
replying_to_user
|
||||||
|
.and_then(|i| i.color.map(|c| c as u32))
|
||||||
|
.unwrap_or(COLOR_WHITE)
|
||||||
|
})
|
||||||
|
.description(ellipse(
|
||||||
|
moderate_message(&replying_to.data.content, false).0,
|
||||||
|
100,
|
||||||
|
))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if participant.is_masked {
|
||||||
|
let last_message =
|
||||||
|
Message::get_last_by_author_id(call.id, event.author.id.get(), &rt.state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if last_message
|
||||||
|
.map(|lm| lm.data.author_mask.is_none())
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
embeds.push(
|
||||||
|
EmbedBuilder::new()
|
||||||
|
.color(COLOR_YELLOW)
|
||||||
|
.description(format!(
|
||||||
|
"🎭 **{}** is wearing a mask! Their real identity is hidden.",
|
||||||
|
participant.display_name()
|
||||||
|
))
|
||||||
|
.footer(EmbedFooterBuilder::new(format!(
|
||||||
|
"You can set your own mask using {}mask on.",
|
||||||
|
prefixes[0]
|
||||||
|
)))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut redis = rt.state.redis.get().await?;
|
||||||
|
|
||||||
|
redis
|
||||||
|
.zadd(
|
||||||
|
"active-calls",
|
||||||
|
format!("{}:{}", call.id, channel.id),
|
||||||
|
UtcDateTime::now().unix_timestamp(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let message_sent = match rt
|
||||||
|
.http
|
||||||
|
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||||
|
.allowed_mentions(None)
|
||||||
|
.username(&moderation::moderate_message(&participant.display_name(), false).0)
|
||||||
|
.avatar_url(&participant.avatar_url())
|
||||||
|
.content(&content.sanitize_mentions())
|
||||||
|
.embeds(&embeds)
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||||
|
call.id,
|
||||||
|
opposite_channel.id
|
||||||
|
);
|
||||||
|
}) {
|
||||||
|
Ok(message) => message,
|
||||||
|
Err(e) => {
|
||||||
|
if let ErrorType::Response {
|
||||||
|
error: ApiError::General(error),
|
||||||
|
..
|
||||||
|
} = e.kind()
|
||||||
|
&& error.code == 10015
|
||||||
|
{
|
||||||
|
log::debug!("Webhook not found, updating...");
|
||||||
|
opposite_channel
|
||||||
|
.update_webhook(&rt.state.db, &rt.http)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.model()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
message = Message::build()
|
||||||
|
.call_id(call.id)
|
||||||
|
.message(event.0.clone())
|
||||||
|
.participant(participant)
|
||||||
|
.is_censored(!rules.is_empty())
|
||||||
|
.replying_to_message_id(replying_to_id)
|
||||||
|
.propagation_message_id(message_sent.id.get())
|
||||||
|
.propagation_channel_id(opposite_channel.id)
|
||||||
|
.build()
|
||||||
|
.expect(
|
||||||
|
"The message builder was missing either the call ID, the message, or the profile",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
message.insert(&rt.state.db).await?;
|
||||||
|
|
||||||
|
if rules
|
||||||
|
.iter()
|
||||||
|
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||||
|
{
|
||||||
|
penalize_user(user, message, rules, &rt).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DisplayProfile {
|
#[derive(Debug)]
|
||||||
pub name: String,
|
pub struct CallMessageEditHandler;
|
||||||
pub avatar_url: String,
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OnMessageEditHandler<State, Extras> for CallMessageEditHandler {
|
||||||
|
async fn on_message_edit(
|
||||||
|
&self,
|
||||||
|
event: &MessageUpdate,
|
||||||
|
rt: Arc<Runtime<State, Extras>>,
|
||||||
|
) -> EventResult {
|
||||||
|
if event.edited_timestamp.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!(
|
||||||
|
"Edited message was '{}' sent by {} (bot? {:?})",
|
||||||
|
event.content,
|
||||||
|
event.author.id.get(),
|
||||||
|
event.author.bot
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let call = if let Some(call_id) = channel.call_id {
|
||||||
|
if let Some(call) = Call::get_by_id(call_id, &rt.state.db).await? {
|
||||||
|
if !call.is_active() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
call
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mut message) =
|
||||||
|
Message::get_by_call_and_message_id(call.id, event.id.get(), &rt.state.db).await?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let opposite_channel_id = call.other_party_channel_id(event.channel_id.get());
|
||||||
|
let Some(opposite_channel) = Channel::get_by_id(opposite_channel_id, &rt.state.db).await?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(propagation_message_id) = message.data.propagation_message_id {
|
||||||
|
let (content, rules) = escape_message(event, false);
|
||||||
|
|
||||||
|
if rules
|
||||||
|
.iter()
|
||||||
|
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||||
|
{
|
||||||
|
rt.http
|
||||||
|
.create_message(event.channel_id)
|
||||||
|
.content(
|
||||||
|
"🚩 Your message's edit has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
|
||||||
|
)
|
||||||
|
.reply(event.id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let user = User::create_or_update(
|
||||||
|
event.author.clone(),
|
||||||
|
&rt.state.db,
|
||||||
|
&rt.state.color_queue_sender,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
penalize_user(user, message, rules, &rt).await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((webhook_id, webhook_token)) = opposite_channel
|
||||||
|
.webhook_id
|
||||||
|
.zip(opposite_channel.webhook_token)
|
||||||
|
{
|
||||||
|
rt.http
|
||||||
|
.update_webhook_message(
|
||||||
|
Id::new(webhook_id),
|
||||||
|
&webhook_token,
|
||||||
|
Id::new(i64_to_u64(propagation_message_id)),
|
||||||
|
)
|
||||||
|
.content(Some(&content))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
message.data.content = content;
|
||||||
|
message.data.is_edited = true;
|
||||||
|
message.data.is_censored = !rules.is_empty();
|
||||||
|
|
||||||
|
message.insert(&rt.state.db).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
#[derive(Debug)]
|
||||||
let member = event.member.clone();
|
pub struct CallMessageDeleteHandler;
|
||||||
|
|
||||||
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
|
#[async_trait]
|
||||||
if webhook_username(nick).is_err() {
|
impl OnMessageDeleteHandler<State, Extras> for CallMessageDeleteHandler {
|
||||||
"Invalid Nickname".to_string()
|
async fn on_message_delete(
|
||||||
|
&self,
|
||||||
|
event: &MessageDelete,
|
||||||
|
rt: Arc<Runtime<State, Extras>>,
|
||||||
|
) -> EventResult {
|
||||||
|
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let call = if let Some(call_id) = channel.call_id {
|
||||||
|
if let Some(call) = Call::get_by_id(call_id, &rt.state.db).await? {
|
||||||
|
if !call.is_active() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
call
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
nick.clone()
|
return Ok(());
|
||||||
}
|
};
|
||||||
} else if let Some(name) = &event.author.global_name {
|
|
||||||
if webhook_username(name).is_err() {
|
|
||||||
"Invalid Global Name".to_string()
|
|
||||||
} else {
|
|
||||||
name.clone()
|
|
||||||
}
|
|
||||||
} else if webhook_username(&event.author.name).is_err() {
|
|
||||||
"Invalid Username".to_string()
|
|
||||||
} else {
|
|
||||||
event.author.name.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let avatar_hash = member
|
let Some(mut message) =
|
||||||
.and_then(|inner| inner.avatar)
|
Message::get_by_call_and_message_id(call.id, event.id.get(), &rt.state.db).await?
|
||||||
.or(event.author.avatar);
|
else {
|
||||||
let avatar_url = images::get_avatar_url(
|
return Ok(());
|
||||||
avatar_hash,
|
};
|
||||||
event.author.id.get(),
|
|
||||||
event.author.discriminator as u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
DisplayProfile { name, avatar_url }
|
let opposite_channel_id = call.other_party_channel_id(event.channel_id.get());
|
||||||
|
let Some(opposite_channel) = Channel::get_by_id(opposite_channel_id, &rt.state.db).await?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(propagation_message_id) = message.data.propagation_message_id
|
||||||
|
&& let Some((webhook_id, webhook_token)) = opposite_channel
|
||||||
|
.webhook_id
|
||||||
|
.zip(opposite_channel.webhook_token)
|
||||||
|
{
|
||||||
|
rt.http
|
||||||
|
.delete_webhook_message(
|
||||||
|
Id::new(webhook_id),
|
||||||
|
&webhook_token,
|
||||||
|
Id::new(i64_to_u64(propagation_message_id)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
message.data.is_deleted = true;
|
||||||
|
|
||||||
|
message.insert(&rt.state.db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
scum
|
scum
|
||||||
idiot
|
idiot
|
||||||
stupid
|
|
||||||
dumb
|
|
||||||
moron
|
moron
|
||||||
fool
|
fool
|
||||||
|
stfu
|
||||||
|
syfm
|
||||||
|
sybau
|
||||||
|
gtfo
|
||||||
|
// Keys is only censored because there have been many genuine conversations about piano keys, keys,
|
||||||
|
// and other stuff.
|
||||||
|
keys
|
||||||
|
// Many people not knowing paki is a slur, it's better to keep it here.
|
||||||
|
paki
|
||||||
|
hoe
|
||||||
|
ho
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
sex
|
sex
|
||||||
|
segs
|
||||||
esex
|
esex
|
||||||
|
esegs
|
||||||
give head
|
give head
|
||||||
gave head
|
gave head
|
||||||
give me head
|
give me head
|
||||||
@@ -35,7 +37,11 @@ stick it in yr pussy
|
|||||||
cock
|
cock
|
||||||
dick
|
dick
|
||||||
dih
|
dih
|
||||||
pussy
|
pusy
|
||||||
|
posay
|
||||||
|
vagina
|
||||||
|
vgina
|
||||||
|
vgna
|
||||||
dck
|
dck
|
||||||
psy
|
psy
|
||||||
pu-c
|
pu-c
|
||||||
@@ -50,27 +56,32 @@ gang bang
|
|||||||
squirt
|
squirt
|
||||||
squirting
|
squirting
|
||||||
squirts
|
squirts
|
||||||
bitch
|
|
||||||
bih
|
|
||||||
bich
|
|
||||||
ho
|
|
||||||
hoe
|
|
||||||
huz
|
|
||||||
slut
|
slut
|
||||||
dldo
|
dldo
|
||||||
dildo
|
dildo
|
||||||
goon
|
goon / go on
|
||||||
gooning
|
gooning
|
||||||
i will fuh you
|
|
||||||
i will fuck you
|
|
||||||
i will fuh u
|
i will fuh u
|
||||||
|
i will fuh you
|
||||||
i will fuck u
|
i will fuck u
|
||||||
|
i will fuck you
|
||||||
|
i will fvck u
|
||||||
|
i will fvck you
|
||||||
|
i will fck u
|
||||||
|
i will fck you
|
||||||
nut
|
nut
|
||||||
nuting
|
nuting
|
||||||
goonbeam
|
goonbeam
|
||||||
goonbeaming
|
goonbeaming
|
||||||
gooncream
|
gooncream
|
||||||
gooncreaming
|
gooncreaming
|
||||||
|
goonbud
|
||||||
|
goonbuds
|
||||||
|
goonete
|
||||||
|
goonetes
|
||||||
|
gooner
|
||||||
|
goonr
|
||||||
|
goner
|
||||||
boty hole
|
boty hole
|
||||||
suck my pp
|
suck my pp
|
||||||
ass cracker
|
ass cracker
|
||||||
@@ -85,4 +96,44 @@ faping
|
|||||||
masturbate
|
masturbate
|
||||||
masturbating
|
masturbating
|
||||||
masterbating
|
masterbating
|
||||||
masterbaiting
|
masterbaiting
|
||||||
|
masturbatigng
|
||||||
|
masterbaiter
|
||||||
|
mastrbaiter
|
||||||
|
mastrbait
|
||||||
|
mastrbate
|
||||||
|
horny
|
||||||
|
hrny
|
||||||
|
whore / who re
|
||||||
|
whre
|
||||||
|
tickle your pickle
|
||||||
|
tickle yr pickle
|
||||||
|
tickle his pickle
|
||||||
|
tickle my pickle
|
||||||
|
peg
|
||||||
|
peged
|
||||||
|
peging
|
||||||
|
butbang
|
||||||
|
butbanged
|
||||||
|
eat my ass
|
||||||
|
eat my ahh
|
||||||
|
eat mi ass
|
||||||
|
eat my ahh
|
||||||
|
eat my bum
|
||||||
|
eat mi bum
|
||||||
|
ima touch you
|
||||||
|
ima touch u
|
||||||
|
i ma touch you
|
||||||
|
i ma touch u
|
||||||
|
il touch you
|
||||||
|
il touch u
|
||||||
|
im going to touch you
|
||||||
|
im going to touch u
|
||||||
|
im gonna touch you
|
||||||
|
im gonna touch u
|
||||||
|
nude
|
||||||
|
nudes
|
||||||
|
nde
|
||||||
|
ndes
|
||||||
|
nood
|
||||||
|
noods
|
||||||
@@ -1,47 +1,53 @@
|
|||||||
niga
|
niga
|
||||||
|
nigas
|
||||||
niger
|
niger
|
||||||
|
nigers
|
||||||
nig
|
nig
|
||||||
|
nigs
|
||||||
nga
|
nga
|
||||||
|
ngas
|
||||||
ngr
|
ngr
|
||||||
|
ngrs
|
||||||
nger
|
nger
|
||||||
|
ngers
|
||||||
qiga
|
qiga
|
||||||
|
qigas
|
||||||
niqa
|
niqa
|
||||||
|
niqas
|
||||||
|
niqer
|
||||||
|
niqers
|
||||||
nqa
|
nqa
|
||||||
|
nqas
|
||||||
|
nqer
|
||||||
|
nlga
|
||||||
nlger
|
nlger
|
||||||
digga
|
nlgers
|
||||||
|
ngiga
|
||||||
|
ngigas
|
||||||
|
negaras
|
||||||
|
nigy
|
||||||
|
nyga
|
||||||
|
diga
|
||||||
|
digas
|
||||||
fag
|
fag
|
||||||
|
fags
|
||||||
fagot
|
fagot
|
||||||
|
fagots
|
||||||
fgot
|
fgot
|
||||||
|
fgots
|
||||||
fgt
|
fgt
|
||||||
paki
|
fgts
|
||||||
|
phagot
|
||||||
|
phagots
|
||||||
retard
|
retard
|
||||||
|
retards
|
||||||
rtrd
|
rtrd
|
||||||
|
rtrds
|
||||||
tard
|
tard
|
||||||
dumbfuck
|
tards
|
||||||
dumbfck
|
blackie
|
||||||
dumbfuk
|
blackies
|
||||||
dumbfuh
|
motherfucker
|
||||||
dumbfuhk
|
motherfuckers
|
||||||
dmbfuck
|
chinese ape
|
||||||
dmbfck
|
chinese apes
|
||||||
dmbfuk
|
|
||||||
dmbfuh
|
|
||||||
dmbfuhk
|
|
||||||
dumb fuck
|
|
||||||
dumb fck
|
|
||||||
dumb fuk
|
|
||||||
dumb fuh
|
|
||||||
dumb fuhk
|
|
||||||
stfu
|
|
||||||
shut the fuck up
|
|
||||||
syfm
|
|
||||||
shut your fucking mouth
|
|
||||||
sybau
|
|
||||||
shut your bitch as up
|
|
||||||
shut your bih as up
|
|
||||||
gtfo
|
|
||||||
get the fuck out
|
|
||||||
pedo
|
|
||||||
pdf
|
|
||||||
pdfile
|
|
||||||
blackie
|
|
||||||
@@ -8,4 +8,11 @@ fuh
|
|||||||
fuck you
|
fuck you
|
||||||
f u
|
f u
|
||||||
f you
|
f you
|
||||||
fuck u
|
fuck u
|
||||||
|
shit
|
||||||
|
sht
|
||||||
|
ass
|
||||||
|
huz
|
||||||
|
bitch
|
||||||
|
bih
|
||||||
|
bich
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pedo
|
||||||
|
pdf
|
||||||
|
pdfile
|
||||||
@@ -5,13 +5,9 @@ hang urself
|
|||||||
hang yourself
|
hang yourself
|
||||||
gkys
|
gkys
|
||||||
gokys
|
gokys
|
||||||
keys
|
|
||||||
get raped
|
get raped
|
||||||
rape you
|
rape you
|
||||||
rape u
|
rape u
|
||||||
slit
|
slit
|
||||||
grape
|
grape
|
||||||
graped
|
graped
|
||||||
throat slit
|
|
||||||
slit your throat
|
|
||||||
slit ur throat
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"dinosaur": 1278255435652010014,
|
||||||
|
"shleepy cat": 1278255475066146816,
|
||||||
|
"cozy cat": 1278255420124696663,
|
||||||
|
"coquette cat": 1278255408254816300,
|
||||||
|
"panic cat": 1278255451993145345
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"ferret": 1278176917123760139,
|
||||||
|
"octopus": 1278176904410697880,
|
||||||
|
"cat": 1278176891290783824,
|
||||||
|
"monkey": 1278176881195094049,
|
||||||
|
"panda": 1278176868767629455,
|
||||||
|
"sheep": 1278176856910073946,
|
||||||
|
"horse": 1278176842116894760,
|
||||||
|
"bee": 1278176823846506577,
|
||||||
|
"wild boar": 1278176806498996235,
|
||||||
|
"duck": 1278176794591232102,
|
||||||
|
"chicken": 1278176781790216233,
|
||||||
|
"elephant": 1278176771413512213,
|
||||||
|
"cow": 1278176761565413483,
|
||||||
|
"deer": 1278176744691597312,
|
||||||
|
"turkey": 1278176730619576321,
|
||||||
|
"dog": 1278176719932751903,
|
||||||
|
"beaver": 1278176709530882091,
|
||||||
|
"camel": 1278176699921465394,
|
||||||
|
"koala": 1278176687493746709,
|
||||||
|
"hippo": 1278176675846295603,
|
||||||
|
"ostrich": 1278176661317095494,
|
||||||
|
"rhino": 1278176650340864000,
|
||||||
|
"squirrel": 1278176637627924570,
|
||||||
|
"chamaleon": 1278176625392877648,
|
||||||
|
"frog": 1278176614512857088,
|
||||||
|
"skunk": 1278176604509442079,
|
||||||
|
"walrus": 1278176554723311678,
|
||||||
|
"hedgehog": 1278176541750067252,
|
||||||
|
"pig": 1278176531415564332,
|
||||||
|
"hen": 1278176518643646485,
|
||||||
|
"turtle": 1278176507428343891,
|
||||||
|
"mouse": 1278175957047578696,
|
||||||
|
"rabbit": 1278175944007487489,
|
||||||
|
"goat": 1278175933915856938,
|
||||||
|
"lion": 1278175922964664382,
|
||||||
|
"wolf": 1278175902047535165,
|
||||||
|
"snake": 1278175869331968061,
|
||||||
|
"llama": 1278175850008674325,
|
||||||
|
"crab": 1278175835634925620,
|
||||||
|
"owl": 1278175822947029152,
|
||||||
|
"bat": 1278175813572886550,
|
||||||
|
"zebra": 1278175799962505216,
|
||||||
|
"fox": 1278175788818104474,
|
||||||
|
"gorilla": 1278175775891259456,
|
||||||
|
"sloth": 1278175765661356175,
|
||||||
|
"puffer fish": 1278175753497743360,
|
||||||
|
"bear": 1278175741569142867,
|
||||||
|
"giraffe": 1278175732601983006,
|
||||||
|
"penguin": 1278175721956573236,
|
||||||
|
"parrot": 1278175706798489642
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rand::{SeedableRng, seq::IndexedRandom};
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
|
use crate::utils::ToTitleCase;
|
||||||
|
|
||||||
|
static MASKS_COMMON: Lazy<HashMap<String, u64>> = Lazy::new(|| {
|
||||||
|
let (file_name, json) = if cfg!(debug_assertions) {
|
||||||
|
("dev/common.json", include_str!("dev/common.json"))
|
||||||
|
} else {
|
||||||
|
("prod/common.json", include_str!("prod/common.json"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let masks: HashMap<String, u64> = serde_json::from_str(json)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to parse masks/{file_name} masks"));
|
||||||
|
|
||||||
|
assert_ne!(masks.len(), 0, "No masks found in masks/{file_name}");
|
||||||
|
|
||||||
|
masks
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn get_random_mask(not_in: &[String]) -> Option<(String, u64)> {
|
||||||
|
let mask_names = MASKS_COMMON
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, _)| !not_in.contains(name))
|
||||||
|
.map(|(name, value)| (name.clone(), *value))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut rng = ChaCha20Rng::from_os_rng();
|
||||||
|
|
||||||
|
mask_names.choose(&mut rng).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mask_display_name(mask: impl ToString) -> String {
|
||||||
|
mask.to_title_case()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mask_emoji(mask: impl ToString) -> Option<String> {
|
||||||
|
let emoji_name = mask.to_string().replace(" ", "");
|
||||||
|
let emoji_id = MASKS_COMMON.get(&mask.to_string())?;
|
||||||
|
|
||||||
|
Some(format!("<:{emoji_name}:{emoji_id}>"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mask_avatar_url(mask: impl ToString) -> Option<String> {
|
||||||
|
let mask_emoji_id = MASKS_COMMON.get(&mask.to_string())?;
|
||||||
|
|
||||||
|
Some(format!(
|
||||||
|
"https://cdn.discordapp.com/emojis/{mask_emoji_id}.png?size=128&quality=lossless"
|
||||||
|
))
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user