Skip to main content

privchat_protocol/
presence.rs

1// Copyright 2025 Shanghai Boyu Information Technology Co., Ltd.
2// https://privchat.dev
3//
4// Author: zoujiaqing <zoujiaqing@gmail.com>
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// 在线状态枚举
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub enum OnlineStatus {
24    /// 在线(最近3分钟活跃)
25    Online,
26    /// 最近在线(1小时内)
27    Recently,
28    /// 上周在线(7天内)
29    LastWeek,
30    /// 上月在线(30天内)
31    LastMonth,
32    /// 很久之前
33    LongTimeAgo,
34    /// 离线(用户主动设置)
35    Offline,
36}
37
38impl OnlineStatus {
39    /// 从秒数计算在线状态
40    pub fn from_elapsed_seconds(elapsed: i64) -> Self {
41        match elapsed {
42            0..=180 => OnlineStatus::Online,             // 3分钟内
43            181..=3600 => OnlineStatus::Recently,        // 1小时内
44            3601..=604800 => OnlineStatus::LastWeek,     // 7天内
45            604801..=2592000 => OnlineStatus::LastMonth, // 30天内
46            _ => OnlineStatus::LongTimeAgo,
47        }
48    }
49
50    /// 转换为字符串
51    pub fn as_str(&self) -> &'static str {
52        match self {
53            OnlineStatus::Online => "online",
54            OnlineStatus::Recently => "recently",
55            OnlineStatus::LastWeek => "last_week",
56            OnlineStatus::LastMonth => "last_month",
57            OnlineStatus::LongTimeAgo => "long_time_ago",
58            OnlineStatus::Offline => "offline",
59        }
60    }
61}
62
63/// 在线状态信息
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct OnlineStatusInfo {
66    /// 用户ID
67    pub user_id: u64,
68    /// 在线状态
69    pub status: OnlineStatus,
70    /// 最后活跃时间(Unix 时间戳)
71    pub last_seen: i64,
72    /// 在线设备列表
73    pub online_devices: Vec<super::DeviceType>,
74}
75
76/// 订阅在线状态请求
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SubscribePresenceRequest {
79    /// 要订阅的用户ID列表
80    pub user_ids: Vec<u64>,
81}
82
83/// 订阅在线状态响应
84/// RPC 层只返回 data 负载;外层 code/message 由 RPC 封装,此处仅保留业务字段
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SubscribePresenceResponse {
87    /// 初始在线状态(订阅时立即返回)
88    pub initial_statuses: HashMap<u64, OnlineStatusInfo>,
89}
90
91/// 取消订阅在线状态请求
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct UnsubscribePresenceRequest {
94    /// 要取消订阅的用户ID列表
95    pub user_ids: Vec<u64>,
96}
97
98/// 取消订阅在线状态响应
99/// 与 reaction 等一致,data 为裸 bool,成功失败由外层 code 表示
100pub type UnsubscribePresenceResponse = bool;
101
102/// 获取在线状态请求(批量查询)
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct GetOnlineStatusRequest {
105    /// 要查询的用户ID列表
106    pub user_ids: Vec<u64>,
107}
108
109/// 获取在线状态响应
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct GetOnlineStatusResponse {
112    /// 响应码
113    pub code: i32,
114    /// 响应消息
115    pub message: String,
116    /// 在线状态映射
117    pub statuses: HashMap<u64, OnlineStatusInfo>,
118}
119
120/// 在线状态变化通知(服务端主动推送)
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct OnlineStatusChangeNotification {
123    /// 用户ID
124    pub user_id: u64,
125    /// 旧状态
126    pub old_status: OnlineStatus,
127    /// 新状态
128    pub new_status: OnlineStatus,
129    /// 最后活跃时间
130    pub last_seen: i64,
131    /// 时间戳
132    pub timestamp: i64,
133}
134
135/// 输入状态动作类型
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub enum TypingActionType {
138    /// 正在输入文字
139    Typing,
140    /// 正在录音
141    Recording,
142    /// 正在上传照片
143    UploadingPhoto,
144    /// 正在上传视频
145    UploadingVideo,
146    /// 正在上传文件
147    UploadingFile,
148    /// 正在选择贴纸
149    ChoosingSticker,
150}
151
152impl TypingActionType {
153    pub fn as_str(&self) -> &'static str {
154        match self {
155            TypingActionType::Typing => "typing",
156            TypingActionType::Recording => "recording",
157            TypingActionType::UploadingPhoto => "uploading_photo",
158            TypingActionType::UploadingVideo => "uploading_video",
159            TypingActionType::UploadingFile => "uploading_file",
160            TypingActionType::ChoosingSticker => "choosing_sticker",
161        }
162    }
163}
164
165/// 输入状态通知请求
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TypingIndicatorRequest {
168    /// 会话ID
169    pub channel_id: u64,
170    /// 会话类型(1=私聊, 2=群聊)
171    pub channel_type: u8,
172    /// 是否正在输入
173    pub is_typing: bool,
174    /// 输入动作类型
175    pub action_type: TypingActionType,
176}
177
178/// 输入状态通知响应
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct TypingIndicatorResponse {
181    /// 响应码
182    pub code: i32,
183    /// 响应消息
184    pub message: String,
185}
186
187/// 输入状态变化通知(服务端推送给会话中的其他成员)
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct TypingStatusNotification {
190    /// 用户ID
191    pub user_id: u64,
192    /// 用户名(可选,用于显示)
193    pub username: Option<String>,
194    /// 会话ID
195    pub channel_id: u64,
196    /// 会话类型
197    pub channel_type: u8,
198    /// 是否正在输入
199    pub is_typing: bool,
200    /// 输入动作类型
201    pub action_type: TypingActionType,
202    /// 时间戳
203    pub timestamp: i64,
204}
205
206/// 在线状态隐私设置
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct PresencePrivacySettings {
209    /// 谁能看到我的在线状态
210    pub show_online_to: PrivacyRule,
211    /// 谁能看到我的最后上线时间
212    pub show_last_seen_to: PrivacyRule,
213}
214
215/// 隐私规则
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub enum PrivacyRule {
218    /// 所有人
219    Everyone,
220    /// 仅联系人
221    Contacts,
222    /// 无人
223    Nobody,
224    /// 自定义
225    Custom {
226        /// 允许的用户列表
227        allow_users: Vec<u64>,
228        /// 拒绝的用户列表
229        deny_users: Vec<u64>,
230    },
231}
232
233impl Default for PresencePrivacySettings {
234    fn default() -> Self {
235        Self {
236            show_online_to: PrivacyRule::Everyone,
237            show_last_seen_to: PrivacyRule::Everyone,
238        }
239    }
240}
241
242/// 获取群组在线统计请求
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct GetGroupOnlineStatsRequest {
245    /// 群组ID
246    pub group_id: u64,
247    /// 是否需要返回在线用户列表(大群不返回)
248    pub include_user_list: bool,
249}
250
251/// 获取群组在线统计响应
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct GetGroupOnlineStatsResponse {
254    /// 响应码
255    pub code: i32,
256    /// 响应消息
257    pub message: String,
258    /// 群组ID
259    pub group_id: u64,
260    /// 总成员数
261    pub total_members: u32,
262    /// 在线成员数
263    pub online_count: u32,
264    /// 在线用户列表(可选,大群不返回)
265    pub online_users: Option<Vec<OnlineStatusInfo>>,
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_online_status_calculation() {
274        assert_eq!(OnlineStatus::from_elapsed_seconds(0), OnlineStatus::Online);
275        assert_eq!(
276            OnlineStatus::from_elapsed_seconds(180),
277            OnlineStatus::Online
278        );
279        assert_eq!(
280            OnlineStatus::from_elapsed_seconds(181),
281            OnlineStatus::Recently
282        );
283        assert_eq!(
284            OnlineStatus::from_elapsed_seconds(3600),
285            OnlineStatus::Recently
286        );
287        assert_eq!(
288            OnlineStatus::from_elapsed_seconds(3601),
289            OnlineStatus::LastWeek
290        );
291    }
292
293    #[test]
294    fn test_privacy_rule_default() {
295        let settings = PresencePrivacySettings::default();
296        assert!(matches!(settings.show_online_to, PrivacyRule::Everyone));
297        assert!(matches!(settings.show_last_seen_to, PrivacyRule::Everyone));
298    }
299}