Persist chat mentions

This commit is contained in:
Max Brunsfeld 2023-10-18 16:56:03 -07:00
parent 821419ee5b
commit d05404a4df
22 changed files with 402 additions and 112 deletions

View file

@ -215,6 +215,14 @@ CREATE TABLE IF NOT EXISTS "channel_messages" (
CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
CREATE TABLE "channel_message_mentions" (
"message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
"start_offset" INTEGER NOT NULL,
"end_offset" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY(message_id, start_offset)
);
CREATE TABLE "channel_paths" (
"id_path" TEXT NOT NULL PRIMARY KEY,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE

View file

@ -0,0 +1,7 @@
CREATE TABLE "channel_message_mentions" (
"message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
"start_offset" INTEGER NOT NULL,
"end_offset" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY(message_id, start_offset)
);

View file

@ -418,7 +418,6 @@ pub struct WaitlistSummary {
pub struct NewUserParams {
pub github_login: String,
pub github_user_id: i32,
pub invite_count: i32,
}
#[derive(Debug)]

View file

@ -103,6 +103,7 @@ impl Database {
sender_id: row.sender_id.to_proto(),
body: row.body,
timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
mentions: vec![],
nonce: Some(proto::Nonce {
upper_half: nonce.0,
lower_half: nonce.1,
@ -111,6 +112,38 @@ impl Database {
}
drop(rows);
messages.reverse();
let mut mentions = channel_message_mention::Entity::find()
.filter(
channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id)),
)
.order_by_asc(channel_message_mention::Column::MessageId)
.order_by_asc(channel_message_mention::Column::StartOffset)
.stream(&*tx)
.await?;
let mut message_ix = 0;
while let Some(mention) = mentions.next().await {
let mention = mention?;
let message_id = mention.message_id.to_proto();
while let Some(message) = messages.get_mut(message_ix) {
if message.id < message_id {
message_ix += 1;
} else {
if message.id == message_id {
message.mentions.push(proto::ChatMention {
range: Some(proto::Range {
start: mention.start_offset as u64,
end: mention.end_offset as u64,
}),
user_id: mention.user_id.to_proto(),
});
}
break;
}
}
}
Ok(messages)
})
.await
@ -121,6 +154,7 @@ impl Database {
channel_id: ChannelId,
user_id: UserId,
body: &str,
mentions: &[proto::ChatMention],
timestamp: OffsetDateTime,
nonce: u128,
) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
@ -150,7 +184,7 @@ impl Database {
let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
let message = channel_message::Entity::insert(channel_message::ActiveModel {
let message_id = channel_message::Entity::insert(channel_message::ActiveModel {
channel_id: ActiveValue::Set(channel_id),
sender_id: ActiveValue::Set(user_id),
body: ActiveValue::Set(body.to_string()),
@ -164,7 +198,31 @@ impl Database {
.to_owned(),
)
.exec(&*tx)
.await?;
.await?
.last_insert_id;
let models = mentions
.iter()
.filter_map(|mention| {
let range = mention.range.as_ref()?;
if !body.is_char_boundary(range.start as usize)
|| !body.is_char_boundary(range.end as usize)
{
return None;
}
Some(channel_message_mention::ActiveModel {
message_id: ActiveValue::Set(message_id),
start_offset: ActiveValue::Set(range.start as i32),
end_offset: ActiveValue::Set(range.end as i32),
user_id: ActiveValue::Set(UserId::from_proto(mention.user_id)),
})
})
.collect::<Vec<_>>();
if !models.is_empty() {
channel_message_mention::Entity::insert_many(models)
.exec(&*tx)
.await?;
}
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryConnectionId {
@ -172,22 +230,13 @@ impl Database {
}
// Observe this message for the sender
self.observe_channel_message_internal(
channel_id,
user_id,
message.last_insert_id,
&*tx,
)
.await?;
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
.await?;
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
channel_members.retain(|member| !participant_user_ids.contains(member));
Ok((
message.last_insert_id,
participant_connection_ids,
channel_members,
))
Ok((message_id, participant_connection_ids, channel_members))
})
.await
}

View file

@ -7,6 +7,7 @@ pub mod channel_buffer_collaborator;
pub mod channel_chat_participant;
pub mod channel_member;
pub mod channel_message;
pub mod channel_message_mention;
pub mod channel_path;
pub mod contact;
pub mod feature_flag;

View file

@ -0,0 +1,43 @@
use crate::db::{MessageId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_message_mentions")]
pub struct Model {
#[sea_orm(primary_key)]
pub message_id: MessageId,
#[sea_orm(primary_key)]
pub start_offset: i32,
pub end_offset: i32,
pub user_id: UserId,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel_message::Entity",
from = "Column::MessageId",
to = "super::channel_message::Column::Id"
)]
Message,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
MentionedUser,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Message.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::MentionedUser.def()
}
}

View file

@ -17,7 +17,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
NewUserParams {
github_login: "user_a".into(),
github_user_id: 101,
invite_count: 0,
},
)
.await
@ -30,7 +29,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
NewUserParams {
github_login: "user_b".into(),
github_user_id: 102,
invite_count: 0,
},
)
.await
@ -45,7 +43,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
NewUserParams {
github_login: "user_c".into(),
github_user_id: 102,
invite_count: 0,
},
)
.await
@ -178,7 +175,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
NewUserParams {
github_login: "user_a".into(),
github_user_id: 101,
invite_count: 0,
},
)
.await
@ -191,7 +187,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
NewUserParams {
github_login: "user_b".into(),
github_user_id: 102,
invite_count: 0,
},
)
.await

