From 7c0cbea716a0b898868f9f90a91589c7b655929e Mon Sep 17 00:00:00 2001
From: Natsu Kagami <nki@nkagami.me>
Date: Thu, 27 Mar 2025 15:13:00 +0100
Subject: [PATCH] `ignore` command (#59)

* New table `ignored_users` and queries

* Update sqlx to 0.8

* Implement `ignore` command and add needed hooks
---
 ...24eefd77bada44bec1043972493146c09bb73.json |  12 +
 ...70e1347329e945de2eefad4bfcab5f81b73ce.json |   6 +-
 ...5bbac57c38ad2c51c1924683d13d045f21ad9.json |   8 +-
 ...ecc54be42e1e8787ae5a06a61bb8ba8e9c366.json |   8 +-
 ...e28d3c6c1c4bb654c82cc40bf61390c3dad4b.json |   2 +-
 ...3e2125b6d8134346144f462325dc025221044.json |   4 +-
 ...d8f32f0bda74ceee62d10c84ac03b710c828c.json |   8 +-
 ...d87ebde6dabdd70e2ba7b110ebec91e7feda7.json |   8 +-
 ...c3b787fa284a74b3e3d491755c88a693810e0.json |   2 +-
 ...f6b51e283c83b1ba7fa17e5cb056b220f7889.json |   6 +-
 ...9e7841b2083ee4b18ea83eedcd46bc4356599.json |   8 +-
 ...7756346aa50a63b911414ee8f7a4a0d6dd1cc.json |   4 +-
 ...63f4efcfe4685c0561e07a2c954538be4789b.json |  32 ++
 ...ce242a241d3499a6b973ecf5f6c6588b077ad.json |  32 ++
 ...4cf30b025c9ac0ac4d5bcc12656459c5e083a.json |   8 +-
 Cargo.lock                                    | 313 ++++++++----------
 youmubot-core/Cargo.toml                      |   2 +
 youmubot-core/src/admin/ignore.rs             | 152 +++++++++
 youmubot-core/src/admin/mod.rs                |   2 +
 youmubot-core/src/lib.rs                      |  31 +-
 ...ecc54be42e1e8787ae5a06a61bb8ba8e9c366.json |  44 ---
 ...d8f32f0bda74ceee62d10c84ac03b710c828c.json |  44 ---
 ...1f46e3521357ec1d7c062bef762d3e4267378.json |  12 -
 ...4cf30b025c9ac0ac4d5bcc12656459c5e083a.json |  44 ---
 youmubot-db-sql/Cargo.toml                    |   2 +-
 .../20250327105109_ignored_users.sql          |   7 +
 youmubot-db-sql/src/models/ignore_list.rs     |  60 ++++
 youmubot-db-sql/src/models/mod.rs             |   1 +
 youmubot-prelude/src/args.rs                  |   8 +-
 youmubot/src/main.rs                          |  93 +++++-
 30 files changed, 580 insertions(+), 383 deletions(-)
 create mode 100644 .sqlx/query-1619aac8983ad68177bd698393b24eefd77bada44bec1043972493146c09bb73.json
 create mode 100644 .sqlx/query-db5960586bc418d2a6a31c18b4a63f4efcfe4685c0561e07a2c954538be4789b.json
 create mode 100644 .sqlx/query-dbb948ade9139445faf17c1c6bece242a241d3499a6b973ecf5f6c6588b077ad.json
 create mode 100644 youmubot-core/src/admin/ignore.rs
 delete mode 100644 youmubot-db-sql/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json
 delete mode 100644 youmubot-db-sql/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json
 delete mode 100644 youmubot-db-sql/.sqlx/query-b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378.json
 delete mode 100644 youmubot-db-sql/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json
 create mode 100644 youmubot-db-sql/migrations/20250327105109_ignored_users.sql
 create mode 100644 youmubot-db-sql/src/models/ignore_list.rs

diff --git a/.sqlx/query-1619aac8983ad68177bd698393b24eefd77bada44bec1043972493146c09bb73.json b/.sqlx/query-1619aac8983ad68177bd698393b24eefd77bada44bec1043972493146c09bb73.json
new file mode 100644
index 0000000..333739c
--- /dev/null
+++ b/.sqlx/query-1619aac8983ad68177bd698393b24eefd77bada44bec1043972493146c09bb73.json
@@ -0,0 +1,12 @@
+{
+  "db_name": "SQLite",
+  "query": "DELETE FROM ignored_users WHERE id = ?",
+  "describe": {
+    "columns": [],
+    "parameters": {
+      "Right": 1
+    },
+    "nullable": []
+  },
+  "hash": "1619aac8983ad68177bd698393b24eefd77bada44bec1043972493146c09bb73"
+}
diff --git a/.sqlx/query-1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce.json b/.sqlx/query-1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce.json
index 9c36779..35fa12f 100644
--- a/.sqlx/query-1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce.json
+++ b/.sqlx/query-1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce.json
@@ -6,7 +6,7 @@
       {
         "name": "channel_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "beatmap",
@@ -16,7 +16,7 @@
       {
         "name": "mode: u8",
         "ordinal": 2,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
@@ -25,7 +25,7 @@
     "nullable": [
       false,
       false,
-      false
+      true
     ]
   },
   "hash": "1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce"
diff --git a/.sqlx/query-235312a1aad1a58c2f7f2d817945bbac57c38ad2c51c1924683d13d045f21ad9.json b/.sqlx/query-235312a1aad1a58c2f7f2d817945bbac57c38ad2c51c1924683d13d045f21ad9.json
index 344ffdd..20caec3 100644
--- a/.sqlx/query-235312a1aad1a58c2f7f2d817945bbac57c38ad2c51c1924683d13d045f21ad9.json
+++ b/.sqlx/query-235312a1aad1a58c2f7f2d817945bbac57c38ad2c51c1924683d13d045f21ad9.json
@@ -6,22 +6,22 @@
       {
         "name": "beatmap_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mode: u8",
         "ordinal": 1,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "user_id: i64",
         "ordinal": 2,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mods: i64",
         "ordinal": 3,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "cached_at: DateTime",
diff --git a/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json b/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json
index 814d59b..592ce81 100644
--- a/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json
+++ b/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json
@@ -6,7 +6,7 @@
       {
         "name": "user_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "username",
@@ -16,17 +16,17 @@
       {
         "name": "id: i64",
         "ordinal": 2,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "preferred_mode: u8",
         "ordinal": 3,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "failures: u8",
         "ordinal": 4,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
diff --git a/.sqlx/query-296c95c7ead4d747a4da007b4b6e28d3c6c1c4bb654c82cc40bf61390c3dad4b.json b/.sqlx/query-296c95c7ead4d747a4da007b4b6e28d3c6c1c4bb654c82cc40bf61390c3dad4b.json
index 2e8b15f..82981ab 100644
--- a/.sqlx/query-296c95c7ead4d747a4da007b4b6e28d3c6c1c4bb654c82cc40bf61390c3dad4b.json
+++ b/.sqlx/query-296c95c7ead4d747a4da007b4b6e28d3c6c1c4bb654c82cc40bf61390c3dad4b.json
@@ -6,7 +6,7 @@
       {
         "name": "beatmap_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "cached_at: DateTime",
diff --git a/.sqlx/query-4b033607229deba540f80e469753e2125b6d8134346144f462325dc025221044.json b/.sqlx/query-4b033607229deba540f80e469753e2125b6d8134346144f462325dc025221044.json
index 91a9e25..a8b69b9 100644
--- a/.sqlx/query-4b033607229deba540f80e469753e2125b6d8134346144f462325dc025221044.json
+++ b/.sqlx/query-4b033607229deba540f80e469753e2125b6d8134346144f462325dc025221044.json
@@ -6,12 +6,12 @@
       {
         "name": "beatmap_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mode: u8",
         "ordinal": 1,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "cached_at: DateTime",
diff --git a/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json b/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json
index 8de5138..064fc63 100644
--- a/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json
+++ b/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json
@@ -6,7 +6,7 @@
       {
         "name": "user_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "username",
@@ -16,17 +16,17 @@
       {
         "name": "id: i64",
         "ordinal": 2,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "preferred_mode: u8",
         "ordinal": 3,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "failures: u8",
         "ordinal": 4,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
diff --git a/.sqlx/query-95541f737a8dfc7f440840617bed87ebde6dabdd70e2ba7b110ebec91e7feda7.json b/.sqlx/query-95541f737a8dfc7f440840617bed87ebde6dabdd70e2ba7b110ebec91e7feda7.json
index 3715e7c..24f5b4a 100644
--- a/.sqlx/query-95541f737a8dfc7f440840617bed87ebde6dabdd70e2ba7b110ebec91e7feda7.json
+++ b/.sqlx/query-95541f737a8dfc7f440840617bed87ebde6dabdd70e2ba7b110ebec91e7feda7.json
@@ -6,22 +6,22 @@
       {
         "name": "beatmap_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mode: u8",
         "ordinal": 1,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "user_id: i64",
         "ordinal": 2,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mods: i64",
         "ordinal": 3,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "cached_at: DateTime",
diff --git a/.sqlx/query-9c620436cfe463780fca2dc5497c3b787fa284a74b3e3d491755c88a693810e0.json b/.sqlx/query-9c620436cfe463780fca2dc5497c3b787fa284a74b3e3d491755c88a693810e0.json
index f5ac4eb..68d6146 100644
--- a/.sqlx/query-9c620436cfe463780fca2dc5497c3b787fa284a74b3e3d491755c88a693810e0.json
+++ b/.sqlx/query-9c620436cfe463780fca2dc5497c3b787fa284a74b3e3d491755c88a693810e0.json
@@ -6,7 +6,7 @@
       {
         "name": "id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
diff --git a/.sqlx/query-a247c7a73bdbf1042fdd8363da1f6b51e283c83b1ba7fa17e5cb056b220f7889.json b/.sqlx/query-a247c7a73bdbf1042fdd8363da1f6b51e283c83b1ba7fa17e5cb056b220f7889.json
index bb0e861..8111b9c 100644
--- a/.sqlx/query-a247c7a73bdbf1042fdd8363da1f6b51e283c83b1ba7fa17e5cb056b220f7889.json
+++ b/.sqlx/query-a247c7a73bdbf1042fdd8363da1f6b51e283c83b1ba7fa17e5cb056b220f7889.json
@@ -6,7 +6,7 @@
       {
         "name": "mode: u8",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "pp",
@@ -21,12 +21,12 @@
       {
         "name": "map_age",
         "ordinal": 3,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "last_update: DateTime",
         "ordinal": 4,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
diff --git a/.sqlx/query-d2c0856276174653a9dd428a2f89e7841b2083ee4b18ea83eedcd46bc4356599.json b/.sqlx/query-d2c0856276174653a9dd428a2f89e7841b2083ee4b18ea83eedcd46bc4356599.json
index 77e4dc3..27b2b2a 100644
--- a/.sqlx/query-d2c0856276174653a9dd428a2f89e7841b2083ee4b18ea83eedcd46bc4356599.json
+++ b/.sqlx/query-d2c0856276174653a9dd428a2f89e7841b2083ee4b18ea83eedcd46bc4356599.json
@@ -6,12 +6,12 @@
       {
         "name": "user_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mode: u8",
         "ordinal": 1,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "pp",
@@ -26,12 +26,12 @@
       {
         "name": "map_age",
         "ordinal": 4,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "last_update: DateTime",
         "ordinal": 5,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
diff --git a/.sqlx/query-d7c91077f904543740a12185fac7756346aa50a63b911414ee8f7a4a0d6dd1cc.json b/.sqlx/query-d7c91077f904543740a12185fac7756346aa50a63b911414ee8f7a4a0d6dd1cc.json
index f68b7a6..0659279 100644
--- a/.sqlx/query-d7c91077f904543740a12185fac7756346aa50a63b911414ee8f7a4a0d6dd1cc.json
+++ b/.sqlx/query-d7c91077f904543740a12185fac7756346aa50a63b911414ee8f7a4a0d6dd1cc.json
@@ -6,12 +6,12 @@
       {
         "name": "beatmap_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "mode: u8",
         "ordinal": 1,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "cached_at: DateTime",
diff --git a/.sqlx/query-db5960586bc418d2a6a31c18b4a63f4efcfe4685c0561e07a2c954538be4789b.json b/.sqlx/query-db5960586bc418d2a6a31c18b4a63f4efcfe4685c0561e07a2c954538be4789b.json
new file mode 100644
index 0000000..f357b1b
--- /dev/null
+++ b/.sqlx/query-db5960586bc418d2a6a31c18b4a63f4efcfe4685c0561e07a2c954538be4789b.json
@@ -0,0 +1,32 @@
+{
+  "db_name": "SQLite",
+  "query": "SELECT\n              id,\n              username,\n              ignored_since as \"ignored_since: DateTime\"\n            FROM ignored_users\n            ORDER BY id ASC",
+  "describe": {
+    "columns": [
+      {
+        "name": "id",
+        "ordinal": 0,
+        "type_info": "Integer"
+      },
+      {
+        "name": "username",
+        "ordinal": 1,
+        "type_info": "Text"
+      },
+      {
+        "name": "ignored_since: DateTime",
+        "ordinal": 2,
+        "type_info": "Datetime"
+      }
+    ],
+    "parameters": {
+      "Right": 0
+    },
+    "nullable": [
+      false,
+      false,
+      false
+    ]
+  },
+  "hash": "db5960586bc418d2a6a31c18b4a63f4efcfe4685c0561e07a2c954538be4789b"
+}
diff --git a/.sqlx/query-dbb948ade9139445faf17c1c6bece242a241d3499a6b973ecf5f6c6588b077ad.json b/.sqlx/query-dbb948ade9139445faf17c1c6bece242a241d3499a6b973ecf5f6c6588b077ad.json
new file mode 100644
index 0000000..3c447a8
--- /dev/null
+++ b/.sqlx/query-dbb948ade9139445faf17c1c6bece242a241d3499a6b973ecf5f6c6588b077ad.json
@@ -0,0 +1,32 @@
+{
+  "db_name": "SQLite",
+  "query": "INSERT INTO ignored_users(id, username) VALUES (?, ?)\n               ON CONFLICT (id) DO UPDATE SET username = excluded.username\n               RETURNING id,\n              username,\n              ignored_since as \"ignored_since: DateTime\"",
+  "describe": {
+    "columns": [
+      {
+        "name": "id",
+        "ordinal": 0,
+        "type_info": "Integer"
+      },
+      {
+        "name": "username",
+        "ordinal": 1,
+        "type_info": "Text"
+      },
+      {
+        "name": "ignored_since: DateTime",
+        "ordinal": 2,
+        "type_info": "Datetime"
+      }
+    ],
+    "parameters": {
+      "Right": 2
+    },
+    "nullable": [
+      false,
+      false,
+      false
+    ]
+  },
+  "hash": "dbb948ade9139445faf17c1c6bece242a241d3499a6b973ecf5f6c6588b077ad"
+}
diff --git a/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json b/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json
index d851042..3432b59 100644
--- a/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json
+++ b/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json
@@ -6,7 +6,7 @@
       {
         "name": "user_id: i64",
         "ordinal": 0,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "username",
@@ -16,17 +16,17 @@
       {
         "name": "id: i64",
         "ordinal": 2,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "preferred_mode: u8",
         "ordinal": 3,
-        "type_info": "Int64"
+        "type_info": "Integer"
       },
       {
         "name": "failures: u8",
         "ordinal": 4,
-        "type_info": "Int64"
+        "type_info": "Integer"
       }
     ],
     "parameters": {
diff --git a/Cargo.lock b/Cargo.lock
index b5281bc..90729c1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,19 +38,6 @@ dependencies = [
  "cpufeatures",
 ]
 
-[[package]]
-name = "ahash"
-version = "0.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
-dependencies = [
- "cfg-if",
- "getrandom",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
 [[package]]
 name = "aho-corasick"
 version = "1.1.2"
@@ -116,16 +103,6 @@ dependencies = [
  "num-traits",
 ]
 
-[[package]]
-name = "atomic-write-file"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
-dependencies = [
- "nix",
- "rand",
-]
-
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -287,12 +264,13 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.83"
+version = "1.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
 dependencies = [
  "jobserver",
  "libc",
+ "shlex",
 ]
 
 [[package]]
@@ -351,6 +329,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "const-oid"
 version = "0.9.6"
@@ -610,9 +597,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "errno"
-version = "0.3.8"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -640,15 +627,20 @@ dependencies = [
 
 [[package]]
 name = "event-listener"
-version = "2.5.3"
+version = "5.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
 
 [[package]]
 name = "fastrand"
-version = "2.0.1"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
 
 [[package]]
 name = "finl_unicode"
@@ -696,6 +688,12 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
 [[package]]
 name = "foreign-types"
 version = "0.3.2"
@@ -847,10 +845,22 @@ dependencies = [
  "cfg-if",
  "js-sys",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
 [[package]]
 name = "gimli"
 version = "0.28.1"
@@ -893,34 +903,32 @@ name = "hashbrown"
 version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
-dependencies = [
- "ahash",
- "allocator-api2",
-]
 
 [[package]]
 name = "hashbrown"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
 
 [[package]]
 name = "hashlink"
-version = "0.8.4"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
 dependencies = [
- "hashbrown 0.14.3",
+ "hashbrown 0.15.2",
 ]
 
 [[package]]
 name = "heck"
-version = "0.4.1"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
 name = "hermit-abi"
@@ -1147,15 +1155,6 @@ version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itoa"
 version = "1.0.10"
@@ -1164,9 +1163,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
 
 [[package]]
 name = "jobserver"
-version = "0.1.28"
+version = "0.1.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
 dependencies = [
  "libc",
 ]
@@ -1206,9 +1205,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.171"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
 
 [[package]]
 name = "libm"
@@ -1218,9 +1217,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
 [[package]]
 name = "libsqlite3-sys"
-version = "0.27.0"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
 dependencies = [
  "cc",
  "pkg-config",
@@ -1235,9 +1234,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
 
 [[package]]
 name = "lock_api"
@@ -1302,12 +1301,6 @@ dependencies = [
  "triomphe",
 ]
 
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
 [[package]]
 name = "miniz_oxide"
 version = "0.7.2"
@@ -1324,7 +1317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.48.0",
 ]
 
@@ -1334,7 +1327,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.12",
 ]
 
 [[package]]
@@ -1355,27 +1348,6 @@ dependencies = [
  "tempfile",
 ]
 
-[[package]]
-name = "nix"
-version = "0.27.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
-dependencies = [
- "bitflags 2.4.2",
- "cfg-if",
- "libc",
-]
-
-[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
 [[package]]
 name = "nu-ansi-term"
 version = "0.46.0"
@@ -1514,6 +1486,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -1710,6 +1688,12 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -1737,7 +1721,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.12",
 ]
 
 [[package]]
@@ -1832,7 +1816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
 dependencies = [
  "cc",
- "getrandom",
+ "getrandom 0.2.12",
  "libc",
  "spin 0.9.8",
  "untrusted",
@@ -1935,9 +1919,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
 
 [[package]]
 name = "rustix"
-version = "0.38.31"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
 dependencies = [
  "bitflags 2.4.2",
  "errno",
@@ -2234,6 +2218,12 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
 [[package]]
 name = "signature"
 version = "2.2.0"
@@ -2283,6 +2273,9 @@ name = "smallvec"
 version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "socket2"
@@ -2319,22 +2312,11 @@ dependencies = [
  "der",
 ]
 
-[[package]]
-name = "sqlformat"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
-dependencies = [
- "itertools",
- "nom",
- "unicode_categories",
-]
-
 [[package]]
 name = "sqlx"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
+checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
 dependencies = [
  "sqlx-core",
  "sqlx-macros",
@@ -2345,39 +2327,32 @@ dependencies = [
 
 [[package]]
 name = "sqlx-core"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
+checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
 dependencies = [
- "ahash",
- "atoi",
- "byteorder",
  "bytes",
  "chrono",
  "crc",
  "crossbeam-queue",
- "dotenvy",
  "either",
  "event-listener",
- "futures-channel",
  "futures-core",
  "futures-intrusive",
  "futures-io",
  "futures-util",
+ "hashbrown 0.15.2",
  "hashlink",
- "hex",
  "indexmap 2.7.0",
  "log",
  "memchr",
  "once_cell",
- "paste",
  "percent-encoding",
  "serde",
  "serde_json",
  "sha2",
  "smallvec",
- "sqlformat",
- "thiserror 1.0.57",
+ "thiserror 2.0.9",
  "tokio",
  "tokio-stream",
  "tracing",
@@ -2386,24 +2361,23 @@ dependencies = [
 
 [[package]]
 name = "sqlx-macros"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
+checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
 dependencies = [
  "proc-macro2",
  "quote",
  "sqlx-core",
  "sqlx-macros-core",
- "syn 1.0.109",
+ "syn 2.0.93",
 ]
 
 [[package]]
 name = "sqlx-macros-core"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
+checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
 dependencies = [
- "atomic-write-file",
  "dotenvy",
  "either",
  "heck",
@@ -2418,7 +2392,7 @@ dependencies = [
  "sqlx-mysql",
  "sqlx-postgres",
  "sqlx-sqlite",
- "syn 1.0.109",
+ "syn 2.0.93",
  "tempfile",
  "tokio",
  "url",
@@ -2426,12 +2400,12 @@ dependencies = [
 
 [[package]]
 name = "sqlx-mysql"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
+checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
 dependencies = [
  "atoi",
- "base64 0.21.7",
+ "base64 0.22.1",
  "bitflags 2.4.2",
  "byteorder",
  "bytes",
@@ -2462,19 +2436,19 @@ dependencies = [
  "smallvec",
  "sqlx-core",
  "stringprep",
- "thiserror 1.0.57",
+ "thiserror 2.0.9",
  "tracing",
  "whoami",
 ]
 
 [[package]]
 name = "sqlx-postgres"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
+checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
 dependencies = [
  "atoi",
- "base64 0.21.7",
+ "base64 0.22.1",
  "bitflags 2.4.2",
  "byteorder",
  "chrono",
@@ -2483,7 +2457,6 @@ dependencies = [
  "etcetera",
  "futures-channel",
  "futures-core",
- "futures-io",
  "futures-util",
  "hex",
  "hkdf",
@@ -2497,21 +2470,20 @@ dependencies = [
  "rand",
  "serde",
  "serde_json",
- "sha1",
  "sha2",
  "smallvec",
  "sqlx-core",
  "stringprep",
- "thiserror 1.0.57",
+ "thiserror 2.0.9",
  "tracing",
  "whoami",
 ]
 
 [[package]]
 name = "sqlx-sqlite"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
+checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
 dependencies = [
  "atoi",
  "chrono",
@@ -2525,10 +2497,10 @@ dependencies = [
  "log",
  "percent-encoding",
  "serde",
+ "serde_urlencoded",
  "sqlx-core",
  "tracing",
  "url",
- "urlencoding",
 ]
 
 [[package]]
@@ -2617,12 +2589,13 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
 
 [[package]]
 name = "tempfile"
-version = "3.10.0"
+version = "3.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
+checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
 dependencies = [
- "cfg-if",
  "fastrand",
+ "getrandom 0.3.2",
+ "once_cell",
  "rustix",
  "windows-sys 0.52.0",
 ]
@@ -3006,18 +2979,6 @@ dependencies = [
  "tinyvec",
 ]
 
-[[package]]
-name = "unicode-segmentation"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
-
-[[package]]
-name = "unicode_categories"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
-
 [[package]]
 name = "untrusted"
 version = "0.9.0"
@@ -3036,12 +2997,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "urlencoding"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
-
 [[package]]
 name = "utf-8"
 version = "0.7.6"
@@ -3097,6 +3052,15 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
 [[package]]
 name = "wasite"
 version = "0.1.0"
@@ -3399,6 +3363,15 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.4.2",
+]
+
 [[package]]
 name = "yaml-rust"
 version = "0.4.5"
@@ -3453,12 +3426,14 @@ dependencies = [
  "dashmap 5.5.3",
  "flume 0.10.14",
  "futures-util",
+ "poise",
  "rand",
  "serde",
  "serenity",
  "static_assertions",
  "tokio",
  "youmubot-db",
+ "youmubot-db-sql",
  "youmubot-prelude",
 ]
 
@@ -3531,26 +3506,6 @@ dependencies = [
  "youmubot-db-sql",
 ]
 
-[[package]]
-name = "zerocopy"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
-dependencies = [
- "zerocopy-derive",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.93",
-]
-
 [[package]]
 name = "zeroize"
 version = "1.7.0"
diff --git a/youmubot-core/Cargo.toml b/youmubot-core/Cargo.toml
index 8b8f26c..ac00c6b 100644
--- a/youmubot-core/Cargo.toml
+++ b/youmubot-core/Cargo.toml
@@ -19,6 +19,8 @@ futures-util = "0.3.21"
 tokio = { version = "1.19.2", features = ["time"] }
 flume = "0.10.13"
 dashmap = "5.3.4"
+poise = { git = "https://github.com/serenity-rs/poise", branch = "current" }
 
 youmubot-db = { path = "../youmubot-db" }
+youmubot-db-sql = { path = "../youmubot-db-sql" }
 youmubot-prelude = { path = "../youmubot-prelude" }
diff --git a/youmubot-core/src/admin/ignore.rs b/youmubot-core/src/admin/ignore.rs
new file mode 100644
index 0000000..379bf0e
--- /dev/null
+++ b/youmubot-core/src/admin/ignore.rs
@@ -0,0 +1,152 @@
+use std::sync::Arc;
+
+use chrono::{DateTime, Utc};
+use dashmap::DashMap;
+use serenity::all::User;
+use youmubot_db_sql::models::ignore_list as model;
+use youmubot_prelude::*;
+
+use crate::HasCoreEnv;
+
+// Should we ignore this user?
+pub fn should_ignore(env: &impl HasCoreEnv, id: UserId) -> bool {
+    env.core_env().ignore.query(id).is_some()
+}
+
+/// Ignore: make Youmu ignore all commands from an user.
+#[poise::command(
+    slash_command,
+    subcommands("add", "remove", "list"),
+    owners_only,
+    install_context = "User",
+    interaction_context = "Guild|BotDm"
+)]
+pub async fn ignore<U: HasCoreEnv>(_ctx: CmdContext<'_, U>) -> Result<()> {
+    Ok(())
+}
+
+/// Add an user to ignore list.
+#[poise::command(slash_command, owners_only)]
+async fn add<U: HasCoreEnv>(
+    ctx: CmdContext<'_, U>,
+    #[description = "Discord username"] discord_name: User,
+) -> Result<()> {
+    let env = ctx.data().core_env();
+    ctx.defer().await?;
+    let msg = format!("User **{}** ignored!", discord_name.name);
+    env.ignore
+        .add(&env.prelude, UserId(discord_name.id), discord_name.name)
+        .await?;
+    ctx.say(msg).await?;
+    Ok(())
+}
+
+/// Remove an user from ignore list.
+#[poise::command(slash_command, owners_only)]
+async fn remove<U: HasCoreEnv>(
+    ctx: CmdContext<'_, U>,
+    #[description = "Discord username"] discord_name: User,
+) -> Result<()> {
+    let env = ctx.data().core_env();
+    ctx.defer().await?;
+    env.ignore
+        .remove(&env.prelude, UserId(discord_name.id))
+        .await?;
+    let msg = format!("User **{}** removed from ignore list!", discord_name.name);
+    ctx.say(msg).await?;
+    Ok(())
+}
+
+/// List ignored users.
+#[poise::command(slash_command, owners_only)]
+async fn list<U: HasCoreEnv>(ctx: CmdContext<'_, U>) -> Result<()> {
+    let env = ctx.data().core_env();
+    let is_dm = ctx.guild_id().is_none();
+    ctx.defer().await?;
+    let users = env
+        .ignore
+        .list
+        .clone()
+        .iter()
+        .map(|v| {
+            format!(
+                "- {} ({}), since <t:{}:R>",
+                v.username,
+                if is_dm {
+                    v.id.0.mention().to_string()
+                } else {
+                    format!("`{}`", v.id.0.get())
+                },
+                v.ignored_since.timestamp(),
+            )
+        })
+        .collect::<Vec<_>>()
+        .join("\n");
+    let users = if users == "" {
+        "No one is being ignored!"
+    } else {
+        &users[..]
+    };
+
+    let msg = format!("Ignored users:\n{}", users);
+    ctx.say(msg).await?;
+    Ok(())
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct IgnoredUsers {
+    list: Arc<DashMap<UserId, IgnoredUser>>,
+}
+
+impl IgnoredUsers {
+    pub async fn from_db(env: &Env) -> Result<Self> {
+        let list = model::IgnoredUser::get_all(&env.sql).await?;
+        let mp: DashMap<_, _> = list
+            .into_iter()
+            .map(|v| {
+                let id = (v.id as u64).into();
+                (
+                    id,
+                    IgnoredUser {
+                        id,
+                        username: v.username,
+                        ignored_since: v.ignored_since,
+                    },
+                )
+            })
+            .collect();
+        Ok(Self { list: Arc::new(mp) })
+    }
+
+    pub fn query<'a>(
+        &'a self,
+        id: UserId,
+    ) -> Option<impl std::ops::Deref<Target = IgnoredUser> + 'a> {
+        self.list.get(&id)
+    }
+
+    pub async fn add(&self, env: &Env, id: UserId, username: String) -> Result<()> {
+        let iu = model::IgnoredUser::add(&env.sql, id.0.get() as i64, username).await?;
+        self.list.insert(
+            id,
+            IgnoredUser {
+                id,
+                username: iu.username,
+                ignored_since: iu.ignored_since,
+            },
+        );
+        Ok(())
+    }
+
+    pub async fn remove(&self, env: &Env, id: UserId) -> Result<bool> {
+        model::IgnoredUser::remove(&env.sql, id.0.get() as i64).await?;
+        Ok(self.list.remove(&id).is_some())
+    }
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct IgnoredUser {
+    pub id: UserId,
+    pub username: String,
+    pub ignored_since: DateTime<Utc>,
+}
diff --git a/youmubot-core/src/admin/mod.rs b/youmubot-core/src/admin/mod.rs
index 2c766e2..6680c0a 100644
--- a/youmubot-core/src/admin/mod.rs
+++ b/youmubot-core/src/admin/mod.rs
@@ -13,6 +13,8 @@ use youmubot_prelude::*;
 mod soft_ban;
 pub use soft_ban::watch_soft_bans;
 
+pub mod ignore;
+
 #[group]
 #[description = "Administrative commands for the server."]
 #[commands(clean, ban, kick, soft_ban, soft_ban_init)]
diff --git a/youmubot-core/src/lib.rs b/youmubot-core/src/lib.rs
index ce32b17..00f2783 100644
--- a/youmubot-core/src/lib.rs
+++ b/youmubot-core/src/lib.rs
@@ -17,11 +17,32 @@ pub mod community;
 mod db;
 pub mod fun;
 
+#[derive(Debug, Clone)]
+pub struct CoreEnv {
+    pub(crate) prelude: Env,
+    pub(crate) ignore: admin::ignore::IgnoredUsers,
+}
+
+impl CoreEnv {
+    async fn new(prelude: Env) -> Result<Self> {
+        let ignore = admin::ignore::IgnoredUsers::from_db(&prelude).await?;
+        Ok(Self { prelude, ignore })
+    }
+}
+
+/// Gets an [CoreEnv] from the current environment.
+pub trait HasCoreEnv: Send + Sync {
+    fn core_env(&self) -> &CoreEnv;
+}
+
+impl<T: AsRef<CoreEnv> + Send + Sync> HasCoreEnv for T {
+    fn core_env(&self) -> &CoreEnv {
+        self.as_ref()
+    }
+}
+
 /// Sets up all databases in the client.
-pub fn setup(
-    path: &std::path::Path,
-    data: &mut TypeMap,
-) -> serenity::framework::standard::CommandResult {
+pub async fn setup(path: &std::path::Path, data: &mut TypeMap, prelude: Env) -> Result<CoreEnv> {
     db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?;
     db::load_role_list(
         &mut *data,
@@ -32,7 +53,7 @@ pub fn setup(
     // Start reaction handlers
     data.insert::<community::ReactionWatchers>(community::ReactionWatchers::new(&*data)?);
 
-    Ok(())
+    CoreEnv::new(prelude).await
 }
 
 pub fn ready_hook(ctx: &Context) -> CommandResult {
diff --git a/youmubot-db-sql/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json b/youmubot-db-sql/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json
deleted file mode 100644
index 814d59b..0000000
--- a/youmubot-db-sql/.sqlx/query-25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "SELECT\n                user_id as \"user_id: i64\",\n                username,\n                id as \"id: i64\",\n                preferred_mode as \"preferred_mode: u8\",\n                failures as \"failures: u8\"\n            FROM osu_users WHERE user_id = ?",
-  "describe": {
-    "columns": [
-      {
-        "name": "user_id: i64",
-        "ordinal": 0,
-        "type_info": "Int64"
-      },
-      {
-        "name": "username",
-        "ordinal": 1,
-        "type_info": "Text"
-      },
-      {
-        "name": "id: i64",
-        "ordinal": 2,
-        "type_info": "Int64"
-      },
-      {
-        "name": "preferred_mode: u8",
-        "ordinal": 3,
-        "type_info": "Int64"
-      },
-      {
-        "name": "failures: u8",
-        "ordinal": 4,
-        "type_info": "Int64"
-      }
-    ],
-    "parameters": {
-      "Right": 1
-    },
-    "nullable": [
-      false,
-      true,
-      false,
-      false,
-      false
-    ]
-  },
-  "hash": "25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366"
-}
diff --git a/youmubot-db-sql/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json b/youmubot-db-sql/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json
deleted file mode 100644
index 8de5138..0000000
--- a/youmubot-db-sql/.sqlx/query-54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "SELECT\n                user_id as \"user_id: i64\",\n                username,\n                id as \"id: i64\",\n                preferred_mode as \"preferred_mode: u8\",\n                failures as \"failures: u8\"\n            FROM osu_users",
-  "describe": {
-    "columns": [
-      {
-        "name": "user_id: i64",
-        "ordinal": 0,
-        "type_info": "Int64"
-      },
-      {
-        "name": "username",
-        "ordinal": 1,
-        "type_info": "Text"
-      },
-      {
-        "name": "id: i64",
-        "ordinal": 2,
-        "type_info": "Int64"
-      },
-      {
-        "name": "preferred_mode: u8",
-        "ordinal": 3,
-        "type_info": "Int64"
-      },
-      {
-        "name": "failures: u8",
-        "ordinal": 4,
-        "type_info": "Int64"
-      }
-    ],
-    "parameters": {
-      "Right": 0
-    },
-    "nullable": [
-      false,
-      true,
-      false,
-      false,
-      false
-    ]
-  },
-  "hash": "54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c"
-}
diff --git a/youmubot-db-sql/.sqlx/query-b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378.json b/youmubot-db-sql/.sqlx/query-b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378.json
deleted file mode 100644
index 2b1820b..0000000
--- a/youmubot-db-sql/.sqlx/query-b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "INSERT\n               INTO osu_users(user_id, username, id, preferred_mode, failures)\n               VALUES(?, ?, ?, ?, ?)\n               ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n               SET\n                username = excluded.username,\n                preferred_mode = excluded.preferred_mode,\n                failures = excluded.failures\n            ",
-  "describe": {
-    "columns": [],
-    "parameters": {
-      "Right": 6
-    },
-    "nullable": []
-  },
-  "hash": "b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378"
-}
diff --git a/youmubot-db-sql/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json b/youmubot-db-sql/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json
deleted file mode 100644
index d851042..0000000
--- a/youmubot-db-sql/.sqlx/query-e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "SELECT\n                user_id as \"user_id: i64\",\n                username,\n                id as \"id: i64\",\n                preferred_mode as \"preferred_mode: u8\",\n                failures as \"failures: u8\"\n            FROM osu_users WHERE id = ?",
-  "describe": {
-    "columns": [
-      {
-        "name": "user_id: i64",
-        "ordinal": 0,
-        "type_info": "Int64"
-      },
-      {
-        "name": "username",
-        "ordinal": 1,
-        "type_info": "Text"
-      },
-      {
-        "name": "id: i64",
-        "ordinal": 2,
-        "type_info": "Int64"
-      },
-      {
-        "name": "preferred_mode: u8",
-        "ordinal": 3,
-        "type_info": "Int64"
-      },
-      {
-        "name": "failures: u8",
-        "ordinal": 4,
-        "type_info": "Int64"
-      }
-    ],
-    "parameters": {
-      "Right": 1
-    },
-    "nullable": [
-      false,
-      true,
-      false,
-      false,
-      false
-    ]
-  },
-  "hash": "e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a"
-}
diff --git a/youmubot-db-sql/Cargo.toml b/youmubot-db-sql/Cargo.toml
index 30b640c..4c5b9b8 100644
--- a/youmubot-db-sql/Cargo.toml
+++ b/youmubot-db-sql/Cargo.toml
@@ -10,7 +10,7 @@ workspace = true
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "chrono"] }
+sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] }
 thiserror = "1.0.31"
 chrono = "0.4.19"
 futures-util = "0.3.21"
diff --git a/youmubot-db-sql/migrations/20250327105109_ignored_users.sql b/youmubot-db-sql/migrations/20250327105109_ignored_users.sql
new file mode 100644
index 0000000..7bf13e5
--- /dev/null
+++ b/youmubot-db-sql/migrations/20250327105109_ignored_users.sql
@@ -0,0 +1,7 @@
+-- Add migration script here
+
+CREATE TABLE ignored_users (
+  id BIGINT NOT NULL PRIMARY KEY,
+  username TEXT NOT NULL UNIQUE,
+  ignored_since DATETIME NOT NULL DEFAULT (DATETIME('now'))
+);
diff --git a/youmubot-db-sql/src/models/ignore_list.rs b/youmubot-db-sql/src/models/ignore_list.rs
new file mode 100644
index 0000000..7f28471
--- /dev/null
+++ b/youmubot-db-sql/src/models/ignore_list.rs
@@ -0,0 +1,60 @@
+use super::*;
+use sqlx::Executor;
+
+/// An ignored user in the ignored user list.
+#[derive(Clone, Debug)]
+pub struct IgnoredUser {
+    pub id: i64,
+    pub username: String,
+    pub ignored_since: DateTime,
+}
+
+impl IgnoredUser {
+    /// Returns a list of all ignored users.
+    pub async fn get_all<'a, E>(conn: E) -> Result<Vec<Self>>
+    where
+        E: Executor<'a, Database = Database>,
+    {
+        Ok(query_as!(
+            IgnoredUser,
+            r#"SELECT
+              id,
+              username,
+              ignored_since as "ignored_since: DateTime"
+            FROM ignored_users
+            ORDER BY id ASC"#
+        )
+        .fetch_all(conn)
+        .await?)
+    }
+
+    /// Add an user to ignore list.
+    pub async fn add<'a, E>(conn: E, user_id: i64, username: String) -> Result<Self>
+    where
+        E: Executor<'a, Database = Database>,
+    {
+        Ok(query_as!(
+            IgnoredUser,
+            r#"INSERT INTO ignored_users(id, username) VALUES (?, ?)
+               ON CONFLICT (id) DO UPDATE SET username = excluded.username
+               RETURNING id,
+              username,
+              ignored_since as "ignored_since: DateTime""#,
+            user_id,
+            username
+        )
+        .fetch_one(conn)
+        .await?)
+    }
+
+    // Remove an user from ignore list.
+    pub async fn remove<'a, E>(conn: E, user_id: i64) -> Result<()>
+    where
+        E: Executor<'a, Database = Database>,
+    {
+        query!(r#"DELETE FROM ignored_users WHERE id = ?"#, user_id)
+            .execute(conn)
+            .await?;
+        Ok(())
+    }
+}
diff --git a/youmubot-db-sql/src/models/mod.rs b/youmubot-db-sql/src/models/mod.rs
index 59f7ecf..8f05c93 100644
--- a/youmubot-db-sql/src/models/mod.rs
+++ b/youmubot-db-sql/src/models/mod.rs
@@ -4,5 +4,6 @@ use sqlx::{query, query_as, Executor};
 /// The DateTime used in the package.
 pub type DateTime = chrono::DateTime<chrono::Utc>;
 
+pub mod ignore_list;
 pub mod osu;
 pub mod osu_user;
diff --git a/youmubot-prelude/src/args.rs b/youmubot-prelude/src/args.rs
index c6410ce..6803dfa 100644
--- a/youmubot-prelude/src/args.rs
+++ b/youmubot-prelude/src/args.rs
@@ -189,9 +189,15 @@ mod ids {
     use super::ParseError;
 
     /// An `UserId` parsed the old way.
-    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
     pub struct UserId(pub id::UserId);
 
+    impl From<u64> for UserId {
+        fn from(value: u64) -> Self {
+            Self(id::UserId::new(value))
+        }
+    }
+
     impl FromStr for UserId {
         type Err = ParseError;
 
diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs
index 9404011..ca2d25a 100644
--- a/youmubot/src/main.rs
+++ b/youmubot/src/main.rs
@@ -53,6 +53,8 @@ struct Env {
     prelude: youmubot_prelude::Env,
     #[cfg(feature = "osu")]
     osu: youmubot_osu::discord::OsuEnv,
+    #[cfg(feature = "core")]
+    core: youmubot_core::CoreEnv,
 }
 
 impl AsRef<youmubot_prelude::Env> for Env {
@@ -68,6 +70,13 @@ impl AsRef<youmubot_osu::discord::OsuEnv> for Env {
     }
 }
 
+#[cfg(feature = "core")]
+impl AsRef<youmubot_core::CoreEnv> for Env {
+    fn as_ref(&self) -> &youmubot_core::CoreEnv {
+        &self.core
+    }
+}
+
 impl TypeMapKey for Env {
     type Value = Env;
 }
@@ -219,7 +228,9 @@ async fn main() {
         let prelude = setup::setup_prelude(&db_path, sql_path, &mut data).await;
         // Setup core
         #[cfg(feature = "core")]
-        youmubot_core::setup(&db_path, &mut data).expect("Setup db should succeed");
+        let core = youmubot_core::setup(&db_path, &mut data, prelude.clone())
+            .await
+            .expect("Setup db should succeed");
         // osu!
         #[cfg(feature = "osu")]
         let osu = youmubot_osu::discord::setup(&mut data, prelude.clone(), &mut announcers)
@@ -233,6 +244,8 @@ async fn main() {
             prelude,
             #[cfg(feature = "osu")]
             osu,
+            #[cfg(feature = "core")]
+            core,
         }
     };
 
@@ -266,20 +279,49 @@ async fn main() {
                 case_insensitive_commands: true,
                 ..Default::default()
             },
+            command_check: Some(|ctx| {
+                Box::pin(command_check(ctx.data(), UserId(ctx.author().id)).map(Ok))
+            }),
             on_error: |err| {
                 Box::pin(async move {
-                    if let poise::FrameworkError::Command { error, ctx, .. } = err {
-                        let reply = format!(
-                            "Command '{}' returned error: {:?}",
-                            ctx.invoked_command_name(),
-                            error
-                        );
-                        eprintln!("{}\n{:?}", reply, error);
-                        ctx.send(poise::CreateReply::default().content(reply).ephemeral(true))
+                    match err {
+                        poise::FrameworkError::Command { error, ctx, .. } => {
+                            let reply = format!(
+                                "Command '{}' returned error: {:?}",
+                                ctx.invoked_command_name(),
+                                error
+                            );
+                            eprintln!("{}\n{:?}", reply, error);
+                            ctx.send(poise::CreateReply::default().content(reply).ephemeral(true))
+                                .await
+                                .pls_ok();
+                        }
+                        poise::FrameworkError::NotAnOwner { ctx, .. } => {
+                            ctx.send(
+                                poise::CreateReply::default()
+                                    .content("You have to be an owner to run this command!")
+                                    .ephemeral(true),
+                            )
                             .await
                             .pls_ok();
-                    } else {
-                        eprintln!("Poise error: {:?}", err)
+                        }
+                        poise::FrameworkError::CommandCheckFailed { error: _, ctx, .. }
+                        | poise::FrameworkError::CooldownHit {
+                            remaining_cooldown: _,
+                            ctx,
+                            ..
+                        } => {
+                            ctx.send(
+                                poise::CreateReply::default()
+                                    .content("You are being rate-limited, please try again later!")
+                                    .ephemeral(true),
+                            )
+                            .await
+                            .pls_ok();
+                        }
+                        _ => {
+                            eprintln!("Poise error: {:?}", err)
+                        }
                     }
                 })
             },
@@ -287,6 +329,8 @@ async fn main() {
                 poise_register(),
                 #[cfg(feature = "osu")]
                 youmubot_osu::discord::osu_command(),
+                #[cfg(feature = "core")]
+                youmubot_core::admin::ignore::ignore(),
             ],
             ..Default::default()
         })
@@ -398,25 +442,37 @@ async fn poise_register(ctx: CmdContext<'_, Env>) -> Result<()> {
     Ok(())
 }
 
+async fn command_check(env: &Env, author: UserId) -> bool {
+    #[cfg(feature = "core")]
+    if youmubot_core::admin::ignore::should_ignore(env, author) {
+        tracing::info!("User is in ignore list, skipping...");
+        return false;
+    }
+    true
+}
+
 // Hooks!
 
 #[hook]
-async fn before_hook(_: &Context, msg: &Message, command_name: &str) -> bool {
-    println!(
+async fn before_hook(ctx: &Context, msg: &Message, command_name: &str) -> bool {
+    let env = ctx.data.read().await;
+    let env = env.get::<Env>().unwrap();
+    tracing::info!(
         "Got command '{}' by user '{}'",
-        command_name, msg.author.name
+        command_name,
+        msg.author.name
     );
-    true
+    command_check(env, UserId(msg.author.id)).await
 }
 
 #[hook]
 async fn after_hook(ctx: &Context, msg: &Message, command_name: &str, error: CommandResult) {
     match error {
-        Ok(()) => println!("Processed command '{}'", command_name),
+        Ok(()) => tracing::info!("Processed command '{}'", command_name),
         Err(why) => {
             let reply = format!("Command '{}' returned error {:?}", command_name, why);
             msg.reply(&ctx, &reply).await.ok();
-            println!("{}", reply)
+            tracing::info!("{}", reply)
         }
     }
 }
@@ -441,6 +497,9 @@ async fn on_dispatch_error(ctx: &Context, msg: &Message, error: DispatchError, _
                 max, given
             ),
             DispatchError::OnlyForGuilds => "🔇 This command cannot be used in DMs.".to_owned(),
+            DispatchError::OnlyForOwners => {
+                "🔇 This command can only be used by bot owners.".to_owned()
+            }
             _ => return,
         },
     )