diff --git a/Cargo.lock b/Cargo.lock index 65e10d8..60dbe4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,11 +363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", - "futures-core", "memchr", - "pin-project-lite", - "tokio", - "tokio-util", ] [[package]] @@ -471,36 +467,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "deadpool" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" -dependencies = [ - "deadpool-runtime", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-redis" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0965b977f1244bc3783bb27cd79cfcff335a8341da18f79232d00504b18eb1a" -dependencies = [ - "deadpool", - "redis", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -dependencies = [ - "tokio", -] - [[package]] name = "deranged" version = "0.4.0" @@ -510,17 +476,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -771,12 +726,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "http" version = "1.3.1" @@ -912,119 +861,12 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "include_dir" version = "0.7.4" @@ -1203,12 +1045,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - [[package]] name = "lock_api" version = "0.4.13" @@ -1310,31 +1146,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1344,16 +1161,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -1431,14 +1238,13 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "payphone" -version = "4.0.15" +version = "4.0.25" dependencies = [ "async-trait", "axum", "clap", "colored", "csv", - "deadpool-redis", "dotenvy", "include_dir", "itertools", @@ -1452,11 +1258,11 @@ dependencies = [ "regex", "scc", "scylla", - "serde", "thiserror 2.0.12", "time", "tokio", "tower-http", + "twilight-cache-inmemory", "twilight-gateway", "twilight-http", "twilight-model", @@ -1490,15 +1296,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1576,27 +1373,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "redis" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" -dependencies = [ - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "socket2 0.6.0", - "tokio", - "tokio-util", - "url", -] - [[package]] name = "redox_syscall" version = "0.5.17" @@ -2166,16 +1942,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tokio" version = "1.47.1" @@ -2343,6 +2109,19 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twilight-cache-inmemory" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9de5f02fa89a48d3fa989811d67a3f3cc3179a46ce71089afa5412ff21fb41" +dependencies = [ + "bitflags 2.9.1", + "dashmap", + "serde", + "twilight-model", + "twilight-util", +] + [[package]] name = "twilight-gateway" version = "0.16.0" @@ -2477,23 +2256,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -2944,12 +2706,6 @@ dependencies = [ "bitflags 2.9.1", ] -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - [[package]] name = "yoke" version = "0.8.0" @@ -3020,36 +2776,3 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 7572ac2..09a3d91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "payphone" -version = "4.0.15" +version = "4.0.25" edition = "2024" default-run = "payphone" @@ -10,7 +10,6 @@ axum = "0.8.4" clap = { version = "4.5.42", features = ["derive", "env", "string"] } colored = "3.0.0" csv = "1.3.1" -deadpool-redis = "0.22.0" dotenvy = "0.15.7" include_dir = "0.7.4" itertools = "0.14.0" @@ -24,11 +23,11 @@ rand_chacha = { version = "0.9.0", features = ["os_rng"] } regex = "1.11.1" scc = "2.3.4" scylla = { version = "1.3.1", features = ["time-03"] } -serde = { version = "1.0.219", features = ["derive"] } thiserror = "2.0.12" time = { version = "0.3.41", features = ["formatting", "local-offset", "macros"] } tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } 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" diff --git a/k8s/redis/01-config-map.yml b/k8s/redis/01-config-map.yml new file mode 100644 index 0000000..9456110 --- /dev/null +++ b/k8s/redis/01-config-map.yml @@ -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 diff --git a/k8s/redis/02-redis.yml b/k8s/redis/02-redis.yml new file mode 100644 index 0000000..7d3bfbd --- /dev/null +++ b/k8s/redis/02-redis.yml @@ -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 \ No newline at end of file diff --git a/k8s/redis/03-cluster-ip.yml b/k8s/redis/03-cluster-ip.yml new file mode 100644 index 0000000..be19ce7 --- /dev/null +++ b/k8s/redis/03-cluster-ip.yml @@ -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 \ No newline at end of file diff --git a/playbooks/00-run-all.sh b/playbooks/00-run-all.sh index dd124b3..5b106a3 100755 --- a/playbooks/00-run-all.sh +++ b/playbooks/00-run-all.sh @@ -8,4 +8,5 @@ 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-payphone.yml --vault-password-file playbooks/00-vault-pwd.txt +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 diff --git a/playbooks/08-redis.yml b/playbooks/08-redis.yml new file mode 100644 index 0000000..a4aa18a --- /dev/null +++ b/playbooks/08-redis.yml @@ -0,0 +1,56 @@ +- 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: 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 diff --git a/playbooks/08-payphone.yml b/playbooks/09-payphone.yml similarity index 98% rename from playbooks/08-payphone.yml rename to playbooks/09-payphone.yml index 074fbd1..298063c 100644 --- a/playbooks/08-payphone.yml +++ b/playbooks/09-payphone.yml @@ -97,7 +97,7 @@ "--shard", "$(SHARD_ID):$(SHARD_COUNT)", "--database-addr", - "scylladb-client.scylla.svc.cluster.local", + "scylladb-client.scylla", "--database-username", "cassandra", "--database-password", diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs deleted file mode 100644 index 45b16d5..0000000 --- a/src/bin/migrate.rs +++ /dev/null @@ -1,432 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use clap::Parser; -use payphone::{ - database::{ - self, - models::{ - MessageData, Report as PayphoneReport, ReportResolution, Server as PayphoneServer, - UserIncident, UserIncidentType, - }, - }, - utils::{Bye, get_random_u64, u64_to_i64}, -}; -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use tokio::{sync::Semaphore, time::Instant}; - -#[derive(Parser)] -struct Args { - dir: String, - - #[arg(long, default_value_t = String::from("127.0.0.1"))] - database_addr: String, - - #[arg(long)] - database_username: Option, - - #[arg(long)] - database_password: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Report { - id: u64, - message_id: u64, - message_content: String, - channel_id: u64, - user_id: u64, - created_at: String, - resolved: String, - resolved_at: Option, - resolved_by_id: Option, - user_avatar_url: String, - user_name: String, - reply_user_name: Option, - reply_message_content: Option, - reply_user_avatar_url: Option, - reply_user_id: Option, - reported_by_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct User { - id: u64, - is_masked: String, - is_moderator: String, - is_supporter_until: String, - warns_count: u64, - bans_count: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Ban { - id: u64, - user_id: u64, - created_at: String, - expires_at: Option, - created_by_id: u64, - expired_by_id: Option, - report_id: Option, - reason: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Server { - id: u64, - prefix: String, - language: String, - name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Warn { - id: u64, - user_id: u64, - reason: String, - created_at: String, - created_by_id: u64, - report_id: u64, -} - -#[tokio::main] -async fn main() { - let args = Args::parse(); - - println!("Testing DB connection..."); - - let db = Arc::new( - database::init( - args.database_addr, - args.database_username, - args.database_password, - ) - .await - .expect("Failed to initialize database"), - ); - - println!("Reading migration file {}/reports.csv", args.dir); - - let mut rdr = csv::Reader::from_path(format!("{}/reports.csv", args.dir)) - .expect("Failed to open reports file"); - - let mut reports = Vec::new(); - - let format = time::format_description::parse( - "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond][offset_hour]", - ) - .expect("Failed to parse format description"); - - for result in rdr.deserialize() { - let report: Report = result.expect("Failed to deserialize report"); - reports.push(report); - } - - println!("Finished reading reports, found {} reports", reports.len()); - println!("Reading migration file {}/users.csv", args.dir); - - let mut rdr = csv::Reader::from_path(format!("{}/users.csv", args.dir)) - .expect("Failed to open users file"); - - let mut users = HashMap::new(); - - for result in rdr.deserialize() { - let user: User = result.expect("Failed to deserialize user"); - users.insert(user.id, user).bye(); - } - - println!("Finished reading users, found {} users", users.len()); - println!("Reading migration file {}/bans.csv", args.dir); - - let mut rdr = - csv::Reader::from_path(format!("{}/bans.csv", args.dir)).expect("Failed to open bans file"); - - let mut bans = HashMap::new(); - - for result in rdr.deserialize() { - let ban: Ban = result.expect("Failed to deserialize ban"); - bans.insert(ban.id, ban).bye(); - } - - println!("Finished reading bans, found {} bans", bans.len()); - println!("Reading migration file {}/servers.csv", args.dir); - - let mut rdr = csv::Reader::from_path(format!("{}/servers.csv", args.dir)) - .expect("Failed to open servers file"); - - let mut servers = HashMap::new(); - - for result in rdr.deserialize() { - let server: Server = result.expect("Failed to deserialize server"); - servers.insert(server.id, server).bye(); - } - - println!("Finished reading servers, found {} servers", servers.len()); - println!("Reading migration file {}/warns.csv", args.dir); - - let mut rdr = csv::Reader::from_path(format!("{}/warns.csv", args.dir)) - .expect("Failed to open warns file"); - - let mut warns = HashMap::new(); - - for result in rdr.deserialize() { - let warn: Warn = result.expect("Failed to deserialize warn"); - warns.insert(warn.id, warn).bye(); - } - - println!("Finished reading warns, found {} warns", warns.len()); - println!("Starting migration..."); - println!("Inserting server configurations..."); - - let semaphore = Arc::new(Semaphore::new(50)); - let mut tasks = Vec::new(); - - for (i, server) in servers.values().enumerate() { - let payphone_server = PayphoneServer { - id: server.id, - prefix: server.prefix.clone(), - language: server.language.clone(), - name: server.name.clone(), - }; - - let task_db = db.clone(); - let task_semaphore = semaphore.clone(); - - tasks.push(tokio::spawn(async move { - let _permit = task_semaphore.acquire().await; - - let start = Instant::now(); - payphone_server - .insert(&task_db) - .await - .expect("Failed to insert server"); - - let duration = start.elapsed(); - println!( - "({i}) Inserted server {} in {:?}", - payphone_server.id, duration - ); - })); - } - - let bans = Arc::new(bans); - let warns = Arc::new(warns); - let reports = Arc::new(reports); - - for (i, report) in reports.iter().enumerate() { - let task_db = db.clone(); - let task_semaphore = semaphore.clone(); - let task_report = report.clone(); - let task_bans = bans.clone(); - let task_warns = warns.clone(); - let task_format = format.clone(); - - tasks.push(tokio::spawn(async move { - let _permit = task_semaphore.acquire().await; - - let start = Instant::now(); - - let resolution = if task_report.resolved == "t" { - if task_bans.iter().any(|(_id, ban)| { - ban.report_id - .map(|report_id| task_report.id == report_id) - .unwrap_or(false) - }) { - Some(ReportResolution::Ban) - } else if task_warns - .iter() - .any(|(_id, warn)| warn.report_id == task_report.id) - { - Some(ReportResolution::Warn) - } else { - Some(ReportResolution::Skip) - } - } else { - None - }; - - let payphone_report = PayphoneReport { - id: task_report.id, - user_id: task_report.user_id, - message: MessageData { - id: u64_to_i64(task_report.message_id), - call_id: 0, - author_id: u64_to_i64(task_report.user_id), - author_name: task_report.user_name, - author_avatar_url: task_report.user_avatar_url, - author_mask: None, - content: task_report.message_content, - attachments: vec![], - sticker: None, - propagation_message_id: None, - propagation_channel_id: None, - replying_to_message_id: None, - is_deleted: false, - is_edited: false, - is_censored: false, - }, - created_at: OffsetDateTime::parse(&task_report.created_at, &task_format) - .expect("Failed to parse created_at") - .to_utc(), - created_by_ids: task_report - .reported_by_id - .map(|id| vec![id]) - .unwrap_or_default(), - replying_to: None, - reviewed_at: task_report.resolved_at.as_ref().map(|s| { - OffsetDateTime::parse(s, &task_format) - .expect("Failed to parse resolved_at") - .to_utc() - }), - resolution, - reviewed_by_id: task_report.resolved_by_id, - }; - - payphone_report - .insert(&task_db) - .await - .expect("Failed to insert report"); - - let duration = start.elapsed(); - println!( - "({i}) Inserted report {} in {:?}", - payphone_report.id, duration - ); - })); - } - - for (i, ban) in bans.values().enumerate() { - let task_semaphore = semaphore.clone(); - let task_format = format.clone(); - let task_db = db.clone(); - let task_reports = reports.clone(); - let task_ban = Arc::new(ban.clone()); - - tasks.push(tokio::spawn(async move { - let _permit = task_semaphore.acquire().await; - - let start = Instant::now(); - - let message = task_reports - .iter() - .find(|report| { - task_ban - .report_id - .map(|id| id == report.id) - .unwrap_or(false) - }) - .map(|report| MessageData { - id: u64_to_i64(report.message_id), - call_id: 0, - author_id: u64_to_i64(report.user_id), - author_name: report.user_name.clone(), - author_avatar_url: report.user_avatar_url.clone(), - author_mask: None, - content: report.message_content.clone(), - attachments: vec![], - sticker: None, - propagation_message_id: None, - propagation_channel_id: None, - replying_to_message_id: None, - is_deleted: false, - is_edited: false, - is_censored: false, - }); - - let payphone_incident = UserIncident { - incident_id: get_random_u64(), - user_id: task_ban.user_id, - report_id: task_ban.report_id, - r#type: UserIncidentType::Ban, - message, - reason: task_ban.reason.clone(), - created_at: OffsetDateTime::parse(&task_ban.created_at, &task_format) - .expect("Failed to parse created_at") - .to_utc(), - created_by_id: Some(task_ban.created_by_id), - duration: None, - expires_at: task_ban.expires_at.as_ref().map(|expires_at| { - OffsetDateTime::parse(expires_at, &task_format) - .expect("Failed to parse expires_at") - .to_utc() - }), - }; - - payphone_incident - .insert(&task_db) - .await - .expect("Failed to insert ban"); - - let duration = start.elapsed(); - println!( - "({i}) Inserted incident (ban) {} in {:?}", - payphone_incident.incident_id, duration - ); - })); - } - - for (i, warn) in warns.values().enumerate() { - let task_semaphore = semaphore.clone(); - let task_format = format.clone(); - let task_db = db.clone(); - let task_reports = reports.clone(); - let task_warn = Arc::new(warn.clone()); - - tasks.push(tokio::spawn(async move { - let _permit = task_semaphore.acquire().await; - - let start = Instant::now(); - - let message = task_reports - .iter() - .find(|report| report.id == task_warn.report_id) - .map(|report| MessageData { - id: u64_to_i64(report.message_id), - call_id: 0, - author_id: u64_to_i64(report.user_id), - author_name: report.user_name.clone(), - author_avatar_url: report.user_avatar_url.clone(), - author_mask: None, - content: report.message_content.clone(), - attachments: vec![], - sticker: None, - propagation_message_id: None, - propagation_channel_id: None, - replying_to_message_id: None, - is_deleted: false, - is_edited: false, - is_censored: false, - }); - - let payphone_incident = UserIncident { - incident_id: get_random_u64(), - user_id: task_warn.user_id, - report_id: Some(task_warn.report_id), - r#type: UserIncidentType::Warning, - message, - reason: task_warn.reason.clone(), - created_at: OffsetDateTime::parse(&task_warn.created_at, &task_format) - .expect("Failed to parse created_at") - .to_utc(), - created_by_id: Some(task_warn.created_by_id), - duration: None, - expires_at: None, - }; - - payphone_incident - .insert(&task_db) - .await - .expect("Failed to insert warn"); - - let duration = start.elapsed(); - println!( - "({i}) Inserted incident (warn) {} in {:?}", - payphone_incident.incident_id, duration - ); - })); - } - - for task in tasks { - task.await.expect("Failed to join task"); - } -} diff --git a/src/cli.rs b/src/cli.rs index b96f7aa..3943831 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -196,21 +196,14 @@ pub enum Commands { database_addr: String, /// The username to use when connecting to the database. - #[arg( - long, - env = "PAYPHONE_DATABASE_USERNAME", - value_name = "USERNAME" - )] + #[arg(long, env = "PAYPHONE_DATABASE_USERNAME", value_name = "USERNAME")] database_username: Option, /// The password to use when connecting to the database. - #[arg( - long, - env = "PAYPHONE_DATABASE_PASSWORD", - value_name = "PASSWORD" - )] + #[arg(long, env = "PAYPHONE_DATABASE_PASSWORD", value_name = "PASSWORD")] database_password: Option, + /// The current shard being run. #[arg( long, short, diff --git a/src/database/migrations/0001_init.cql b/src/database/migrations/0001_init.cql index e229477..f4bd6ab 100644 --- a/src/database/migrations/0001_init.cql +++ b/src/database/migrations/0001_init.cql @@ -144,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 ( diff --git a/src/framework/bot.rs b/src/framework/bot.rs index c9beffd..a72dfad 100644 --- a/src/framework/bot.rs +++ b/src/framework/bot.rs @@ -2,6 +2,7 @@ use std::{fmt::Debug, sync::Arc}; use async_trait::async_trait; use thiserror::Error; +use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType}; use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; use twilight_http::Client; @@ -88,11 +89,12 @@ where } } -pub struct Bot { +pub struct Bot { shard: ShardId, state: Arc, prefixes: Arc + Send + Sync>, intents: Intents, + cached_resource_types: Vec, commands: Vec>>, components: Vec>>, on_ready_handlers: Vec>>, @@ -113,6 +115,7 @@ impl Bot { state: Arc::new(state), prefixes: Arc::new(prefixes), intents: Intents::empty(), + cached_resource_types: Vec::new(), commands: Vec::new(), components: Vec::new(), on_ready_handlers: Vec::new(), @@ -147,6 +150,14 @@ impl Bot { 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 + 'static) -> Self { self.on_ready_handlers.push(Arc::new(handler)); self @@ -219,6 +230,16 @@ impl Bot { 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 runtime = Arc::new(Runtime { http, app, @@ -226,6 +247,7 @@ impl Bot { state: bot.state.clone(), prefixes: bot.prefixes.clone(), commands: bot.commands.clone(), + cache, }); let mut gateway = Shard::new(bot.shard, token.to_string(), bot.intents); @@ -236,6 +258,8 @@ impl Bot { 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 { diff --git a/src/framework/cache/mod.rs b/src/framework/cache/mod.rs deleted file mode 100644 index aacafb6..0000000 --- a/src/framework/cache/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub struct RedisCache { - -} \ No newline at end of file diff --git a/src/framework/commands/mod.rs b/src/framework/commands/mod.rs index 507aa64..dcd1cb8 100644 --- a/src/framework/commands/mod.rs +++ b/src/framework/commands/mod.rs @@ -33,6 +33,9 @@ pub enum CommandError { #[error(transparent)] DiscordDeserializationFailed(#[from] DeserializeBodyError), + + #[error("The cache's state was inconsistent.")] + CacheStateInconsistent, } #[async_trait] diff --git a/src/framework/mod.rs b/src/framework/mod.rs index d806d86..3823c8e 100644 --- a/src/framework/mod.rs +++ b/src/framework/mod.rs @@ -158,7 +158,6 @@ //! ``` pub mod bot; -pub mod cache; pub mod commands; pub mod events; pub mod interactions; diff --git a/src/framework/runtime.rs b/src/framework/runtime.rs index 1382530..66205e6 100644 --- a/src/framework/runtime.rs +++ b/src/framework/runtime.rs @@ -19,6 +19,7 @@ use std::sync::Arc; +use twilight_cache_inmemory::InMemoryCache; use twilight_http::Client; use twilight_model::oauth::Application; @@ -45,6 +46,8 @@ pub struct Runtime { /// The application state. pub state: Arc, + + pub cache: InMemoryCache, } impl Runtime { diff --git a/src/groups/calls/lists/casual-insults-and-teasing.txt b/src/groups/calls/lists/casual-insults-and-teasing.txt index c140a14..d5963de 100644 --- a/src/groups/calls/lists/casual-insults-and-teasing.txt +++ b/src/groups/calls/lists/casual-insults-and-teasing.txt @@ -3,4 +3,8 @@ idiot stupid dumb moron -fool \ No newline at end of file +fool +stfu +syfm +sybau +gtfo \ No newline at end of file diff --git a/src/groups/calls/lists/explicit-sexual-language.txt b/src/groups/calls/lists/explicit-sexual-language.txt index ac61cec..3db96cb 100644 --- a/src/groups/calls/lists/explicit-sexual-language.txt +++ b/src/groups/calls/lists/explicit-sexual-language.txt @@ -75,6 +75,8 @@ goonbeam goonbeaming gooncream gooncreaming +goonbud +goonbuds boty hole suck my pp ass cracker @@ -97,4 +99,23 @@ whre tickle your pickle tickle yr pickle tickle his pickle -tickle my pickle \ No newline at end of file +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 +il touch you +il touch u +im going to touch you +im going to touch u +im gonna touch you +im gonna touch u \ No newline at end of file diff --git a/src/groups/calls/lists/hate-speech-and-slurs.txt b/src/groups/calls/lists/hate-speech-and-slurs.txt index 271d728..a86ad49 100644 --- a/src/groups/calls/lists/hate-speech-and-slurs.txt +++ b/src/groups/calls/lists/hate-speech-and-slurs.txt @@ -32,15 +32,17 @@ dumb fck dumb fuk dumb fuh dumb fuhk -stfu +shut the fvck up +shut the fck up shut the fuck up -syfm shut your fucking mouth -sybau +shut your fcking mouth +shut your fvcking mouth shut your bitch as up shut your bih as up -gtfo get the fuck out +get the fvck out +get the fck out +get tfo blackie -motherfucker -mf \ No newline at end of file +motherfucker \ No newline at end of file diff --git a/src/groups/servers/commands.rs b/src/groups/servers/commands.rs index 660877a..6b40ba8 100644 --- a/src/groups/servers/commands.rs +++ b/src/groups/servers/commands.rs @@ -8,7 +8,9 @@ use crate::{ Command, CommandError, arguments::{Argument, GreedyString, ParseArguments}, context::CommandContext, + permissions::Permission, }, + groups::servers::permissions::{GuildOnlyPermission, ServerModeratorPermission}, state::State, }; @@ -37,6 +39,13 @@ impl Command for PrefixCommand { }] } + fn permissions(&self) -> Vec + Send + Sync + 'static>> { + vec![ + Box::new(GuildOnlyPermission), + Box::new(ServerModeratorPermission), + ] + } + async fn execute( &self, ctx: CommandContext<'_, State>, diff --git a/src/groups/servers/permissions.rs b/src/groups/servers/permissions.rs index 8f09a4d..535fc17 100644 --- a/src/groups/servers/permissions.rs +++ b/src/groups/servers/permissions.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use twilight_model::guild::Permissions; use crate::{ framework::commands::{CommandError, context::CommandContext, permissions::Permission}, @@ -11,7 +12,15 @@ pub struct GuildOnlyPermission; #[async_trait] impl Permission for GuildOnlyPermission { async fn check(&self, ctx: &CommandContext) -> Result { - Ok(ctx.event.guild_id.is_none()) + Ok(ctx.event.guild_id.is_some()) + } + + async fn on_handle_denied(&self, ctx: &CommandContext) -> Result<(), CommandError> { + ctx.send() + .content("❌ This command can only be used in a server.") + .await?; + + Ok(()) } } @@ -20,7 +29,37 @@ pub struct ServerModeratorPermission; #[async_trait] impl Permission for ServerModeratorPermission { - async fn check(&self, _ctx: &CommandContext) -> Result { - Ok(true) + async fn check(&self, ctx: &CommandContext) -> Result { + if let Some(guild_id) = ctx.event.guild_id { + let permissions = ctx + .rt + .cache + .permissions() + .root(ctx.event.author.id, guild_id); + + if let Ok(permissions) = permissions { + let guild = ctx + .rt + .cache + .guild(guild_id) + .ok_or(CommandError::CacheStateInconsistent)?; + + Ok(permissions + .contains(Permissions::MANAGE_CHANNELS | Permissions::MANAGE_MESSAGES) + || guild.owner_id() == ctx.event.author.id) + } else { + Err(CommandError::CacheStateInconsistent) + } + } else { + Ok(true) + } + } + + async fn on_handle_denied(&self, ctx: &CommandContext) -> Result<(), CommandError> { + ctx.send() + .content("❌ This command can only be run by a server moderator.\n-# 🛈 Moderators must have permission to manage messages and to manage channels.") + .await?; + + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index be460e2..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod cli; -pub mod constants; -pub mod database; -pub mod discord; -pub mod framework; -pub mod groups; -pub mod logging; -pub mod state; -pub mod utils; diff --git a/src/main.rs b/src/main.rs index 1259820..449a3f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -113,6 +113,7 @@ use clap::Parser; use notify::{Event, EventKind, Watcher}; use tokio::net::TcpListener; use tower_http::services::ServeDir; +use twilight_cache_inmemory::ResourceType; use twilight_gateway::{Intents, ShardId}; use crate::{ @@ -227,7 +228,7 @@ async fn start( .await .expect("Could not connect to the database!"); - log::info!("Database connection established successfully!"); + log::info!("Database connection established successfully."); let state = State { db }; @@ -235,6 +236,7 @@ async fn start( .shard(shard) .intent(Intents::GUILDS) .intent(Intents::GUILD_MESSAGES) + .intent(Intents::GUILD_MEMBERS) .intent(Intents::MESSAGE_CONTENT) .command(HelpCommand) .command(CallCommand::new()) @@ -243,6 +245,10 @@ async fn start( .command(FriendRequestCommand) .command(PrefixCommand) .component(HelpGoBackButton) + .cache_resource_type(ResourceType::GUILD) + .cache_resource_type(ResourceType::CHANNEL) + .cache_resource_type(ResourceType::MEMBER) + .cache_resource_type(ResourceType::ROLE) .on_ready(ReadyHandler) .on_reconnect(ReconnectHandler) .on_message(CallMessageHandler)