View file

@ -24,7 +24,6 @@ async fn test_channels(db: &Arc<Database>) {
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
@ -38,7 +37,6 @@ async fn test_channels(db: &Arc<Database>) {
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
@ -179,7 +177,6 @@ async fn test_joining_channels(db: &Arc<Database>) {
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
@ -192,7 +189,6 @@ async fn test_joining_channels(db: &Arc<Database>) {
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
@ -246,7 +242,6 @@ async fn test_channel_invites(db: &Arc<Database>) {
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
@ -259,7 +254,6 @@ async fn test_channel_invites(db: &Arc<Database>) {
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
@ -273,7 +267,6 @@ async fn test_channel_invites(db: &Arc<Database>) {
NewUserParams {
github_login: "user3".into(),
github_user_id: 7,
invite_count: 0,
},
)
.await
@ -385,7 +378,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
@ -399,7 +391,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
@ -444,7 +435,6 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
@ -802,7 +792,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await

View file

@ -22,7 +22,6 @@ async fn test_get_users(db: &Arc<Database>) {
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
invite_count: 0,
},
)
.await
@ -88,7 +87,6 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
NewUserParams {
github_login: "login1".into(),
github_user_id: 101,
invite_count: 0,
},
)
.await
@ -101,7 +99,6 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
NewUserParams {
github_login: "login2".into(),
github_user_id: 102,
invite_count: 0,
},
)
.await
@ -156,7 +153,6 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
NewUserParams {
github_login: "u1".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
@ -238,7 +234,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
invite_count: 0,
},
)
.await
@ -400,7 +395,6 @@ async fn test_metrics_id(db: &Arc<Database>) {
NewUserParams {
github_login: "person1".into(),
github_user_id: 101,
invite_count: 5,
},
)
.await
@ -416,7 +410,6 @@ async fn test_metrics_id(db: &Arc<Database>) {
NewUserParams {
github_login: "person2".into(),
github_user_id: 102,
invite_count: 5,
},
)
.await
@ -445,7 +438,6 @@ async fn test_project_count(db: &Arc<Database>) {
NewUserParams {
github_login: "admin".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
@ -457,7 +449,6 @@ async fn test_project_count(db: &Arc<Database>) {
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
@ -539,7 +530,6 @@ async fn test_fuzzy_search_users() {
NewUserParams {
github_login: github_login.into(),
github_user_id: i as i32,
invite_count: 0,
},
)
.await
@ -581,7 +571,6 @@ async fn test_non_matching_release_channels(db: &Arc<Database>) {
NewUserParams {
github_login: "admin".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
@ -593,7 +582,6 @@ async fn test_non_matching_release_channels(db: &Arc<Database>) {
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await

View file

@ -18,7 +18,6 @@ async fn test_get_user_flags(db: &Arc<Database>) {
NewUserParams {
github_login: format!("user1"),
github_user_id: 1,
invite_count: 0,
},
)
.await
@ -32,7 +31,6 @@ async fn test_get_user_flags(db: &Arc<Database>) {
NewUserParams {
github_login: format!("user2"),
github_user_id: 2,
invite_count: 0,
},
)
.await

View file

@ -2,6 +2,7 @@ use crate::{
db::{Database, MessageId, NewUserParams},
test_both_dbs,
};
use channel::mentions_to_proto;
use std::sync::Arc;
use time::OffsetDateTime;
@ -19,7 +20,6 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
@ -35,11 +35,18 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
let mut all_messages = Vec::new();
for i in 0..10 {
all_messages.push(
db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
.await
.unwrap()
.0
.to_proto(),
db.create_channel_message(
channel,
user,
&i.to_string(),
&[],
OffsetDateTime::now_utc(),
i,
)
.await
.unwrap()
.0
.to_proto(),
);
}
@ -81,7 +88,6 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
@ -96,19 +102,19 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.unwrap();
let msg1_id = db
.create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
.create_channel_message(channel, user, "1", &[], OffsetDateTime::now_utc(), 1)
.await
.unwrap();
let msg2_id = db
.create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
.create_channel_message(channel, user, "2", &[], OffsetDateTime::now_utc(), 2)
.await
.unwrap();
let msg3_id = db
.create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
.create_channel_message(channel, user, "3", &[], OffsetDateTime::now_utc(), 1)
.await
.unwrap();
let msg4_id = db
.create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
.create_channel_message(channel, user, "4", &[], OffsetDateTime::now_utc(), 2)
.await
.unwrap();
@ -118,12 +124,12 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
}
test_both_dbs!(
test_channel_message_new_notification,
test_channel_message_new_notification_postgres,
test_channel_message_new_notification_sqlite
test_unseen_channel_messages,
test_unseen_channel_messages_postgres,
test_unseen_channel_messages_sqlite
);
async fn test_channel_message_new_notification(db: &Arc<Database>) {
async fn test_unseen_channel_messages(db: &Arc<Database>) {
let user = db
.create_user(
"user_a@example.com",
@ -131,7 +137,6 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
NewUserParams {
github_login: "user_a".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
@ -143,8 +148,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
false,
NewUserParams {
github_login: "user_b".into(),
github_user_id: 1,
invite_count: 0,
github_user_id: 2,
},
)
.await
@ -152,21 +156,17 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.user_id;
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
db.invite_channel_member(channel_1, observer, user, false)
.await
.unwrap();
db.respond_to_channel_invite(channel_1, observer, true)
.await
.unwrap();
db.invite_channel_member(channel_2, observer, user, false)
.await
.unwrap();
db.respond_to_channel_invite(channel_1, observer, true)
.await
.unwrap();
db.respond_to_channel_invite(channel_2, observer, true)
.await
.unwrap();
@ -179,17 +179,17 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.unwrap();
let _ = db
.create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1)
.create_channel_message(channel_1, user, "1_1", &[], OffsetDateTime::now_utc(), 1)
.await
.unwrap();
let (second_message, _, _) = db
.create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2)
.create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
.await
.unwrap();
let (third_message, _, _) = db
.create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3)
.create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
.await
.unwrap();
@ -198,7 +198,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.unwrap();
let (fourth_message, _, _) = db
.create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4)
.create_channel_message(channel_2, user, "2_1", &[], OffsetDateTime::now_utc(), 4)
.await
.unwrap();
@ -295,3 +295,129 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
}]
);
}
test_both_dbs!(
test_channel_message_mentions,
test_channel_message_mentions_postgres,
test_channel_message_mentions_sqlite
);
async fn test_channel_message_mentions(db: &Arc<Database>) {
let user_a = db
.create_user(
"user_a@example.com",
false,
NewUserParams {
github_login: "user_a".into(),
github_user_id: 1,
},
)
.await
.unwrap()
.user_id;
let user_b = db
.create_user(
"user_b@example.com",
false,
NewUserParams {
github_login: "user_b".into(),
github_user_id: 2,
},
)
.await
.unwrap()
.user_id;
let user_c = db
.create_user(
"user_b@example.com",
false,
NewUserParams {
github_login: "user_c".into(),
github_user_id: 3,
},
)
.await
.unwrap()
.user_id;
let channel = db.create_channel("channel", None, user_a).await.unwrap();
db.invite_channel_member(channel, user_b, user_a, false)
.await
.unwrap();
db.respond_to_channel_invite(channel, user_b, true)
.await
.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let connection_id = rpc::ConnectionId { owner_id, id: 0 };
db.join_channel_chat(channel, connection_id, user_a)
.await
.unwrap();
db.create_channel_message(
channel,
user_a,
"hi @user_b and @user_c",
&mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
OffsetDateTime::now_utc(),
1,
)
.await
.unwrap();
db.create_channel_message(
channel,
user_a,
"bye @user_c",
&mentions_to_proto(&[(4..11, user_c.to_proto())]),
OffsetDateTime::now_utc(),
2,
)
.await
.unwrap();
db.create_channel_message(
channel,
user_a,
"umm",
&mentions_to_proto(&[]),
OffsetDateTime::now_utc(),
3,
)
.await
.unwrap();
db.create_channel_message(
channel,
user_a,
"@user_b, stop.",
&mentions_to_proto(&[(0..7, user_b.to_proto())]),
OffsetDateTime::now_utc(),
4,
)
.await
.unwrap();
let messages = db
.get_channel_messages(channel, user_b, 5, None)
.await
.unwrap()
.into_iter()
.map(|m| (m.body, m.mentions))
.collect::<Vec<_>>();
assert_eq!(
&messages,
&[
(
"hi @user_b and @user_c".into(),
mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
),
(
"bye @user_c".into(),
mentions_to_proto(&[(4..11, user_c.to_proto())]),
),
("umm".into(), mentions_to_proto(&[]),),
(
"@user_b, stop.".into(),
mentions_to_proto(&[(0..7, user_b.to_proto())]),
),
]
);
}

View file

@ -2885,6 +2885,8 @@ async fn send_channel_message(
return Err(anyhow!("message can't be blank"))?;
}
// TODO: adjust mentions if body is trimmed
let timestamp = OffsetDateTime::now_utc();
let nonce = request
.nonce
@ -2898,6 +2900,7 @@ async fn send_channel_message(
channel_id,
session.user_id,
&body,
&request.mentions,
timestamp,
nonce.clone().into(),
)
@ -2906,6 +2909,7 @@ async fn send_channel_message(
sender_id: session.user_id.to_proto(),
id: message_id.to_proto(),
body,
mentions: request.mentions,
timestamp: timestamp.unix_timestamp() as u64,
nonce: Some(nonce),
};

View file

@ -1,5 +1,5 @@
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
use channel::{ChannelChat, ChannelMessageId};
use channel::{ChannelChat, ChannelMessageId, MessageParams};
use collab_ui::chat_panel::ChatPanel;
use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext};
use std::sync::Arc;
@ -8,20 +8,22 @@ use workspace::dock::Panel;
#[gpui::test]
async fn test_basic_channel_messages(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
mut cx_a: &mut TestAppContext,
mut cx_b: &mut TestAppContext,
mut cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
@ -37,7 +39,16 @@ async fn test_basic_channel_messages(
.unwrap();
channel_chat_a
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
.update(cx_a, |c, cx| {
c.send_message(
MessageParams {
text: "hi @user_c!".into(),
mentions: vec![(3..10, client_c.id())],
},
cx,
)
.unwrap()
})
.await
.unwrap();
channel_chat_a
@ -52,15 +63,34 @@ async fn test_basic_channel_messages(
.unwrap();
deterministic.run_until_parked();
channel_chat_a.update(cx_a, |c, _| {
assert_eq!(
c.messages()
.iter()
.map(|m| m.body.as_str())
.collect::<Vec<_>>(),
vec!["one", "two", "three"]
);
})
let channel_chat_c = client_c
.channel_store()
.update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
.await
.unwrap();
for (chat, cx) in [
(&channel_chat_a, &mut cx_a),
(&channel_chat_b, &mut cx_b),
(&channel_chat_c, &mut cx_c),
] {
chat.update(*cx, |c, _| {
assert_eq!(
c.messages()
.iter()
.map(|m| (m.body.as_str(), m.mentions.as_slice()))
.collect::<Vec<_>>(),
vec![
("hi @user_c!", [(3..10, client_c.id())].as_slice()),
("two", &[]),
("three", &[])
],
"results for user {}",
c.client().id(),
);
});
}
}
#[gpui::test]

View file

@ -208,8 +208,7 @@ impl<T: RandomizedTest> TestPlan<T> {
false,
NewUserParams {
github_login: username.clone(),
github_user_id: (ix + 1) as i32,
invite_count: 0,
github_user_id: ix as i32,
},
)
.await

View file

@ -140,7 +140,6 @@ impl TestServer {
NewUserParams {
github_login: name.into(),
github_user_id: 0,
invite_count: 0,
},
)
.await