Repository: importantimport/hatsu (language: rust)

Issue:
Block Instances and Actors
Since I don't want to add too much complexity, this method is instance level, and only the admin can block other instances/actors.

This will only affect blocked instances/actors interactions with Hatsu instances, not their ability to view the contents of Hatsu instances.

## /api/v0/admin/block_instance

Accepts a domain. `https://instance.tld`

## /api/v0/admin/block_actor

Accepts a URL, this should be the `id` of the actor. `https://instance.tld/users/foo`

## /api/v0/admin/block_acct

Accepts a `foo@instance.tld` string, the Hatsu instance sends a WebFinger request to query the corresponding id.

`foo@instance.tld` => `https://instance.tld/.well-known/webfinger?resource=acct:foo@instance.tld`

## /api/v0/admin/unblock_{instance,actor,acct}

Unblock versions of the three APIs above.


Discussion / hints:
~~I'll probably implement block actor/acct in v0.1.2 and block instance after v0.2.0.~~ v0.3.0

The block_instance should accept a string and do an ends_with match.

```
block_instance example.com
https://example.com (blocked)
https://foo.example.com (blocked)

block_instance https://example.com
https://example.com (blocked)
https://foo.example.com (not blocked)
```

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 13820018..64916afd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1389,6 +1389,7 @@ dependencies = [
  "hatsu_utils",
  "sea-orm",
  "serde",
+ "url",
  "utoipa",
  "utoipa-axum",
 ]
diff --git a/crates/api_admin/Cargo.toml b/crates/api_admin/Cargo.toml
index 830045ff..406fb05f 100644
--- a/crates/api_admin/Cargo.toml
+++ b/crates/api_admin/Cargo.toml
@@ -23,5 +23,6 @@ activitypub_federation = { workspace = true }
 axum = { workspace = true }
 sea-orm = { workspace = true }
 serde = { workspace = true }
+url = { workspace = true }
 utoipa = { workspace = true }
 utoipa-axum = { workspace = true }
diff --git a/crates/api_admin/src/entities/block_url_or_acct.rs b/crates/api_admin/src/entities/block_url_or_acct.rs
new file mode 100644
index 00000000..f6a24660
--- /dev/null
+++ b/crates/api_admin/src/entities/block_url_or_acct.rs
@@ -0,0 +1,14 @@
+use serde::{Deserialize, Serialize};
+use url::Url;
+use utoipa::{IntoParams, ToSchema};
+
+#[derive(Deserialize, IntoParams)]
+pub struct BlockUrlQuery {
+    pub url: Url,
+}
+
+#[derive(Serialize, ToSchema)]
+pub struct BlockUrlResult {
+    pub url: Url,
+    pub message: String,
+}
diff --git a/crates/api_admin/src/entities/mod.rs b/crates/api_admin/src/entities/mod.rs
index 7c6f2bfa..f09595d8 100644
--- a/crates/api_admin/src/entities/mod.rs
+++ b/crates/api_admin/src/entities/mod.rs
@@ -1,3 +1,5 @@
+mod block_url_or_acct;
 mod create_remove_account;
 
+pub use block_url_or_acct::{BlockUrlQuery, BlockUrlResult};
 pub use create_remove_account::{CreateRemoveAccount, CreateRemoveAccountResult};
