headless-talk 0.6.1

Headless talk implementation
Documentation
pub mod normal;
pub mod open;

use std::ops::Bound;

use crate::{
    conn::Conn,
    database::{
        model::{channel::ChannelListRow, chat::ChatRow},
        schema::{chat, user_profile},
        DatabasePool, PoolTaskError,
    },
    user::{DisplayUser, DisplayUserProfile},
    ClientResult,
};
use arrayvec::ArrayVec;
use diesel::{
    dsl::sql, sql_types::Integer, BoolExpressionMethods, ExpressionMethods, OptionalExtension,
    QueryDsl, RunQueryDsl,
};
use nohash_hasher::IntMap;
use serde::Deserialize;
use talk_loco_client::talk::{
    channel::{ChannelMeta, ChannelType},
    chat::{Chat, ChatType, Chatlog},
    session::{channel::write, TalkSession},
};

use self::{normal::NormalChannel, open::OpenChannel};

pub type ChannelMetaMap = IntMap<i32, ChannelMeta>;

pub type UserList<T> = Vec<(i64, T)>;

#[derive(Debug, Clone)]
pub struct ListPreviewChat {
    pub user: Option<DisplayUser>,
    pub chatlog: Chatlog,
}

#[derive(Debug, Clone)]
pub struct ListChannelProfile {
    pub name: String,
    pub image: Option<ListChannelProfileImage>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListChannelProfileImage {
    pub image_url: String,
    pub full_image_url: String,
}

#[derive(Debug, Clone)]
pub struct ChannelListItem {
    pub channel_type: ChannelType,

    pub last_chat: Option<ListPreviewChat>,

    pub display_users: ArrayVec<DisplayUser, 4>,

    pub unread_count: i32,

    pub active_user_count: i32,

    pub profile: ListChannelProfile,
}

#[derive(Debug, Clone)]
pub enum ClientChannel {
    Normal(NormalChannel),
    Open(OpenChannel),
}

#[derive(Debug, Clone, Copy)]
pub struct ChannelOp<'a> {
    id: i64,
    conn: &'a Conn,
}

impl<'a> ChannelOp<'a> {
    pub(crate) const fn new(id: i64, conn: &'a Conn) -> Self {
        Self { id, conn }
    }

    pub async fn send_chat(self, chat: Chat, no_seen: bool) -> ClientResult<Chatlog> {
        let res = TalkSession(&self.conn.session)
            .channel(self.id)
            .write_chat(&write::Request {
                chat_type: chat.chat_type.0,
                msg_id: chat.message_id,
                message: chat.content.message.as_deref(),
                no_seen,
                attachment: chat.content.attachment.as_deref(),
                supplement: chat.content.supplement.as_deref(),
            })
            .await?;

        let logged = res.chatlog.unwrap_or(Chatlog {
            channel_id: self.id,

            log_id: res.log_id,
            prev_log_id: Some(res.prev_id),

            author_id: self.conn.user_id,

            send_at: res.send_at,

            chat,

            referer: None,
        });

        self.conn
            .pool
            .spawn({
                let logged = logged.clone();

                move |conn| {
                    diesel::replace_into(chat::table)
                        .values(ChatRow::from_chatlog(logged, None))
                        .execute(conn)?;

                    Ok(())
                }
            })
            .await?;

        Ok(logged)
    }

    pub async fn delete_chat(self, log_id: i64) -> ClientResult<()> {
        let id = self.id;

        TalkSession(&self.conn.session)
            .channel(id)
            .delete_chat(log_id)
            .await?;

        self.conn
            .pool
            .spawn(move |conn| {
                diesel::update(chat::table)
                    .filter(chat::channel_id.eq(id).and(chat::log_id.eq(log_id)))
                    .set(
                        chat::type_.eq(sql("(type | ")
                            .bind::<Integer, _>(ChatType::DELETED_MASK)
                            .sql(")")),
                    )
                    .execute(conn)?;

                Ok(())
            })
            .await?;

        Ok(())
    }

