267 lines
9.7 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|