diff --git a/crates/api_admin/src/routes/block_url.rs b/crates/api_admin/src/routes/block_url.rs
new file mode 100644
index 00000000..3a2e9d63
--- /dev/null
+++ b/crates/api_admin/src/routes/block_url.rs
@@ -0,0 +1,68 @@
+use activitypub_federation::config::Data;
+use axum::{debug_handler, extract::Query, http::StatusCode, Json};
+use hatsu_db_schema::{blocked_url, prelude::BlockedUrl};
+use hatsu_utils::{AppData, AppError};
+use sea_orm::{ActiveModelTrait, EntityTrait, Set};
+
+use crate::{
+    entities::{BlockUrlQuery, BlockUrlResult},
+    TAG,
+};
+
+/// Block URL
+#[utoipa::path(
+    post,
+    tag = TAG,
+    path = "/api/v0/admin/block-url",
+    params(BlockUrlQuery),
+    responses(
+        (status = OK, description = "block successfully", body = BlockUrlResult),
+        (status = BAD_REQUEST, description = "error", body = AppError)
+    ),
+    security(("api_key" = ["token"]))
+)]
+#[debug_handler]
+pub async fn block_url(
+    data: Data<AppData>,
+    query: Query<BlockUrlQuery>,
+) -> Result<(StatusCode, Json<BlockUrlResult>), AppError> {
+    match &query.url {
+        url if url.query().is_some() => Err(AppError::new(
+            format!(
+                "wrong url: {} (can't contain search params)",
+                url.to_string()
+            ),
+            None,
+            Some(StatusCode::BAD_REQUEST),
+        )),
+        _ => match BlockedUrl::find_by_id(&query.url.to_string())
+            .one(&data.conn)
+            .await?
+        {
+            Some(url) => Err(AppError::new(
+                format!("The url already blocked: {}", url.id),
+                None,
+                Some(StatusCode::BAD_REQUEST),
+            )),
+            None => {
+                blocked_url::ActiveModel {
+                    id: Set(query.url.to_string()),
+                    is_instance: Set(query.url.path().eq("/")),
+                }
+                .insert(&data.conn)
+                .await?;
+
+                Ok((
+                    StatusCode::OK,
+                    Json(BlockUrlResult {
+                        url: query.url.clone(),
+                        message: format!(
+                            "The url was successfully blocked: {}",
+                            &query.url.to_string()
+                        ),
+                    }),
+                ))
+            },
+        },
+    }
+}
diff --git a/crates/api_admin/src/routes/mod.rs b/crates/api_admin/src/routes/mod.rs
index cf4c8917..d86a07d1 100644
--- a/crates/api_admin/src/routes/mod.rs
+++ b/crates/api_admin/src/routes/mod.rs
@@ -13,16 +13,22 @@ use utoipa::{
 };
 use utoipa_axum::{router::OpenApiRouter, routes};
 
-use crate::entities::{CreateRemoveAccount, CreateRemoveAccountResult};
+use crate::entities::{BlockUrlResult, CreateRemoveAccount, CreateRemoveAccountResult};
 
+mod block_url;
 mod create_account;
 mod remove_account;
+mod unblock_url;
 
 pub const TAG: &str = "hatsu::admin";
 
 #[derive(OpenApi)]
 #[openapi(
