Files
2026-05-21 15:50:14 +03:00

267 lines
9.7 KiB
Swift

import Foundation
public protocol SessionBridge: Sendable {
func authState() async throws -> AuthState
func networkState() async throws -> NetworkState
func pollEvents() async throws -> [SessionEvent]
func sendPhoneNumber(_ phone: String) async throws
func sendCode(_ code: String) async throws
func sendPassword(_ password: String) async throws
func loadFolders() async throws -> [Folder]
func loadChats(folderId: Int32?) async throws -> [ChatSummary]
func loadHistory(chatId: Int64) async throws -> [Message]
func searchMessages(chatId: Int64, query: String) async throws -> [Message]
func openProfile(chatId: Int64) async throws -> Profile
func leaveChat(chatId: Int64) async throws
func sendMessage(chatId: Int64, text: String, replyToMessageId: Int64?) async throws -> Message
func editMessage(chatId: Int64, messageId: Int64, text: String) async throws -> Message
func deleteMessages(chatId: Int64, messageIds: [Int64]) async throws
func forwardMessages(toChatId: Int64, fromChatId: Int64, messageIds: [Int64]) async throws
func react(chatId: Int64, messageId: Int64, reaction: String) async throws -> [Reaction]
func pinnedMessages(chatId: Int64) async throws -> [Message]
func copyPayload(chatId: Int64, messageId: Int64) async throws -> String
func setDraft(chatId: Int64, text: String) async throws
func downloadPhoto(fileId: Int32) async throws -> DownloadedFile
func downloadVoice(fileId: Int32) async throws -> DownloadedFile
}
public actor FakeSessionBridge: SessionBridge {
private var auth: AuthState
private var chats: [ChatSummary]
private var messages: [Int64: [Message]]
private var events: [SessionEvent]
private var nextMessageId: Int64
private static let baseMessageDate: Int32 = 1_700_000_000
public init(auth: AuthState = .waitPhoneNumber) {
self.auth = auth
let saved = ChatSummary(
id: 1,
title: "Saved Messages",
username: "saved",
lastMessage: "Hello from fake TDLib",
unreadCount: 1,
isPinned: true
)
let team = ChatSummary(
id: 2,
title: "iOS Team",
lastMessage: "Bridge smoke is green",
unreadMentionCount: 1,
folderIds: [0, 2],
isMuted: true,
draft: Draft(chatId: 2, text: "Follow up")
)
self.chats = [saved, team]
self.messages = [
1: [
Message(
id: 1,
chatId: 1,
senderName: "Alice",
text: "Hello from fake TDLib",
date: Self.baseMessageDate,
media: .photo(PhotoMedia(fileId: 100, width: 1280, height: 720)),
isOutgoing: false,
isRead: false
)
],
2: [
Message(
id: 2,
chatId: 2,
senderName: "Mikhail",
text: "Bridge smoke is green",
date: Self.baseMessageDate + 60,
media: .voice(VoiceMedia(fileId: 200, duration: 12, mimeType: "audio/ogg")),
isOutgoing: true
)
],
]
self.events = [.chatListChanged([saved, team])]
self.nextMessageId = 3
}
public func authState() async throws -> AuthState {
auth
}
public func networkState() async throws -> NetworkState {
.ready
}
public func pollEvents() async throws -> [SessionEvent] {
let drained = events
events.removeAll()
return drained
}
public func sendPhoneNumber(_ phone: String) async throws {
auth = .waitCode
events.append(.authChanged(auth))
}
public func sendCode(_ code: String) async throws {
auth = .waitPassword
events.append(.authChanged(auth))
}
public func sendPassword(_ password: String) async throws {
auth = .ready
events.append(.authChanged(auth))
}
public func loadFolders() async throws -> [Folder] {
[Folder(id: 0, name: "All"), Folder(id: 2, name: "Work")]
}
public func loadChats(folderId: Int32?) async throws -> [ChatSummary] {
let result = folderId.map { folderId in
chats.filter { $0.folderIds.contains(folderId) }
} ?? chats
events.append(.chatListChanged(result))
return result
}
public func loadHistory(chatId: Int64) async throws -> [Message] {
messages[chatId] ?? []
}
public func searchMessages(chatId: Int64, query: String) async throws -> [Message] {
guard !query.isEmpty else {
return messages[chatId] ?? []
}
return (messages[chatId] ?? []).filter {
$0.text.localizedCaseInsensitiveContains(query)
|| $0.senderName.localizedCaseInsensitiveContains(query)
}
}
public func openProfile(chatId: Int64) async throws -> Profile {
let chat = chats.first { $0.id == chatId }
let profile = Profile(
chatId: chatId,
title: chat?.title ?? "Unknown",
username: chat?.username,
bio: chatId == 1 ? "Fake profile for the iOS app shell" : "Team chat",
isGroup: chatId != 1,
memberCount: chatId == 1 ? nil : 4
)
events.append(.profileLoaded(profile))
return profile
}
public func leaveChat(chatId: Int64) async throws {
chats.removeAll { $0.id == chatId }
messages.removeValue(forKey: chatId)
events.append(.chatListChanged(chats))
}
public func sendMessage(chatId: Int64, text: String, replyToMessageId: Int64?) async throws -> Message {
let message = Message(
id: nextMessageId,
chatId: chatId,
senderName: "Me",
text: text,
date: Int32(Date().timeIntervalSince1970),
isOutgoing: true,
replyText: replyToMessageId.map { "Reply to #\($0)" }
)
nextMessageId += 1
messages[chatId, default: []].append(message)
if let index = chats.firstIndex(where: { $0.id == chatId }) {
chats[index].lastMessage = text
chats[index].draft = nil
}
events.append(.messageAdded(chatId, message))
return message
}
public func editMessage(chatId: Int64, messageId: Int64, text: String) async throws -> Message {
guard var chatMessages = messages[chatId],
let index = chatMessages.firstIndex(where: { $0.id == messageId })
else {
throw FakeBridgeError.messageNotFound
}
chatMessages[index].text = text
chatMessages[index].editDate = Int32(Date().timeIntervalSince1970)
messages[chatId] = chatMessages
return chatMessages[index]
}
public func deleteMessages(chatId: Int64, messageIds: [Int64]) async throws {
messages[chatId]?.removeAll { messageIds.contains($0.id) }
}
public func forwardMessages(toChatId: Int64, fromChatId: Int64, messageIds: [Int64]) async throws {
let sourceMessages = (messages[fromChatId] ?? []).filter { messageIds.contains($0.id) }
for source in sourceMessages {
let forwarded = Message(
id: nextMessageId,
chatId: toChatId,
senderName: "Me",
text: source.text,
date: Int32(Date().timeIntervalSince1970),
isOutgoing: true,
forwardSenderName: source.senderName
)
nextMessageId += 1
messages[toChatId, default: []].append(forwarded)
events.append(.messageAdded(toChatId, forwarded))
}
}
public func react(chatId: Int64, messageId: Int64, reaction: String) async throws -> [Reaction] {
guard var chatMessages = messages[chatId],
let index = chatMessages.firstIndex(where: { $0.id == messageId })
else {
throw FakeBridgeError.messageNotFound
}
if let reactionIndex = chatMessages[index].reactions.firstIndex(where: { $0.emoji == reaction }) {
chatMessages[index].reactions.remove(at: reactionIndex)
} else {
chatMessages[index].reactions.append(Reaction(emoji: reaction, count: 1, isChosen: true))
}
messages[chatId] = chatMessages
return chatMessages[index].reactions
}
public func pinnedMessages(chatId: Int64) async throws -> [Message] {
Array((messages[chatId] ?? []).prefix(1))
}
public func copyPayload(chatId: Int64, messageId: Int64) async throws -> String {
guard let message = messages[chatId]?.first(where: { $0.id == messageId }) else {
throw FakeBridgeError.messageNotFound
}
return message.text
}
public func setDraft(chatId: Int64, text: String) async throws {
let draft = Draft(chatId: chatId, text: text)
if let index = chats.firstIndex(where: { $0.id == chatId }) {
chats[index].draft = text.isEmpty ? nil : draft
}
events.append(.draftChanged(draft))
}
public func downloadPhoto(fileId: Int32) async throws -> DownloadedFile {
DownloadedFile(fileId: fileId, path: "/tmp/fake-photo-\(fileId).jpg")
}
public func downloadVoice(fileId: Int32) async throws -> DownloadedFile {
DownloadedFile(fileId: fileId, path: "/tmp/fake-voice-\(fileId).ogg")
}
}
public enum FakeBridgeError: LocalizedError {
case messageNotFound
public var errorDescription: String? {
switch self {
case .messageNotFound:
"Message not found"
}
}
}