Compare commits
198
Commits
6cf299ea0b
..
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 | ||
|
|
458f89a3a3 | ||
|
|
4df7c8ce7d | ||
|
|
28f8c8e702 |
@@ -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
|
||||
/.venv
|
||||
/.ansible
|
||||
/backup
|
||||
|
||||
.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]
|
||||
name = "payphone"
|
||||
version = "4.0.0"
|
||||
version = "4.4.57"
|
||||
edition = "2024"
|
||||
default-run = "payphone"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
async-trait = "0.1.88"
|
||||
axum = "0.8.4"
|
||||
clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
||||
color-thief = "0.2.2"
|
||||
colored = "3.0.0"
|
||||
csv = "1.3.1"
|
||||
deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] }
|
||||
dotenvy = "0.15.7"
|
||||
humantime = "2.2.0"
|
||||
image = "0.25.6"
|
||||
include_dir = "0.7.4"
|
||||
itertools = "0.14.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.27", features = ["serde"] }
|
||||
notify = "8.2.0"
|
||||
@@ -18,16 +26,36 @@ open = "5.3.2"
|
||||
rand = "0.9.2"
|
||||
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.23", features = [
|
||||
"gzip",
|
||||
"brotli",
|
||||
"zstd",
|
||||
"native-tls-vendored",
|
||||
] }
|
||||
rgb = "0.8.52"
|
||||
scc = "2.3.4"
|
||||
scylla = { version = "1.3.1", features = ["time-03"] }
|
||||
serde_json = "1.0.143"
|
||||
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"] }
|
||||
topgg = { version = "1.4.2", features = ["twilight"] }
|
||||
tower-http = { version = "0.6.6", features = ["fs"] }
|
||||
twilight-cache-inmemory = { version = "0.16.0", features = [
|
||||
"permission-calculator",
|
||||
] }
|
||||
twilight-gateway = "0.16.0"
|
||||
twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
||||
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"
|
||||
unicode-segmentation = "1.12.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
|
||||
|
||||
```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
|
||||
$ 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:
|
||||
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:
|
||||
scylla-data-1:
|
||||
scylla-data-2:
|
||||
@@ -42,3 +102,9 @@ networks:
|
||||
ipam:
|
||||
config:
|
||||
- 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"
|
||||
+101
@@ -46,6 +46,11 @@
|
||||
//! $ 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
|
||||
//!
|
||||
//! Payphone has a built-in documentation server that serves the project's documentation and
|
||||
@@ -59,7 +64,11 @@
|
||||
//! 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`.
|
||||
|
||||
use std::fmt;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
use twilight_gateway::ShardId;
|
||||
|
||||
/// Payphone's argument 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)]
|
||||
pub enum Commands {
|
||||
/// Starts the bot with the provided token.
|
||||
@@ -139,6 +196,50 @@ pub enum Commands {
|
||||
default_value_t = String::from("127.0.0.1:9042"))
|
||||
]
|
||||
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
|
||||
|
||||
+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.
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// The default command prefix used by the bot for new servers.
|
||||
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.
|
||||
pub const COLOR_RED: u32 = 0xFF8787;
|
||||
|
||||
/// A yellow color used for mask notifications and warnings.
|
||||
pub const COLOR_YELLOW: u32 = 0xFFC868;
|
||||
|
||||
@@ -6,28 +6,6 @@ CREATE KEYSPACE IF NOT EXISTS payphone
|
||||
'enabled': false
|
||||
};
|
||||
|
||||
CREATE TYPE IF NOT EXISTS payphone.message (
|
||||
id BIGINT,
|
||||
|
||||
author_id BIGINT,
|
||||
author_name TEXT,
|
||||
author_avatar_url TEXT,
|
||||
author_mask ASCII,
|
||||
|
||||
content TEXT,
|
||||
attachments LIST<TEXT>,
|
||||
sticker BIGINT,
|
||||
|
||||
propagation_message_id BIGINT,
|
||||
propagation_channel_id BIGINT,
|
||||
|
||||
replying_to_id,
|
||||
|
||||
is_deleted BOOLEAN,
|
||||
is_edited BOOLEAN,
|
||||
is_censored BOOLEAN,
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.users (
|
||||
user_id BIGINT PRIMARY KEY,
|
||||
|
||||
@@ -82,24 +60,57 @@ CREATE TABLE IF NOT EXISTS payphone.participants (
|
||||
PRIMARY KEY (call_id, user_id, channel_id)
|
||||
);
|
||||
|
||||
CREATE TYPE IF NOT EXISTS payphone.message (
|
||||
id BIGINT,
|
||||
call_id BIGINT,
|
||||
|
||||
author_id BIGINT,
|
||||
author_name TEXT,
|
||||
author_avatar_url TEXT,
|
||||
author_mask ASCII,
|
||||
|
||||
content TEXT,
|
||||
attachments LIST<TEXT>,
|
||||
sticker BIGINT,
|
||||
|
||||
propagation_message_id BIGINT,
|
||||
propagation_channel_id BIGINT,
|
||||
|
||||
replying_to_message_id BIGINT,
|
||||
|
||||
is_deleted BOOLEAN,
|
||||
is_edited 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)
|
||||
);
|
||||
) 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 (
|
||||
server_id BIGINT PRIMARY KEY,
|
||||
|
||||
name TEXT,
|
||||
prefix TEXT,
|
||||
language TEXT,
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.reports (
|
||||
report_id BIGINT,
|
||||
user_id BIGINT,
|
||||
|
||||
message FROZEN<payphone.message>,
|
||||
replying_to FROZEN<payphone.message>,
|
||||
@@ -112,21 +123,18 @@ CREATE TABLE IF NOT EXISTS payphone.reports (
|
||||
reviewed_by_id BIGINT,
|
||||
resolution ASCII,
|
||||
|
||||
PRIMARY KEY (message_author_id, report_id)
|
||||
PRIMARY KEY (user_id, report_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
||||
user_id BIGINT,
|
||||
incident_id BIGINT,
|
||||
report_id BIGINT,
|
||||
|
||||
-- ban, warning
|
||||
type ASCII,
|
||||
|
||||
call_id BIGINT,
|
||||
report_id BIGINT,
|
||||
|
||||
message FROZEN<payphone.message>,
|
||||
replying_to FROZEN<payphone.message>,
|
||||
|
||||
reason TEXT,
|
||||
|
||||
@@ -136,7 +144,7 @@ CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
||||
expires_at TIMESTAMP,
|
||||
duration ASCII,
|
||||
|
||||
PRIMARY KEY (user_id)
|
||||
PRIMARY KEY (user_id, incident_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.server_incidents (
|
||||
|
||||
@@ -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);
|
||||
+20
-7
@@ -1,10 +1,9 @@
|
||||
pub mod models;
|
||||
|
||||
use include_dir::{Dir, include_dir};
|
||||
use scylla::{
|
||||
client::{session::Session, session_builder::SessionBuilder},
|
||||
errors::ExecutionError,
|
||||
};
|
||||
use scylla::client::session::Session;
|
||||
use scylla::client::session_builder::SessionBuilder;
|
||||
use scylla::errors::ExecutionError;
|
||||
use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -22,13 +21,27 @@ pub enum DatabaseError {
|
||||
|
||||
static MIGRATIONS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/database/migrations");
|
||||
|
||||
pub async fn init(uri: impl ToString) -> Result<Session, DatabaseError> {
|
||||
let session = SessionBuilder::new()
|
||||
pub async fn init(
|
||||
uri: impl ToString,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<Session, DatabaseError> {
|
||||
let session = if let Some((username, password)) = username.zip(password) {
|
||||
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)?;
|
||||
.map_err(|_| DatabaseError::ConnectionFailed)?
|
||||
};
|
||||
|
||||
execute_file(
|
||||
&session,
|
||||
|
||||
+1271
-326
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;
|
||||
|
||||
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(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")
|
||||
} else {
|
||||
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() {
|
||||
"0" => (user_id >> 22) % 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 sanitizing;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
//! Utilities for sanitizing Discord messages (removing markdown from user input).
|
||||
|
||||
/// Escapes Discord markdown in a string.
|
||||
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;
|
||||
|
||||
/// Sanitizes mentions in a string.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The sanitized string.
|
||||
fn sanitize_mentions(&self) -> String;
|
||||
}
|
||||
|
||||
impl<T> SanitizeDiscordMarkdown for T
|
||||
@@ -14,7 +31,8 @@ where
|
||||
|
||||
for ch in input.chars() {
|
||||
match ch {
|
||||
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' => {
|
||||
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' | '[' | ']' | '/'
|
||||
| '(' | ')' => {
|
||||
escaped.push('\\');
|
||||
escaped.push(ch);
|
||||
}
|
||||
@@ -28,7 +46,13 @@ where
|
||||
};
|
||||
|
||||
// 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("@here", "@\u{200B}here")
|
||||
.replace("<@", "<@\u{200B}") // user mentions
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
use crate::database::models::User;
|
||||
|
||||
pub struct Extras {
|
||||
pub user: User,
|
||||
}
|
||||
+250
-70
@@ -1,123 +1,158 @@
|
||||
use crate::framework::{
|
||||
commands::{Command, runner::CommandRunner},
|
||||
events::traits::{
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
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_model::gateway::payload::outgoing::update_presence::UpdatePresencePayload;
|
||||
|
||||
use crate::framework::commands::Command;
|
||||
use crate::framework::commands::extras::ExtrasGetter;
|
||||
use crate::framework::commands::runner::CommandRunner;
|
||||
use crate::framework::events::traits::{
|
||||
OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
|
||||
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
|
||||
OnServerUpdateHandler,
|
||||
},
|
||||
interactions::{
|
||||
components::Component, message_command::MessageCommand,
|
||||
message_command_registry::MessageCommandRegistry,
|
||||
message_command_runner::MessageCommandRunner, registry::ComponentRegistry,
|
||||
},
|
||||
runtime::Runtime,
|
||||
};
|
||||
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;
|
||||
use async_trait::async_trait;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
||||
use twilight_http::Client;
|
||||
|
||||
/// Errors occurred while attempting to run the bot.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeError {
|
||||
#[error("The application's details could not be fetched")]
|
||||
ApplicationFetchingFailed,
|
||||
}
|
||||
|
||||
/// Getter trait for command prefixes.
|
||||
#[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(
|
||||
&self,
|
||||
runtime: &Runtime<T>,
|
||||
runtime: &Runtime<T, E>,
|
||||
server_id: Option<u64>,
|
||||
user_id: Option<u64>,
|
||||
) -> Vec<String>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> IntoPrefixes<T> for String {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
impl<T, E> PrefixGetter<T, E> for String {
|
||||
async fn get(&self, _: &Runtime<T, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
vec![self.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> IntoPrefixes<T> for &str {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
impl<T, E> PrefixGetter<T, E> for &str {
|
||||
async fn get(&self, _: &Runtime<T, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
vec![self.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
T: ToString,
|
||||
T: ToString + Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
async fn get(&self, _: &Runtime<S>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
async fn get(&self, _: &Runtime<S, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
self.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F, Fut, T> IntoPrefixes<T> for F
|
||||
impl<F, Fut, T, E> PrefixGetter<T, E> for F
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = Vec<String>> + Send,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
async fn get(&self, _: &Runtime<T, E>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
(self)().await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bot<T: Send + Sync + Debug = ()> {
|
||||
state: Arc<T>,
|
||||
prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync>,
|
||||
pub struct Bot<S: Send + Sync, E: Send + Sync = ()> {
|
||||
shard: ShardId,
|
||||
state: Arc<S>,
|
||||
presence: Option<UpdatePresencePayload>,
|
||||
prefixes: Arc<dyn PrefixGetter<S, E> + Send + Sync>,
|
||||
intents: Intents,
|
||||
commands: Vec<Arc<dyn Command<T>>>,
|
||||
components: Vec<Arc<dyn Component<T>>>,
|
||||
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T>>>,
|
||||
message_commands: Vec<Arc<dyn MessageCommand<T>>>,
|
||||
message_command_registry: Arc<MessageCommandRegistry<T>>,
|
||||
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T>>>,
|
||||
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T>>>,
|
||||
on_message_handlers: Vec<Arc<dyn OnMessageHandler<T>>>,
|
||||
on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<T>>>,
|
||||
on_message_delete_handlers: Vec<Arc<dyn OnMessageDeleteHandler<T>>>,
|
||||
on_server_create_handlers: Vec<Arc<dyn OnServerCreateHandler<T>>>,
|
||||
on_server_update_handlers: Vec<Arc<dyn OnServerUpdateHandler<T>>>,
|
||||
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<T>>>,
|
||||
extras: Arc<dyn ExtrasGetter<S, Extras = E>>,
|
||||
cached_resource_types: Vec<ResourceType>,
|
||||
commands: Vec<Arc<dyn Command<S, E>>>,
|
||||
components: Vec<Arc<dyn Component<S, E>>>,
|
||||
tasks: Vec<Arc<dyn Task<S, E>>>,
|
||||
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<S, E>>>,
|
||||
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<S, E>>>,
|
||||
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<S, E>>>,
|
||||
on_message_handlers: Vec<Arc<dyn OnMessageHandler<S, E>>>,
|
||||
on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<S, E>>>,
|
||||
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 + Debug> Bot<T> {
|
||||
pub fn new(state: T, prefixes: impl IntoPrefixes<T> + Send + Sync + 'static) -> Self {
|
||||
let message_command_registry = Arc::new(MessageCommandRegistry::new());
|
||||
impl<T, E> Bot<T, E>
|
||||
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 {
|
||||
state: Arc::new(state),
|
||||
shard: ShardId::new(0, 1),
|
||||
state,
|
||||
presence: None,
|
||||
prefixes: Arc::new(prefixes),
|
||||
intents: Intents::empty(),
|
||||
extras: Arc::new(extras),
|
||||
cached_resource_types: Vec::new(),
|
||||
commands: Vec::new(),
|
||||
components: Vec::new(),
|
||||
message_commands: Vec::new(),
|
||||
message_command_registry: message_command_registry.clone(),
|
||||
tasks: Vec::new(),
|
||||
on_ready_handlers: Vec::new(),
|
||||
on_reconnect_handlers: Vec::new(),
|
||||
on_interaction_handlers: vec![Arc::new(MessageCommandRunner {
|
||||
registry: message_command_registry,
|
||||
})],
|
||||
on_interaction_handlers: vec![Arc::new(InteractionRunner)],
|
||||
on_message_handlers: vec![Arc::new(CommandRunner)],
|
||||
on_message_edit_handlers: Vec::new(),
|
||||
on_message_delete_handlers: Vec::new(),
|
||||
on_server_create_handlers: Vec::new(),
|
||||
on_server_update_handlers: Vec::new(),
|
||||
on_server_delete_handlers: Vec::new(),
|
||||
topgg_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_command(mut self, command: impl MessageCommand<T> + 'static) -> Self {
|
||||
let arc_command = Arc::new(command);
|
||||
self.message_command_registry.register(arc_command.name(), arc_command.clone());
|
||||
self.message_commands.push(arc_command);
|
||||
pub fn shard(mut self, shard: ShardId) -> Self {
|
||||
self.shard = shard;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -126,61 +161,91 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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> {
|
||||
let http = Client::new(token.to_string());
|
||||
let app = http
|
||||
@@ -193,9 +258,64 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
|
||||
.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();
|
||||
|
||||
for component in &self.components {
|
||||
let component_id = component.custom_id();
|
||||
|
||||
registry.register(
|
||||
component_id.expect("The component registered by default must have a custom ID"),
|
||||
component.clone(),
|
||||
@@ -204,6 +324,18 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
|
||||
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 {
|
||||
http,
|
||||
app,
|
||||
@@ -211,15 +343,48 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
state: bot.state.clone(),
|
||||
prefixes: bot.prefixes.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 {
|
||||
if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await {
|
||||
tokio::select! {
|
||||
Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()) => {
|
||||
let task_rt = runtime.clone();
|
||||
let task_bot = bot.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
task_rt.cache.update(&event);
|
||||
|
||||
match event {
|
||||
Event::Ready(event) => {
|
||||
for handler in &task_bot.on_ready_handlers {
|
||||
@@ -246,7 +411,7 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
Event::InteractionCreate(interaction) => {
|
||||
for handler in &task_bot.on_interaction_handlers {
|
||||
handler
|
||||
.on_interaction(&interaction, task_rt.clone())
|
||||
.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:?}")
|
||||
@@ -259,7 +424,7 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
log::trace!("Running {handler:?}");
|
||||
|
||||
handler
|
||||
.on_message(&message, task_rt.clone())
|
||||
.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:?}")
|
||||
@@ -300,6 +465,17 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
.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
|
||||
@@ -317,6 +493,10 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(json) = gateway_rx.recv() => {
|
||||
gateway.command(&json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use std::fmt;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::framework::commands::context::CommandContext;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ArgumentParsingError {
|
||||
#[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")]
|
||||
InvalidArgument,
|
||||
|
||||
#[error("A database operation failed")]
|
||||
DatabaseFailed(#[from] DatabaseError),
|
||||
}
|
||||
|
||||
pub trait FromArguments: Sized {
|
||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError>;
|
||||
pub trait FromArguments<S, E>: Sized {
|
||||
async fn from_args<'a>(
|
||||
args: &'a str,
|
||||
ctx: &'a CommandContext<'_, S, E>,
|
||||
) -> Result<(Self, &'a str), ArgumentParsingError>;
|
||||
}
|
||||
|
||||
impl FromArguments for String {
|
||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
||||
impl<S, E> FromArguments<S, E> for String {
|
||||
async fn from_args<'a>(
|
||||
args: &'a str,
|
||||
_ctx: &'a CommandContext<'_, S, E>,
|
||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||
if !args.is_empty() {
|
||||
match args.split_once(" ") {
|
||||
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);
|
||||
|
||||
impl FromArguments for GreedyString {
|
||||
fn from_args<'a>(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
||||
impl<S, E> FromArguments<S, E> for GreedyString {
|
||||
async fn from_args<'a>(
|
||||
args: &'a str,
|
||||
_ctx: &'a CommandContext<'_, S, E>,
|
||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||
let value = GreedyString(args.to_string());
|
||||
|
||||
Ok((value, ""))
|
||||
@@ -83,9 +100,12 @@ impl fmt::Display for NeedyString {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArguments for NeedyString {
|
||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
||||
if !args.is_empty() {
|
||||
impl<S, E> FromArguments<S, E> for NeedyString {
|
||||
async fn from_args<'a>(
|
||||
args: &'a str,
|
||||
_ctx: &'a CommandContext<'_, S, E>,
|
||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||
if !args.trim().is_empty() {
|
||||
match args.split_once(" ") {
|
||||
Some((arg, rest)) => Ok((Self(arg.to_string()), rest)),
|
||||
None => Ok((Self(args.to_string()), "")),
|
||||
@@ -118,18 +138,24 @@ impl fmt::Display for NeedyGreedyString {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArguments for NeedyGreedyString {
|
||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
||||
if !args.is_empty() {
|
||||
Ok((Self(args.to_string()), ""))
|
||||
impl<S, E> FromArguments<S, E> for NeedyGreedyString {
|
||||
async fn from_args<'a>(
|
||||
args: &'a str,
|
||||
_ctx: &'a CommandContext<'_, S, E>,
|
||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||
if !args.trim().is_empty() {
|
||||
Ok((Self(args.trim().to_string()), ""))
|
||||
} else {
|
||||
Err(ArgumentParsingError::MissingArgument)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArguments for bool {
|
||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
||||
impl<S, E> FromArguments<S, E> for bool {
|
||||
async fn from_args<'a>(
|
||||
args: &'a str,
|
||||
_ctx: &'a CommandContext<'_, S, E>,
|
||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||
let arg: &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)),
|
||||
"n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)),
|
||||
_ => 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
|
||||
T: FromArguments,
|
||||
T: FromArguments<S, E>,
|
||||
{
|
||||
fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> {
|
||||
match T::from_args(args) {
|
||||
async fn from_args<'a>(
|
||||
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)),
|
||||
Err(_) => Ok((None, args)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ParseArguments: Sized {
|
||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError>;
|
||||
}
|
||||
pub struct Arg<T>(pub T);
|
||||
|
||||
impl ParseArguments for () {
|
||||
fn parse(_: &str) -> Result<Self, ArgumentParsingError> {
|
||||
Ok(())
|
||||
impl<T> Deref for Arg<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> ParseArguments for A
|
||||
where
|
||||
A: FromArguments,
|
||||
{
|
||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
||||
let (a, _) = A::from_args(args)?;
|
||||
|
||||
Ok(a)
|
||||
impl<T> DerefMut for Arg<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
A: FromArguments,
|
||||
T: FromArguments<S, E>,
|
||||
{
|
||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
||||
let (a, _) = A::from_args(args)?;
|
||||
async fn parse(
|
||||
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,))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> ParseArguments for (A, B)
|
||||
impl<A, B, S, E> ParseArguments<S, E> for (A, B)
|
||||
where
|
||||
A: FromArguments,
|
||||
B: FromArguments,
|
||||
A: FromArguments<S, E>,
|
||||
B: FromArguments<S, E>,
|
||||
{
|
||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
||||
let (a, args) = A::from_args(args)?;
|
||||
let (b, _) = B::from_args(args)?;
|
||||
async fn parse(
|
||||
args: &str,
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, C> ParseArguments for (A, B, C)
|
||||
impl<A, B, C, S, E> ParseArguments<S, E> for (A, B, C)
|
||||
where
|
||||
A: FromArguments,
|
||||
B: FromArguments,
|
||||
C: FromArguments,
|
||||
A: FromArguments<S, E>,
|
||||
B: FromArguments<S, E>,
|
||||
C: FromArguments<S, E>,
|
||||
{
|
||||
fn parse(args: &str) -> Result<Self, ArgumentParsingError> {
|
||||
let (a, args) = A::from_args(args)?;
|
||||
let (b, args) = B::from_args(args)?;
|
||||
let (c, _) = C::from_args(args)?;
|
||||
async fn parse(
|
||||
args: &str,
|
||||
ctx: &CommandContext<'_, S, E>,
|
||||
) -> 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))
|
||||
}
|
||||
|
||||
@@ -1,24 +1,97 @@
|
||||
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 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 rt: Arc<Runtime<T>>,
|
||||
pub rt: Arc<Runtime<T, E>>,
|
||||
pub prefix: &'a str,
|
||||
pub extras: E,
|
||||
}
|
||||
|
||||
impl<'a, T> CommandContext<'a, T>
|
||||
impl<'a, T, E> CommandContext<'a, T, E>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(event: &'a MessageCreate, rt: Arc<Runtime<T>>, prefix: &'a str) -> Self {
|
||||
Self { event, rt, prefix }
|
||||
pub fn new(
|
||||
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())
|
||||
}
|
||||
|
||||
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::{
|
||||
future::{Future, IntoFuture},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::future::{Future, IntoFuture};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use twilight_http::{Error, Response, response::marker::EmptyBody};
|
||||
use twilight_model::{
|
||||
channel::{
|
||||
Message,
|
||||
message::{Component as TwilightComponent, Embed},
|
||||
},
|
||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||
id::{
|
||||
Id,
|
||||
marker::{ChannelMarker, InteractionMarker},
|
||||
},
|
||||
};
|
||||
use twilight_http::response::marker::EmptyBody;
|
||||
use twilight_http::{Error, Response};
|
||||
use twilight_model::channel::Message;
|
||||
use twilight_model::channel::message::{Component as TwilightComponent, Embed};
|
||||
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
|
||||
use twilight_model::id::Id;
|
||||
use twilight_model::id::marker::{ChannelMarker, InteractionMarker, MessageMarker};
|
||||
use twilight_util::builder::InteractionResponseDataBuilder;
|
||||
|
||||
use crate::framework::{
|
||||
interactions::components::{ActionRow, Component},
|
||||
runtime::Runtime,
|
||||
};
|
||||
use crate::framework::interactions::components::{ActionRow, Component};
|
||||
use crate::framework::runtime::Runtime;
|
||||
|
||||
pub struct MessageSender<T> {
|
||||
pub struct MessageSender<T, E> {
|
||||
channel_id: Id<ChannelMarker>,
|
||||
|
||||
rt: Arc<Runtime<T>>,
|
||||
rt: Arc<Runtime<T, E>>,
|
||||
|
||||
content: Option<String>,
|
||||
embeds: Vec<Embed>,
|
||||
reply_to: Option<Id<MessageMarker>>,
|
||||
|
||||
// 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
|
||||
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 {
|
||||
channel_id,
|
||||
rt,
|
||||
content: None,
|
||||
components: vec![],
|
||||
reply_to: None,
|
||||
embeds: vec![],
|
||||
}
|
||||
}
|
||||
@@ -61,12 +55,18 @@ where
|
||||
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
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut builder = self.rt.http.create_message(self.channel_id);
|
||||
|
||||
@@ -82,7 +82,7 @@ where
|
||||
|
||||
for (index, component) in self.components {
|
||||
if index != current_index {
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
current_row = ActionRow::new(&[]);
|
||||
}
|
||||
@@ -101,13 +101,17 @@ where
|
||||
current_row.add(custom_id.clone(), component.clone());
|
||||
}
|
||||
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
}
|
||||
|
||||
builder = builder.components(&rows);
|
||||
builder = builder.embeds(&self.embeds);
|
||||
|
||||
if let Some(reply_to) = self.reply_to {
|
||||
builder = builder.reply(reply_to);
|
||||
}
|
||||
|
||||
let result = builder.await;
|
||||
|
||||
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 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_token: String,
|
||||
|
||||
rt: Arc<Runtime<T>>,
|
||||
rt: Arc<Runtime<T, E>>,
|
||||
|
||||
content: Option<String>,
|
||||
embeds: Vec<Embed>,
|
||||
|
||||
// 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
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(
|
||||
interaction_id: Id<InteractionMarker>,
|
||||
interaction_token: String,
|
||||
rt: Arc<Runtime<T>>,
|
||||
rt: Arc<Runtime<T, E>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
interaction_id,
|
||||
@@ -161,6 +166,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn content(mut self, content: impl ToString) -> Self {
|
||||
self.content = Some(content.to_string());
|
||||
|
||||
@@ -173,7 +179,7 @@ where
|
||||
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
|
||||
@@ -194,7 +200,7 @@ where
|
||||
|
||||
for (index, component) in self.components {
|
||||
if index != current_index {
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
current_row = ActionRow::new(&[]);
|
||||
}
|
||||
@@ -213,7 +219,7 @@ where
|
||||
current_row.add(custom_id.clone(), component.clone());
|
||||
}
|
||||
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
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 IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
|
||||
|
||||
|
||||
+161
-17
@@ -1,40 +1,130 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use deadpool_redis::cluster::PoolError;
|
||||
use deadpool_redis::redis::RedisError;
|
||||
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 context;
|
||||
pub mod extras;
|
||||
pub mod messages;
|
||||
pub mod permissions;
|
||||
pub mod runner;
|
||||
|
||||
/// An error occurred during command execution.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CommandError {
|
||||
#[error("A message was attempted to be sent but it failed")]
|
||||
MessageSendingFailed,
|
||||
|
||||
#[error("A database query failed")]
|
||||
DatabaseQueryFailed,
|
||||
DatabaseFailed(#[from] DatabaseError),
|
||||
|
||||
#[error("A database operation failed due to an inconsistent state")]
|
||||
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]
|
||||
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;
|
||||
|
||||
/// The command's aliases.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`&'static [&'static str]`] - The command's aliases.
|
||||
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 {
|
||||
"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] {
|
||||
&[]
|
||||
}
|
||||
|
||||
/// 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:
|
||||
@@ -45,32 +135,86 @@ pub trait Command<T>: Send + Sync + Debug {
|
||||
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> {
|
||||
/// The name of the command.
|
||||
pub name: &'a str,
|
||||
|
||||
/// The arguments passed to the command, raw as a string.
|
||||
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> {
|
||||
fn new(name: &'a str, args: &'a str) -> Self {
|
||||
Self { name, args }
|
||||
/// Creates a new [`CommandParts`] struct.
|
||||
///
|
||||
/// 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>> {
|
||||
let message_trimmed_start = message.trim_ascii_start();
|
||||
let message_trimmed_start_lowercase = message_trimmed_start.to_ascii_lowercase();
|
||||
|
||||
if message_trimmed_start
|
||||
.to_ascii_lowercase()
|
||||
.starts_with(&prefix.to_ascii_lowercase())
|
||||
{
|
||||
let message = message_trimmed_start.split_at(prefix.len()).1;
|
||||
let prefix_lowercase = prefix.to_ascii_lowercase();
|
||||
|
||||
match message.split_once(" ") {
|
||||
Some((name, args)) => Some(CommandParts::new(name, args)),
|
||||
None => Some(CommandParts::new(message, "")),
|
||||
if let Some(index) = message_trimmed_start_lowercase.find(&prefix_lowercase) {
|
||||
let command_text = &message_trimmed_start[index + prefix.len()..];
|
||||
|
||||
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 {
|
||||
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 twilight_model::gateway::payload::incoming::MessageCreate;
|
||||
|
||||
use crate::framework::{
|
||||
commands::{context::CommandContext, get_command_parts},
|
||||
events::traits::{EventResult, OnMessageHandler},
|
||||
runtime::Runtime,
|
||||
};
|
||||
use crate::framework::commands::context::CommandContext;
|
||||
use crate::framework::commands::extras::ExtrasGetter;
|
||||
use crate::framework::commands::get_command_parts;
|
||||
use crate::framework::events::traits::{EventResult, OnMessageHandler};
|
||||
use crate::framework::runtime::Runtime;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandRunner;
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sync + 'static> OnMessageHandler<T> for CommandRunner {
|
||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<T>>) -> EventResult {
|
||||
impl<T, E> OnMessageHandler<T, E> for CommandRunner
|
||||
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) {
|
||||
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()))
|
||||
.await;
|
||||
|
||||
for prefix in prefixes {
|
||||
if let Some(parts) = get_command_parts(&prefix, &event.content) {
|
||||
for prefix in &prefixes {
|
||||
if let Some(parts) = get_command_parts(prefix, &event.content) {
|
||||
let command = rt.commands.iter().find(|cmd| {
|
||||
cmd.identifiers()
|
||||
.contains(&parts.name.to_ascii_lowercase().as_str())
|
||||
});
|
||||
|
||||
if let Some(command) = command {
|
||||
command
|
||||
.execute(CommandContext::new(event, rt.clone(), &prefix), parts.args)
|
||||
.await?;
|
||||
if command.only_runs_if_startswith() && parts.start_index != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 twilight_model::gateway::payload::incoming::{
|
||||
@@ -6,78 +8,99 @@ use twilight_model::gateway::payload::incoming::{
|
||||
MessageUpdate, Ready,
|
||||
};
|
||||
|
||||
use crate::framework::commands::extras::ExtrasGetter;
|
||||
use crate::framework::runtime::Runtime;
|
||||
|
||||
pub type EventResult = Result<(), Box<dyn Error>>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnReadyHandler<T>: Send + Sync + Debug
|
||||
pub trait OnReadyHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnReconnectHandler<T>: Send + Sync + Debug
|
||||
pub trait OnReconnectHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnInteractionHandler<T>: Send + Sync + Debug
|
||||
pub trait OnInteractionHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnMessageHandler<T>: Send + Sync + Debug
|
||||
pub trait OnMessageHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnMessageEditHandler<T>: Send + Sync + Debug
|
||||
pub trait OnMessageEditHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnMessageDeleteHandler<T>: Send + Sync + Debug
|
||||
pub trait OnMessageDeleteHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnServerCreateHandler<T>: Send + Sync + Debug
|
||||
pub trait OnServerCreateHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnServerUpdateHandler<T>: Send + Sync + Debug
|
||||
pub trait OnServerUpdateHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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]
|
||||
pub trait OnServerDeleteHandler<T>: Send + Sync + Debug
|
||||
pub trait OnServerDeleteHandler<T, E>: Send + Sync + Debug
|
||||
where
|
||||
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 twilight_model::channel::message::{
|
||||
Component as TwilightComponent,
|
||||
component::{ActionRow as TwilightActionRow, Button as TwilightButton, ButtonStyle},
|
||||
use twilight_model::channel::message::Component as TwilightComponent;
|
||||
use twilight_model::channel::message::component::{
|
||||
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> {
|
||||
components: Vec<ComponentWithID<T>>,
|
||||
pub struct ActionRow<T, E> {
|
||||
components: Vec<ComponentWithID<T, E>>,
|
||||
}
|
||||
|
||||
impl<T> ActionRow<T> {
|
||||
pub fn new(components: &[ComponentWithID<T>]) -> Self {
|
||||
impl<T, E> ActionRow<T, E> {
|
||||
pub fn new(components: &[ComponentWithID<T, E>]) -> Self {
|
||||
Self {
|
||||
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));
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.components.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.components.is_empty()
|
||||
}
|
||||
@@ -45,19 +43,21 @@ impl<T> ActionRow<T> {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Component<T>: Send + Sync + Debug {
|
||||
pub trait Component<T, E>: Send + Sync + Debug {
|
||||
fn custom_id(&self) -> Option<String> {
|
||||
Some(String::from("component"))
|
||||
}
|
||||
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]
|
||||
impl<T, S: Send + Sync + 'static> Component<S> for T
|
||||
impl<T, S, E> Component<S, E> for T
|
||||
where
|
||||
T: Button<S>,
|
||||
T: Button<S, E>,
|
||||
S: Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
fn custom_id(&self) -> Option<String> {
|
||||
Button::id(self)
|
||||
@@ -66,7 +66,7 @@ where
|
||||
fn build(&self, id: Option<String>) -> TwilightComponent {
|
||||
TwilightComponent::Button(TwilightButton {
|
||||
custom_id: id,
|
||||
disabled: false,
|
||||
disabled: self.disabled(),
|
||||
emoji: None,
|
||||
label: self.label().map(|i| i.to_string()),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
if self.url().is_none() {
|
||||
Some(String::from("Button"))
|
||||
@@ -93,17 +97,24 @@ pub trait Button<S: Send + Sync + 'static>: Send + Sync + Debug {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&self) -> ButtonStyle {
|
||||
ButtonStyle::Secondary
|
||||
}
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
|
||||
fn label(&self) -> Option<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
use std::sync::Arc;
|
||||
use twilight_model::{
|
||||
gateway::payload::incoming::InteractionCreate,
|
||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||
id::{Id, marker::InteractionMarker},
|
||||
};
|
||||
|
||||
use crate::framework::{
|
||||
interactions::messages::InteractionResponder,
|
||||
commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime,
|
||||
};
|
||||
use twilight_http::response::marker::EmptyBody;
|
||||
use twilight_http::{Error, Response};
|
||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
|
||||
use twilight_util::builder::InteractionResponseDataBuilder;
|
||||
|
||||
pub struct InteractionContext<T> {
|
||||
use crate::framework::interactions::InteractionError;
|
||||
use crate::framework::interactions::components::ActionRow;
|
||||
use crate::framework::messages::MessageEditBuilder;
|
||||
use crate::framework::runtime::Runtime;
|
||||
|
||||
pub struct InteractionContext<T, E> {
|
||||
pub event: InteractionCreate,
|
||||
pub rt: Arc<Runtime<T>>,
|
||||
pub rt: Arc<Runtime<T, E>>,
|
||||
pub prefix: String,
|
||||
pub extras: E,
|
||||
}
|
||||
|
||||
impl<T> InteractionContext<T> {
|
||||
pub fn send(
|
||||
self: &Arc<Self>,
|
||||
) -> InteractionResponder<T> {
|
||||
InteractionResponder::new(self.clone())
|
||||
impl<T, E> InteractionContext<T, E>
|
||||
where
|
||||
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 fn new(event: InteractionCreate, rt: Arc<Runtime<T>>, prefix: String) -> Self {
|
||||
Self { event, rt, prefix }
|
||||
}
|
||||
|
||||
pub async fn defer(&self) -> Result<(), InteractionError> {
|
||||
@@ -38,54 +49,85 @@ impl<T> InteractionContext<T> {
|
||||
kind: InteractionResponseType::DeferredUpdateMessage,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| InteractionError::DeferringFailed)?;
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn think(&self) -> Result<(), InteractionError> {
|
||||
self.rt
|
||||
pub async fn edit_original_message(
|
||||
&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
|
||||
.interaction(self.rt.app.id)
|
||||
.create_response(
|
||||
self.event.id,
|
||||
&self.event.token,
|
||||
&InteractionResponse {
|
||||
data: None,
|
||||
kind: InteractionResponseType::DeferredChannelMessageWithSource,
|
||||
data: Some(sender.build()),
|
||||
kind: InteractionResponseType::UpdateMessage,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| InteractionError::DeferringFailed)?;
|
||||
.await;
|
||||
|
||||
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>> {
|
||||
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(),
|
||||
)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
use crate::framework::interactions::{InteractionError, context::InteractionContext};
|
||||
use async_trait::async_trait;
|
||||
use std::fmt::Debug;
|
||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||
|
||||
#[async_trait]
|
||||
pub trait MessageCommand<T>: Send + Sync + Debug {
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"A Payphone context menu command."
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: InteractionContext<T>,
|
||||
target_message_id: u64,
|
||||
) -> Result<(), InteractionError>;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// to be later integrated into interaction registry...currently component registry
|
||||
use crate::framework::{
|
||||
interactions::{
|
||||
InteractionError, context::InteractionContext, message_command::MessageCommand,
|
||||
},
|
||||
runtime::Runtime,
|
||||
};
|
||||
use scc::HashMap;
|
||||
use std::sync::Arc;
|
||||
use twilight_model::{
|
||||
application::command::CommandType, application::interaction::InteractionData,
|
||||
gateway::payload::incoming::InteractionCreate,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MessageCommandRegistry<T> {
|
||||
inner: HashMap<String, Arc<dyn MessageCommand<T>>>,
|
||||
}
|
||||
|
||||
impl<T> MessageCommandRegistry<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&self, name: impl ToString, command: Arc<dyn MessageCommand<T>>) {
|
||||
self.inner.upsert(name.to_string(), command);
|
||||
}
|
||||
|
||||
pub fn unregister(&self, name: impl ToString) {
|
||||
self.inner.remove(&name.to_string());
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
event: &InteractionCreate,
|
||||
rt: Arc<Runtime<T>>,
|
||||
) -> Result<(), InteractionError> {
|
||||
if let Some(data) = &event.data {
|
||||
if let InteractionData::ApplicationCommand(data) = data {
|
||||
if data.kind == CommandType::Message {
|
||||
if let Some(command) = self.inner.get_async(&data.name).await {
|
||||
let prefixes = rt
|
||||
.get_prefixes(
|
||||
event.guild_id.map(|id| id.get()),
|
||||
event.author_id().map(|id| id.get()),
|
||||
)
|
||||
.await;
|
||||
let prefix = prefixes
|
||||
.first()
|
||||
.ok_or(InteractionError::PrefixesVectorEmpty)?;
|
||||
// Message commands have a resolved target_id
|
||||
if let Some(target_id) = data.target_id {
|
||||
command
|
||||
.execute(
|
||||
InteractionContext::new(
|
||||
event.clone(),
|
||||
rt.clone(),
|
||||
prefix.to_string(),
|
||||
),
|
||||
target_id.get(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(InteractionError::InteractionNotFound)
|
||||
}
|
||||
} else {
|
||||
Err(InteractionError::InteractionNotFound)
|
||||
}
|
||||
} else {
|
||||
Err(InteractionError::InteractionTypeUnsent)
|
||||
}
|
||||
} else {
|
||||
Err(InteractionError::InteractionTypeUnsent)
|
||||
}
|
||||
} else {
|
||||
Err(InteractionError::InteractionTypeUnsent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use crate::framework::interactions::InteractionError::InteractionTypeUnsent;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||
|
||||
use crate::framework::{
|
||||
events::traits::{EventResult, OnInteractionHandler},
|
||||
interactions::message_command_registry::MessageCommandRegistry,
|
||||
runtime::Runtime,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MessageCommandRunner<T> {
|
||||
pub registry: Arc<MessageCommandRegistry<T>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sync + std::fmt::Debug + 'static> OnInteractionHandler<T>
|
||||
for MessageCommandRunner<T>
|
||||
{
|
||||
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult {
|
||||
if let Err(e) = self.registry.execute(event, rt).await {
|
||||
// Only log errors that aren't "not a message command"
|
||||
if !matches!(e, InteractionTypeUnsent) {
|
||||
log::error!("Message command dispatch error: {e:?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use twilight_model::http::interaction::{
|
||||
InteractionResponse, InteractionResponseData, InteractionResponseType,
|
||||
};
|
||||
|
||||
use crate::framework::{
|
||||
interactions::{InteractionError, context::InteractionContext},
|
||||
runtime::Runtime,
|
||||
};
|
||||
|
||||
pub struct InteractionResponder<T> {
|
||||
ctx: Arc<InteractionContext<T>>,
|
||||
}
|
||||
|
||||
impl<T> InteractionResponder<T> {
|
||||
pub fn new(ctx: Arc<InteractionContext<T>>) -> Self {
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub async fn content(&self, content: impl Into<String>) -> Result<(), InteractionError> {
|
||||
self.ctx
|
||||
.rt
|
||||
.http
|
||||
.interaction(self.ctx.rt.app.id)
|
||||
.create_response(
|
||||
self.ctx.event.id,
|
||||
&self.ctx.event.token,
|
||||
&InteractionResponse {
|
||||
kind: InteractionResponseType::ChannelMessageWithSource,
|
||||
data: Some(InteractionResponseData {
|
||||
content: Some(content.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| InteractionError::RespondingFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
pub mod message_command_runner;
|
||||
pub mod messages;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::database::models::DatabaseError;
|
||||
|
||||
pub mod components;
|
||||
pub mod context;
|
||||
pub mod message_command;
|
||||
pub mod message_command_registry;
|
||||
pub mod registry;
|
||||
pub mod runner;
|
||||
|
||||
@@ -15,21 +14,21 @@ pub enum InteractionError {
|
||||
)]
|
||||
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")]
|
||||
InteractionTypeUnsent,
|
||||
|
||||
#[error("The original message was attempted to be edited, but it failed")]
|
||||
OriginalMessageEditingFailed,
|
||||
|
||||
#[error("Responding to the interaction failed")]
|
||||
RespondingFailed,
|
||||
|
||||
#[error("No prefixes were returned for the event")]
|
||||
PrefixesVectorEmpty,
|
||||
|
||||
#[error("The extras getter failed to retrieve extras")]
|
||||
ExtrasGetterFailed,
|
||||
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||
DatabaseError(#[from] DatabaseError),
|
||||
|
||||
#[error(transparent)]
|
||||
DiscordAPIError(#[from] twilight_http::Error),
|
||||
}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use scc::HashMap;
|
||||
|
||||
use twilight_model::{
|
||||
application::interaction::InteractionData, gateway::payload::incoming::InteractionCreate,
|
||||
};
|
||||
|
||||
use crate::framework::{
|
||||
interactions::{InteractionError, context::InteractionContext},
|
||||
runtime::Runtime,
|
||||
};
|
||||
use twilight_model::application::interaction::InteractionData;
|
||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||
|
||||
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)]
|
||||
pub struct ComponentRegistry<T> {
|
||||
inner: HashMap<String, Arc<dyn Component<T>>>,
|
||||
pub struct ComponentRegistry<T, E> {
|
||||
inner: HashMap<String, Arc<dyn Component<T, E>>>,
|
||||
}
|
||||
|
||||
impl<T> ComponentRegistry<T>
|
||||
impl<T, E> ComponentRegistry<T, E>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
pub fn new() -> 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);
|
||||
}
|
||||
|
||||
@@ -39,12 +37,16 @@ where
|
||||
pub async fn execute(
|
||||
&self,
|
||||
event: &InteractionCreate,
|
||||
rt: Arc<Runtime<T>>,
|
||||
rt: Arc<Runtime<T, E>>,
|
||||
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||
) -> Result<(), InteractionError> {
|
||||
if let Some(data) = &event.data {
|
||||
match 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
|
||||
.get_prefixes(
|
||||
event.guild_id.map(|id| id.get()),
|
||||
@@ -55,13 +57,27 @@ where
|
||||
.first()
|
||||
.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
|
||||
.execute(InteractionContext::new(
|
||||
event.clone(),
|
||||
rt.clone(),
|
||||
prefix.to_string(),
|
||||
extras,
|
||||
))
|
||||
.await
|
||||
.inspect(|_| log::debug!("Executed component interaction"))
|
||||
.inspect_err(|e| log::error!("Interaction execution failed: {e:?}"))
|
||||
} else {
|
||||
Err(InteractionError::InteractionNotFound)
|
||||
}
|
||||
|
||||
@@ -3,24 +3,25 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||
|
||||
use crate::{
|
||||
framework::{
|
||||
events::traits::{EventResult, OnInteractionHandler},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
use crate::framework::commands::extras::ExtrasGetter;
|
||||
use crate::framework::events::traits::{EventResult, OnInteractionHandler};
|
||||
use crate::framework::runtime::Runtime;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InteractionRunner;
|
||||
|
||||
#[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(
|
||||
&self,
|
||||
event: &InteractionCreate,
|
||||
rt: Arc<Runtime<State>>,
|
||||
rt: Arc<Runtime<T, E>>,
|
||||
extras: Arc<dyn ExtrasGetter<T, Extras = E>>,
|
||||
) -> 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,7 @@
|
||||
//! // Your command logic here.
|
||||
//! ctx.send()
|
||||
//! .content("Hello, world!")
|
||||
//! .await
|
||||
//! .map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
//! .await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
@@ -149,8 +148,7 @@
|
||||
//!
|
||||
//! ctx.send()
|
||||
//! .content(format!("Hello, {}!", name.0))
|
||||
//! .await
|
||||
//! .map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
//! .await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
@@ -161,4 +159,6 @@ pub mod bot;
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
pub mod interactions;
|
||||
pub mod messages;
|
||||
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 tokio::sync::mpsc::UnboundedSender;
|
||||
use twilight_cache_inmemory::InMemoryCache;
|
||||
use twilight_http::Client;
|
||||
use twilight_model::gateway::payload::outgoing::UpdatePresence;
|
||||
use twilight_model::oauth::Application;
|
||||
|
||||
use crate::framework::{
|
||||
bot::IntoPrefixes, commands::Command, interactions::registry::ComponentRegistry,
|
||||
};
|
||||
use crate::framework::bot::PrefixGetter;
|
||||
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,
|
||||
|
||||
/// Details about the application.
|
||||
pub app: Application,
|
||||
pub registry: ComponentRegistry<T>,
|
||||
pub prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync + 'static>,
|
||||
pub commands: Vec<Arc<dyn Command<T>>>,
|
||||
|
||||
/// The component registry.
|
||||
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 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> {
|
||||
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
|
||||
.iter()
|
||||
.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<()>;
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
framework::commands::{
|
||||
Command, CommandError,
|
||||
arguments::{Argument, NeedyString, ParseArguments},
|
||||
context::CommandContext,
|
||||
},
|
||||
groups::about::{
|
||||
components::{
|
||||
use crate::extras::Extras;
|
||||
use crate::framework::commands::arguments::{Arg, Argument, NeedyString, ParseArguments};
|
||||
use crate::framework::commands::context::CommandContext;
|
||||
use crate::framework::commands::{Command, CommandError};
|
||||
use crate::framework::runtime::Runtime;
|
||||
use crate::groups::about::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)]
|
||||
pub struct HelpCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for HelpCommand {
|
||||
impl Command<State, Extras> for HelpCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"help"
|
||||
}
|
||||
@@ -38,38 +37,114 @@ impl Command<State> for HelpCommand {
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
ctx: CommandContext<'_, State, Extras>,
|
||||
args: &str,
|
||||
) -> 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) {
|
||||
ctx.send()
|
||||
.embed(get_command_help_embed(&command, &ctx))
|
||||
.component(HelpGoBackButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to send message! {e}"))
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.inspect_err(|e| log::error!("Failed to send message! {e}"))?;
|
||||
} else {
|
||||
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(HelpSupportLinkButton, 1)
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
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(HelpSupportLinkButton, 1)
|
||||
.component(HelpDonateLinkButton, 1)
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.component(CodeOfConductButton, 1)
|
||||
.await?;
|
||||
}
|
||||
|
||||
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 crate::{
|
||||
framework::interactions::{InteractionError, components::Button, context::InteractionContext},
|
||||
groups::about::embeds::get_all_command_help_embed,
|
||||
state::State,
|
||||
};
|
||||
use crate::extras::Extras;
|
||||
use crate::framework::interactions::InteractionError;
|
||||
use crate::framework::interactions::components::Button;
|
||||
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)]
|
||||
pub struct HelpGoBackButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpGoBackButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("← Go back to commands list")
|
||||
impl Button<State, Extras> for HelpGoBackButton {
|
||||
fn id(&self) -> Option<String> {
|
||||
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
|
||||
.original_message()
|
||||
.expect(
|
||||
"Buttons always have the original message in the interaction context, this one didn't (got None)"
|
||||
.edit_original_message(
|
||||
MessageEditBuilder::new().embed(
|
||||
get_all_command_help_embed(&ctx.prefix, &ctx.rt, &ctx.extras)
|
||||
)
|
||||
.edit()
|
||||
.embed(get_all_command_help_embed(&ctx.prefix, ctx.rt))
|
||||
.component(HelpWebsiteLinkButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.component(HelpDonateLinkButton, 1)
|
||||
.component(CodeOfConductLinkButton, 1)
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
||||
.map_err(
|
||||
@@ -40,13 +49,13 @@ impl Button<State> for HelpGoBackButton {
|
||||
pub struct HelpSupportLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpSupportLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Get help")
|
||||
impl Button<State, Extras> for HelpSupportLinkButton {
|
||||
fn label(&self) -> Option<String> {
|
||||
Some("Get help".into())
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://discord.gg/9daSsrfTmE")
|
||||
fn url(&self) -> Option<String> {
|
||||
Some("https://discord.gg/9daSsrfTmE".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +63,13 @@ impl Button<State> for HelpSupportLinkButton {
|
||||
pub struct HelpWebsiteLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpWebsiteLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Website")
|
||||
impl Button<State, Extras> for HelpWebsiteLinkButton {
|
||||
fn label(&self) -> Option<String> {
|
||||
Some("Website".into())
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://payphone.lat/")
|
||||
fn url(&self) -> Option<String> {
|
||||
Some("https://payphone.lat/".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +77,26 @@ impl Button<State> for HelpWebsiteLinkButton {
|
||||
pub struct HelpDonateLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for HelpDonateLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Donate")
|
||||
impl Button<State, Extras> for HelpDonateLinkButton {
|
||||
fn label(&self) -> Option<String> {
|
||||
Some("Donate".into())
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://ko-fi.com/payphonedev")
|
||||
fn url(&self) -> Option<String> {
|
||||
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,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::{COLOR_RED, COLOR_WHITE},
|
||||
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
||||
framework::{
|
||||
commands::{Command, context::CommandContext},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
utils::ToTitleCase,
|
||||
};
|
||||
use crate::constants::{COLOR_RED, COLOR_WHITE, VERSION};
|
||||
use crate::discord::images;
|
||||
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||
use crate::extras::Extras;
|
||||
use crate::framework::commands::Command;
|
||||
use crate::framework::commands::context::CommandContext;
|
||||
use crate::framework::runtime::Runtime;
|
||||
use crate::state::State;
|
||||
use crate::utils::ToTitleCase;
|
||||
|
||||
pub(super) fn get_command_help_embed(
|
||||
command: &Arc<dyn Command<State>>,
|
||||
ctx: &CommandContext<'_, State>,
|
||||
command: &Arc<dyn Command<State, Extras>>,
|
||||
ctx: &CommandContext<'_, State, Extras>,
|
||||
) -> Embed {
|
||||
let name = format!("{}{}", ctx.prefix, command.name());
|
||||
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().id.get(),
|
||||
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
None,
|
||||
))
|
||||
.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().id.get(),
|
||||
ctx.rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
None,
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -79,12 +80,17 @@ pub(super) fn get_command_help_embed(
|
||||
.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()
|
||||
.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().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
None
|
||||
))
|
||||
.unwrap()))
|
||||
.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().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
None
|
||||
))
|
||||
.unwrap())
|
||||
.color(COLOR_WHITE)
|
||||
.field(EmbedFieldBuilder::new("Commands", get_commands_table(rt)).build())
|
||||
.footer(EmbedFooterBuilder::new(format!("v{} - Use {}help <command> to get more information about a specific command.", env!("CARGO_PKG_VERSION"), prefix)))
|
||||
.field(EmbedFieldBuilder::new("Commands", get_commands_table(rt, extras)).build())
|
||||
.footer(EmbedFooterBuilder::new(format!("v{VERSION}-{} - Use {}help <command> to get more information about a specific command.", rt.shard_number + 1, prefix)))
|
||||
.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();
|
||||
|
||||
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```")
|
||||
}
|
||||
|
||||
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()
|
||||
.color(COLOR_RED)
|
||||
.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().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
None
|
||||
))
|
||||
.unwrap()))
|
||||
.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().id.get(),
|
||||
rt.app.bot.as_ref().unwrap().discriminator as u64,
|
||||
None
|
||||
))
|
||||
.unwrap())
|
||||
.build()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod commands;
|
||||
pub mod components;
|
||||
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
-134
@@ -1,97 +1,27 @@
|
||||
use async_trait::async_trait;
|
||||
use deadpool_redis::redis::AsyncTypedCommands;
|
||||
use scc::HashSet;
|
||||
use std::sync::Arc;
|
||||
use time::UtcDateTime;
|
||||
use twilight_model::id::Id;
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel, Message, MessageRow, Report},
|
||||
framework::commands::{Command, CommandError, context::CommandContext},
|
||||
framework::interactions::{
|
||||
InteractionError, context::InteractionContext, message_command::MessageCommand,
|
||||
},
|
||||
state::State,
|
||||
utils::{Bye, get_random_u64},
|
||||
use crate::database::models::{Call, Channel, Participant};
|
||||
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||
use crate::extras::Extras;
|
||||
use crate::framework::commands::arguments::{Arg, Argument, ParseArguments};
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ReportMessageCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MessageCommand<State> for ReportMessageCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"Report Message"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Report a message to the moderators."
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: InteractionContext<State>,
|
||||
target_message_id: u64,
|
||||
) -> Result<(), InteractionError> {
|
||||
// Extract channel_id from resolved data or fallback to event.channel (Discord API v10+)
|
||||
let channel_id = ctx
|
||||
.event
|
||||
.channel
|
||||
.as_ref()
|
||||
.map(|c| c.id.get())
|
||||
.or_else(|| ctx.event.channel.as_ref().map(|c| c.id.get()))
|
||||
.ok_or(InteractionError::InteractionNotFound)?;
|
||||
let db = &ctx.rt.state.db;
|
||||
let channel = Channel::get_by_id(channel_id, db)
|
||||
.await
|
||||
.map_err(|_| InteractionError::InteractionNotFound)?;
|
||||
let call_id = channel
|
||||
.and_then(|c| c.call_id)
|
||||
.ok_or(InteractionError::InteractionNotFound)?;
|
||||
|
||||
// Fetch the message from the database
|
||||
let message_row = MessageRow::get_by_call_and_message_id(call_id, target_message_id, db)
|
||||
.await
|
||||
.map_err(|_| InteractionError::InteractionNotFound)?
|
||||
.ok_or(InteractionError::InteractionNotFound)?;
|
||||
|
||||
// If the message is a reply, fetch the replied-to message and store in replying_to
|
||||
let replying_to = if let Some(replying_to_id) = message_row.data.replying_to_id {
|
||||
if let Ok(Some(reply_msg_row)) =
|
||||
MessageRow::get_by_call_and_message_id(call_id, replying_to_id as u64, db).await
|
||||
{
|
||||
Some(reply_msg_row.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build the report using the new flat message and replying_to
|
||||
let report = Report {
|
||||
report_id: get_random_u64(),
|
||||
message: message_row.data,
|
||||
replying_to,
|
||||
created_at: time::OffsetDateTime::now_utc(),
|
||||
reviewed_at: None,
|
||||
created_by_ids: vec![ctx.event.author_id().map(|id| id.get()).unwrap_or(0)],
|
||||
reviewed_by_id: None,
|
||||
resolution: None,
|
||||
};
|
||||
|
||||
// Insert the report
|
||||
report
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(InteractionError::DatabaseError)?;
|
||||
|
||||
// Respond to the user
|
||||
Arc::new(ctx)
|
||||
.send()
|
||||
.content("Message has been reported to the moderators.")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
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)]
|
||||
pub struct CallCommand {
|
||||
@@ -111,7 +41,7 @@ impl CallCommand {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for CallCommand {
|
||||
impl Command<State, Extras> for CallCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"call"
|
||||
}
|
||||
@@ -120,9 +50,16 @@ impl Command<State> for CallCommand {
|
||||
&["c"]
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
||||
vec![
|
||||
Box::new(NotBannedPermission),
|
||||
Box::new(NonNewAccountPermission),
|
||||
]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
ctx: CommandContext<'_, State, Extras>,
|
||||
_args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let channel = Channel::get_or_create_by_id(
|
||||
@@ -133,8 +70,7 @@ impl Command<State> for CallCommand {
|
||||
.await
|
||||
.inspect_err(|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()) {
|
||||
ctx.send()
|
||||
@@ -142,8 +78,7 @@ impl Command<State> for CallCommand {
|
||||
"☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -152,41 +87,213 @@ impl Command<State> for CallCommand {
|
||||
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))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.channel_lock
|
||||
.insert_async(ctx.event.channel_id.get())
|
||||
.await
|
||||
.bye();
|
||||
self.channel_lock.insert(ctx.event.channel_id.get()).bye();
|
||||
|
||||
let call = Call::start(&channel, &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to start call: {e:?}");
|
||||
self.channel_lock.remove(&ctx.event.channel_id.get());
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
})?;
|
||||
|
||||
self.channel_lock
|
||||
.remove_async(&ctx.event.channel_id.get())
|
||||
.await;
|
||||
self.channel_lock.remove(&ctx.event.channel_id.get());
|
||||
|
||||
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
|
||||
.http
|
||||
.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
|
||||
.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("☎️ 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
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.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
|
||||
.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(())
|
||||
@@ -197,7 +304,7 @@ impl Command<State> for CallCommand {
|
||||
pub struct HangupCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for HangupCommand {
|
||||
impl Command<State, Extras> for HangupCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"hangup"
|
||||
}
|
||||
@@ -206,23 +313,29 @@ impl Command<State> for HangupCommand {
|
||||
&["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(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
ctx: CommandContext<'_, State, Extras>,
|
||||
_args: &str,
|
||||
) -> 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
|
||||
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))?
|
||||
else {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
@@ -233,52 +346,73 @@ impl Command<State> for HangupCommand {
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to cancel search: {e:?}");
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
})?;
|
||||
|
||||
ctx.send()
|
||||
.content("☎️ The phone was hung up.")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.content(format!("☎️ {}", get_string_call_no_answer(ctx.prefix)))
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))?
|
||||
else {
|
||||
ctx.send()
|
||||
.content("❌ Call not found.")
|
||||
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?;
|
||||
|
||||
channel.call_id = None;
|
||||
channel.upsert(&ctx.rt.state.db).await.bye();
|
||||
|
||||
redis
|
||||
.zrem("active-calls", format!("{}:{}", call_id, channel.id))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.bye();
|
||||
|
||||
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)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?;
|
||||
|
||||
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
|
||||
.http
|
||||
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||
.content("☎️ The other party hung up the phone.")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.content(&format!(
|
||||
"☎️ {}",
|
||||
get_string_call_hangup_other_party(ctx.prefix)
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -286,3 +420,209 @@ impl Command<State> for HangupCommand {
|
||||
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()
|
||||
}
|
||||
+562
-54
@@ -1,31 +1,120 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use linkify::LinkFinder;
|
||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||
use twilight_validate::request::webhook_username;
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel},
|
||||
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
||||
framework::{
|
||||
events::traits::{EventResult, OnMessageHandler},
|
||||
runtime::Runtime,
|
||||
},
|
||||
groups::calls::moderation::escape_message,
|
||||
state::State,
|
||||
use deadpool_redis::redis::AsyncTypedCommands;
|
||||
use itertools::Itertools;
|
||||
use time::{Duration, UtcDateTime};
|
||||
use twilight_http::api_error::ApiError;
|
||||
use twilight_http::error::ErrorType;
|
||||
use twilight_model::gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate};
|
||||
use twilight_model::id::Id;
|
||||
use twilight_util::builder::embed::{
|
||||
EmbedAuthorBuilder, EmbedBuilder, EmbedFooterBuilder, ImageSource,
|
||||
};
|
||||
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)]
|
||||
pub struct MessageRedirector;
|
||||
pub struct CallMessageHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMessageHandler<State> for MessageRedirector {
|
||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
impl OnMessageHandler<State, Extras> for CallMessageHandler {
|
||||
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) {
|
||||
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
|
||||
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||
.await;
|
||||
@@ -76,7 +165,44 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
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()),
|
||||
&rt.state.db,
|
||||
)
|
||||
@@ -90,22 +216,236 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
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
|
||||
.webhook_id
|
||||
.zip(opposite_channel.webhook_token)
|
||||
.zip(opposite_channel.webhook_token.clone())
|
||||
else {
|
||||
// Handle DM calls in the future.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let profile = get_display_profile(event);
|
||||
let (content, rules) = escape_message(event, user.is_moderator);
|
||||
|
||||
let message: Message;
|
||||
|
||||
let replying_to = if let Some(reference) = &event.reference {
|
||||
if let Some(replying_to_message_id) = reference.message_id {
|
||||
Message::get_by_call_and_message_id(
|
||||
call.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(&profile.name)
|
||||
.avatar_url(&profile.avatar_url)
|
||||
.content(&escape_message(event))
|
||||
.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!(
|
||||
@@ -113,46 +453,214 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayProfile {
|
||||
pub name: String,
|
||||
pub avatar_url: String,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct CallMessageEditHandler;
|
||||
|
||||
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
||||
let member = event.member.clone();
|
||||
|
||||
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
|
||||
if webhook_username(nick).is_err() {
|
||||
"Invalid Nickname".to_string()
|
||||
} else {
|
||||
nick.clone()
|
||||
#[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(());
|
||||
}
|
||||
} 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
|
||||
.and_then(|inner| inner.avatar)
|
||||
.or(event.author.avatar);
|
||||
let avatar_url = images::get_avatar_url(
|
||||
avatar_hash,
|
||||
log::trace!(
|
||||
"Edited message was '{}' sent by {} (bot? {:?})",
|
||||
event.content,
|
||||
event.author.id.get(),
|
||||
event.author.discriminator as u64,
|
||||
event.author.bot
|
||||
);
|
||||
|
||||
DisplayProfile { name, avatar_url }
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CallMessageDeleteHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMessageDeleteHandler<State, Extras> for CallMessageDeleteHandler {
|
||||
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 {
|
||||
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 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
|
||||
idiot
|
||||
stupid
|
||||
dumb
|
||||
moron
|
||||
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
|
||||
segs
|
||||
esex
|
||||
esegs
|
||||
give head
|
||||
gave head
|
||||
give me head
|
||||
@@ -35,7 +37,11 @@ stick it in yr pussy
|
||||
cock
|
||||
dick
|
||||
dih
|
||||
pussy
|
||||
pusy
|
||||
posay
|
||||
vagina
|
||||
vgina
|
||||
vgna
|
||||
dck
|
||||
psy
|
||||
pu-c
|
||||
@@ -50,27 +56,32 @@ gang bang
|
||||
squirt
|
||||
squirting
|
||||
squirts
|
||||
bitch
|
||||
bih
|
||||
bich
|
||||
ho
|
||||
hoe
|
||||
huz
|
||||
slut
|
||||
dldo
|
||||
dildo
|
||||
goon
|
||||
goon / go on
|
||||
gooning
|
||||
i will fuh you
|
||||
i will fuck you
|
||||
i will fuh u
|
||||
i will fuh you
|
||||
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
|
||||
nuting
|
||||
goonbeam
|
||||
goonbeaming
|
||||
gooncream
|
||||
gooncreaming
|
||||
goonbud
|
||||
goonbuds
|
||||
goonete
|
||||
goonetes
|
||||
gooner
|
||||
goonr
|
||||
goner
|
||||
boty hole
|
||||
suck my pp
|
||||
ass cracker
|
||||
@@ -86,3 +97,43 @@ masturbate
|
||||
masturbating
|
||||
masterbating
|
||||
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
|
||||
nigas
|
||||
niger
|
||||
nigers
|
||||
nig
|
||||
nigs
|
||||
nga
|
||||
ngas
|
||||
ngr
|
||||
ngrs
|
||||
nger
|
||||
ngers
|
||||
qiga
|
||||
qigas
|
||||
niqa
|
||||
niqas
|
||||
niqer
|
||||
niqers
|
||||
nqa
|
||||
nqas
|
||||
nqer
|
||||
nlga
|
||||
nlger
|
||||
digga
|
||||
nlgers
|
||||
ngiga
|
||||
ngigas
|
||||
negaras
|
||||
nigy
|
||||
nyga
|
||||
diga
|
||||
digas
|
||||
fag
|
||||
fags
|
||||
fagot
|
||||
fagots
|
||||
fgot
|
||||
fgots
|
||||
fgt
|
||||
paki
|
||||
fgts
|
||||
phagot
|
||||
phagots
|
||||
retard
|
||||
retards
|
||||
rtrd
|
||||
rtrds
|
||||
tard
|
||||
dumbfuck
|
||||
dumbfck
|
||||
dumbfuk
|
||||
dumbfuh
|
||||
dumbfuhk
|
||||
dmbfuck
|
||||
dmbfck
|
||||
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
|
||||
tards
|
||||
blackie
|
||||
blackies
|
||||
motherfucker
|
||||
motherfuckers
|
||||
chinese ape
|
||||
chinese apes
|
||||
@@ -9,3 +9,10 @@ fuck you
|
||||
f u
|
||||
f you
|
||||
fuck u
|
||||
shit
|
||||
sht
|
||||
ass
|
||||
huz
|
||||
bitch
|
||||
bih
|
||||
bich
|
||||
@@ -0,0 +1,3 @@
|
||||
pedo
|
||||
pdf
|
||||
pdfile
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user