-    components(schemas(CreateRemoveAccount, CreateRemoveAccountResult)),
+    components(schemas(
+        BlockUrlResult,
+        CreateRemoveAccount,
+        CreateRemoveAccountResult
+    )),
     modifiers(&SecurityAddon),
     tags(
         (name = TAG, description = "Hatsu Admin API (/api/v0/admin/)"),
@@ -45,8 +51,10 @@ impl Modify for SecurityAddon {
 
 pub fn routes() -> OpenApiRouter {
     OpenApiRouter::with_openapi(HatsuAdminApi::openapi())
+        .routes(routes!(block_url::block_url))
         .routes(routes!(create_account::create_account))
         .routes(routes!(remove_account::remove_account))
+        .routes(routes!(unblock_url::unblock_url))
         .layer(middleware::from_fn(auth))
 }
 
@@ -57,9 +65,12 @@ async fn auth(
 ) -> Result<Response, StatusCode> {
     match &data.env.hatsu_access_token {
         Some(token) => match request.uri().query() {
-            Some(query) if query == format!("token={token}") => Ok(next.run(request).await),
-            Some(query) if query != format!("token={token}") => Err(StatusCode::UNAUTHORIZED),
-            _ => Err(StatusCode::BAD_REQUEST),
+            Some(queries)
+                if queries
+                    .split('&')
+                    .any(|query| query.eq(&format!("token={token}"))) =>
+                Ok(next.run(request).await),
+            _ => Err(StatusCode::UNAUTHORIZED),
         },
         None => Err(StatusCode::UNAUTHORIZED),
     }
diff --git a/crates/api_admin/src/routes/unblock_url.rs b/crates/api_admin/src/routes/unblock_url.rs
new file mode 100644
index 00000000..98ca34e3
--- /dev/null
+++ b/crates/api_admin/src/routes/unblock_url.rs
@@ -0,0 +1,53 @@
+use activitypub_federation::config::Data;
+use axum::{debug_handler, extract::Query, http::StatusCode, Json};
+use hatsu_db_schema::prelude::BlockedUrl;
+use hatsu_utils::{AppData, AppError};
+use sea_orm::{EntityTrait, ModelTrait};
+
+use crate::{
+    entities::{BlockUrlQuery, BlockUrlResult},
+    TAG,
+};
+
+/// Unblock URL
+#[utoipa::path(
+    post,
+    tag = TAG,
+    path = "/api/v0/admin/unblock-url",
+    params(BlockUrlQuery),
+    responses(
+        (status = OK, description = "unblock successfully", body = BlockUrlResult),
+        (status = BAD_REQUEST, description = "error", body = AppError)
+    ),
+    security(("api_key" = ["token"]))
+)]
+#[debug_handler]
+pub async fn unblock_url(
+    data: Data<AppData>,
+    query: Query<BlockUrlQuery>,
+) -> Result<(StatusCode, Json<BlockUrlResult>), AppError> {
+    match BlockedUrl::find_by_id(&query.url.to_string())
+        .one(&data.conn)
+        .await?
+    {
+        Some(url) => {
+            url.delete(&data.conn).await?;
+
+            Ok((
+                StatusCode::OK,
+                Json(BlockUrlResult {
+                    url: query.url.clone(),
+                    message: format!(
+                        "The url was successfully unblocked: {}",
+                        &query.url.to_string()
+                    ),
+                }),
+            ))
+        },
+        None => Err(AppError::new(
+            format!("The url doesn't exist: {}", query.url.to_string()),
+            None,
+            Some(StatusCode::BAD_REQUEST),
+        )),
+    }
+}
diff --git a/crates/apub/src/activities/create_or_update/note.rs b/crates/apub/src/activities/create_or_update/note.rs
index 53734876..4a5e5f1b 100644
--- a/crates/apub/src/activities/create_or_update/note.rs
+++ b/crates/apub/src/activities/create_or_update/note.rs
@@ -15,6 +15,7 @@ use crate::{
     activities::CreateOrUpdateType,
     actors::ApubUser,
     objects::{ApubPost, Note},
+    utils::verify_blocked,
 };
 
 #[derive(Deserialize, Serialize, Debug)]
@@ -53,7 +54,7 @@ impl CreateOrUpdateNote {
             kind: activity.kind.to_string(),
             published: Some(activity.published.clone()),
             actor: activity.actor().to_string(),
-            activity: serde_json::to_string(&activity)?,
+            activity: serde_json::to_value(&activity)?,
         }
         .into_active_model()
         .insert(&data.conn)
@@ -97,6 +98,7 @@ impl ActivityHandler for CreateOrUpdateNote {
     async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
         // TODO
         ApubPost::verify(&self.object, &self.id, data).await?;
+        verify_blocked(&self.id, data).await?;
         Ok(())
     }
 
diff --git a/crates/apub/src/activities/db_activity_impl.rs b/crates/apub/src/activities/db_activity_impl.rs
index b2fa1c85..7bd54709 100644
--- a/crates/apub/src/activities/db_activity_impl.rs
+++ b/crates/apub/src/activities/db_activity_impl.rs
@@ -6,6 +6,6 @@ use super::ApubActivity;
 impl ApubActivity {
     // 转换为 JSON
     pub fn into_json(self) -> Result<Value, AppError> {
-        Ok(serde_json::from_str(&self.activity)?)
+        Ok(serde_json::from_value(self.activity.clone())?)
     }
 }
diff --git a/crates/apub/src/activities/following/accept_follow.rs b/crates/apub/src/activities/following/accept_follow.rs
index be1766a0..4ae7d623 100644
--- a/crates/apub/src/activities/following/accept_follow.rs
+++ b/crates/apub/src/activities/following/accept_follow.rs
@@ -48,7 +48,7 @@ impl AcceptFollow {
 
         let _insert_activity = DbActivity {
             id: activity.id().to_string(),
-            activity: serde_json::to_string(&activity)?,
+            activity: serde_json::to_value(&activity)?,
             actor: activity.actor().to_string(),
             kind: activity.kind.to_string(),
             published: Some(hatsu_utils::date::now()),
@@ -78,7 +78,7 @@ impl ActivityHandler for AcceptFollow {
     }
 
     async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
-        // TODO
+        // TODO: just throw error
         Ok(())
     }
 
diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs
index 3b1b9513..c0d4d4cd 100644
--- a/crates/apub/src/activities/following/follow.rs
+++ b/crates/apub/src/activities/following/follow.rs
@@ -23,6 +23,7 @@ use url::Url;
 use crate::{
     activities::{AcceptFollow, ApubReceivedFollow},
     actors::ApubUser,
+    utils::verify_blocked,
 };
 
 /// <https://github.com/LemmyNet/lemmy/blob/963d04b3526f8a5e9ff762960bfb5215e353bb27/crates/apub/src/protocol/activities/following/follow.rs>
@@ -58,8 +59,9 @@ impl ActivityHandler for Follow {
         self.actor.inner()
     }
 
-    async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
+    async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
         // TODO
+        verify_blocked(&self.id, data).await?;
         Ok(())
     }
 
diff --git a/crates/apub/src/activities/following/undo_follow.rs b/crates/apub/src/activities/following/undo_follow.rs
index 075dc5f8..9166b269 100644
--- a/crates/apub/src/activities/following/undo_follow.rs
+++ b/crates/apub/src/activities/following/undo_follow.rs
@@ -11,7 +11,7 @@ use sea_orm::EntityTrait;
 use serde::{Deserialize, Serialize};
 use url::Url;
 
-use crate::{activities::Follow, actors::ApubUser};
+use crate::{activities::Follow, actors::ApubUser, utils::verify_blocked};
 
 // https://github.com/LemmyNet/lemmy/blob/963d04b3526f8a5e9ff762960bfb5215e353bb27/crates/apub/src/protocol/activities/following/undo_follow.rs
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -44,8 +44,9 @@ impl ActivityHandler for UndoFollow {
         self.actor.inner()
     }
 
-    async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
+    async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
         // TODO
+        verify_blocked(&self.id, data).await?;
         Ok(())
     }
 