    pub async fn delete_chat_local(
        self,
        id: i64,
        log_id: i64,
        deleted_time: i64,
    ) -> Result<bool, PoolTaskError> {
        self.conn
            .pool
            .spawn(move |conn| {
                let count = diesel::update(chat::table)
                    .filter(chat::channel_id.eq(id).and(chat::log_id.eq(log_id)))
                    .set(chat::deleted_time.eq(deleted_time))
                    .execute(conn)?;

                Ok(count > 0)
            })
            .await
    }

    pub async fn load_chat_from(
        self,
        log_id: Bound<i64>,
        count: i64,
    ) -> Result<Vec<Chatlog>, PoolTaskError> {
        let id = self.id;

        self.conn
            .pool
            .spawn(move |conn| {
                let iter = match log_id {
                    Bound::Included(log_id) => chat::table
                        .filter(chat::channel_id.eq(id).and(chat::log_id.le(log_id)))
                        .order_by(chat::log_id.desc())
                        .limit(count)
                        .load_iter::<ChatRow, _>(conn),
                    Bound::Excluded(log_id) => chat::table
                        .filter(chat::channel_id.eq(id).and(chat::log_id.lt(log_id)))
                        .order_by(chat::log_id.desc())
                        .limit(count)
                        .load_iter::<ChatRow, _>(conn),
                    Bound::Unbounded => chat::table
                        .filter(chat::channel_id.eq(id))
                        .order_by(chat::log_id.desc())
                        .limit(count)
                        .load_iter::<ChatRow, _>(conn),
                }?;

                Ok(iter
                    .map(|row| row.map(|row| row.into()))
                    .collect::<Result<_, _>>()?)
            })
            .await
    }
}

pub(crate) async fn load_list_item(
    pool: &DatabasePool,
    row: &ChannelListRow,
) -> Result<Option<ChannelListItem>, PoolTaskError> {
    let channel_type = ChannelType::from(row.channel_type.as_str());

    let (last_chat, display_users) = pool
        .spawn_transaction({
            let channel_id = row.id;
            let display_user_id_list =
                serde_json::from_str::<ArrayVec<i64, 4>>(&row.display_users).unwrap_or_default();

            move |conn| {
                let last_chat: Option<Chatlog> = chat::table
                    .filter(
                        chat::channel_id
                            .eq(channel_id)
                            .and(chat::deleted_time.is_null()),
                    )
                    .order(chat::log_id.desc())
                    .select(chat::all_columns)
                    .first::<ChatRow>(conn)
                    .optional()?
                    .map(Into::into);

                let last_chat: Option<ListPreviewChat> = if let Some(chat) = last_chat {
                    let user = if let Some((nickname, image_url)) = user_profile::table
                        .select((user_profile::nickname, user_profile::profile_url))
                        .filter(
                            user_profile::channel_id
                                .eq(channel_id)
                                .and(user_profile::id.eq(chat.author_id)),
                        )
                        .first::<(String, String)>(conn)
                        .optional()?
                    {
                        Some(DisplayUser {
                            id: chat.author_id,
                            profile: DisplayUserProfile {
                                nickname,
                                image_url: Some(image_url),
                            },
                        })
                    } else {
                        None
                    };

                    Some(ListPreviewChat {
                        user,
                        chatlog: chat,
                    })
                } else {
                    None
                };

                let mut display_users = ArrayVec::<DisplayUser, 4>::new();

                for id in display_user_id_list {
                    if let Some((nickname, profile_url)) = user_profile::table
                        .select((user_profile::nickname, user_profile::profile_url))
                        .filter(
                            user_profile::channel_id
                                .eq(channel_id)
                                .and(user_profile::id.eq(id)),
                        )
                        .first::<(String, String)>(conn)
                        .optional()?
                    {
                        display_users.push(DisplayUser {
                            id,
                            profile: DisplayUserProfile {
                                nickname,
                                image_url: Some(profile_url),
                            },
                        });
                    }
                }

                Ok((last_chat, display_users))
            }
        })
        .await?;

    let profile = match channel_type {
        ChannelType::DirectChat | ChannelType::MultiChat => {
            normal::load_list_profile(pool, &display_users, row).await?
        }

        _ => return Ok(None),
    };

    Ok(Some(ChannelListItem {
        channel_type,
        last_chat: last_chat.map(Into::into),
        display_users,
        unread_count: row.unread_count,
        active_user_count: row.active_user_count,
        profile,
    }))
}