diff --git a/crates/apub/src/activities/like_or_announce/like_or_announce.rs b/crates/apub/src/activities/like_or_announce/like_or_announce.rs
index 4fcc3daa..a47822aa 100644
--- a/crates/apub/src/activities/like_or_announce/like_or_announce.rs
+++ b/crates/apub/src/activities/like_or_announce/like_or_announce.rs
@@ -22,6 +22,7 @@ use crate::{
     activities::{ApubReceivedAnnounce, ApubReceivedLike, LikeOrAnnounceType},
     actors::ApubUser,
     objects::ApubPost,
+    utils::verify_blocked,
 };
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -48,8 +49,9 @@ impl ActivityHandler for LikeOrAnnounce {
         self.actor.inner()
     }
 
-    async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
+    async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
         // TODO
+        verify_blocked(&self.id, data).await?;
         Ok(())
     }
 
diff --git a/crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs b/crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs
index 7a4371a3..b69a382b 100644
--- a/crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs
+++ b/crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
 use url::Url;
 
 use super::LikeOrAnnounceType;
-use crate::{activities::LikeOrAnnounce, actors::ApubUser};
+use crate::{activities::LikeOrAnnounce, actors::ApubUser, utils::verify_blocked};
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
@@ -37,8 +37,9 @@ impl ActivityHandler for UndoLikeOrAnnounce {
         self.actor.inner()
     }
 
-    async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
+    async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
         // TODO
+        verify_blocked(&self.id, data).await?;
         Ok(())
     }
 
diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs
index 96ee4800..c2e77ea4 100644
--- a/crates/apub/src/lib.rs
+++ b/crates/apub/src/lib.rs
@@ -3,6 +3,7 @@ pub mod actors;
 pub mod collections;
 pub mod links;
 pub mod objects;
+mod utils;
 
 // #[cfg(test)]
 pub mod tests {
diff --git a/crates/apub/src/utils/mod.rs b/crates/apub/src/utils/mod.rs
new file mode 100644
index 00000000..babd8222
--- /dev/null
+++ b/crates/apub/src/utils/mod.rs
@@ -0,0 +1,3 @@
+mod verify_blocked;
+
+pub use verify_blocked::verify_blocked;
diff --git a/crates/apub/src/utils/verify_blocked.rs b/crates/apub/src/utils/verify_blocked.rs
new file mode 100644
index 00000000..c0036312
--- /dev/null
+++ b/crates/apub/src/utils/verify_blocked.rs
@@ -0,0 +1,37 @@
+use activitypub_federation::config::Data;
+use axum::http::StatusCode;
+use hatsu_db_schema::prelude::BlockedUrl;
+use hatsu_utils::{AppData, AppError};
+use sea_orm::EntityTrait;
+use url::Url;
+
+pub async fn verify_blocked(url: &Url, data: &Data<AppData>) -> Result<(), AppError> {
+    let blocked_url = BlockedUrl::find().all(&data.conn).await?;
+
+    if blocked_url
+        .iter()
+        .filter(|url| url.is_instance)
+        .filter_map(|url| Url::parse(&url.id).ok())
+        .map(|url| url.origin())
+        .any(|instance| url.origin().eq(&instance))
+    {
+        Err(AppError::new(
+            format!("blocked instance: {:?}", url.host_str()),
+            None,
+            Some(StatusCode::BAD_REQUEST),
+        ))
+    } else if blocked_url
+        .iter()
+        .filter(|url| !url.is_instance)
+        .filter_map(|url| Url::parse(&url.id).ok())
+        .any(|actor| url.eq(&actor))
+    {
+        Err(AppError::new(
+            format!("blocked actor: {}", url),
+            None,
+            Some(StatusCode::BAD_REQUEST),
+        ))
+    } else {
+        Ok(())
+    }
+}
diff --git a/crates/db_migration/src/lib.rs b/crates/db_migration/src/lib.rs
index e810cab9..5ceb4a4d 100644
--- a/crates/db_migration/src/lib.rs
+++ b/crates/db_migration/src/lib.rs
@@ -9,6 +9,7 @@ mod m20240501_000001_received_like;
 mod m20240501_000002_received_announce;
 mod m20240515_000001_user_feed_hatsu_extension;
 mod m20240515_000002_user_feed;
+mod m20240926_000001_blocked_url;
 
 pub struct Migrator;
 
@@ -25,6 +26,7 @@ impl MigratorTrait for Migrator {
             Box::new(m20240501_000002_received_announce::Migration),
             Box::new(m20240515_000001_user_feed_hatsu_extension::Migration),
             Box::new(m20240515_000002_user_feed::Migration),
+            Box::new(m20240926_000001_blocked_url::Migration),
         ]
     }
 }
diff --git a/crates/db_migration/src/m20240131_000004_activity.rs b/crates/db_migration/src/m20240131_000004_activity.rs
index d5a70d2e..1bb51da7 100644
--- a/crates/db_migration/src/m20240131_000004_activity.rs
+++ b/crates/db_migration/src/m20240131_000004_activity.rs
@@ -11,7 +11,7 @@ impl MigrationTrait for Migration {
                 Table::create()
                     .table(Activity::Table)
                     .if_not_exists()
-                    .col(uuid(Activity::Id).primary_key())
+                    .col(string(Activity::Id).primary_key())
                     .col(json(Activity::Activity))
                     .col(string(Activity::Actor))
                     .col(string(Activity::Kind))
@@ -35,7 +35,7 @@ impl MigrationTrait for Migration {
 #[derive(Iden)]
 enum Activity {
     Table,
-    // Activity UUID
+    // Activity URL
     Id,
     // Activity JSON
     /// <https://www.w3.org/TR/activitystreams-vocabulary/#activity-types>
diff --git a/crates/db_migration/src/m20240926_000001_blocked_url.rs b/crates/db_migration/src/m20240926_000001_blocked_url.rs
new file mode 100644
index 00000000..23731855
--- /dev/null
+++ b/crates/db_migration/src/m20240926_000001_blocked_url.rs
@@ -0,0 +1,39 @@
+use sea_orm_migration::{prelude::*, schema::*};
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        manager
+            .create_table(
+                Table::create()
+                    .table(BlockedUrl::Table)
+                    .if_not_exists()
+                    .col(string(BlockedUrl::Id).primary_key())
+                    .col(boolean(BlockedUrl::IsInstance))
+                    .to_owned(),
+            )
+            .await?;
+
+        Ok(())
+    }
+
+    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        manager
+            .drop_table(Table::drop().table(BlockedUrl::Table).to_owned())
+            .await?;
+
+        Ok(())
+    }
+}
+
+#[derive(Iden)]
+enum BlockedUrl {
+    Table,
+    // Url
+    Id,
+    // is instance (if false, then this is actor)
+    IsInstance,
+}
diff --git a/crates/db_schema/src/activity.rs b/crates/db_schema/src/activity.rs
index 3a445877..d86f6c99 100644
--- a/crates/db_schema/src/activity.rs
+++ b/crates/db_schema/src/activity.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::entity::prelude::*;
 
@@ -7,7 +7,7 @@ use sea_orm::entity::prelude::*;
 pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub id: String,
-    pub activity: String,
+    pub activity: Json,
     pub actor: String,
     pub kind: String,
     pub published: Option<String>,
diff --git a/crates/db_schema/src/blocked_url.rs b/crates/db_schema/src/blocked_url.rs
new file mode 100644
index 00000000..8cac4b55
--- /dev/null
+++ b/crates/db_schema/src/blocked_url.rs
@@ -0,0 +1,16 @@
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
+
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+#[sea_orm(table_name = "blocked_url")]
+pub struct Model {
+    #[sea_orm(primary_key, auto_increment = false)]
+    pub id: String,
+    pub is_instance: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs
index 5bfa26eb..ee7a9b43 100644
--- a/crates/db_schema/src/lib.rs
+++ b/crates/db_schema/src/lib.rs
@@ -1,8 +1,9 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 pub mod prelude;
 
 pub mod activity;
+pub mod blocked_url;
 pub mod post;
 pub mod received_announce;
 pub mod received_follow;
diff --git a/crates/db_schema/src/post.rs b/crates/db_schema/src/post.rs
index 56697fbb..ec612195 100644
--- a/crates/db_schema/src/post.rs
+++ b/crates/db_schema/src/post.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::entity::prelude::*;
 
@@ -7,6 +7,7 @@ use sea_orm::entity::prelude::*;
 pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub id: String,
+    #[sea_orm(column_type = "Text")]
     pub object: String,
     pub attributed_to: String,
     pub in_reply_to: Option<String>,
diff --git a/crates/db_schema/src/prelude.rs b/crates/db_schema/src/prelude.rs
index 29f3cd06..62b98fb8 100644
--- a/crates/db_schema/src/prelude.rs
+++ b/crates/db_schema/src/prelude.rs
@@ -1,7 +1,8 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 pub use super::{
     activity::Entity as Activity,
+    blocked_url::Entity as BlockedUrl,
     post::Entity as Post,
     received_announce::Entity as ReceivedAnnounce,
     received_follow::Entity as ReceivedFollow,
diff --git a/crates/db_schema/src/received_announce.rs b/crates/db_schema/src/received_announce.rs
index a28b3e34..b1899cae 100644
--- a/crates/db_schema/src/received_announce.rs
+++ b/crates/db_schema/src/received_announce.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::entity::prelude::*;
 
diff --git a/crates/db_schema/src/received_follow.rs b/crates/db_schema/src/received_follow.rs
index b95b9f79..f9c579fa 100644
--- a/crates/db_schema/src/received_follow.rs
+++ b/crates/db_schema/src/received_follow.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::entity::prelude::*;
 
@@ -8,6 +8,7 @@ pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub id: String,
     pub actor: String,
+    #[sea_orm(column_type = "Text", nullable)]
     pub to: Option<String>,
     pub object: String,
 }
diff --git a/crates/db_schema/src/received_like.rs b/crates/db_schema/src/received_like.rs
index daf971df..38ec338b 100644
--- a/crates/db_schema/src/received_like.rs
+++ b/crates/db_schema/src/received_like.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::entity::prelude::*;
 
diff --git a/crates/db_schema/src/user.rs b/crates/db_schema/src/user.rs
index 18354312..43f85986 100644
--- a/crates/db_schema/src/user.rs
+++ b/crates/db_schema/src/user.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::{entity::prelude::*, FromJsonQueryResult};
 use serde::{Deserialize, Serialize};
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
 pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub id: String,
+    #[sea_orm(unique)]
     pub name: String,
     pub preferred_username: String,
     pub summary: Option<String>,
@@ -19,9 +20,9 @@ pub struct Model {
     pub local: bool,
     pub public_key: String,
     pub private_key: Option<String>,
+    pub last_refreshed_at: String,
     pub hatsu: Option<UserHatsu>,
     pub feed: Option<UserFeed>,
-    pub last_refreshed_at: String,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, FromJsonQueryResult)]
diff --git a/crates/db_schema/src/user_feed_item.rs b/crates/db_schema/src/user_feed_item.rs
index fe61efc9..b5a24e6b 100644
--- a/crates/db_schema/src/user_feed_item.rs
+++ b/crates/db_schema/src/user_feed_item.rs
@@ -1,4 +1,4 @@
-//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
 
 use sea_orm::{entity::prelude::*, FromJsonQueryResult};
 use serde::{Deserialize, Serialize};
diff --git a/flake.lock b/flake.lock
index 46bc6104..a8a63036 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1725409566,
-        "narHash": "sha256-PrtLmqhM6UtJP7v7IGyzjBFhbG4eOAHT6LPYOFmYfbk=",
+        "lastModified": 1727235847,
+        "narHash": "sha256-MoEot8izwkfGm1h5ak8hS2bu59mLmKeevlP/OvFLCzM=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "7e4586bad4e3f8f97a9271def747cf58c4b68f3c",
+        "rev": "08f45b5a2e01cf34ca6081188c6d16aa35581b09",
         "type": "github"
       },
       "original": {
@@ -42,11 +42,11 @@
         "nixpkgs-lib": "nixpkgs-lib"
       },
       "locked": {
-        "lastModified": 1725234343,
-        "narHash": "sha256-+ebgonl3NbiKD2UD0x4BszCZQ6sTfL4xioaM49o5B3Y=",
+        "lastModified": 1726153070,
+        "narHash": "sha256-HO4zgY0ekfwO5bX0QH/3kJ/h4KvUDFZg8YpkNwIbg1U=",
         "owner": "hercules-ci",
         "repo": "flake-parts",
-        "rev": "567b938d64d4b4112ee253b9274472dc3a346eb6",
+        "rev": "bcef6817a8b2aa20a5a6dbb19b43e63c5bf8619a",
         "type": "github"
       },
       "original": {
@@ -57,11 +57,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1726042813,
-        "narHash": "sha256-LnNKCCxnwgF+575y0pxUdlGZBO/ru1CtGHIqQVfvjlA=",
+        "lastModified": 1727140925,
+        "narHash": "sha256-ZHSasdLwEEjSOD/WTW1o7dr3/EjuYsdwYB4NSgICZ2I=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "159be5db480d1df880a0135ca0bfed84c2f88353",
+        "rev": "189e5f171b163feb7791a9118afa778d9a1db81f",
         "type": "github"
       },
       "original": {


Test patch: