mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73ee18a610 | |||
| aa65c197f3 | |||
| b078d62921 | |||
| 0a0990c14c | |||
| 07a9050af5 | |||
| 0b3e1614e3 | |||
| bc97b262e5 | |||
| 1b2d7e93f7 | |||
| 4847d2222f | |||
| 73d2929af3 | |||
| 674032b888 | |||
| d47898877a | |||
| 096b9f9613 | |||
| 171abeb145 | |||
| fbddf1d34e | |||
| 9974460dcf | |||
| a8e2fd3c97 | |||
| de1c0f2bf8 | |||
| f167b31e85 | |||
| a2f4615895 | |||
| 339394213b | |||
| c832f9b290 | |||
| b4342c4037 | |||
| 7376b289ca | |||
| f1168a9973 | |||
| 13ece144f2 | |||
| 7168fd9094 | |||
| 1720c843b1 | |||
| 0d0f0aa434 | |||
| 1c697c4a31 | |||
| 9155a2f02a | |||
| 9b6365ca88 | |||
| 6301acd9ff | |||
| 5eaf563b96 | |||
| f8e69ea6e7 | |||
| 5c36d15b2b | |||
| 77df3cc208 | |||
| ff7fcaf7f3 | |||
| 547dbc5271 | |||
| 0e0eeb4a57 | |||
| f8226554ff | |||
| 4cf3da05c3 | |||
| 65c136c7fb | |||
| a84b56d39d | |||
| a9ca467a80 | |||
| f738d2731c | |||
| ea61fd7e2b |
@@ -58,6 +58,7 @@ class ItemsModel: ObservableObject {
|
|||||||
// this will cause reversedChatItems to be rendered without throttling
|
// this will cause reversedChatItems to be rendered without throttling
|
||||||
@Published var isLoading = false
|
@Published var isLoading = false
|
||||||
@Published var showLoadingProgress = false
|
@Published var showLoadingProgress = false
|
||||||
|
@State var anchors: [ChatItem.ID] = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
publisher
|
publisher
|
||||||
|
|||||||
@@ -318,10 +318,12 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadItemsPerPage = 50
|
let loadItemsPerPage = 100
|
||||||
|
let preloadItem = 25
|
||||||
|
let idealChatListSize = 300
|
||||||
|
|
||||||
func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat {
|
func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat {
|
||||||
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search))
|
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: .initial(count: loadItemsPerPage), search: search))
|
||||||
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
|
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
@@ -329,6 +331,15 @@ func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws ->
|
|||||||
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
|
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
|
||||||
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
|
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
|
||||||
if case let .apiChat(_, chat) = r { return chat.chatItems }
|
if case let .apiChat(_, chat) = r { return chat.chatItems }
|
||||||
|
if case .chatCmdError(_, _) = r {
|
||||||
|
if case .chatError(_, let chatError) = r {
|
||||||
|
if case .errorStore(let storeError) = chatError {
|
||||||
|
if case .chatItemNotFound(_) = storeError {
|
||||||
|
itemNotFoundAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +350,9 @@ func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async {
|
|||||||
let im = ItemsModel.shared
|
let im = ItemsModel.shared
|
||||||
m.chatItemStatuses = [:]
|
m.chatItemStatuses = [:]
|
||||||
if clearItems {
|
if clearItems {
|
||||||
await MainActor.run { im.reversedChatItems = [] }
|
await MainActor.run {
|
||||||
|
im.reversedChatItems = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
|
let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -418,6 +431,13 @@ func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func itemNotFoundAlert() {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title: "Message no longer available",
|
||||||
|
message: "The quoted message you are trying to view has been deleted."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func sendMessageErrorAlert(_ r: ChatResponse) {
|
private func sendMessageErrorAlert(_ r: ChatResponse) {
|
||||||
logger.error("send message error: \(String(describing: r))")
|
logger.error("send message error: \(String(describing: r))")
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
|
|||||||
@@ -48,10 +48,18 @@ struct FramedItemView: View {
|
|||||||
if let qi = chatItem.quotedItem {
|
if let qi = chatItem.quotedItem {
|
||||||
ciQuoteView(qi)
|
ciQuoteView(qi)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
if let itemId = qi.itemId {
|
||||||
withAnimation {
|
if !scrollToItem(itemId) {
|
||||||
scrollModel.scrollToItem(id: ci.id)
|
Task {
|
||||||
|
//if await loadItemsAround(chat.chatInfo, itemId) != nil {
|
||||||
|
// await MainActor.run {
|
||||||
|
// let _ = scrollToItem(itemId)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
itemNotFoundAlert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let itemForwarded = chatItem.meta.itemForwarded {
|
} else if let itemForwarded = chatItem.meta.itemForwarded {
|
||||||
@@ -323,6 +331,18 @@ struct FramedItemView: View {
|
|||||||
return videoWidth
|
return videoWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll to an item, if success returns true otherwise false
|
||||||
|
private func scrollToItem(_ itemId: Int64) -> Bool {
|
||||||
|
if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == itemId }) {
|
||||||
|
withAnimation {
|
||||||
|
scrollModel.scrollToItem(id: ci.id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func toggleSecrets<V: View>(_ ft: [FormattedText]?, _ showSecrets: Binding<Bool>, _ v: V) -> some View {
|
@ViewBuilder func toggleSecrets<V: View>(_ ft: [FormattedText]?, _ showSecrets: Binding<Bool>, _ v: V) -> some View {
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
//
|
||||||
|
// ChatItemGroups.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Diogo Cunha on 11/11/2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
/// Represents an anchor in a list of chat items, indicating where data is missing and should be loaded.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - itemId: The unique identifier of the last item in the loaded list before the anchor.
|
||||||
|
/// This ID corresponds to an item in the chat history, ordered from older to newer items.
|
||||||
|
/// It is typically used when loading items via .around or .initial pagination when loading items
|
||||||
|
/// - indexRange: The range of indexes within `reversedChatItems` array that
|
||||||
|
/// represents the anchor. The first index in this range is the position of the anchor itself.
|
||||||
|
/// For instance, if the array `[0, 1, 2, -100-, 101]` has an anchor at index 3, `indexRange`
|
||||||
|
/// would be `3..<5`, indicating the anchor starts at index 3.
|
||||||
|
/// - indexRangeInParentItems: The range of indexes in the `ReverseList` or parent UI component
|
||||||
|
/// that considers revealed or hidden items, showing where the anchor appears in the visible list.
|
||||||
|
/// The first index in this range points to where the anchor starts in the UI.
|
||||||
|
struct AnchoredRange {
|
||||||
|
let itemId: Int64
|
||||||
|
let indexRange: Range<Int>
|
||||||
|
let indexRangeInParentItems: Range<Int>
|
||||||
|
}
|
||||||
|
struct SectionGroups {
|
||||||
|
let sections: [SectionItems]
|
||||||
|
let anchoredRanges: [AnchoredRange]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ListItem: Hashable, Equatable {
|
||||||
|
let item: ChatItem
|
||||||
|
let separation: ItemSeparation
|
||||||
|
let prevItemSeparationLargeGap: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
class SectionItems: ObservableObject {
|
||||||
|
var mergeCategory: CIMergeCategory?
|
||||||
|
var items: [ListItem]
|
||||||
|
@Published var revealed: Bool
|
||||||
|
var showAvatar: Set<Int64>
|
||||||
|
var startIndexInParentItems: Int
|
||||||
|
|
||||||
|
init(mergeCategory: CIMergeCategory?, items: [ListItem], revealed: Bool, showAvatar: Set<Int64>, startIndexInParentItems: Int) {
|
||||||
|
self.mergeCategory = mergeCategory
|
||||||
|
self.items = items
|
||||||
|
self.revealed = revealed
|
||||||
|
self.showAvatar = showAvatar
|
||||||
|
self.startIndexInParentItems = startIndexInParentItems
|
||||||
|
}
|
||||||
|
|
||||||
|
func reveal(_ reveal: Bool, revealedItems: inout Set<Int64>) {
|
||||||
|
if reveal {
|
||||||
|
for item in items {
|
||||||
|
revealedItems.insert(item.item.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for item in items {
|
||||||
|
revealedItems.remove(item.item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.revealed = reveal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putIntoGroups(chatItems: [ChatItem], revealedItems: Set<ChatItem.ID>, itemAnchors: Array<ChatItem.ID>) -> SectionGroups {
|
||||||
|
guard !chatItems.isEmpty else { return SectionGroups(sections: [], anchoredRanges: []) }
|
||||||
|
|
||||||
|
var groups: [SectionItems] = []
|
||||||
|
var anchoredRanges: [AnchoredRange] = []
|
||||||
|
var index = 0
|
||||||
|
var unclosedAnchorIndex: Int?
|
||||||
|
var unclosedAnchorIndexInParent: Int?
|
||||||
|
var unclosedAnchorItemId: Int64?
|
||||||
|
var visibleItemIndexInParent = -1
|
||||||
|
var recent: SectionItems?
|
||||||
|
|
||||||
|
while index < chatItems.count {
|
||||||
|
let item = chatItems[index]
|
||||||
|
let next = index + 1 < chatItems.count ? chatItems[index + 1] : nil
|
||||||
|
let category = item.mergeCategory
|
||||||
|
let itemIsAnchor = itemAnchors.contains(item.id)
|
||||||
|
|
||||||
|
let itemSeparation: ItemSeparation
|
||||||
|
let prevItemSeparationLargeGap: Bool
|
||||||
|
|
||||||
|
if let recentSection = recent, index > 0, recentSection.mergeCategory == category, !itemIsAnchor {
|
||||||
|
if recentSection.revealed {
|
||||||
|
let prev = index > 0 ? chatItems[index - 1] : nil
|
||||||
|
itemSeparation = getItemSeparation(item, at: index)
|
||||||
|
let nextForGap = (category != nil && category == prev?.mergeCategory) || index + 1 == chatItems.count ? nil : next
|
||||||
|
prevItemSeparationLargeGap = nextForGap == nil ? false : getItemSeparationLargeGap(item, at: index)
|
||||||
|
visibleItemIndexInParent += 1
|
||||||
|
} else {
|
||||||
|
itemSeparation = getItemSeparation(item, at: index)
|
||||||
|
prevItemSeparationLargeGap = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let listItem = ListItem(item: item, separation: itemSeparation, prevItemSeparationLargeGap: prevItemSeparationLargeGap)
|
||||||
|
recentSection.items.append(listItem)
|
||||||
|
if shouldShowAvatar(current: item, older: next) {
|
||||||
|
recentSection.showAvatar.insert(item.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let revealed = item.mergeCategory == nil || revealedItems.contains(item.id)
|
||||||
|
visibleItemIndexInParent += 1
|
||||||
|
|
||||||
|
if revealed {
|
||||||
|
let prev = index > 0 ? chatItems[index - 1] : nil
|
||||||
|
itemSeparation = getItemSeparation(item, at: index)
|
||||||
|
let nextForGap = (category != nil && category == prev?.mergeCategory) || index + 1 == chatItems.count ? nil : next
|
||||||
|
prevItemSeparationLargeGap = nextForGap == nil ? false : getItemSeparationLargeGap(item, at: index)
|
||||||
|
} else {
|
||||||
|
itemSeparation = getItemSeparation(item, at: index)
|
||||||
|
prevItemSeparationLargeGap = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let listItem = ListItem(item: item, separation: itemSeparation, prevItemSeparationLargeGap: prevItemSeparationLargeGap)
|
||||||
|
let newSection = SectionItems(
|
||||||
|
mergeCategory: item.mergeCategory,
|
||||||
|
items: [listItem],
|
||||||
|
revealed: revealed,
|
||||||
|
showAvatar: shouldShowAvatar(current: item, older: next) ? [item.id] : [],
|
||||||
|
startIndexInParentItems: visibleItemIndexInParent
|
||||||
|
)
|
||||||
|
groups.append(newSection)
|
||||||
|
recent = newSection
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemIsAnchor {
|
||||||
|
if let unclosedIndex = unclosedAnchorIndex, let unclosedId = unclosedAnchorItemId, let unclosedIndexInParent = unclosedAnchorIndexInParent {
|
||||||
|
anchoredRanges.append(
|
||||||
|
AnchoredRange(
|
||||||
|
itemId: unclosedId,
|
||||||
|
indexRange: unclosedIndex..<index,
|
||||||
|
indexRangeInParentItems: unclosedIndexInParent..<visibleItemIndexInParent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
unclosedAnchorIndex = index
|
||||||
|
unclosedAnchorIndexInParent = visibleItemIndexInParent
|
||||||
|
unclosedAnchorItemId = item.id
|
||||||
|
} else if index + 1 == chatItems.count, let unclosedIndex = unclosedAnchorIndex, let unclosedId = unclosedAnchorItemId, let unclosedIndexInParent = unclosedAnchorIndexInParent {
|
||||||
|
anchoredRanges.append(
|
||||||
|
AnchoredRange(
|
||||||
|
itemId: unclosedId,
|
||||||
|
indexRange: unclosedIndex..<index + 1,
|
||||||
|
indexRangeInParentItems: unclosedIndexInParent..<visibleItemIndexInParent + 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return SectionGroups(sections: groups, anchoredRanges: anchoredRanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getItemSectionItems(sections: Array<SectionItems>, itemId: ChatItem.ID) -> SectionItems? {
|
||||||
|
for sec in sections {
|
||||||
|
if sec.items.firstIndex(where: { $0.item.id == itemId }) != nil {
|
||||||
|
return sec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexInParentItems(sections: Array<SectionItems>, itemId: ChatItem.ID) -> Int {
|
||||||
|
for sec in sections {
|
||||||
|
if let index = sec.items.firstIndex(where: { $0.item.id == itemId }) {
|
||||||
|
return sec.startIndexInParentItems + (sec.revealed ? index : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNewestItemAtParentIndexOrNull(sections: [SectionItems], parentIndex: Int) -> ChatItem? {
|
||||||
|
for group in sections {
|
||||||
|
let range = group.startIndexInParentItems...(group.startIndexInParentItems + group.items.count - 1)
|
||||||
|
if range.contains(parentIndex) {
|
||||||
|
if group.revealed {
|
||||||
|
return group.items[parentIndex - group.startIndexInParentItems].item
|
||||||
|
} else {
|
||||||
|
return group.items.first?.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func shouldShowAvatar(current: ChatItem, older: ChatItem?) -> Bool {
|
||||||
|
if case let .groupRcv(currentMember) = current.chatDir {
|
||||||
|
if let older = older, case let .groupRcv(olderMember) = older.chatDir {
|
||||||
|
return olderMember.memberId != currentMember.memberId
|
||||||
|
}
|
||||||
|
return true // Show avatar if there is no older item or if older is not a GroupRcv
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getItemSeparationLargeGap(_ chatItem: ChatItem, at index: Int?) -> Bool {
|
||||||
|
let im = ItemsModel.shared
|
||||||
|
|
||||||
|
if let index = index, index > 0, index < im.reversedChatItems.count {
|
||||||
|
let nextItem = im.reversedChatItems[index - 1]
|
||||||
|
let sameMemberAndDirection = nextItem.chatDir.sameDirection(chatItem.chatDir)
|
||||||
|
|
||||||
|
// Return true if they are not the same direction or the time interval is more than 60 seconds.
|
||||||
|
return !sameMemberAndDirection || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
|
||||||
|
} else {
|
||||||
|
// If there is no next item or it's out of bounds, consider it a large gap.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,27 @@ import Combine
|
|||||||
|
|
||||||
private let memberImageSize: CGFloat = 34
|
private let memberImageSize: CGFloat = 34
|
||||||
|
|
||||||
|
struct ItemSeparation: Equatable, Hashable {
|
||||||
|
let timestamp: Bool;
|
||||||
|
let largeGap: Bool;
|
||||||
|
let date: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation {
|
||||||
|
let im = ItemsModel.shared
|
||||||
|
if let i, i > 0 && im.reversedChatItems.count >= i {
|
||||||
|
let nextItem = im.reversedChatItems[i - 1]
|
||||||
|
let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
|
||||||
|
return ItemSeparation(
|
||||||
|
timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
|
||||||
|
largeGap: largeGap,
|
||||||
|
date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return ItemSeparation(timestamp: true, largeGap: true, date: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatView: View {
|
struct ChatView: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@ObservedObject var im = ItemsModel.shared
|
@ObservedObject var im = ItemsModel.shared
|
||||||
@@ -32,7 +53,6 @@ struct ChatView: View {
|
|||||||
@State private var connectionCode: String?
|
@State private var connectionCode: String?
|
||||||
@State private var loadingItems = false
|
@State private var loadingItems = false
|
||||||
@State private var firstPage = false
|
@State private var firstPage = false
|
||||||
@State private var revealedChatItem: ChatItem?
|
|
||||||
@State private var searchMode = false
|
@State private var searchMode = false
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
@FocusState private var searchFocussed
|
@FocusState private var searchFocussed
|
||||||
@@ -46,6 +66,9 @@ struct ChatView: View {
|
|||||||
@State private var selectedChatItems: Set<Int64>? = nil
|
@State private var selectedChatItems: Set<Int64>? = nil
|
||||||
@State private var showDeleteSelectedMessages: Bool = false
|
@State private var showDeleteSelectedMessages: Bool = false
|
||||||
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
||||||
|
@State private var initialChatItem: ChatItem? = nil
|
||||||
|
@State private var revealedItems: Set<ChatItem.ID> = []
|
||||||
|
@State private var anchors: Array<ChatItem.ID> = []
|
||||||
|
|
||||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||||
|
|
||||||
@@ -167,6 +190,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: chatModel.chatId) { cId in
|
.onChange(of: chatModel.chatId) { cId in
|
||||||
showChatInfoSheet = false
|
showChatInfoSheet = false
|
||||||
|
firstPage = false
|
||||||
selectedChatItems = nil
|
selectedChatItems = nil
|
||||||
scrollModel.scrollToBottom()
|
scrollModel.scrollToBottom()
|
||||||
stopAudioPlayer()
|
stopAudioPlayer()
|
||||||
@@ -180,7 +204,7 @@ struct ChatView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: revealedChatItem) { _ in
|
.onChange(of: revealedItems.count) { _ in
|
||||||
NotificationCenter.postReverseListNeedsLayout()
|
NotificationCenter.postReverseListNeedsLayout()
|
||||||
}
|
}
|
||||||
.onChange(of: im.isLoading) { isLoading in
|
.onChange(of: im.isLoading) { isLoading in
|
||||||
@@ -362,7 +386,18 @@ struct ChatView: View {
|
|||||||
await markChatUnread(chat, unreadChat: false)
|
await markChatUnread(chat, unreadChat: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if im.reversedChatItems.count == loadItemsPerPage {
|
||||||
|
loadChatItems(chat.chatInfo, .last(count: loadItemsPerPage))
|
||||||
|
}
|
||||||
|
|
||||||
ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
|
ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
|
||||||
|
Task {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let firstunreadItem = self.getFirstUnreadItem() {
|
||||||
|
initialChatItem = firstunreadItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func searchToolbar() -> some View {
|
private func searchToolbar() -> some View {
|
||||||
@@ -416,9 +451,10 @@ struct ChatView: View {
|
|||||||
|
|
||||||
private func chatItemsList() -> some View {
|
private func chatItemsList() -> some View {
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
let mergedItems = filtered(im.reversedChatItems)
|
let groups = putIntoGroups(chatItems: im.reversedChatItems, revealedItems: self.revealedItems, itemAnchors: self.anchors)
|
||||||
return GeometryReader { g in
|
return GeometryReader { g in
|
||||||
ReverseList(items: mergedItems, scrollState: $scrollModel.state) { ci in
|
ReverseList(groups: groups, scrollState: $scrollModel.state, initialChatItem: $initialChatItem) { li in
|
||||||
|
let ci = li.item
|
||||||
let voiceNoFrame = voiceWithoutFrame(ci)
|
let voiceNoFrame = voiceWithoutFrame(ci)
|
||||||
let maxWidth = cInfo.chatType == .group
|
let maxWidth = cInfo.chatType == .group
|
||||||
? voiceNoFrame
|
? voiceNoFrame
|
||||||
@@ -433,13 +469,19 @@ struct ChatView: View {
|
|||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
composeState: $composeState,
|
composeState: $composeState,
|
||||||
selectedMember: $selectedMember,
|
selectedMember: $selectedMember,
|
||||||
revealedChatItem: $revealedChatItem,
|
revealedChatItems: $revealedItems,
|
||||||
selectedChatItems: $selectedChatItems,
|
selectedChatItems: $selectedChatItems,
|
||||||
forwardedChatItems: $forwardedChatItems
|
forwardedChatItems: $forwardedChatItems,
|
||||||
|
onReveal: { revealState in
|
||||||
|
if let sec = getItemSectionItems(sections: groups.sections, itemId: ci.id) {
|
||||||
|
sec.reveal(revealState, revealedItems: &self.revealedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.id(ci.id) // Required to trigger `onAppear` on iOS15
|
.id(ci.id) // Required to trigger `onAppear` on iOS15
|
||||||
} loadPage: {
|
|
||||||
loadChatItems(cInfo)
|
} loadPage: { pagination in
|
||||||
|
loadChatItems(cInfo, pagination)
|
||||||
}
|
}
|
||||||
.opacity(ItemsModel.shared.isLoading ? 0 : 1)
|
.opacity(ItemsModel.shared.isLoading ? 0 : 1)
|
||||||
.padding(.vertical, -InvertedTableView.inset)
|
.padding(.vertical, -InvertedTableView.inset)
|
||||||
@@ -471,6 +513,23 @@ struct ChatView: View {
|
|||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getFirstUnreadItem() -> ChatItem? {
|
||||||
|
var maybeItem: ChatItem? = nil
|
||||||
|
for i in stride(from: im.reversedChatItems.count - 1, through: 0, by: -1) {
|
||||||
|
let item = im.reversedChatItems[i]
|
||||||
|
if item.isRcvNew {
|
||||||
|
if item.mergeCategory == nil {
|
||||||
|
return maybeItem ?? item
|
||||||
|
} else {
|
||||||
|
maybeItem = item
|
||||||
|
}
|
||||||
|
} else if maybeItem != nil {
|
||||||
|
return maybeItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
class FloatingButtonModel: ObservableObject {
|
class FloatingButtonModel: ObservableObject {
|
||||||
static let shared = FloatingButtonModel()
|
static let shared = FloatingButtonModel()
|
||||||
@@ -478,17 +537,24 @@ struct ChatView: View {
|
|||||||
@Published var isNearBottom: Bool = true
|
@Published var isNearBottom: Bool = true
|
||||||
@Published var date: Date?
|
@Published var date: Date?
|
||||||
@Published var isDateVisible: Bool = false
|
@Published var isDateVisible: Bool = false
|
||||||
|
@Published var bottomItemIndex: Int = 0
|
||||||
var totalUnread: Int = 0
|
var totalUnread: Int = 0
|
||||||
var isReallyNearBottom: Bool = true
|
var isReallyNearBottom: Bool = true
|
||||||
var hideDateWorkItem: DispatchWorkItem?
|
var hideDateWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
func updateOnListChange(_ listState: ListState) {
|
func updateOnListChange(_ listState: ListState) {
|
||||||
let im = ItemsModel.shared
|
let im = ItemsModel.shared
|
||||||
let unreadBelow =
|
let bottomItemIndex =
|
||||||
if let id = listState.bottomItemId,
|
if let id = listState.bottomItemId,
|
||||||
let index = im.reversedChatItems.firstIndex(where: { $0.id == id })
|
let index = im.reversedChatItems.firstIndex(where: { $0.id == id }) {
|
||||||
{
|
index
|
||||||
im.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
|
||||||
|
var unreadBelow =
|
||||||
|
if bottomItemIndex != -1 {
|
||||||
|
im.reversedChatItems[..<bottomItemIndex].reduce(into: 0) { unread, chatItem in
|
||||||
if chatItem.isRcvNew { unread += 1 }
|
if chatItem.isRcvNew { unread += 1 }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -508,6 +574,7 @@ struct ChatView: View {
|
|||||||
it.unreadBelow = unreadBelow
|
it.unreadBelow = unreadBelow
|
||||||
it.date = date
|
it.date = date
|
||||||
it.isReallyNearBottom = listState.scrollOffset > 0 && listState.scrollOffset < 500
|
it.isReallyNearBottom = listState.scrollOffset > 0 && listState.scrollOffset < 500
|
||||||
|
it.bottomItemIndex = bottomItemIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// set floating button indication mode
|
// set floating button indication mode
|
||||||
@@ -839,38 +906,133 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadChatItems(_ cInfo: ChatInfo) {
|
private func loadChatItems(_ cInfo: ChatInfo, _ pagination: ChatPagination = .initial(count: loadItemsPerPage)) {
|
||||||
Task {
|
Task {
|
||||||
if loadingItems || firstPage { return }
|
if loadingItems { return }
|
||||||
loadingItems = true
|
loadingItems = true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
var reversedPage = Array<ChatItem>()
|
let chatItems = try await apiGetChatItems(
|
||||||
var chatItemsAvailable = true
|
type: cInfo.chatType,
|
||||||
// Load additional items until the page is +50 large after merging
|
id: cInfo.apiId,
|
||||||
while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage {
|
pagination: pagination,
|
||||||
let pagination: ChatPagination =
|
search: searchText
|
||||||
if let lastItem = reversedPage.last ?? im.reversedChatItems.last {
|
)
|
||||||
.before(chatItemId: lastItem.id, count: loadItemsPerPage)
|
|
||||||
} else {
|
if (cInfo.id != chatModel.chatId) {
|
||||||
.last(count: loadItemsPerPage)
|
await MainActor.run { loadingItems = false }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let im = ItemsModel.shared
|
||||||
|
var newItems = im.reversedChatItems
|
||||||
|
|
||||||
|
switch pagination {
|
||||||
|
case .last:
|
||||||
|
await MainActor.run {
|
||||||
|
let newItemIds = Set(chatItems.map { $0.id })
|
||||||
|
var duplicateFound = false
|
||||||
|
newItems.removeAll {
|
||||||
|
let isDuplicate = newItemIds.contains($0.id)
|
||||||
|
duplicateFound = duplicateFound || isDuplicate
|
||||||
|
return isDuplicate
|
||||||
}
|
}
|
||||||
let chatItems = try await apiGetChatItems(
|
|
||||||
type: cInfo.chatType,
|
if !duplicateFound {
|
||||||
id: cInfo.apiId,
|
if let existingItem = im.reversedChatItems.first {
|
||||||
pagination: pagination,
|
anchors = [existingItem.id]
|
||||||
search: searchText
|
}
|
||||||
)
|
}
|
||||||
chatItemsAvailable = !chatItems.isEmpty
|
|
||||||
reversedPage.append(contentsOf: chatItems.reversed())
|
newItems.insert(contentsOf: chatItems.reversed(), at: 0)
|
||||||
}
|
im.reversedChatItems = newItems
|
||||||
await MainActor.run {
|
loadingItems = false
|
||||||
if reversedPage.count == 0 {
|
}
|
||||||
firstPage = true
|
case .initial:
|
||||||
} else {
|
await MainActor.run {
|
||||||
im.reversedChatItems.append(contentsOf: reversedPage)
|
im.reversedChatItems = chatItems.reversed()
|
||||||
|
anchors = []
|
||||||
|
loadingItems = false
|
||||||
|
}
|
||||||
|
case let .after(chatItemId, _):
|
||||||
|
guard let indexInCurrentItems = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let wasSize = newItems.count
|
||||||
|
let newItemIds = Set(chatItems.map { $0.id })
|
||||||
|
let indexInAnchors = anchors.firstIndex { $0 == chatItemId }
|
||||||
|
var anchorAfterChatItem: [Int64] = []
|
||||||
|
if let indexInAnchors = indexInAnchors, indexInAnchors + 1 <= anchors.count {
|
||||||
|
anchorAfterChatItem = Array(anchors[indexInAnchors + 1..<anchors.count])
|
||||||
|
}
|
||||||
|
var anchorsToRemove = Set<Int64>()
|
||||||
|
var reachedBottom: Bool = false
|
||||||
|
|
||||||
|
newItems.removeAll { item in
|
||||||
|
let isDuplicate = newItemIds.contains(item.id)
|
||||||
|
if indexInAnchors != nil && newItemIds.contains(item.id) {
|
||||||
|
if anchorAfterChatItem.contains(item.id) {
|
||||||
|
anchorAfterChatItem.removeAll { $0 == item.id }
|
||||||
|
anchorsToRemove.insert(item.id)
|
||||||
|
} else if reachedBottom == false && anchorAfterChatItem.isEmpty {
|
||||||
|
// We passed all anchors and found a duplicated item below all of them, indicating no more anchors below the loaded items.
|
||||||
|
reachedBottom = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDuplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertAt = indexInCurrentItems - (wasSize - newItems.count)
|
||||||
|
newItems.insert(contentsOf: chatItems.reversed(), at: insertAt)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
im.reversedChatItems = newItems
|
||||||
|
var newAnchors = anchors.filter { !anchorsToRemove.contains($0) }
|
||||||
|
|
||||||
|
if reachedBottom {
|
||||||
|
newAnchors = []
|
||||||
|
} else {
|
||||||
|
if let enlargedAnchorIndex = anchors.firstIndex(where: { $0 == chatItemId }) {
|
||||||
|
// Move the anchor to the end of the loaded items.
|
||||||
|
newAnchors[enlargedAnchorIndex] = chatItems.last?.id ?? newAnchors[enlargedAnchorIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors = newAnchors
|
||||||
|
loadingItems = false
|
||||||
|
}
|
||||||
|
case let .before(chatItemId, _):
|
||||||
|
guard let indexInCurrentItems = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let newItemIds = Set(chatItems.map { $0.id })
|
||||||
|
newItems.removeAll { newItemIds.contains($0.id) }
|
||||||
|
newItems.insert(contentsOf: chatItems.reversed(), at: min(indexInCurrentItems + 1, newItems.count))
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
if chatItems.count == 0 || newItems.count == im.reversedChatItems.count {
|
||||||
|
firstPage = true
|
||||||
|
} else {
|
||||||
|
im.reversedChatItems = newItems
|
||||||
|
}
|
||||||
|
anchors = anchors.filter { !newItemIds.contains($0) }
|
||||||
|
loadingItems = false
|
||||||
|
}
|
||||||
|
case .around(_, _):
|
||||||
|
let newItemIds = Set(chatItems.map { $0.id })
|
||||||
|
newItems.removeAll { newItemIds.contains($0.id) }
|
||||||
|
newItems.insert(contentsOf: chatItems, at: 0)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
im.reversedChatItems = newItems
|
||||||
|
if let lastItemId = chatItems.last?.id {
|
||||||
|
anchors.insert(lastItemId, at: 0)
|
||||||
|
}
|
||||||
|
loadingItems = false
|
||||||
}
|
}
|
||||||
loadingItems = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("apiGetChat error: \(responseError(error))")
|
logger.error("apiGetChat error: \(responseError(error))")
|
||||||
await MainActor.run { loadingItems = false }
|
await MainActor.run { loadingItems = false }
|
||||||
@@ -892,7 +1054,7 @@ struct ChatView: View {
|
|||||||
let maxWidth: CGFloat
|
let maxWidth: CGFloat
|
||||||
@Binding var composeState: ComposeState
|
@Binding var composeState: ComposeState
|
||||||
@Binding var selectedMember: GMember?
|
@Binding var selectedMember: GMember?
|
||||||
@Binding var revealedChatItem: ChatItem?
|
@Binding var revealedChatItems: Set<ChatItem.ID>
|
||||||
|
|
||||||
@State private var deletingItem: ChatItem? = nil
|
@State private var deletingItem: ChatItem? = nil
|
||||||
@State private var showDeleteMessage = false
|
@State private var showDeleteMessage = false
|
||||||
@@ -907,25 +1069,8 @@ struct ChatView: View {
|
|||||||
|
|
||||||
@State private var allowMenu: Bool = true
|
@State private var allowMenu: Bool = true
|
||||||
@State private var markedRead = false
|
@State private var markedRead = false
|
||||||
|
let onReveal: (Bool) -> Void
|
||||||
var revealed: Bool { chatItem == revealedChatItem }
|
var revealed: Bool { revealedChatItems.contains(chatItem.id) }
|
||||||
|
|
||||||
typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?)
|
|
||||||
|
|
||||||
func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation {
|
|
||||||
let im = ItemsModel.shared
|
|
||||||
if let i, i > 0 && im.reversedChatItems.count >= i {
|
|
||||||
let nextItem = im.reversedChatItems[i - 1]
|
|
||||||
let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
|
|
||||||
return (
|
|
||||||
timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
|
|
||||||
largeGap: largeGap,
|
|
||||||
date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (timestamp: true, largeGap: true, date: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let currIndex = m.getChatItemIndex(chatItem)
|
let currIndex = m.getChatItemIndex(chatItem)
|
||||||
@@ -933,46 +1078,7 @@ struct ChatView: View {
|
|||||||
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
|
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||||
let range = itemsRange(currIndex, prevHidden)
|
let range = itemsRange(currIndex, prevHidden)
|
||||||
let timeSeparation = getItemSeparation(chatItem, at: currIndex)
|
let timeSeparation = getItemSeparation(chatItem, at: currIndex)
|
||||||
let im = ItemsModel.shared
|
func markAsRead() {
|
||||||
Group {
|
|
||||||
if revealed, let range = range {
|
|
||||||
let items = Array(zip(Array(range), im.reversedChatItems[range]))
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
|
|
||||||
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
|
|
||||||
chatItemView(ci, nil, prev, getItemSeparation(ci, at: i))
|
|
||||||
.overlay {
|
|
||||||
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
|
|
||||||
Color.clear
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture {
|
|
||||||
let checked = selected.contains(ci.id)
|
|
||||||
selectUnselectChatItem(select: !checked, ci)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
chatItemView(chatItem, range, prevItem, timeSeparation)
|
|
||||||
if let date = timeSeparation.date {
|
|
||||||
DateSeparator(date: date).padding(8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.overlay {
|
|
||||||
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
|
||||||
Color.clear
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture {
|
|
||||||
let checked = selected.contains(chatItem.id)
|
|
||||||
selectUnselectChatItem(select: !checked, chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if markedRead {
|
if markedRead {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@@ -991,6 +1097,30 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Group {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
chatItemView(chatItem, range, prevItem, timeSeparation)
|
||||||
|
if let date = timeSeparation.date {
|
||||||
|
DateSeparator(date: date).padding(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
||||||
|
Color.clear
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
let checked = selected.contains(chatItem.id)
|
||||||
|
selectUnselectChatItem(select: !checked, chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
markAsRead()
|
||||||
|
}
|
||||||
|
.onChange(of: ChatView.FloatingButtonModel.shared.bottomItemIndex) { _ in
|
||||||
|
markAsRead()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func unreadItemIds(_ range: ClosedRange<Int>) -> [ChatItem.ID] {
|
private func unreadItemIds(_ range: ClosedRange<Int>) -> [ChatItem.ID] {
|
||||||
@@ -1008,8 +1138,13 @@ struct ChatView: View {
|
|||||||
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
|
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
|
||||||
Task {
|
Task {
|
||||||
_ = try? await Task.sleep(nanoseconds: 600_000000)
|
_ = try? await Task.sleep(nanoseconds: 600_000000)
|
||||||
if m.chatId == chat.chatInfo.id {
|
let currIndex = m.getChatItemIndex(chatItem)
|
||||||
await op()
|
if let currIndex = currIndex, currIndex >= ChatView.FloatingButtonModel.shared.bottomItemIndex - 3 {
|
||||||
|
if m.chatId == chat.chatInfo.id {
|
||||||
|
await op()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
markedRead = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1573,7 +1708,7 @@ struct ChatView: View {
|
|||||||
private func hideButton() -> Button<some View> {
|
private func hideButton() -> Button<some View> {
|
||||||
Button {
|
Button {
|
||||||
withConditionalAnimation {
|
withConditionalAnimation {
|
||||||
revealedChatItem = nil
|
onReveal(false)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
@@ -1648,7 +1783,7 @@ struct ChatView: View {
|
|||||||
private func revealButton(_ ci: ChatItem) -> Button<some View> {
|
private func revealButton(_ ci: ChatItem) -> Button<some View> {
|
||||||
Button {
|
Button {
|
||||||
withConditionalAnimation {
|
withConditionalAnimation {
|
||||||
revealedChatItem = ci
|
onReveal(true)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
@@ -1661,7 +1796,7 @@ struct ChatView: View {
|
|||||||
private func expandButton() -> Button<some View> {
|
private func expandButton() -> Button<some View> {
|
||||||
Button {
|
Button {
|
||||||
withConditionalAnimation {
|
withConditionalAnimation {
|
||||||
revealedChatItem = chatItem
|
onReveal(true)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
@@ -1674,7 +1809,7 @@ struct ChatView: View {
|
|||||||
private func shrinkButton() -> Button<some View> {
|
private func shrinkButton() -> Button<some View> {
|
||||||
Button {
|
Button {
|
||||||
withConditionalAnimation {
|
withConditionalAnimation {
|
||||||
revealedChatItem = nil
|
onReveal(false)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label (
|
Label (
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ import SimpleXChat
|
|||||||
|
|
||||||
/// A List, which displays it's items in reverse order - from bottom to top
|
/// A List, which displays it's items in reverse order - from bottom to top
|
||||||
struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||||
let items: Array<ChatItem>
|
let groups: SectionGroups
|
||||||
|
|
||||||
@Binding var scrollState: ReverseListScrollModel.State
|
@Binding var scrollState: ReverseListScrollModel.State
|
||||||
|
@Binding var initialChatItem: ChatItem?
|
||||||
|
|
||||||
/// Closure, that returns user interface for a given item
|
/// Closure, that returns user interface for a given item
|
||||||
let content: (ChatItem) -> Content
|
let content: (ListItem) -> Content
|
||||||
|
|
||||||
let loadPage: () -> Void
|
let loadPage: (_ pagination: ChatPagination) -> Void
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> Controller {
|
func makeUIViewController(context: Context) -> Controller {
|
||||||
Controller(representer: self)
|
Controller(representer: self)
|
||||||
@@ -27,18 +28,18 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
func updateUIViewController(_ controller: Controller, context: Context) {
|
func updateUIViewController(_ controller: Controller, context: Context) {
|
||||||
controller.representer = self
|
controller.representer = self
|
||||||
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
|
if case let .scrollingTo(destination) = scrollState, !groups.sections.isEmpty {
|
||||||
controller.view.layer.removeAllAnimations()
|
controller.view.layer.removeAllAnimations()
|
||||||
switch destination {
|
switch destination {
|
||||||
case .nextPage:
|
case .nextPage:
|
||||||
controller.scrollToNextPage()
|
controller.scrollToNextPage()
|
||||||
case let .item(id):
|
case let .item(id):
|
||||||
controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom)
|
controller.scroll(to: getIndexInParentItems(sections: groups.sections, itemId: id), position: .bottom)
|
||||||
case .bottom:
|
case .bottom:
|
||||||
controller.scroll(to: 0, position: .top)
|
controller.scroll(to: 0, position: .top)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
controller.update(items: items)
|
controller.update(groups: groups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +47,11 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
class Controller: UITableViewController {
|
class Controller: UITableViewController {
|
||||||
private enum Section { case main }
|
private enum Section { case main }
|
||||||
var representer: ReverseList
|
var representer: ReverseList
|
||||||
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
|
private var dataSource: UITableViewDiffableDataSource<Section, ListItem>!
|
||||||
private var itemCount: Int = 0
|
private var itemCount: Int = 0
|
||||||
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
|
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
|
||||||
private var bag = Set<AnyCancellable>()
|
private var bag = Set<AnyCancellable>()
|
||||||
|
private var revealedItems: Array<ListItem> = []
|
||||||
|
|
||||||
init(representer: ReverseList) {
|
init(representer: ReverseList) {
|
||||||
self.representer = representer
|
self.representer = representer
|
||||||
@@ -75,11 +77,16 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Configure data source
|
// 3. Configure data source
|
||||||
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
|
self.dataSource = UITableViewDiffableDataSource<Section, ListItem>(
|
||||||
tableView: tableView
|
tableView: tableView
|
||||||
) { (tableView, indexPath, item) -> UITableViewCell? in
|
) { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
if indexPath.item > self.itemCount - 8 {
|
if self.representer.scrollState == .atDestination, self.representer.initialChatItem == nil {
|
||||||
self.representer.loadPage()
|
if indexPath.item > self.itemCount - preloadItem,
|
||||||
|
let item = getNewestItemAtParentIndexOrNull(sections: self.representer.groups.sections, parentIndex: self.itemCount - 1) {
|
||||||
|
self.representer.loadPage(.before(chatItemId: item.id, count: loadItemsPerPage))
|
||||||
|
} else if let item = self.getFirstItemAfterPlacholder(indexPath) {
|
||||||
|
self.representer.loadPage(.after(chatItemId: item.id, count: loadItemsPerPage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
|
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@@ -149,6 +156,29 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
tableView.clipsToBounds = false
|
tableView.clipsToBounds = false
|
||||||
parent?.viewIfLoaded?.clipsToBounds = false
|
parent?.viewIfLoaded?.clipsToBounds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
if let cItem = self.representer.initialChatItem {
|
||||||
|
let index = getIndexInParentItems(sections: self.representer.groups.sections, itemId: cItem.id)
|
||||||
|
|
||||||
|
if index == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let indexPath = IndexPath(row: index, section: 0)
|
||||||
|
if !isVisible(indexPath: indexPath) {
|
||||||
|
if tableView.numberOfRows(inSection: indexPath.section) > indexPath.row {
|
||||||
|
let cellRect = tableView.rectForRow(at: indexPath)
|
||||||
|
tableView.setContentOffset(CGPoint(x: 0, y: cellRect.maxY - tableView.bounds.height), animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.representer.initialChatItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Scrolls up
|
/// Scrolls up
|
||||||
func scrollToNextPage() {
|
func scrollToNextPage() {
|
||||||
@@ -169,6 +199,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
animated = true
|
animated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if let index, tableView.numberOfRows(inSection: 0) != 0 {
|
if let index, tableView.numberOfRows(inSection: 0) != 0 {
|
||||||
tableView.scrollToRow(
|
tableView.scrollToRow(
|
||||||
at: IndexPath(row: index, section: 0),
|
at: IndexPath(row: index, section: 0),
|
||||||
@@ -181,18 +212,49 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
animated: animated
|
animated: animated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Task { representer.scrollState = .atDestination }
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
Task {
|
||||||
|
self.representer.scrollState = .atDestination
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(items: [ChatItem]) {
|
func update(groups: SectionGroups) {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, ListItem>()
|
||||||
|
revealedItems.removeAll()
|
||||||
|
groups.sections.forEach { sc in
|
||||||
|
if sc.revealed {
|
||||||
|
revealedItems.append(contentsOf: sc.items)
|
||||||
|
} else if let item = sc.items.first {
|
||||||
|
revealedItems.append(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
snapshot.appendItems(items)
|
snapshot.appendItems(revealedItems, toSection: .main)
|
||||||
dataSource.defaultRowAnimation = .none
|
dataSource.defaultRowAnimation = .none
|
||||||
dataSource.apply(
|
|
||||||
snapshot,
|
let countDiff = max(0, revealedItems.count - itemCount)
|
||||||
animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1
|
if tableView.contentOffset.y == 100, itemCount < revealedItems.count, itemCount > 0 {
|
||||||
)
|
dataSource.apply(
|
||||||
|
snapshot,
|
||||||
|
animatingDifferences: false
|
||||||
|
)
|
||||||
|
|
||||||
|
tableView.scrollToRow(
|
||||||
|
at: IndexPath(row: countDiff, section: 0),
|
||||||
|
at: .top,
|
||||||
|
animated: false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tableView.beginUpdates()
|
||||||
|
dataSource.apply(
|
||||||
|
snapshot,
|
||||||
|
animatingDifferences: false
|
||||||
|
)
|
||||||
|
tableView.endUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
// Sets content offset on initial load
|
// Sets content offset on initial load
|
||||||
if itemCount == 0 {
|
if itemCount == 0 {
|
||||||
tableView.setContentOffset(
|
tableView.setContentOffset(
|
||||||
@@ -200,7 +262,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
animated: false
|
animated: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemCount = items.count
|
itemCount = revealedItems.count
|
||||||
updateFloatingButtons.send()
|
updateFloatingButtons.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,17 +272,18 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
func getListState() -> ListState? {
|
func getListState() -> ListState? {
|
||||||
if let visibleRows = tableView.indexPathsForVisibleRows,
|
if let visibleRows = tableView.indexPathsForVisibleRows,
|
||||||
visibleRows.last?.item ?? 0 < representer.items.count {
|
visibleRows.last?.item ?? 0 < revealedItems.count {
|
||||||
let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset
|
let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset
|
||||||
|
|
||||||
let topItemDate: Date? =
|
let topItemDate: Date? =
|
||||||
if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) {
|
if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) {
|
||||||
representer.items[lastVisible.item].meta.itemTs
|
revealedItems[lastVisible.item].item.meta.itemTs
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
let bottomItemId: ChatItem.ID? =
|
let bottomItemId: ChatItem.ID? =
|
||||||
if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) {
|
if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) {
|
||||||
representer.items[firstVisible.item].id
|
revealedItems[firstVisible.item].item.id
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
@@ -238,6 +301,10 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
|||||||
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
|
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
|
||||||
} else { false }
|
} else { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getFirstItemAfterPlacholder(_ indexPath: IndexPath) -> ChatItem? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
|
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
||||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
||||||
|
B72540EB2CE277AC0041D1B4 /* ChatItemGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B72540EA2CE277AC0041D1B4 /* ChatItemGroups.swift */; };
|
||||||
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
|
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
|
||||||
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; };
|
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; };
|
||||||
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
|
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
|
||||||
@@ -544,6 +545,7 @@
|
|||||||
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
||||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
||||||
|
B72540EA2CE277AC0041D1B4 /* ChatItemGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemGroups.swift; sourceTree = "<group>"; };
|
||||||
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
||||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = "<group>"; };
|
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = "<group>"; };
|
||||||
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
|
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
|
||||||
@@ -734,6 +736,7 @@
|
|||||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
|
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
|
||||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
|
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
|
||||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */,
|
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */,
|
||||||
|
B72540EA2CE277AC0041D1B4 /* ChatItemGroups.swift */,
|
||||||
);
|
);
|
||||||
path = Chat;
|
path = Chat;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -912,9 +915,10 @@
|
|||||||
5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
|
5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */,
|
|
||||||
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
|
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
|
||||||
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
|
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
|
||||||
|
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
|
||||||
|
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
|
||||||
5CADE79929211BB900072E13 /* PreferencesView.swift */,
|
5CADE79929211BB900072E13 /* PreferencesView.swift */,
|
||||||
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
|
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
|
||||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
|
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
|
||||||
@@ -922,6 +926,9 @@
|
|||||||
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
|
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
|
||||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
||||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
||||||
|
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
|
||||||
|
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
|
||||||
|
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
|
||||||
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
|
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
|
||||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
||||||
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
|
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
|
||||||
@@ -1052,18 +1059,6 @@
|
|||||||
path = Database;
|
path = Database;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
|
|
||||||
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
|
|
||||||
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
|
|
||||||
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
|
|
||||||
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
|
|
||||||
);
|
|
||||||
path = NetworkAndServers;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
6440CA01288AEC770062C672 /* Group */ = {
|
6440CA01288AEC770062C672 /* Group */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1491,6 +1486,7 @@
|
|||||||
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
|
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
|
||||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
|
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
|
||||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
||||||
|
B72540EB2CE277AC0041D1B4 /* ChatItemGroups.swift in Sources */,
|
||||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
||||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
|
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
|
||||||
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */,
|
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */,
|
||||||
|
|||||||
@@ -1133,12 +1133,16 @@ public enum ChatPagination {
|
|||||||
case last(count: Int)
|
case last(count: Int)
|
||||||
case after(chatItemId: Int64, count: Int)
|
case after(chatItemId: Int64, count: Int)
|
||||||
case before(chatItemId: Int64, count: Int)
|
case before(chatItemId: Int64, count: Int)
|
||||||
|
case around(chatItemId: Int64, count: Int)
|
||||||
|
case initial(count: Int)
|
||||||
|
|
||||||
var cmdString: String {
|
var cmdString: String {
|
||||||
switch self {
|
switch self {
|
||||||
case let .last(count): return "count=\(count)"
|
case let .last(count): return "count=\(count)"
|
||||||
case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)"
|
case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)"
|
||||||
case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)"
|
case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)"
|
||||||
|
case let .around(chatItemId, count): return "around=\(chatItemId) count=\(count)"
|
||||||
|
case let .initial(count): return "initial=\(count)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2663,7 +2663,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
|||||||
item.isLiveDummy = true
|
item.isLiveDummy = true
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String) -> ChatItem {
|
public static func invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String) -> ChatItem {
|
||||||
ChatItem(
|
ChatItem(
|
||||||
chatDir: chatDir ?? .directSnd,
|
chatDir: chatDir ?? .directSnd,
|
||||||
|
|||||||
+8
@@ -381,6 +381,14 @@ fun ComposeView(
|
|||||||
|
|
||||||
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||||
val cInfo = chat.chatInfo
|
val cInfo = chat.chatInfo
|
||||||
|
|
||||||
|
// val composedMessages = Array(300) { index ->
|
||||||
|
// ComposedMessage(
|
||||||
|
// file,
|
||||||
|
// quoted,
|
||||||
|
// MsgContent.MCText("$index")
|
||||||
|
// )
|
||||||
|
// }.toList()
|
||||||
val chatItems = if (chat.chatInfo.chatType == ChatType.Local)
|
val chatItems = if (chat.chatInfo.chatType == ChatType.Local)
|
||||||
chatModel.controller.apiCreateChatItems(
|
chatModel.controller.apiCreateChatItems(
|
||||||
rh = chat.remoteHostId,
|
rh = chat.remoteHostId,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ module Main where
|
|||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
import Control.Concurrent.STM
|
import Control.Concurrent.STM
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Simplex.Chat.Bot
|
import Simplex.Chat.Bot
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
@@ -19,7 +18,6 @@ import Simplex.Chat.Messages.CIContent
|
|||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Terminal (terminalChatConfig)
|
import Simplex.Chat.Terminal (terminalChatConfig)
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Messaging.Util (tshow)
|
|
||||||
import System.Directory (getAppUserDataDirectory)
|
import System.Directory (getAppUserDataDirectory)
|
||||||
import Text.Read
|
import Text.Read
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ welcomeGetOpts = do
|
|||||||
putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
|
putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
|
||||||
pure opts
|
pure opts
|
||||||
|
|
||||||
welcomeMessage :: Text
|
welcomeMessage :: String
|
||||||
welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I will calculate its square"
|
welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I will calculate its square"
|
||||||
|
|
||||||
mySquaringBot :: User -> ChatController -> IO ()
|
mySquaringBot :: User -> ChatController -> IO ()
|
||||||
@@ -49,10 +47,10 @@ mySquaringBot _user cc = do
|
|||||||
contactConnected contact
|
contactConnected contact
|
||||||
sendMessage cc contact welcomeMessage
|
sendMessage cc contact welcomeMessage
|
||||||
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
|
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
|
||||||
let msg = ciContentToText mc
|
let msg = T.unpack $ ciContentToText mc
|
||||||
number_ = readMaybe (T.unpack msg) :: Maybe Integer
|
number_ = readMaybe msg :: Maybe Integer
|
||||||
sendMessage cc contact $ case number_ of
|
sendMessage cc contact $ case number_ of
|
||||||
Just n -> msg <> " * " <> msg <> " = " <> tshow (n * n)
|
Just n -> msg <> " * " <> msg <> " = " <> show (n * n)
|
||||||
_ -> "\"" <> msg <> "\" is not a number"
|
_ -> "\"" <> msg <> "\" is not a number"
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import Simplex.Chat.Messages.CIContent
|
|||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Protocol (MsgContent (..))
|
import Simplex.Chat.Protocol (MsgContent (..))
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Messaging.Util (tshow)
|
|
||||||
import System.Directory (getAppUserDataDirectory)
|
import System.Directory (getAppUserDataDirectory)
|
||||||
|
|
||||||
welcomeGetOpts :: IO BroadcastBotOpts
|
welcomeGetOpts :: IO BroadcastBotOpts
|
||||||
@@ -49,14 +48,14 @@ broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _u
|
|||||||
CRContactsList _ cts -> void . forkIO $ do
|
CRContactsList _ cts -> void . forkIO $ do
|
||||||
let cts' = filter broadcastTo cts
|
let cts' = filter broadcastTo cts
|
||||||
forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc
|
forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc
|
||||||
sendReply $ "Forwarded to " <> tshow (length cts') <> " contact(s)"
|
sendReply $ "Forwarded to " <> show (length cts') <> " contact(s)"
|
||||||
r -> putStrLn $ "Error getting contacts list: " <> show r
|
r -> putStrLn $ "Error getting contacts list: " <> show r
|
||||||
else sendReply "!1 Message is not supported!"
|
else sendReply "!1 Message is not supported!"
|
||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
sendReply prohibitedMessage
|
sendReply prohibitedMessage
|
||||||
deleteMessage cc ct $ chatItemId' ci
|
deleteMessage cc ct $ chatItemId' ci
|
||||||
where
|
where
|
||||||
sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . MCText
|
sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . textMsgContent
|
||||||
publisher = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
|
publisher = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
|
||||||
allowContent = \case
|
allowContent = \case
|
||||||
MCText _ -> True
|
MCText _ -> True
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
module Broadcast.Options where
|
module Broadcast.Options where
|
||||||
|
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text (Text)
|
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Simplex.Chat.Bot.KnownContacts
|
import Simplex.Chat.Bot.KnownContacts
|
||||||
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
|
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
|
||||||
@@ -16,14 +15,14 @@ import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreC
|
|||||||
data BroadcastBotOpts = BroadcastBotOpts
|
data BroadcastBotOpts = BroadcastBotOpts
|
||||||
{ coreOptions :: CoreChatOpts,
|
{ coreOptions :: CoreChatOpts,
|
||||||
publishers :: [KnownContact],
|
publishers :: [KnownContact],
|
||||||
welcomeMessage :: Text,
|
welcomeMessage :: String,
|
||||||
prohibitedMessage :: Text
|
prohibitedMessage :: String
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultWelcomeMessage :: [KnownContact] -> Text
|
defaultWelcomeMessage :: [KnownContact] -> String
|
||||||
defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> knownContactNames ps <> "."
|
defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> knownContactNames ps <> "."
|
||||||
|
|
||||||
defaultProhibitedMessage :: [KnownContact] -> Text
|
defaultProhibitedMessage :: [KnownContact] -> String
|
||||||
defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> knownContactNames ps <> ". Your message is deleted."
|
defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> knownContactNames ps <> ". Your message is deleted."
|
||||||
|
|
||||||
broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts
|
broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts
|
||||||
|
|||||||
@@ -89,11 +89,10 @@ crDirectoryEvent = \case
|
|||||||
CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors)
|
CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors)
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
data DirectoryRole = DRUser | DRAdmin | DRSuperUser
|
data DirectoryRole = DRUser | DRSuperUser
|
||||||
|
|
||||||
data SDirectoryRole (r :: DirectoryRole) where
|
data SDirectoryRole (r :: DirectoryRole) where
|
||||||
SDRUser :: SDirectoryRole 'DRUser
|
SDRUser :: SDirectoryRole 'DRUser
|
||||||
SDRAdmin :: SDirectoryRole 'DRAdmin
|
|
||||||
SDRSuperUser :: SDirectoryRole 'DRSuperUser
|
SDRSuperUser :: SDirectoryRole 'DRSuperUser
|
||||||
|
|
||||||
deriving instance Show (SDirectoryRole r)
|
deriving instance Show (SDirectoryRole r)
|
||||||
@@ -108,14 +107,12 @@ data DirectoryCmdTag (r :: DirectoryRole) where
|
|||||||
DCListUserGroups_ :: DirectoryCmdTag 'DRUser
|
DCListUserGroups_ :: DirectoryCmdTag 'DRUser
|
||||||
DCDeleteGroup_ :: DirectoryCmdTag 'DRUser
|
DCDeleteGroup_ :: DirectoryCmdTag 'DRUser
|
||||||
DCSetRole_ :: DirectoryCmdTag 'DRUser
|
DCSetRole_ :: DirectoryCmdTag 'DRUser
|
||||||
DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin
|
DCApproveGroup_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin
|
DCRejectGroup_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin
|
DCSuspendGroup_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
DCResumeGroup_ :: DirectoryCmdTag 'DRAdmin
|
DCResumeGroup_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
DCListLastGroups_ :: DirectoryCmdTag 'DRAdmin
|
DCListLastGroups_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
DCListPendingGroups_ :: DirectoryCmdTag 'DRAdmin
|
DCListPendingGroups_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
DCShowGroupLink_ :: DirectoryCmdTag 'DRAdmin
|
|
||||||
DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin
|
|
||||||
DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser
|
DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser
|
||||||
|
|
||||||
deriving instance Show (DirectoryCmdTag r)
|
deriving instance Show (DirectoryCmdTag r)
|
||||||
@@ -133,14 +130,12 @@ data DirectoryCmd (r :: DirectoryRole) where
|
|||||||
DCListUserGroups :: DirectoryCmd 'DRUser
|
DCListUserGroups :: DirectoryCmd 'DRUser
|
||||||
DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser
|
DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser
|
||||||
DCSetRole :: GroupId -> GroupName -> GroupMemberRole -> DirectoryCmd 'DRUser
|
DCSetRole :: GroupId -> GroupName -> GroupMemberRole -> DirectoryCmd 'DRUser
|
||||||
DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin
|
DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRSuperUser
|
||||||
DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin
|
DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser
|
||||||
DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin
|
DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser
|
||||||
DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin
|
DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser
|
||||||
DCListLastGroups :: Int -> DirectoryCmd 'DRAdmin
|
DCListLastGroups :: Int -> DirectoryCmd 'DRSuperUser
|
||||||
DCListPendingGroups :: Int -> DirectoryCmd 'DRAdmin
|
DCListPendingGroups :: Int -> DirectoryCmd 'DRSuperUser
|
||||||
DCShowGroupLink :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin
|
|
||||||
DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin
|
|
||||||
DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser
|
DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser
|
||||||
DCUnknownCommand :: DirectoryCmd 'DRUser
|
DCUnknownCommand :: DirectoryCmd 'DRUser
|
||||||
DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r
|
DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r
|
||||||
@@ -173,20 +168,17 @@ directoryCmdP =
|
|||||||
"ls" -> u DCListUserGroups_
|
"ls" -> u DCListUserGroups_
|
||||||
"delete" -> u DCDeleteGroup_
|
"delete" -> u DCDeleteGroup_
|
||||||
"role" -> u DCSetRole_
|
"role" -> u DCSetRole_
|
||||||
"approve" -> au DCApproveGroup_
|
"approve" -> su DCApproveGroup_
|
||||||
"reject" -> au DCRejectGroup_
|
"reject" -> su DCRejectGroup_
|
||||||
"suspend" -> au DCSuspendGroup_
|
"suspend" -> su DCSuspendGroup_
|
||||||
"resume" -> au DCResumeGroup_
|
"resume" -> su DCResumeGroup_
|
||||||
"last" -> au DCListLastGroups_
|
"last" -> su DCListLastGroups_
|
||||||
"pending" -> au DCListPendingGroups_
|
"pending" -> su DCListPendingGroups_
|
||||||
"link" -> au DCShowGroupLink_
|
|
||||||
"owner" -> au DCSendToGroupOwner_
|
|
||||||
"exec" -> su DCExecuteCommand_
|
"exec" -> su DCExecuteCommand_
|
||||||
"x" -> su DCExecuteCommand_
|
"x" -> su DCExecuteCommand_
|
||||||
_ -> fail "bad command tag"
|
_ -> fail "bad command tag"
|
||||||
where
|
where
|
||||||
u = pure . ADCT SDRUser
|
u = pure . ADCT SDRUser
|
||||||
au = pure . ADCT SDRAdmin
|
|
||||||
su = pure . ADCT SDRSuperUser
|
su = pure . ADCT SDRSuperUser
|
||||||
cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r)
|
cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r)
|
||||||
cmdP = \case
|
cmdP = \case
|
||||||
@@ -211,11 +203,6 @@ directoryCmdP =
|
|||||||
DCResumeGroup_ -> gc DCResumeGroup
|
DCResumeGroup_ -> gc DCResumeGroup
|
||||||
DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10)
|
DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10)
|
||||||
DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10)
|
DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10)
|
||||||
DCShowGroupLink_ -> gc DCShowGroupLink
|
|
||||||
DCSendToGroupOwner_ -> do
|
|
||||||
(groupId, displayName) <- gc (,)
|
|
||||||
msg <- A.space *> A.takeText
|
|
||||||
pure $ DCSendToGroupOwner groupId displayName msg
|
|
||||||
DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText)
|
DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText)
|
||||||
where
|
where
|
||||||
gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> displayNameP
|
gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> displayNameP
|
||||||
@@ -226,8 +213,8 @@ directoryCmdP =
|
|||||||
quoted c = A.char c *> takeNameTill (== c) <* A.char c
|
quoted c = A.char c *> takeNameTill (== c) <* A.char c
|
||||||
refChar c = c > ' ' && c /= '#' && c /= '@'
|
refChar c = c > ' ' && c /= '#' && c /= '@'
|
||||||
|
|
||||||
viewName :: Text -> Text
|
viewName :: String -> String
|
||||||
viewName n = if any (== ' ') (T.unpack n) then "'" <> n <> "'" else n
|
viewName n = if ' ' `elem` n then "'" <> n <> "'" else n
|
||||||
|
|
||||||
directoryCmdTag :: DirectoryCmd r -> Text
|
directoryCmdTag :: DirectoryCmd r -> Text
|
||||||
directoryCmdTag = \case
|
directoryCmdTag = \case
|
||||||
@@ -247,8 +234,6 @@ directoryCmdTag = \case
|
|||||||
DCResumeGroup {} -> "resume"
|
DCResumeGroup {} -> "resume"
|
||||||
DCListLastGroups _ -> "last"
|
DCListLastGroups _ -> "last"
|
||||||
DCListPendingGroups _ -> "pending"
|
DCListPendingGroups _ -> "pending"
|
||||||
DCShowGroupLink {} -> "link"
|
|
||||||
DCSendToGroupOwner {} -> "owner"
|
|
||||||
DCExecuteCommand _ -> "exec"
|
DCExecuteCommand _ -> "exec"
|
||||||
DCUnknownCommand -> "unknown"
|
DCUnknownCommand -> "unknown"
|
||||||
DCCommandError _ -> "error"
|
DCCommandError _ -> "error"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ module Directory.Options
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Simplex.Chat.Bot.KnownContacts
|
import Simplex.Chat.Bot.KnownContacts
|
||||||
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
|
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
|
||||||
@@ -19,10 +18,9 @@ import Simplex.Chat.Options (ChatOpts (..), ChatCmdLog (..), CoreChatOpts, coreC
|
|||||||
|
|
||||||
data DirectoryOpts = DirectoryOpts
|
data DirectoryOpts = DirectoryOpts
|
||||||
{ coreOptions :: CoreChatOpts,
|
{ coreOptions :: CoreChatOpts,
|
||||||
adminUsers :: [KnownContact],
|
|
||||||
superUsers :: [KnownContact],
|
superUsers :: [KnownContact],
|
||||||
directoryLog :: Maybe FilePath,
|
directoryLog :: Maybe FilePath,
|
||||||
serviceName :: T.Text,
|
serviceName :: String,
|
||||||
searchResults :: Int,
|
searchResults :: Int,
|
||||||
testing :: Bool
|
testing :: Bool
|
||||||
}
|
}
|
||||||
@@ -30,13 +28,6 @@ data DirectoryOpts = DirectoryOpts
|
|||||||
directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts
|
directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts
|
||||||
directoryOpts appDir defaultDbFileName = do
|
directoryOpts appDir defaultDbFileName = do
|
||||||
coreOptions <- coreChatOptsP appDir defaultDbFileName
|
coreOptions <- coreChatOptsP appDir defaultDbFileName
|
||||||
adminUsers <-
|
|
||||||
option
|
|
||||||
parseKnownContacts
|
|
||||||
( long "admin-users"
|
|
||||||
<> metavar "ADMIN_USERS"
|
|
||||||
<> help "Comma-separated list of admin-users in the format CONTACT_ID:DISPLAY_NAME who will be allowed to manage the directory"
|
|
||||||
)
|
|
||||||
superUsers <-
|
superUsers <-
|
||||||
option
|
option
|
||||||
parseKnownContacts
|
parseKnownContacts
|
||||||
@@ -61,10 +52,9 @@ directoryOpts appDir defaultDbFileName = do
|
|||||||
pure
|
pure
|
||||||
DirectoryOpts
|
DirectoryOpts
|
||||||
{ coreOptions,
|
{ coreOptions,
|
||||||
adminUsers,
|
|
||||||
superUsers,
|
superUsers,
|
||||||
directoryLog,
|
directoryLog,
|
||||||
serviceName = T.pack serviceName,
|
serviceName,
|
||||||
searchResults = 10,
|
searchResults = 10,
|
||||||
testing = False
|
testing = False
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import Control.Concurrent.Async
|
|||||||
import Control.Concurrent.STM
|
import Control.Concurrent.STM
|
||||||
import Control.Logger.Simple
|
import Control.Logger.Simple
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.Maybe (fromMaybe, maybeToList)
|
import Data.Maybe (fromMaybe, maybeToList)
|
||||||
import Data.Set (Set)
|
import Data.Set (Set)
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Data.Text.Encoding (decodeLatin1)
|
||||||
import Data.Time.Clock (diffUTCTime, getCurrentTime)
|
import Data.Time.Clock (diffUTCTime, getCurrentTime)
|
||||||
import Data.Time.LocalTime (getCurrentTimeZone)
|
import Data.Time.LocalTime (getCurrentTimeZone)
|
||||||
import Directory.Events
|
import Directory.Events
|
||||||
@@ -35,7 +37,6 @@ import Simplex.Chat.Core
|
|||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Protocol (MsgContent (..))
|
import Simplex.Chat.Protocol (MsgContent (..))
|
||||||
import Simplex.Chat.Store.Shared (StoreError (..))
|
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.Types.Shared
|
import Simplex.Chat.Types.Shared
|
||||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||||
@@ -78,7 +79,7 @@ welcomeGetOpts = do
|
|||||||
pure opts
|
pure opts
|
||||||
|
|
||||||
directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO ()
|
directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO ()
|
||||||
directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchResults, testing} user@User {userId} cc = do
|
directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testing} user@User {userId} cc = do
|
||||||
initializeBotAddress' (not testing) cc
|
initializeBotAddress' (not testing) cc
|
||||||
env <- newServiceState
|
env <- newServiceState
|
||||||
race_ (forever $ void getLine) . forever $ do
|
race_ (forever $ void getLine) . forever $ do
|
||||||
@@ -101,7 +102,6 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
logInfo $ "command received " <> directoryCmdTag cmd
|
logInfo $ "command received " <> directoryCmdTag cmd
|
||||||
case sUser of
|
case sUser of
|
||||||
SDRUser -> deUserCommand env ct ciId cmd
|
SDRUser -> deUserCommand env ct ciId cmd
|
||||||
SDRAdmin -> deAdminCommand ct ciId cmd
|
|
||||||
SDRSuperUser -> deSuperUserCommand ct ciId cmd
|
SDRSuperUser -> deSuperUserCommand ct ciId cmd
|
||||||
DELogChatResponse r -> logInfo r
|
DELogChatResponse r -> logInfo r
|
||||||
where
|
where
|
||||||
@@ -118,9 +118,9 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName
|
userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName
|
||||||
userGroupReference' GroupReg {userGroupRegId} displayName = groupReference' userGroupRegId displayName
|
userGroupReference' GroupReg {userGroupRegId} displayName = groupReference' userGroupRegId displayName
|
||||||
groupReference GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = groupReference' groupId displayName
|
groupReference GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = groupReference' groupId displayName
|
||||||
groupReference' groupId displayName = "ID " <> tshow groupId <> " (" <> displayName <> ")"
|
groupReference' groupId displayName = "ID " <> show groupId <> " (" <> T.unpack displayName <> ")"
|
||||||
groupAlreadyListed GroupInfo {groupProfile = GroupProfile {displayName, fullName}} =
|
groupAlreadyListed GroupInfo {groupProfile = GroupProfile {displayName, fullName}} =
|
||||||
"The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name."
|
T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name."
|
||||||
|
|
||||||
getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
||||||
getGroups = getGroups_ . Just
|
getGroups = getGroups_ . Just
|
||||||
@@ -151,7 +151,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do
|
processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do
|
||||||
void $ addGroupReg st ct g GRSProposed
|
void $ addGroupReg st ct g GRSProposed
|
||||||
r <- sendChatCmd cc $ APIJoinGroup groupId
|
r <- sendChatCmd cc $ APIJoinGroup groupId
|
||||||
sendMessage cc ct $ case r of
|
sendMessage cc ct $ T.unpack $ case r of
|
||||||
CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…"
|
CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…"
|
||||||
_ -> "Error joining group " <> displayName <> ", please re-send the invitation!"
|
_ -> "Error joining group " <> displayName <> ", please re-send the invitation!"
|
||||||
|
|
||||||
@@ -179,10 +179,10 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
where
|
where
|
||||||
askConfirmation = do
|
askConfirmation = do
|
||||||
ugrId <- addGroupReg st ct g GRSPendingConfirmation
|
ugrId <- addGroupReg st ct g GRSPendingConfirmation
|
||||||
sendMessage cc ct $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:"
|
sendMessage cc ct $ T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:"
|
||||||
sendMessage cc ct $ "/confirm " <> tshow ugrId <> ":" <> viewName displayName
|
sendMessage cc ct $ "/confirm " <> show ugrId <> ":" <> viewName (T.unpack displayName)
|
||||||
|
|
||||||
badRolesMsg :: GroupRolesStatus -> Maybe Text
|
badRolesMsg :: GroupRolesStatus -> Maybe String
|
||||||
badRolesMsg = \case
|
badRolesMsg = \case
|
||||||
GRSOk -> Nothing
|
GRSOk -> Nothing
|
||||||
GRSServiceNotAdmin -> Just "You must grant directory service *admin* role to register the group"
|
GRSServiceNotAdmin -> Just "You must grant directory service *admin* role to register the group"
|
||||||
@@ -218,7 +218,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
when (ctId `isOwner` gr) $ do
|
when (ctId `isOwner` gr) $ do
|
||||||
setGroupRegOwner st gr owner
|
setGroupRegOwner st gr owner
|
||||||
let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g
|
let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g
|
||||||
notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…"
|
notifyOwner gr $ T.unpack $ "Joined the group " <> displayName <> ", creating the link…"
|
||||||
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case
|
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case
|
||||||
CRGroupLinkCreated {connReqContact} -> do
|
CRGroupLinkCreated {connReqContact} -> do
|
||||||
setGroupStatus st gr GRSPendingUpdate
|
setGroupStatus st gr GRSPendingUpdate
|
||||||
@@ -227,7 +227,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
"Created the public link to join the group via this directory service that is always online.\n\n\
|
"Created the public link to join the group via this directory service that is always online.\n\n\
|
||||||
\Please add it to the group welcome message.\n\
|
\Please add it to the group welcome message.\n\
|
||||||
\For example, add:"
|
\For example, add:"
|
||||||
notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact connReqContact)
|
notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode $ simplexChatContact connReqContact)
|
||||||
CRChatCmdError _ (ChatError e) -> case e of
|
CRChatCmdError _ (ChatError e) -> case e of
|
||||||
CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin."
|
CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin."
|
||||||
CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
|
CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
|
||||||
@@ -256,7 +256,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
|
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
|
||||||
GPServiceLinkError -> do
|
GPServiceLinkError -> do
|
||||||
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
|
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
|
||||||
logError $ "Error: no group link for " <> userGroupRef
|
logError $ "Error: no group link for " <> T.pack userGroupRef
|
||||||
GRSPendingApproval n -> processProfileChange gr $ n + 1
|
GRSPendingApproval n -> processProfileChange gr $ n + 1
|
||||||
GRSActive -> processProfileChange gr 1
|
GRSActive -> processProfileChange gr 1
|
||||||
GRSSuspended -> processProfileChange gr 1
|
GRSSuspended -> processProfileChange gr 1
|
||||||
@@ -277,7 +277,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
_ -> do
|
_ -> do
|
||||||
let gaId = 1
|
let gaId = 1
|
||||||
setGroupStatus st gr $ GRSPendingApproval gaId
|
setGroupStatus st gr $ GRSPendingApproval gaId
|
||||||
notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 48 hours."
|
notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
checkRolesSendToApprove gr gaId
|
checkRolesSendToApprove gr gaId
|
||||||
processProfileChange gr n' = do
|
processProfileChange gr n' = do
|
||||||
setGroupStatus st gr GRSPendingUpdate
|
setGroupStatus st gr GRSPendingUpdate
|
||||||
@@ -299,13 +299,13 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved."
|
notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved."
|
||||||
notifySuperUsers $ "The group " <> groupRef <> " is updated."
|
notifySuperUsers $ "The group " <> groupRef <> " is updated."
|
||||||
checkRolesSendToApprove gr n'
|
checkRolesSendToApprove gr n'
|
||||||
GPServiceLinkError -> logError $ "Error: no group link for " <> groupRef <> " pending approval."
|
GPServiceLinkError -> logError $ "Error: no group link for " <> T.pack groupRef <> " pending approval."
|
||||||
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
|
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
|
||||||
where
|
where
|
||||||
profileUpdate = \case
|
profileUpdate = \case
|
||||||
CRGroupLink {connReqContact} ->
|
CRGroupLink {connReqContact} ->
|
||||||
let groupLink1 = strEncodeTxt connReqContact
|
let groupLink1 = safeDecodeUtf8 $ strEncode connReqContact
|
||||||
groupLink2 = strEncodeTxt $ simplexChatContact connReqContact
|
groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact
|
||||||
hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p
|
hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p
|
||||||
hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p'
|
hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p'
|
||||||
in if
|
in if
|
||||||
@@ -331,7 +331,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image'
|
msg = maybe (MCText text) (\image -> MCImage {text, image}) image'
|
||||||
withSuperUsers $ \cId -> do
|
withSuperUsers $ \cId -> do
|
||||||
sendComposedMessage' cc cId Nothing msg
|
sendComposedMessage' cc cId Nothing msg
|
||||||
sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId
|
sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId
|
||||||
|
|
||||||
deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO ()
|
deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO ()
|
||||||
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do
|
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do
|
||||||
@@ -356,7 +356,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
where
|
where
|
||||||
rStatus = groupRolesStatus contactRole serviceRole
|
rStatus = groupRolesStatus contactRole serviceRole
|
||||||
groupRef = groupReference g
|
groupRef = groupReference g
|
||||||
ctRole = "*" <> strEncodeTxt contactRole <> "*"
|
ctRole = "*" <> B.unpack (strEncode contactRole) <> "*"
|
||||||
suCtRole = "(user role is set to " <> ctRole <> ")."
|
suCtRole = "(user role is set to " <> ctRole <> ")."
|
||||||
|
|
||||||
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
|
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
|
||||||
@@ -382,7 +382,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
where
|
where
|
||||||
groupRef = groupReference g
|
groupRef = groupReference g
|
||||||
srvRole = "*" <> strEncodeTxt serviceRole <> "*"
|
srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*"
|
||||||
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
|
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
|
||||||
whenContactIsOwner gr action =
|
whenContactIsOwner gr action =
|
||||||
getGroupMember gr
|
getGroupMember gr
|
||||||
@@ -426,7 +426,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
<> serviceName
|
<> serviceName
|
||||||
<> " bot will create a public group link for the new members to join even when you are offline.\n\
|
<> " bot will create a public group link for the new members to join even when you are offline.\n\
|
||||||
\3. You will then need to add this link to the group welcome message.\n\
|
\3. You will then need to add this link to the group welcome message.\n\
|
||||||
\4. Once the link is added, service admins will approve the group (it can take up to 48 hours), and everybody will be able to find it in directory.\n\n\
|
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
||||||
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
||||||
DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s
|
DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s
|
||||||
DCSearchNext ->
|
DCSearchNext ->
|
||||||
@@ -448,47 +448,44 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent
|
DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent
|
||||||
DCSubmitGroup _link -> pure ()
|
DCSubmitGroup _link -> pure ()
|
||||||
DCConfirmDuplicateGroup ugrId gName ->
|
DCConfirmDuplicateGroup ugrId gName ->
|
||||||
withUserGroupReg ugrId gName $ \g@GroupInfo {groupProfile = GroupProfile {displayName}} gr ->
|
withUserGroupReg ugrId gName $ \gr g@GroupInfo {groupProfile = GroupProfile {displayName}} ->
|
||||||
readTVarIO (groupRegStatus gr) >>= \case
|
readTVarIO (groupRegStatus gr) >>= \case
|
||||||
GRSPendingConfirmation ->
|
GRSPendingConfirmation ->
|
||||||
getDuplicateGroup g >>= \case
|
getDuplicateGroup g >>= \case
|
||||||
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
|
||||||
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
|
||||||
_ -> processInvitation ct g
|
_ -> processInvitation ct g
|
||||||
_ -> sendReply $ "Error: the group ID " <> tshow ugrId <> " (" <> displayName <> ") is not pending confirmation."
|
_ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation."
|
||||||
DCListUserGroups ->
|
DCListUserGroups ->
|
||||||
atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do
|
atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do
|
||||||
sendReply $ tshow (length grs) <> " registered group(s)"
|
sendReply $ show (length grs) <> " registered group(s)"
|
||||||
void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} ->
|
void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} ->
|
||||||
sendGroupInfo ct gr userGroupRegId Nothing
|
sendGroupInfo ct gr userGroupRegId Nothing
|
||||||
DCDeleteGroup ugrId gName ->
|
DCDeleteGroup ugrId gName ->
|
||||||
withUserGroupReg ugrId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do
|
withUserGroupReg ugrId gName $ \gr GroupInfo {groupProfile = GroupProfile {displayName}} -> do
|
||||||
delGroupReg st gr
|
delGroupReg st gr
|
||||||
sendReply $ "Your group " <> displayName <> " is deleted from the directory"
|
sendReply $ T.unpack $ "Your group " <> displayName <> " is deleted from the directory"
|
||||||
DCSetRole gId gName mRole ->
|
DCSetRole ugrId gName mRole ->
|
||||||
(if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $
|
withUserGroupReg ugrId gName $ \_gr GroupInfo {groupId, groupProfile = GroupProfile {displayName}} -> do
|
||||||
\GroupInfo {groupId, groupProfile = GroupProfile {displayName}} _gr -> do
|
gLink_ <- setGroupLinkRole cc groupId mRole
|
||||||
gLink_ <- setGroupLinkRole cc groupId mRole
|
sendReply $ T.unpack $ case gLink_ of
|
||||||
sendReply $ case gLink_ of
|
Nothing -> "Error: the initial member role for the group " <> displayName <> " was NOT upgated"
|
||||||
Nothing -> "Error: the initial member role for the group " <> displayName <> " was NOT upgated"
|
Just gLink ->
|
||||||
Just gLink ->
|
("The initial member role for the group " <> displayName <> " is set to *" <> decodeLatin1 (strEncode mRole) <> "*\n\n")
|
||||||
("The initial member role for the group " <> displayName <> " is set to *" <> strEncodeTxt mRole <> "*\n\n")
|
<> ("*Please note*: it applies only to members joining via this link: " <> safeDecodeUtf8 (strEncode $ simplexChatContact gLink))
|
||||||
<> ("*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink))
|
|
||||||
DCUnknownCommand -> sendReply "Unknown command"
|
DCUnknownCommand -> sendReply "Unknown command"
|
||||||
DCCommandError tag -> sendReply $ "Command error: " <> tshow tag
|
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||||
where
|
where
|
||||||
knownCt = knownContact ct
|
|
||||||
isAdmin = knownCt `elem` adminUsers || knownCt `elem` superUsers
|
|
||||||
withUserGroupReg ugrId gName action =
|
withUserGroupReg ugrId gName action =
|
||||||
atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case
|
atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case
|
||||||
Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found"
|
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
|
||||||
Just gr@GroupReg {dbGroupId} -> do
|
Just gr@GroupReg {dbGroupId} -> do
|
||||||
getGroup cc dbGroupId >>= \case
|
getGroup cc dbGroupId >>= \case
|
||||||
Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found"
|
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
|
||||||
Just g@GroupInfo {groupProfile = GroupProfile {displayName}}
|
Just g@GroupInfo {groupProfile = GroupProfile {displayName}}
|
||||||
| displayName == gName -> action g gr
|
| displayName == gName -> action gr g
|
||||||
| otherwise -> sendReply $ "Group ID " <> tshow ugrId <> " has the display name " <> displayName
|
| otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName
|
||||||
sendReply = mkSendReply ct ciId
|
sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent
|
||||||
withFoundListedGroups s_ action =
|
withFoundListedGroups s_ action =
|
||||||
getGroups_ s_ >>= \case
|
getGroups_ s_ >>= \case
|
||||||
Just groups -> atomically (filterListedGroups st groups) >>= action
|
Just groups -> atomically (filterListedGroups st groups) >>= action
|
||||||
@@ -498,8 +495,8 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
gs -> do
|
gs -> do
|
||||||
let gs' = takeTop searchResults gs
|
let gs' = takeTop searchResults gs
|
||||||
moreGroups = length gs - length gs'
|
moreGroups = length gs - length gs'
|
||||||
more = if moreGroups > 0 then ", sending top " <> tshow (length gs') else ""
|
more = if moreGroups > 0 then ", sending top " <> show (length gs') else ""
|
||||||
sendReply $ "Found " <> tshow (length gs) <> " group(s)" <> more <> "."
|
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
|
||||||
updateSearchRequest (STSearch s) $ groupIds gs'
|
updateSearchRequest (STSearch s) $ groupIds gs'
|
||||||
sendFoundGroups gs' moreGroups
|
sendFoundGroups gs' moreGroups
|
||||||
sendAllGroups takeFirst sortName searchType = \case
|
sendAllGroups takeFirst sortName searchType = \case
|
||||||
@@ -507,8 +504,8 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
gs -> do
|
gs -> do
|
||||||
let gs' = takeFirst searchResults gs
|
let gs' = takeFirst searchResults gs
|
||||||
moreGroups = length gs - length gs'
|
moreGroups = length gs - length gs'
|
||||||
more = if moreGroups > 0 then ", sending " <> sortName <> " " <> tshow (length gs') else ""
|
more = if moreGroups > 0 then ", sending " <> sortName <> " " <> show (length gs') else ""
|
||||||
sendReply $ tshow (length gs) <> " group(s) listed" <> more <> "."
|
sendReply $ show (length gs) <> " group(s) listed" <> more <> "."
|
||||||
updateSearchRequest searchType $ groupIds gs'
|
updateSearchRequest searchType $ groupIds gs'
|
||||||
sendFoundGroups gs' moreGroups
|
sendFoundGroups gs' moreGroups
|
||||||
sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case
|
sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case
|
||||||
@@ -519,7 +516,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
let gs' = takeFirst searchResults $ filterNotSent sentGroups gs
|
let gs' = takeFirst searchResults $ filterNotSent sentGroups gs
|
||||||
sentGroups' = sentGroups <> groupIds gs'
|
sentGroups' = sentGroups <> groupIds gs'
|
||||||
moreGroups = length gs - S.size sentGroups'
|
moreGroups = length gs - S.size sentGroups'
|
||||||
sendReply $ "Sending " <> tshow (length gs') <> " more group(s)."
|
sendReply $ "Sending " <> show (length gs') <> " more group(s)."
|
||||||
updateSearchRequest searchType sentGroups'
|
updateSearchRequest searchType sentGroups'
|
||||||
sendFoundGroups gs' moreGroups
|
sendFoundGroups gs' moreGroups
|
||||||
updateSearchRequest :: SearchType -> Set GroupId -> IO ()
|
updateSearchRequest :: SearchType -> Set GroupId -> IO ()
|
||||||
@@ -530,10 +527,9 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
sendFoundGroups gs moreGroups =
|
sendFoundGroups gs moreGroups =
|
||||||
void . forkIO $ do
|
void . forkIO $ do
|
||||||
forM_ gs $
|
forM_ gs $
|
||||||
\(GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do
|
\(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do
|
||||||
let membersStr = "_" <> tshow currentMembers <> " members_"
|
let membersStr = "_" <> tshow currentMembers <> " members_"
|
||||||
showId = if isAdmin then tshow groupId <> ". " else ""
|
text = groupInfoText p <> "\n" <> membersStr
|
||||||
text = showId <> groupInfoText p <> "\n" <> membersStr
|
|
||||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
||||||
sendComposedMessage cc ct Nothing msg
|
sendComposedMessage cc ct Nothing msg
|
||||||
when (moreGroups > 0) $
|
when (moreGroups > 0) $
|
||||||
@@ -541,134 +537,92 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
|||||||
MCText $
|
MCText $
|
||||||
"Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
"Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
||||||
|
|
||||||
deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO ()
|
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
|
||||||
deAdminCommand ct ciId cmd
|
deSuperUserCommand ct ciId cmd
|
||||||
| knownCt `elem` adminUsers || knownCt `elem` superUsers = case cmd of
|
| superUser `elem` superUsers = case cmd of
|
||||||
DCApproveGroup {groupId, displayName = n, groupApprovalId} ->
|
DCApproveGroup {groupId, displayName = n, groupApprovalId} ->
|
||||||
withGroupAndReg sendReply groupId n $ \g gr ->
|
getGroupAndReg groupId n >>= \case
|
||||||
readTVarIO (groupRegStatus gr) >>= \case
|
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||||
GRSPendingApproval gaId
|
Just (g, gr) ->
|
||||||
| gaId == groupApprovalId -> do
|
readTVarIO (groupRegStatus gr) >>= \case
|
||||||
getDuplicateGroup g >>= \case
|
GRSPendingApproval gaId
|
||||||
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
|
| gaId == groupApprovalId -> do
|
||||||
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
|
getDuplicateGroup g >>= \case
|
||||||
_ -> do
|
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
|
||||||
getGroupRolesStatus g gr >>= \case
|
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
|
||||||
Just GRSOk -> do
|
_ -> do
|
||||||
setGroupStatus st gr GRSActive
|
getGroupRolesStatus g gr >>= \case
|
||||||
let approved = "The group " <> userGroupReference' gr n <> " is approved"
|
Just GRSOk -> do
|
||||||
notifyOwner gr $ approved <> " and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
|
setGroupStatus st gr GRSActive
|
||||||
sendReply "Group approved!"
|
sendReply "Group approved!"
|
||||||
notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct)
|
notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
|
||||||
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
|
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
|
||||||
Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
|
Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
|
||||||
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
|
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
|
||||||
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
|
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
|
||||||
where
|
where
|
||||||
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
|
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
|
||||||
serviceNotAdmin = serviceName <> " is not an admin."
|
serviceNotAdmin = serviceName <> " is not an admin."
|
||||||
| otherwise -> sendReply "Incorrect approval code"
|
| otherwise -> sendReply "Incorrect approval code"
|
||||||
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
|
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
|
||||||
where
|
where
|
||||||
groupRef = groupReference' groupId n
|
groupRef = groupReference' groupId n
|
||||||
DCRejectGroup _gaId _gName -> pure ()
|
DCRejectGroup _gaId _gName -> pure ()
|
||||||
DCSuspendGroup groupId gName -> do
|
DCSuspendGroup groupId gName -> do
|
||||||
let groupRef = groupReference' groupId gName
|
let groupRef = groupReference' groupId gName
|
||||||
withGroupAndReg sendReply groupId gName $ \_ gr ->
|
getGroupAndReg groupId gName >>= \case
|
||||||
readTVarIO (groupRegStatus gr) >>= \case
|
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||||
GRSActive -> do
|
Just (_, gr) ->
|
||||||
setGroupStatus st gr GRSSuspended
|
readTVarIO (groupRegStatus gr) >>= \case
|
||||||
let suspended = "The group " <> userGroupReference' gr gName <> " is suspended"
|
GRSActive -> do
|
||||||
notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators."
|
setGroupStatus st gr GRSSuspended
|
||||||
sendReply "Group suspended!"
|
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators."
|
||||||
notifyOtherSuperUsers $ suspended <> " by " <> viewName (localDisplayName' ct)
|
sendReply "Group suspended!"
|
||||||
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
|
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
|
||||||
DCResumeGroup groupId gName -> do
|
DCResumeGroup groupId gName -> do
|
||||||
let groupRef = groupReference' groupId gName
|
let groupRef = groupReference' groupId gName
|
||||||
withGroupAndReg sendReply groupId gName $ \_ gr ->
|
getGroupAndReg groupId gName >>= \case
|
||||||
readTVarIO (groupRegStatus gr) >>= \case
|
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
|
||||||
GRSSuspended -> do
|
Just (_, gr) ->
|
||||||
setGroupStatus st gr GRSActive
|
readTVarIO (groupRegStatus gr) >>= \case
|
||||||
let groupStr = "The group " <> userGroupReference' gr gName
|
GRSSuspended -> do
|
||||||
notifyOwner gr $ groupStr <> " is listed in the directory again!"
|
setGroupStatus st gr GRSActive
|
||||||
sendReply "Group listing resumed!"
|
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!"
|
||||||
notifyOtherSuperUsers $ groupStr <> " listing resumed by " <> viewName (localDisplayName' ct)
|
sendReply "Group listing resumed!"
|
||||||
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
|
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
|
||||||
DCListLastGroups count -> listGroups count False
|
DCListLastGroups count -> listGroups count False
|
||||||
DCListPendingGroups count -> listGroups count True
|
DCListPendingGroups count -> listGroups count True
|
||||||
DCShowGroupLink groupId gName -> do
|
DCExecuteCommand cmdStr ->
|
||||||
let groupRef = groupReference' groupId gName
|
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||||
withGroupAndReg sendReply groupId gName $ \_ _ ->
|
ts <- getCurrentTime
|
||||||
sendChatCmd cc (APIGetGroupLink groupId) >>= \case
|
tz <- getCurrentTimeZone
|
||||||
CRGroupLink {connReqContact, memberRole} ->
|
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||||
sendReply $ T.unlines
|
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||||
[ "The link to join the group " <> groupRef <> ":",
|
|
||||||
strEncodeTxt $ simplexChatContact connReqContact,
|
|
||||||
"New member role: " <> strEncodeTxt memberRole
|
|
||||||
]
|
|
||||||
CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) ->
|
|
||||||
sendReply $ "The group " <> groupRef <> " has no public link."
|
|
||||||
r -> do
|
|
||||||
ts <- getCurrentTime
|
|
||||||
tz <- getCurrentTimeZone
|
|
||||||
let resp = T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
|
||||||
sendReply $ "Unexpected error:\n" <> resp
|
|
||||||
DCSendToGroupOwner groupId gName msg -> do
|
|
||||||
let groupRef = groupReference' groupId gName
|
|
||||||
withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId} -> do
|
|
||||||
notifyOwner gr msg
|
|
||||||
owner_ <- getContact cc dbContactId
|
|
||||||
let ownerInfo = "the owner of the group " <> groupRef
|
|
||||||
ownerName ct' = "@" <> viewName (localDisplayName' ct') <> ", "
|
|
||||||
sendReply $ "Forwarded to " <> maybe "" ownerName owner_ <> ownerInfo
|
|
||||||
DCCommandError tag -> sendReply $ "Command error: " <> tshow tag
|
|
||||||
| otherwise = sendReply "You are not allowed to use this command"
|
| otherwise = sendReply "You are not allowed to use this command"
|
||||||
where
|
where
|
||||||
knownCt = knownContact ct
|
superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
|
||||||
sendReply = mkSendReply ct ciId
|
sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent
|
||||||
notifyOtherSuperUsers s = withSuperUsers $ \ctId -> unless (ctId == contactId' ct) $ sendMessage' cc ctId s
|
|
||||||
listGroups count pending =
|
listGroups count pending =
|
||||||
readTVarIO (groupRegs st) >>= \groups -> do
|
readTVarIO (groupRegs st) >>= \groups -> do
|
||||||
grs <-
|
grs <-
|
||||||
if pending
|
if pending
|
||||||
then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups
|
then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups
|
||||||
else pure groups
|
else pure groups
|
||||||
sendReply $ tshow (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> tshow count else "")
|
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
|
||||||
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
|
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
|
||||||
ct_ <- getContact cc dbContactId
|
ct_ <- getContact cc dbContactId
|
||||||
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
|
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
|
||||||
sendGroupInfo ct gr dbGroupId $ Just ownerStr
|
sendGroupInfo ct gr dbGroupId $ Just ownerStr
|
||||||
|
|
||||||
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
|
getGroupAndReg :: GroupId -> GroupName -> IO (Maybe (GroupInfo, GroupReg))
|
||||||
deSuperUserCommand ct ciId cmd
|
getGroupAndReg gId gName =
|
||||||
| knownContact ct `elem` superUsers = case cmd of
|
getGroup cc gId
|
||||||
DCExecuteCommand cmdStr ->
|
$>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} ->
|
||||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
if displayName == gName
|
||||||
ts <- getCurrentTime
|
then
|
||||||
tz <- getCurrentTimeZone
|
atomically (getGroupReg st gId)
|
||||||
sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
$>>= \gr -> pure $ Just (g, gr)
|
||||||
DCCommandError tag -> sendReply $ "Command error: " <> tshow tag
|
else pure Nothing
|
||||||
| otherwise = sendReply "You are not allowed to use this command"
|
|
||||||
where
|
|
||||||
sendReply = mkSendReply ct ciId
|
|
||||||
|
|
||||||
knownContact :: Contact -> KnownContact
|
|
||||||
knownContact ct = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
|
|
||||||
|
|
||||||
mkSendReply :: Contact -> ChatItemId -> Text -> IO ()
|
|
||||||
mkSendReply ct ciId = sendComposedMessage cc ct (Just ciId) . MCText
|
|
||||||
|
|
||||||
withGroupAndReg :: (Text -> IO ()) -> GroupId -> GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO ()
|
|
||||||
withGroupAndReg sendReply gId gName action =
|
|
||||||
getGroup cc gId >>= \case
|
|
||||||
Nothing -> sendReply $ "Group ID " <> tshow gId <> " not found (getGroup)"
|
|
||||||
Just g@GroupInfo {groupProfile = GroupProfile {displayName}}
|
|
||||||
| displayName == gName ->
|
|
||||||
atomically (getGroupReg st gId) >>= \case
|
|
||||||
Nothing -> sendReply $ "Registration for group ID " <> tshow gId <> " not found (getGroupReg)"
|
|
||||||
Just gr -> action g gr
|
|
||||||
| otherwise ->
|
|
||||||
sendReply $ "Group ID " <> tshow gId <> " has the display name " <> displayName
|
|
||||||
|
|
||||||
sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO ()
|
sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO ()
|
||||||
sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do
|
sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do
|
||||||
@@ -714,8 +668,5 @@ setGroupLinkRole cc gId mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole
|
|||||||
CRGroupLink _ _ gLink _ -> Just gLink
|
CRGroupLink _ _ gLink _ -> Just gLink
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
unexpectedError :: Text -> Text
|
unexpectedError :: String -> String
|
||||||
unexpectedError err = "Unexpected error: " <> err <> ", please notify the developers."
|
unexpectedError err = "Unexpected error: " <> err <> ", please notify the developers."
|
||||||
|
|
||||||
strEncodeTxt :: StrEncoding a => a -> Text
|
|
||||||
strEncodeTxt = safeDecodeUtf8 . strEncode
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 93f30c8edf9243ad2291dd6427d87328e282560a
|
tag: ffecf200d4874dfa34f6d15b269964c0115a54ca
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Server operators
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
All preconfigured servers operated by a single company create a risk that user connections can be analysed by aggregating transport information from these servers.
|
|
||||||
|
|
||||||
The solution is to have more than one operator servers pre-configured in the app.
|
|
||||||
|
|
||||||
For operators to be protected from any violations of rights of other users or third parties by the users who use servers of these operators, the users have to explicitely accept conditions of use with the operator, in the same way they accept conditions of use with SimpleX Chat Ltd by downloading the app.
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
|
|
||||||
Allow to assign operators to servers, both with preconfigured operators and servers, and with user-defined operators. Agent added support for server roles, chat app could:
|
|
||||||
- allow assigning server roles only on the operator level.
|
|
||||||
- only on server level.
|
|
||||||
- on both, with server roles overriding operator roles (that would require a different type for server for chat app).
|
|
||||||
|
|
||||||
For simplicity of both UX and logic it is probably better to allow assigning roles only on operators' level, and servers without set operators can be used for both roles.
|
|
||||||
|
|
||||||
For agreements, it is sufficient to record the signatures of these agreements on users' devices, together with the copy of signed agreement (or its hash and version) in a separate table. The terms themselves could be:
|
|
||||||
- included in the app - either in code or in migration.
|
|
||||||
- referenced with a stable link to a particular commit.
|
|
||||||
|
|
||||||
The first solution seems better, as it avoids any third party dependency, and the agreement size is relatively small (~31kb), to reduce size we can store it compressed.
|
|
||||||
@@ -29,7 +29,6 @@ dependencies:
|
|||||||
- email-validate == 2.3.*
|
- email-validate == 2.3.*
|
||||||
- exceptions == 0.10.*
|
- exceptions == 0.10.*
|
||||||
- filepath == 1.4.*
|
- filepath == 1.4.*
|
||||||
- file-embed == 0.0.15.*
|
|
||||||
- http-types == 0.12.*
|
- http-types == 0.12.*
|
||||||
- http2 >= 4.2.2 && < 4.3
|
- http2 >= 4.2.2 && < 4.3
|
||||||
- memory == 0.18.*
|
- memory == 0.18.*
|
||||||
@@ -39,7 +38,6 @@ dependencies:
|
|||||||
- optparse-applicative >= 0.15 && < 0.17
|
- optparse-applicative >= 0.15 && < 0.17
|
||||||
- random >= 1.1 && < 1.3
|
- random >= 1.1 && < 1.3
|
||||||
- record-hasfield == 1.0.*
|
- record-hasfield == 1.0.*
|
||||||
- scientific ==0.3.7.*
|
|
||||||
- simple-logger == 0.1.*
|
- simple-logger == 0.1.*
|
||||||
- simplexmq >= 5.0
|
- simplexmq >= 5.0
|
||||||
- socks == 0.6.*
|
- socks == 0.6.*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."93f30c8edf9243ad2291dd6427d87328e282560a" = "1zf0sp9dy6kz4zvyz6mdgmhydps7khcq84n30irp983w1xh7gzs7";
|
"https://github.com/simplex-chat/simplexmq.git"."ffecf200d4874dfa34f6d15b269964c0115a54ca" = "0kb8hq37fc5g198wq7dswnlwjzk67q8rrzil2dii5lc6xfr47jbs";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||||
|
|||||||
+1
-17
@@ -150,13 +150,11 @@ library
|
|||||||
Simplex.Chat.Migrations.M20240920_user_order
|
Simplex.Chat.Migrations.M20240920_user_order
|
||||||
Simplex.Chat.Migrations.M20241008_indexes
|
Simplex.Chat.Migrations.M20241008_indexes
|
||||||
Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
||||||
Simplex.Chat.Migrations.M20241027_server_operators
|
Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
Simplex.Chat.Mobile.WebRTC
|
Simplex.Chat.Mobile.WebRTC
|
||||||
Simplex.Chat.Operators
|
|
||||||
Simplex.Chat.Operators.Conditions
|
|
||||||
Simplex.Chat.Options
|
Simplex.Chat.Options
|
||||||
Simplex.Chat.ProfileGenerator
|
Simplex.Chat.ProfileGenerator
|
||||||
Simplex.Chat.Protocol
|
Simplex.Chat.Protocol
|
||||||
@@ -216,7 +214,6 @@ library
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
@@ -227,7 +224,6 @@ library
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplexmq >=5.0
|
, simplexmq >=5.0
|
||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
@@ -281,7 +277,6 @@ executable simplex-bot
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
@@ -292,7 +287,6 @@ executable simplex-bot
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=5.0
|
||||||
@@ -347,7 +341,6 @@ executable simplex-bot-advanced
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
@@ -358,7 +351,6 @@ executable simplex-bot-advanced
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=5.0
|
||||||
@@ -416,7 +408,6 @@ executable simplex-broadcast-bot
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
@@ -427,7 +418,6 @@ executable simplex-broadcast-bot
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=5.0
|
||||||
@@ -483,7 +473,6 @@ executable simplex-chat
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
@@ -494,7 +483,6 @@ executable simplex-chat
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=5.0
|
||||||
@@ -556,7 +544,6 @@ executable simplex-directory-service
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
@@ -567,7 +554,6 @@ executable simplex-directory-service
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=5.0
|
||||||
@@ -657,7 +643,6 @@ test-suite simplex-chat-test
|
|||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
, email-validate ==2.3.*
|
||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, generic-random ==1.5.*
|
, generic-random ==1.5.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
@@ -669,7 +654,6 @@ test-suite simplex-chat-test
|
|||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
, random >=1.1 && <1.3
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, silently ==1.2.*
|
, silently ==1.2.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
|
|||||||
+143
-301
@@ -6,7 +6,6 @@
|
|||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedLists #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PatternSynonyms #-}
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
{-# LANGUAGE RankNTypes #-}
|
{-# LANGUAGE RankNTypes #-}
|
||||||
@@ -44,7 +43,7 @@ import Data.Functor (($>))
|
|||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn, zipWith4)
|
import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn, zipWith4)
|
||||||
import Data.List.NonEmpty (NonEmpty (..), (<|))
|
import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|))
|
||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
import Data.Map.Strict (Map)
|
import Data.Map.Strict (Map)
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
@@ -68,7 +67,6 @@ import Simplex.Chat.Messages
|
|||||||
import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages)
|
import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages)
|
||||||
import Simplex.Chat.Messages.CIContent
|
import Simplex.Chat.Messages.CIContent
|
||||||
import Simplex.Chat.Messages.CIContent.Events
|
import Simplex.Chat.Messages.CIContent.Events
|
||||||
import Simplex.Chat.Operators
|
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.ProfileGenerator (generateRandomProfile)
|
import Simplex.Chat.ProfileGenerator (generateRandomProfile)
|
||||||
import Simplex.Chat.Protocol
|
import Simplex.Chat.Protocol
|
||||||
@@ -99,7 +97,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP
|
|||||||
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
|
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
|
||||||
import Simplex.Messaging.Agent as Agent
|
import Simplex.Messaging.Agent as Agent
|
||||||
import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap)
|
import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap)
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg)
|
||||||
import Simplex.Messaging.Agent.Lock (withLock)
|
import Simplex.Messaging.Agent.Lock (withLock)
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
|
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
|
||||||
@@ -139,32 +137,6 @@ import qualified UnliftIO.Exception as E
|
|||||||
import UnliftIO.IO (hClose, hSeek, hTell, openFile)
|
import UnliftIO.IO (hClose, hSeek, hTell, openFile)
|
||||||
import UnliftIO.STM
|
import UnliftIO.STM
|
||||||
|
|
||||||
operatorSimpleXChat :: NewServerOperator
|
|
||||||
operatorSimpleXChat =
|
|
||||||
ServerOperator
|
|
||||||
{ operatorId = DBNewEntity,
|
|
||||||
operatorTag = Just OTSimplex,
|
|
||||||
tradeName = "SimpleX Chat",
|
|
||||||
legalName = Just "SimpleX Chat Ltd",
|
|
||||||
serverDomains = ["simplex.im"],
|
|
||||||
conditionsAcceptance = CARequired Nothing,
|
|
||||||
enabled = True,
|
|
||||||
roles = allRoles
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorXYZ :: NewServerOperator
|
|
||||||
operatorXYZ =
|
|
||||||
ServerOperator
|
|
||||||
{ operatorId = DBNewEntity,
|
|
||||||
operatorTag = Just OTXyz,
|
|
||||||
tradeName = "XYZ",
|
|
||||||
legalName = Just "XYZ Ltd",
|
|
||||||
serverDomains = ["xyz.com"],
|
|
||||||
conditionsAcceptance = CARequired Nothing,
|
|
||||||
enabled = False,
|
|
||||||
roles = ServerRoles {storage = False, proxy = True}
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultChatConfig :: ChatConfig
|
defaultChatConfig :: ChatConfig
|
||||||
defaultChatConfig =
|
defaultChatConfig =
|
||||||
ChatConfig
|
ChatConfig
|
||||||
@@ -175,25 +147,13 @@ defaultChatConfig =
|
|||||||
},
|
},
|
||||||
chatVRange = supportedChatVRange,
|
chatVRange = supportedChatVRange,
|
||||||
confirmMigrations = MCConsole,
|
confirmMigrations = MCConsole,
|
||||||
presetServers =
|
defaultServers =
|
||||||
PresetServers
|
DefaultAgentServers
|
||||||
{ operators =
|
{ smp = _defaultSMPServers,
|
||||||
[ PresetOperator
|
useSMP = 4,
|
||||||
{ operator = Just operatorSimpleXChat,
|
|
||||||
smp = simplexChatSMPServers,
|
|
||||||
useSMP = 4,
|
|
||||||
xftp = map (presetServer True) $ L.toList defaultXFTPServers,
|
|
||||||
useXFTP = 3
|
|
||||||
},
|
|
||||||
PresetOperator
|
|
||||||
{ operator = Just operatorXYZ,
|
|
||||||
smp = xyzSMPServers,
|
|
||||||
useSMP = 3,
|
|
||||||
xftp = xyzXFTPServers,
|
|
||||||
useXFTP = 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
ntf = _defaultNtfServers,
|
ntf = _defaultNtfServers,
|
||||||
|
xftp = L.map (presetServerCfg True) defaultXFTPServers,
|
||||||
|
useXFTP = L.length defaultXFTPServers,
|
||||||
netCfg = defaultNetworkConfig
|
netCfg = defaultNetworkConfig
|
||||||
},
|
},
|
||||||
tbqSize = 1024,
|
tbqSize = 1024,
|
||||||
@@ -217,52 +177,29 @@ defaultChatConfig =
|
|||||||
chatHooks = defaultChatHooks
|
chatHooks = defaultChatHooks
|
||||||
}
|
}
|
||||||
|
|
||||||
simplexChatSMPServers :: [NewUserServer 'PSMP]
|
_defaultSMPServers :: NonEmpty (ServerCfg 'PSMP)
|
||||||
simplexChatSMPServers =
|
_defaultSMPServers =
|
||||||
map
|
L.fromList $
|
||||||
(presetServer True)
|
map
|
||||||
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
|
(presetServerCfg True)
|
||||||
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
|
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
|
||||||
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion",
|
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
|
||||||
"smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
|
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion",
|
||||||
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
|
"smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
|
||||||
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion",
|
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
|
||||||
"smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
|
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion",
|
||||||
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
|
"smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
|
||||||
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
|
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
|
||||||
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
|
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
|
||||||
"smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion"
|
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
|
||||||
]
|
"smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion"
|
||||||
<> map
|
|
||||||
(presetServer False)
|
|
||||||
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
|
|
||||||
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion",
|
|
||||||
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion"
|
|
||||||
]
|
]
|
||||||
|
<> map
|
||||||
xyzSMPServers :: [NewUserServer 'PSMP]
|
(presetServerCfg False)
|
||||||
xyzSMPServers =
|
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
|
||||||
map
|
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion",
|
||||||
(presetServer True)
|
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion"
|
||||||
[ "smp://abcd@smp1.xyz.com",
|
]
|
||||||
"smp://abcd@smp2.xyz.com",
|
|
||||||
"smp://abcd@smp3.xyz.com",
|
|
||||||
"smp://abcd@smp4.xyz.com",
|
|
||||||
"smp://abcd@smp5.xyz.com",
|
|
||||||
"smp://abcd@smp6.xyz.com"
|
|
||||||
]
|
|
||||||
|
|
||||||
xyzXFTPServers :: [NewUserServer 'PXFTP]
|
|
||||||
xyzXFTPServers =
|
|
||||||
map
|
|
||||||
(presetServer True)
|
|
||||||
[ "xftp://abcd@xftp1.xyz.com",
|
|
||||||
"xftp://abcd@xftp2.xyz.com",
|
|
||||||
"xftp://abcd@xftp3.xyz.com",
|
|
||||||
"xftp://abcd@xftp4.xyz.com",
|
|
||||||
"xftp://abcd@xftp5.xyz.com",
|
|
||||||
"xftp://abcd@xftp6.xyz.com"
|
|
||||||
]
|
|
||||||
|
|
||||||
_defaultNtfServers :: [NtfServer]
|
_defaultNtfServers :: [NtfServer]
|
||||||
_defaultNtfServers =
|
_defaultNtfServers =
|
||||||
@@ -299,19 +236,16 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo
|
|||||||
newChatController
|
newChatController
|
||||||
ChatDatabase {chatStore, agentStore}
|
ChatDatabase {chatStore, agentStore}
|
||||||
user
|
user
|
||||||
cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations}
|
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations}
|
||||||
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
||||||
backgroundMode = do
|
backgroundMode = do
|
||||||
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
||||||
confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
|
confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
|
||||||
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'}
|
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'}
|
||||||
firstTime = dbNew chatStore
|
firstTime = dbNew chatStore
|
||||||
currentUser <- newTVarIO user
|
currentUser <- newTVarIO user
|
||||||
randomSMP <- randomPresetServers SPSMP presetServers'
|
|
||||||
randomXFTP <- randomPresetServers SPXFTP presetServers'
|
|
||||||
let randomServers = RandomServers {smpServers = randomSMP, xftpServers = randomXFTP}
|
|
||||||
currentRemoteHost <- newTVarIO Nothing
|
currentRemoteHost <- newTVarIO Nothing
|
||||||
servers <- withTransaction chatStore $ \db -> agentServers db config randomServers
|
servers <- agentServers config
|
||||||
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode
|
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode
|
||||||
agentAsync <- newTVarIO Nothing
|
agentAsync <- newTVarIO Nothing
|
||||||
random <- liftIO C.newRandom
|
random <- liftIO C.newRandom
|
||||||
@@ -347,7 +281,6 @@ newChatController
|
|||||||
ChatController
|
ChatController
|
||||||
{ firstTime,
|
{ firstTime,
|
||||||
currentUser,
|
currentUser,
|
||||||
randomServers,
|
|
||||||
currentRemoteHost,
|
currentRemoteHost,
|
||||||
smpAgent,
|
smpAgent,
|
||||||
agentAsync,
|
agentAsync,
|
||||||
@@ -385,39 +318,28 @@ newChatController
|
|||||||
contactMergeEnabled
|
contactMergeEnabled
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
presetServers' :: PresetServers
|
configServers :: DefaultAgentServers
|
||||||
presetServers' = presetServers {operators = operators', netCfg = netCfg'}
|
configServers =
|
||||||
where
|
let DefaultAgentServers {smp = defSmp, xftp = defXftp, netCfg} = defaultServers
|
||||||
PresetServers {operators, netCfg} = presetServers
|
smp' = maybe defSmp (L.map enabledServerCfg) (nonEmpty smpServers)
|
||||||
netCfg' = updateNetworkConfig netCfg simpleNetCfg
|
xftp' = maybe defXftp (L.map enabledServerCfg) (nonEmpty xftpServers)
|
||||||
operators' = case (smpServers, xftpServers) of
|
in defaultServers {smp = smp', xftp = xftp', netCfg = updateNetworkConfig netCfg simpleNetCfg}
|
||||||
([], []) -> operators
|
agentServers :: ChatConfig -> IO InitialAgentServers
|
||||||
(smpSrvs, []) -> L.map removeSMP operators <> [custom smpSrvs []]
|
agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do
|
||||||
([], xftpSrvs) -> L.map removeXFTP operators <> [custom [] xftpSrvs]
|
users <- withTransaction chatStore getUsers
|
||||||
(smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs]
|
smp' <- getUserServers users SPSMP
|
||||||
removeSMP op = (op :: PresetOperator) {smp = []}
|
xftp' <- getUserServers users SPXFTP
|
||||||
removeXFTP op = (op :: PresetOperator) {xftp = []}
|
|
||||||
custom smpSrvs xftpSrvs =
|
|
||||||
PresetOperator
|
|
||||||
{ operator = Nothing,
|
|
||||||
smp = map (presetServer True) smpSrvs,
|
|
||||||
useSMP = 0,
|
|
||||||
xftp = map (presetServer True) xftpSrvs,
|
|
||||||
useXFTP = 0
|
|
||||||
}
|
|
||||||
agentServers :: DB.Connection -> ChatConfig -> RandomServers -> IO InitialAgentServers
|
|
||||||
agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} randomServers = do
|
|
||||||
users <- getUsers db
|
|
||||||
opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users)
|
|
||||||
smp' <- getUserServers SPSMP users opDomains
|
|
||||||
xftp' <- getUserServers SPXFTP users opDomains
|
|
||||||
pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg}
|
pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg}
|
||||||
where
|
where
|
||||||
getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p)))
|
getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ServerCfg p)))
|
||||||
getUserServers p users opDomains = do
|
getUserServers users protocol = case users of
|
||||||
let randomSrvs = rndServers p randomServers
|
[] -> pure $ M.fromList [(1, cfgServers protocol defServers)]
|
||||||
fmap M.fromList $ forM users $ \u ->
|
_ -> M.fromList <$> initialServers
|
||||||
(aUserId u,) . agentServerCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u
|
where
|
||||||
|
initialServers :: IO [(UserId, NonEmpty (ServerCfg p))]
|
||||||
|
initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users
|
||||||
|
userServers :: User -> IO (NonEmpty (ServerCfg p))
|
||||||
|
userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user')
|
||||||
|
|
||||||
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
|
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
|
||||||
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} =
|
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} =
|
||||||
@@ -460,37 +382,33 @@ withFileLock :: String -> Int64 -> CM a -> CM a
|
|||||||
withFileLock name = withEntityLock name . CLFile
|
withFileLock name = withEntityLock name . CLFile
|
||||||
{-# INLINE withFileLock #-}
|
{-# INLINE withFileLock #-}
|
||||||
|
|
||||||
serverCfg :: ProtoServerWithAuth p -> ServerCfg p
|
useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ServerCfg p)
|
||||||
serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles}
|
useServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) . nonEmpty
|
||||||
|
|
||||||
useServers :: forall p. UserProtocol p => SProtocolType p -> RandomServers -> [UserServer p] -> NonEmpty (NewUserServer p)
|
randomServers :: forall p. UserProtocol p => SProtocolType p -> ChatConfig -> IO (NonEmpty (ServerCfg p), [ServerCfg p])
|
||||||
useServers p rs servers = case L.nonEmpty servers of
|
randomServers p ChatConfig {defaultServers} = do
|
||||||
Nothing -> rndServers p rs
|
let srvs = cfgServers p defaultServers
|
||||||
Just srvs -> L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs
|
(enbldSrvs, dsbldSrvs) = L.partition (\ServerCfg {enabled} -> enabled) srvs
|
||||||
|
toUse = cfgServersToUse p defaultServers
|
||||||
rndServers :: UserProtocol p => SProtocolType p -> RandomServers -> NonEmpty (NewUserServer p)
|
if length enbldSrvs <= toUse
|
||||||
rndServers p RandomServers {smpServers, xftpServers} = case p of
|
then pure (srvs, [])
|
||||||
SPSMP -> smpServers
|
else do
|
||||||
SPXFTP -> xftpServers
|
(enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs
|
||||||
|
let dsbldSrvs' = map (\srv -> (srv :: ServerCfg p) {enabled = False}) srvsToDisable
|
||||||
randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> PresetServers -> IO (NonEmpty (NewUserServer p))
|
srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs
|
||||||
randomPresetServers p PresetServers {operators} = toJust . L.nonEmpty . concat =<< mapM opSrvs operators
|
pure (fromMaybe srvs $ L.nonEmpty srvs', srvs')
|
||||||
where
|
where
|
||||||
toJust = \case
|
server' ServerCfg {server = ProtoServerWithAuth srv _} = srv
|
||||||
Just a -> pure a
|
|
||||||
Nothing -> E.throwIO $ userError "no preset servers"
|
cfgServers :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> NonEmpty (ServerCfg p)
|
||||||
opSrvs :: PresetOperator -> IO [NewUserServer p]
|
cfgServers p DefaultAgentServers {smp, xftp} = case p of
|
||||||
opSrvs op = do
|
SPSMP -> smp
|
||||||
let srvs = operatorServers p op
|
SPXFTP -> xftp
|
||||||
toUse = operatorServersToUse p op
|
|
||||||
(enbldSrvs, dsbldSrvs) = partition (\UserServer {enabled} -> enabled) srvs
|
cfgServersToUse :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> Int
|
||||||
if toUse <= 0 || toUse >= length enbldSrvs
|
cfgServersToUse p DefaultAgentServers {useSMP, useXFTP} = case p of
|
||||||
then pure srvs
|
SPSMP -> useSMP
|
||||||
else do
|
SPXFTP -> useXFTP
|
||||||
(enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs
|
|
||||||
let dsbldSrvs' = map (\srv -> (srv :: NewUserServer p) {enabled = False}) srvsToDisable
|
|
||||||
pure $ sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs
|
|
||||||
server' UserServer {server = ProtoServerWithAuth srv _} = srv
|
|
||||||
|
|
||||||
-- enableSndFiles has no effect when mainApp is True
|
-- enableSndFiles has no effect when mainApp is True
|
||||||
startChatController :: Bool -> Bool -> CM' (Async ())
|
startChatController :: Bool -> Bool -> CM' (Async ())
|
||||||
@@ -634,23 +552,19 @@ processChatCommand' vr = \case
|
|||||||
forM_ profile $ \Profile {displayName} -> checkValidName displayName
|
forM_ profile $ \Profile {displayName} -> checkValidName displayName
|
||||||
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
|
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
|
||||||
u <- asks currentUser
|
u <- asks currentUser
|
||||||
smpServers <- chooseServers SPSMP
|
(smp, smpServers) <- chooseServers SPSMP
|
||||||
xftpServers <- chooseServers SPXFTP
|
(xftp, xftpServers) <- chooseServers SPXFTP
|
||||||
users <- withFastStore' getUsers
|
users <- withFastStore' getUsers
|
||||||
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
|
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
|
||||||
when (n == displayName) . throwChatError $
|
when (n == displayName) . throwChatError $
|
||||||
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
|
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
|
||||||
opDomains <- operatorDomains . fst <$> withFastStore getServerOperators
|
|
||||||
let smp = agentServerCfgs opDomains smpServers
|
|
||||||
xftp = agentServerCfgs opDomains xftpServers
|
|
||||||
auId <- withAgent (\a -> createUser a smp xftp)
|
auId <- withAgent (\a -> createUser a smp xftp)
|
||||||
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
|
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
|
||||||
user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
|
user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
|
||||||
createPresetContactCards user `catchChatError` \_ -> pure ()
|
createPresetContactCards user `catchChatError` \_ -> pure ()
|
||||||
withFastStore $ \db -> do
|
withFastStore $ \db -> createNoteFolder db user
|
||||||
createNoteFolder db user
|
storeServers user smpServers
|
||||||
liftIO $ mapM_ (insertProtocolServer db SPSMP user ts) smpServers
|
storeServers user xftpServers
|
||||||
liftIO $ mapM_ (insertProtocolServer db SPXFTP user ts) xftpServers
|
|
||||||
atomically . writeTVar u $ Just user
|
atomically . writeTVar u $ Just user
|
||||||
pure $ CRActiveUser user
|
pure $ CRActiveUser user
|
||||||
where
|
where
|
||||||
@@ -659,11 +573,18 @@ processChatCommand' vr = \case
|
|||||||
withFastStore $ \db -> do
|
withFastStore $ \db -> do
|
||||||
createContact db user simplexStatusContactProfile
|
createContact db user simplexStatusContactProfile
|
||||||
createContact db user simplexTeamContactProfile
|
createContact db user simplexTeamContactProfile
|
||||||
chooseServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (NewUserServer p))
|
chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (ServerCfg p), [ServerCfg p])
|
||||||
chooseServers p = do
|
chooseServers protocol =
|
||||||
rs <- asks randomServers
|
asks currentUser >>= readTVarIO >>= \case
|
||||||
srvs <- chatReadVar currentUser >>= mapM (\user -> withFastStore' $ \db -> getProtocolServers db p user)
|
Nothing -> asks config >>= liftIO . randomServers protocol
|
||||||
pure $ useServers p rs $ fromMaybe [] srvs
|
Just user -> chosenServers =<< withFastStore' (`getProtocolServers` user)
|
||||||
|
where
|
||||||
|
chosenServers servers = do
|
||||||
|
cfg <- asks config
|
||||||
|
pure (useServers cfg protocol servers, servers)
|
||||||
|
storeServers user servers =
|
||||||
|
unless (null servers) . withFastStore $
|
||||||
|
\db -> overwriteProtocolServers db user servers
|
||||||
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
|
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
|
||||||
day = 86400
|
day = 86400
|
||||||
ListUsers -> CRUsersList <$> withFastStore' getUsersInfo
|
ListUsers -> CRUsersList <$> withFastStore' getUsersInfo
|
||||||
@@ -1561,95 +1482,25 @@ processChatCommand' vr = \case
|
|||||||
msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds
|
msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds
|
||||||
let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs
|
let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs
|
||||||
pure $ CRConnNtfMessages ntfMsgs
|
pure $ CRConnNtfMessages ntfMsgs
|
||||||
GetUserProtoServers (AProtocolType p) -> withUser $ \user@User {userId} -> withServerProtocol p $ do
|
APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do
|
||||||
(operators, smpServers, xftpServers) <- withFastStore (`getUserServers` user)
|
cfg@ChatConfig {defaultServers} <- asks config
|
||||||
userServers <- liftIO $ groupByOperator $ case p of
|
servers <- withFastStore' (`getProtocolServers` user)
|
||||||
SPSMP -> (operators, smpServers, [])
|
pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p (useServers cfg p servers) (cfgServers p defaultServers)
|
||||||
SPXFTP -> (operators, [], xftpServers)
|
GetUserProtoServers aProtocol -> withUser $ \User {userId} ->
|
||||||
pure $ CRUserServers user userServers
|
processChatCommand $ APIGetUserProtoServers userId aProtocol
|
||||||
SetUserProtoServers (AProtocolType p) servers -> withUser $ \user@User {userId} -> withServerProtocol p $ do
|
APISetUserProtoServers userId (APSC p (ProtoServersConfig servers))
|
||||||
userServers <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user)
|
| null servers || any (\ServerCfg {enabled} -> enabled) servers -> withUserId userId $ \user -> withServerProtocol p $ do
|
||||||
-- disable operators servers and repace (or add) custom servers, or restore random defaults if empty list
|
withFastStore $ \db -> overwriteProtocolServers db user servers
|
||||||
case L.nonEmpty userServers of
|
cfg <- asks config
|
||||||
Just srvs -> processChatCommand $ APISetUserServers userId $ L.map updated srvs
|
lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ useServers cfg p servers
|
||||||
where
|
ok user
|
||||||
updated UserOperatorServers {operator, smpServers, xftpServers} =
|
| otherwise -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "all servers are disabled"
|
||||||
UpdatedUserOperatorServers
|
SetUserProtoServers serversConfig -> withUser $ \User {userId} ->
|
||||||
{ operator,
|
processChatCommand $ APISetUserProtoServers userId serversConfig
|
||||||
smpServers = map (AUS SDBStored) smpServers,
|
|
||||||
xftpServers = map (AUS SDBStored) xftpServers
|
|
||||||
}
|
|
||||||
Nothing -> throwChatError $ CECommandError "no servers"
|
|
||||||
APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user ->
|
APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user ->
|
||||||
lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server)
|
lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server)
|
||||||
TestProtoServer srv -> withUser $ \User {userId} ->
|
TestProtoServer srv -> withUser $ \User {userId} ->
|
||||||
processChatCommand $ APITestProtoServer userId srv
|
processChatCommand $ APITestProtoServer userId srv
|
||||||
APITestServerOperator -> do
|
|
||||||
let serverOperator = ServerOperator {
|
|
||||||
operatorId = DBEntityId 1,
|
|
||||||
operatorTag = Just OTSimplex,
|
|
||||||
tradeName = "Simplex",
|
|
||||||
legalName = Just "Simplex",
|
|
||||||
serverDomains = ["simplex.im"],
|
|
||||||
conditionsAcceptance = CAAccepted Nothing,
|
|
||||||
enabled = True,
|
|
||||||
roles = ServerRoles {storage = True, proxy = True}
|
|
||||||
}
|
|
||||||
pure $ CRTestOperator serverOperator
|
|
||||||
APITestUsageConditionsAction -> do
|
|
||||||
ts <- liftIO getCurrentTime
|
|
||||||
let conditionsAction = Just $ UCAReview {
|
|
||||||
operators = [],
|
|
||||||
deadline = Just ts,
|
|
||||||
showNotice = True
|
|
||||||
}
|
|
||||||
pure $ CRTestUsageConditionsAction conditionsAction
|
|
||||||
APITestConditionsAcceptance -> do
|
|
||||||
let acceptance = CAAccepted Nothing
|
|
||||||
pure $ CRTestConditionsAcceptance acceptance
|
|
||||||
APITestServerRoles -> do
|
|
||||||
let serverRoles = ServerRoles {storage = True, proxy = True}
|
|
||||||
pure $ CRTestServerRoles serverRoles
|
|
||||||
APIGetServerOperators -> uncurry CRServerOperators <$> withFastStore getServerOperators
|
|
||||||
APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do
|
|
||||||
liftIO $ setServerOperators db operatorsEnabled
|
|
||||||
uncurry CRServerOperators <$> getServerOperators db
|
|
||||||
APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db ->
|
|
||||||
CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user)
|
|
||||||
APISetUserServers userId userServers -> withUserId userId $ \user -> do
|
|
||||||
let errors = validateUserServers userServers
|
|
||||||
unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors)
|
|
||||||
(operators, smpServers, xftpServers) <- withFastStore $ \db -> do
|
|
||||||
setUserServers db user userServers
|
|
||||||
getUserServers db user
|
|
||||||
let opDomains = operatorDomains operators
|
|
||||||
rs <- asks randomServers
|
|
||||||
lift $ withAgent' $ \a -> do
|
|
||||||
let auId = aUserId user
|
|
||||||
setProtocolServers a auId $ agentServerCfgs opDomains $ useServers SPSMP rs smpServers
|
|
||||||
setProtocolServers a auId $ agentServerCfgs opDomains $ useServers SPXFTP rs xftpServers
|
|
||||||
ok_
|
|
||||||
APIValidateServers userServers -> pure $ CRUserServersValidation $ validateUserServers userServers
|
|
||||||
APIGetUsageConditions -> do
|
|
||||||
(usageConditions, acceptedConditions) <- withFastStore $ \db -> do
|
|
||||||
usageConditions <- getCurrentUsageConditions db
|
|
||||||
acceptedConditions <- liftIO $ getLatestAcceptedConditions db
|
|
||||||
pure (usageConditions, acceptedConditions)
|
|
||||||
-- TODO if db commit is different from source commit, conditionsText should be nothing in response
|
|
||||||
pure
|
|
||||||
CRUsageConditions
|
|
||||||
{ usageConditions,
|
|
||||||
conditionsText = usageConditionsText,
|
|
||||||
acceptedConditions
|
|
||||||
}
|
|
||||||
APISetConditionsNotified condId -> do
|
|
||||||
currentTs <- liftIO getCurrentTime
|
|
||||||
withFastStore' $ \db -> setConditionsNotified db condId currentTs
|
|
||||||
ok_
|
|
||||||
APIAcceptConditions condId opIds -> withFastStore $ \db -> do
|
|
||||||
currentTs <- liftIO getCurrentTime
|
|
||||||
acceptConditions db condId opIds currentTs
|
|
||||||
uncurry CRServerOperators <$> getServerOperators db
|
|
||||||
APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user ->
|
APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user ->
|
||||||
checkStoreNotChanged $
|
checkStoreNotChanged $
|
||||||
withChatLock "setChatItemTTL" $ do
|
withChatLock "setChatItemTTL" $ do
|
||||||
@@ -1902,7 +1753,8 @@ processChatCommand' vr = \case
|
|||||||
canKeepLink (CRInvitationUri crData _) newUser = do
|
canKeepLink (CRInvitationUri crData _) newUser = do
|
||||||
let ConnReqUriData {crSmpQueues = q :| _} = crData
|
let ConnReqUriData {crSmpQueues = q :| _} = crData
|
||||||
SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q
|
SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q
|
||||||
newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (\db -> getProtocolServers db SPSMP newUser)
|
cfg <- asks config
|
||||||
|
newUserServers <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser)
|
||||||
pure $ smpServer `elem` newUserServers
|
pure $ smpServer `elem` newUserServers
|
||||||
updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do
|
updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do
|
||||||
withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser)
|
withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser)
|
||||||
@@ -2236,7 +2088,7 @@ processChatCommand' vr = \case
|
|||||||
where
|
where
|
||||||
changeMemberRole user gInfo members m gEvent = do
|
changeMemberRole user gInfo members m gEvent = do
|
||||||
let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m
|
let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m
|
||||||
assertUserGroupRole gInfo $ maximum ([GRAdmin, mRole, memRole] :: [GroupMemberRole])
|
assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole]
|
||||||
withGroupLock "memberRole" groupId . procCmd $ do
|
withGroupLock "memberRole" groupId . procCmd $ do
|
||||||
unless (mRole == memRole) $ do
|
unless (mRole == memRole) $ do
|
||||||
withFastStore' $ \db -> updateGroupMemberRole db user m memRole
|
withFastStore' $ \db -> updateGroupMemberRole db user m memRole
|
||||||
@@ -2634,15 +2486,14 @@ processChatCommand' vr = \case
|
|||||||
pure $ CRAgentSubsTotal user subsTotal hasSession
|
pure $ CRAgentSubsTotal user subsTotal hasSession
|
||||||
GetAgentServersSummary userId -> withUserId userId $ \user -> do
|
GetAgentServersSummary userId -> withUserId userId $ \user -> do
|
||||||
agentServersSummary <- lift $ withAgent' getAgentServersSummary
|
agentServersSummary <- lift $ withAgent' getAgentServersSummary
|
||||||
withStore' $ \db -> do
|
cfg <- asks config
|
||||||
users <- getUsers db
|
(users, smpServers, xftpServers) <-
|
||||||
smpServers <- getServers db user SPSMP
|
withStore' $ \db -> (,,) <$> getUsers db <*> getServers db cfg user SPSMP <*> getServers db cfg user SPXFTP
|
||||||
xftpServers <- getServers db user SPXFTP
|
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers
|
||||||
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers
|
pure $ CRAgentServersSummary user presentedServersSummary
|
||||||
pure $ CRAgentServersSummary user presentedServersSummary
|
|
||||||
where
|
where
|
||||||
getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p]
|
getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> ChatConfig -> User -> SProtocolType p -> IO (NonEmpty (ProtocolServer p))
|
||||||
getServers db user p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db p user
|
getServers db cfg user p = L.map (\ServerCfg {server} -> protoServer server) . useServers cfg p <$> getProtocolServers db user
|
||||||
ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_
|
ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_
|
||||||
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
|
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
|
||||||
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
|
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
|
||||||
@@ -3760,7 +3611,8 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
|
|||||||
S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks
|
S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks
|
||||||
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
|
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
|
||||||
getUnknownSrvs srvs = do
|
getUnknownSrvs srvs = do
|
||||||
knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (\db -> getProtocolServers db SPXFTP user)
|
cfg <- asks config
|
||||||
|
knownSrvs <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPXFTP <$> withStore' (`getProtocolServers` user)
|
||||||
pure $ filter (`notElem` knownSrvs) srvs
|
pure $ filter (`notElem` knownSrvs) srvs
|
||||||
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
|
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
|
||||||
ipProtectedForSrvs srvs = do
|
ipProtectedForSrvs srvs = do
|
||||||
@@ -3972,7 +3824,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
|
|||||||
(sftConns, sfts) <- getSndFileTransferConns
|
(sftConns, sfts) <- getSndFileTransferConns
|
||||||
(rftConns, rfts) <- getRcvFileTransferConns
|
(rftConns, rfts) <- getRcvFileTransferConns
|
||||||
(pcConns, pcs) <- getPendingContactConns
|
(pcConns, pcs) <- getPendingContactConns
|
||||||
let conns = concat ([ctConns, ucConns, mConns, sftConns, rftConns, pcConns] :: [[ConnId]])
|
let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]
|
||||||
pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs)
|
pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs)
|
||||||
-- subscribe using batched commands
|
-- subscribe using batched commands
|
||||||
rs <- withAgent $ \a -> agentBatchSubscribe a conns
|
rs <- withAgent $ \a -> agentBatchSubscribe a conns
|
||||||
@@ -4780,7 +4632,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct)
|
ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct)
|
||||||
SWITCH qd phase cStats -> do
|
SWITCH qd phase cStats -> do
|
||||||
toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats)
|
toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats)
|
||||||
when (phase == SPStarted || phase == SPCompleted) $ case qd of
|
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||||
QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing
|
QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing
|
||||||
QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
|
QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
|
||||||
RSYNC rss cryptoErr_ cStats ->
|
RSYNC rss cryptoErr_ cStats ->
|
||||||
@@ -5065,7 +4917,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
(Just fileDescrText, Just msgId) -> do
|
(Just fileDescrText, Just msgId) -> do
|
||||||
partSize <- asks $ xftpDescrPartSize . config
|
partSize <- asks $ xftpDescrPartSize . config
|
||||||
let parts = splitFileDescr partSize fileDescrText
|
let parts = splitFileDescr partSize fileDescrText
|
||||||
pure . L.toList $ L.map (XMsgFileDescr msgId) parts
|
pure . toList $ L.map (XMsgFileDescr msgId) parts
|
||||||
_ -> pure []
|
_ -> pure []
|
||||||
let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents
|
let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents
|
||||||
GroupMember {memberId} = sender
|
GroupMember {memberId} = sender
|
||||||
@@ -5191,7 +5043,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
when continued $ sendPendingGroupMessages user m conn
|
when continued $ sendPendingGroupMessages user m conn
|
||||||
SWITCH qd phase cStats -> do
|
SWITCH qd phase cStats -> do
|
||||||
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
|
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
|
||||||
when (phase == SPStarted || phase == SPCompleted) $ case qd of
|
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||||
QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing
|
QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing
|
||||||
QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
|
QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
|
||||||
RSYNC rss cryptoErr_ cStats ->
|
RSYNC rss cryptoErr_ cStats ->
|
||||||
@@ -6755,17 +6607,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
messageWarning "x.grp.mem.con: neither member is invitee"
|
messageWarning "x.grp.mem.con: neither member is invitee"
|
||||||
where
|
where
|
||||||
inviteeXGrpMemCon :: GroupMemberIntro -> CM ()
|
inviteeXGrpMemCon :: GroupMemberIntro -> CM ()
|
||||||
inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of
|
inviteeXGrpMemCon GroupMemberIntro {introId, introStatus}
|
||||||
GMIntroReConnected -> updateStatus introId GMIntroConnected
|
| introStatus == GMIntroReConnected = updateStatus introId GMIntroConnected
|
||||||
GMIntroToConnected -> pure ()
|
| introStatus `elem` [GMIntroToConnected, GMIntroConnected] = pure ()
|
||||||
GMIntroConnected -> pure ()
|
| otherwise = updateStatus introId GMIntroToConnected
|
||||||
_ -> updateStatus introId GMIntroToConnected
|
|
||||||
forwardMemberXGrpMemCon :: GroupMemberIntro -> CM ()
|
forwardMemberXGrpMemCon :: GroupMemberIntro -> CM ()
|
||||||
forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of
|
forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus}
|
||||||
GMIntroToConnected -> updateStatus introId GMIntroConnected
|
| introStatus == GMIntroToConnected = updateStatus introId GMIntroConnected
|
||||||
GMIntroReConnected -> pure ()
|
| introStatus `elem` [GMIntroReConnected, GMIntroConnected] = pure ()
|
||||||
GMIntroConnected -> pure ()
|
| otherwise = updateStatus introId GMIntroReConnected
|
||||||
_ -> updateStatus introId GMIntroReConnected
|
|
||||||
updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status
|
updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status
|
||||||
|
|
||||||
xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> UTCTime -> CM ()
|
xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> UTCTime -> CM ()
|
||||||
@@ -8230,24 +8080,14 @@ chatCommandP =
|
|||||||
"/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP),
|
"/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP),
|
||||||
"/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP),
|
"/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP),
|
||||||
"/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP),
|
"/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP),
|
||||||
"/_dec_operator" $> APITestServerOperator,
|
"/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP),
|
||||||
"/_dec_conditions_action" $> APITestUsageConditionsAction,
|
"/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP),
|
||||||
"/_dec_acceptance" $> APITestConditionsAcceptance,
|
"/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []),
|
||||||
"/_dec_roles" $> APITestServerRoles,
|
"/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP),
|
||||||
"/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP),
|
"/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []),
|
||||||
"/smp default" $> SetUserProtoServers (AProtocolType SPSMP) [],
|
"/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP),
|
||||||
"/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP),
|
|
||||||
"/xftp default" $> SetUserProtoServers (AProtocolType SPXFTP) [],
|
|
||||||
"/smp" $> GetUserProtoServers (AProtocolType SPSMP),
|
"/smp" $> GetUserProtoServers (AProtocolType SPSMP),
|
||||||
"/xftp" $> GetUserProtoServers (AProtocolType SPXFTP),
|
"/xftp" $> GetUserProtoServers (AProtocolType SPXFTP),
|
||||||
"/_operators" $> APIGetServerOperators,
|
|
||||||
"/_operators " *> (APISetServerOperators <$> jsonP),
|
|
||||||
"/_servers " *> (APIGetUserServers <$> A.decimal),
|
|
||||||
"/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP),
|
|
||||||
"/_validate_servers " *> (APIValidateServers <$> jsonP),
|
|
||||||
"/_conditions" $> APIGetUsageConditions,
|
|
||||||
"/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal),
|
|
||||||
"/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <*> _strP),
|
|
||||||
"/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal),
|
"/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal),
|
||||||
"/ttl " *> (SetChatItemTTL <$> ciTTL),
|
"/ttl " *> (SetChatItemTTL <$> ciTTL),
|
||||||
"/_ttl " *> (APIGetChatItemTTL <$> A.decimal),
|
"/_ttl " *> (APIGetChatItemTTL <$> A.decimal),
|
||||||
@@ -8461,6 +8301,8 @@ chatCommandP =
|
|||||||
(CPLast <$ "count=" <*> A.decimal)
|
(CPLast <$ "count=" <*> A.decimal)
|
||||||
<|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
<|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||||
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||||
|
<|> (CPAround <$ "around=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||||
|
<|> (CPInitial <$ "initial=" <*> A.decimal)
|
||||||
paginationByTimeP =
|
paginationByTimeP =
|
||||||
(PTLast <$ "count=" <*> A.decimal)
|
(PTLast <$ "count=" <*> A.decimal)
|
||||||
<|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal)
|
<|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal)
|
||||||
@@ -8589,7 +8431,7 @@ chatCommandP =
|
|||||||
onOffP
|
onOffP
|
||||||
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
|
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
|
||||||
(pure Nothing)
|
(pure Nothing)
|
||||||
-- srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
|
srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
|
||||||
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
|
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
|
||||||
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
|
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
|
||||||
char_ = optional . A.char
|
char_ = optional . A.char
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import Control.Concurrent.STM
|
|||||||
import Control.Monad
|
import Control.Monad
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.List.NonEmpty (NonEmpty (..))
|
import Data.List.NonEmpty (NonEmpty (..))
|
||||||
import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Chat.Core
|
import Simplex.Chat.Core
|
||||||
@@ -32,10 +31,10 @@ chatBotRepl welcome answer _user cc = do
|
|||||||
case resp of
|
case resp of
|
||||||
CRContactConnected _ contact _ -> do
|
CRContactConnected _ contact _ -> do
|
||||||
contactConnected contact
|
contactConnected contact
|
||||||
void $ sendMessage cc contact $ T.pack welcome
|
void $ sendMessage cc contact welcome
|
||||||
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
|
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
|
||||||
let msg = T.unpack $ ciContentToText mc
|
let msg = T.unpack $ ciContentToText mc
|
||||||
void $ sendMessage cc contact . T.pack =<< answer contact msg
|
void $ sendMessage cc contact =<< answer contact msg
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
where
|
where
|
||||||
contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected"
|
contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected"
|
||||||
@@ -58,11 +57,11 @@ initializeBotAddress' logAddress cc = do
|
|||||||
when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri)
|
when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri)
|
||||||
void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing}
|
void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing}
|
||||||
|
|
||||||
sendMessage :: ChatController -> Contact -> Text -> IO ()
|
sendMessage :: ChatController -> Contact -> String -> IO ()
|
||||||
sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText
|
sendMessage cc ct = sendComposedMessage cc ct Nothing . textMsgContent
|
||||||
|
|
||||||
sendMessage' :: ChatController -> ContactId -> Text -> IO ()
|
sendMessage' :: ChatController -> ContactId -> String -> IO ()
|
||||||
sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . MCText
|
sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . textMsgContent
|
||||||
|
|
||||||
sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgContent -> IO ()
|
sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgContent -> IO ()
|
||||||
sendComposedMessage cc = sendComposedMessage' cc . contactId'
|
sendComposedMessage cc = sendComposedMessage' cc . contactId'
|
||||||
@@ -84,6 +83,9 @@ deleteMessage cc ct chatItemId = do
|
|||||||
contactRef :: Contact -> ChatRef
|
contactRef :: Contact -> ChatRef
|
||||||
contactRef = ChatRef CTDirect . contactId'
|
contactRef = ChatRef CTDirect . contactId'
|
||||||
|
|
||||||
|
textMsgContent :: String -> MsgContent
|
||||||
|
textMsgContent = MCText . T.pack
|
||||||
|
|
||||||
printLog :: ChatController -> ChatLogLevel -> String -> IO ()
|
printLog :: ChatController -> ChatLogLevel -> String -> IO ()
|
||||||
printLog cc level s
|
printLog cc level s
|
||||||
| logLevel (config cc) <= level = putStrLn s
|
| logLevel (config cc) <= level = putStrLn s
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ data KnownContact = KnownContact
|
|||||||
}
|
}
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
knownContactNames :: [KnownContact] -> Text
|
knownContactNames :: [KnownContact] -> String
|
||||||
knownContactNames = T.intercalate ", " . map (("@" <>) . localDisplayName)
|
knownContactNames = T.unpack . T.intercalate ", " . map (("@" <>) . localDisplayName)
|
||||||
|
|
||||||
parseKnownContacts :: ReadM [KnownContact]
|
parseKnownContacts :: ReadM [KnownContact]
|
||||||
parseKnownContacts = eitherReader $ parseAll knownContactsP . encodeUtf8 . T.pack
|
parseKnownContacts = eitherReader $ parseAll knownContactsP . encodeUtf8 . T.pack
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import qualified Data.ByteArray as BA
|
|||||||
import Data.ByteString.Char8 (ByteString)
|
import Data.ByteString.Char8 (ByteString)
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.Char (ord)
|
import Data.Char (ord)
|
||||||
|
import Data.Constraint (Dict (..))
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
import Data.List.NonEmpty (NonEmpty)
|
||||||
import Data.Map.Strict (Map)
|
import Data.Map.Strict (Map)
|
||||||
@@ -56,7 +57,6 @@ import Simplex.Chat.Call
|
|||||||
import Simplex.Chat.Markdown (MarkdownList)
|
import Simplex.Chat.Markdown (MarkdownList)
|
||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Messages.CIContent
|
import Simplex.Chat.Messages.CIContent
|
||||||
import Simplex.Chat.Operators
|
|
||||||
import Simplex.Chat.Protocol
|
import Simplex.Chat.Protocol
|
||||||
import Simplex.Chat.Remote.AppVersion
|
import Simplex.Chat.Remote.AppVersion
|
||||||
import Simplex.Chat.Remote.Types
|
import Simplex.Chat.Remote.Types
|
||||||
@@ -70,7 +70,7 @@ import Simplex.Chat.Util (liftIOEither)
|
|||||||
import Simplex.FileTransfer.Description (FileDescriptionURI)
|
import Simplex.FileTransfer.Description (FileDescriptionURI)
|
||||||
import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo)
|
import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo)
|
||||||
import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo)
|
import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo)
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerRoles)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg)
|
||||||
import Simplex.Messaging.Agent.Lock
|
import Simplex.Messaging.Agent.Lock
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority)
|
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority)
|
||||||
@@ -84,7 +84,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption)
|
|||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer)
|
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
|
||||||
import Simplex.Messaging.TMap (TMap)
|
import Simplex.Messaging.TMap (TMap)
|
||||||
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
||||||
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost)
|
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost)
|
||||||
@@ -132,7 +132,7 @@ data ChatConfig = ChatConfig
|
|||||||
{ agentConfig :: AgentConfig,
|
{ agentConfig :: AgentConfig,
|
||||||
chatVRange :: VersionRangeChat,
|
chatVRange :: VersionRangeChat,
|
||||||
confirmMigrations :: MigrationConfirmation,
|
confirmMigrations :: MigrationConfirmation,
|
||||||
presetServers :: PresetServers,
|
defaultServers :: DefaultAgentServers,
|
||||||
tbqSize :: Natural,
|
tbqSize :: Natural,
|
||||||
fileChunkSize :: Integer,
|
fileChunkSize :: Integer,
|
||||||
xftpDescrPartSize :: Int,
|
xftpDescrPartSize :: Int,
|
||||||
@@ -154,11 +154,6 @@ data ChatConfig = ChatConfig
|
|||||||
chatHooks :: ChatHooks
|
chatHooks :: ChatHooks
|
||||||
}
|
}
|
||||||
|
|
||||||
data RandomServers = RandomServers
|
|
||||||
{ smpServers :: NonEmpty (NewUserServer 'PSMP),
|
|
||||||
xftpServers :: NonEmpty (NewUserServer 'PXFTP)
|
|
||||||
}
|
|
||||||
|
|
||||||
-- The hooks can be used to extend or customize chat core in mobile or CLI clients.
|
-- The hooks can be used to extend or customize chat core in mobile or CLI clients.
|
||||||
data ChatHooks = ChatHooks
|
data ChatHooks = ChatHooks
|
||||||
{ -- preCmdHook can be used to process or modify the commands before they are processed.
|
{ -- preCmdHook can be used to process or modify the commands before they are processed.
|
||||||
@@ -177,9 +172,12 @@ defaultChatHooks =
|
|||||||
eventHook = \_ -> pure
|
eventHook = \_ -> pure
|
||||||
}
|
}
|
||||||
|
|
||||||
data PresetServers = PresetServers
|
data DefaultAgentServers = DefaultAgentServers
|
||||||
{ operators :: NonEmpty PresetOperator,
|
{ smp :: NonEmpty (ServerCfg 'PSMP),
|
||||||
|
useSMP :: Int,
|
||||||
ntf :: [NtfServer],
|
ntf :: [NtfServer],
|
||||||
|
xftp :: NonEmpty (ServerCfg 'PXFTP),
|
||||||
|
useXFTP :: Int,
|
||||||
netCfg :: NetworkConfig
|
netCfg :: NetworkConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +203,6 @@ data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLite
|
|||||||
|
|
||||||
data ChatController = ChatController
|
data ChatController = ChatController
|
||||||
{ currentUser :: TVar (Maybe User),
|
{ currentUser :: TVar (Maybe User),
|
||||||
randomServers :: RandomServers,
|
|
||||||
currentRemoteHost :: TVar (Maybe RemoteHostId),
|
currentRemoteHost :: TVar (Maybe RemoteHostId),
|
||||||
firstTime :: Bool,
|
firstTime :: Bool,
|
||||||
smpAgent :: AgentClient,
|
smpAgent :: AgentClient,
|
||||||
@@ -349,22 +346,12 @@ data ChatCommand
|
|||||||
| APIGetGroupLink GroupId
|
| APIGetGroupLink GroupId
|
||||||
| APICreateMemberContact GroupId GroupMemberId
|
| APICreateMemberContact GroupId GroupMemberId
|
||||||
| APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent}
|
| APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent}
|
||||||
|
| APIGetUserProtoServers UserId AProtocolType
|
||||||
| GetUserProtoServers AProtocolType
|
| GetUserProtoServers AProtocolType
|
||||||
| SetUserProtoServers AProtocolType [AProtoServerWithAuth]
|
| APISetUserProtoServers UserId AProtoServersConfig
|
||||||
|
| SetUserProtoServers AProtoServersConfig
|
||||||
| APITestProtoServer UserId AProtoServerWithAuth
|
| APITestProtoServer UserId AProtoServerWithAuth
|
||||||
| TestProtoServer AProtoServerWithAuth
|
| TestProtoServer AProtoServerWithAuth
|
||||||
| APITestServerOperator
|
|
||||||
| APITestUsageConditionsAction
|
|
||||||
| APITestConditionsAcceptance
|
|
||||||
| APITestServerRoles
|
|
||||||
| APIGetServerOperators
|
|
||||||
| APISetServerOperators (NonEmpty ServerOperator)
|
|
||||||
| APIGetUserServers UserId
|
|
||||||
| APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers)
|
|
||||||
| APIValidateServers (NonEmpty UpdatedUserOperatorServers) -- response is CRUserServersValidation
|
|
||||||
| APIGetUsageConditions
|
|
||||||
| APISetConditionsNotified Int64
|
|
||||||
| APIAcceptConditions Int64 (NonEmpty Int64)
|
|
||||||
| APISetChatItemTTL UserId (Maybe Int64)
|
| APISetChatItemTTL UserId (Maybe Int64)
|
||||||
| SetChatItemTTL (Maybe Int64)
|
| SetChatItemTTL (Maybe Int64)
|
||||||
| APIGetChatItemTTL UserId
|
| APIGetChatItemTTL UserId
|
||||||
@@ -590,15 +577,8 @@ data ChatResponse
|
|||||||
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
|
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
|
||||||
| CRChatItemId User (Maybe ChatItemId)
|
| CRChatItemId User (Maybe ChatItemId)
|
||||||
| CRApiParsedMarkdown {formattedText :: Maybe MarkdownList}
|
| CRApiParsedMarkdown {formattedText :: Maybe MarkdownList}
|
||||||
|
| CRUserProtoServers {user :: User, servers :: AUserProtoServers}
|
||||||
| CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure}
|
| CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure}
|
||||||
| CRTestOperator {operator :: ServerOperator}
|
|
||||||
| CRTestUsageConditionsAction {conditionsAction :: Maybe UsageConditionsAction}
|
|
||||||
| CRTestConditionsAcceptance {acceptance :: ConditionsAcceptance}
|
|
||||||
| CRTestServerRoles {roles :: ServerRoles}
|
|
||||||
| CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction}
|
|
||||||
| CRUserServers {user :: User, userServers :: [UserOperatorServers]}
|
|
||||||
| CRUserServersValidation {serverErrors :: [UserServersError]}
|
|
||||||
| CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions}
|
|
||||||
| CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64}
|
| CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64}
|
||||||
| CRNetworkConfig {networkConfig :: NetworkConfig}
|
| CRNetworkConfig {networkConfig :: NetworkConfig}
|
||||||
| CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile}
|
| CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile}
|
||||||
@@ -859,6 +839,8 @@ data ChatPagination
|
|||||||
= CPLast Int
|
= CPLast Int
|
||||||
| CPAfter ChatItemId Int
|
| CPAfter ChatItemId Int
|
||||||
| CPBefore ChatItemId Int
|
| CPBefore ChatItemId Int
|
||||||
|
| CPAround ChatItemId Int
|
||||||
|
| CPInitial Int
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data PaginationByTime
|
data PaginationByTime
|
||||||
@@ -961,23 +943,23 @@ instance ToJSON AgentQueueId where
|
|||||||
toJSON = strToJSON
|
toJSON = strToJSON
|
||||||
toEncoding = strToJEncoding
|
toEncoding = strToJEncoding
|
||||||
|
|
||||||
-- data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]}
|
data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]}
|
||||||
-- deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
-- data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p)
|
data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p)
|
||||||
|
|
||||||
-- deriving instance Show AProtoServersConfig
|
deriving instance Show AProtoServersConfig
|
||||||
|
|
||||||
-- data UserProtoServers p = UserProtoServers
|
data UserProtoServers p = UserProtoServers
|
||||||
-- { serverProtocol :: SProtocolType p,
|
{ serverProtocol :: SProtocolType p,
|
||||||
-- protoServers :: NonEmpty (ServerCfg p),
|
protoServers :: NonEmpty (ServerCfg p),
|
||||||
-- presetServers :: NonEmpty (ServerCfg p)
|
presetServers :: NonEmpty (ServerCfg p)
|
||||||
-- }
|
}
|
||||||
-- deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
-- data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p)
|
data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p)
|
||||||
|
|
||||||
-- deriving instance Show AUserProtoServers
|
deriving instance Show AUserProtoServers
|
||||||
|
|
||||||
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath}
|
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
@@ -1580,28 +1562,28 @@ $(JQ.deriveJSON defaultJSON ''CoreVersionInfo)
|
|||||||
|
|
||||||
$(JQ.deriveJSON defaultJSON ''SlowSQLQuery)
|
$(JQ.deriveJSON defaultJSON ''SlowSQLQuery)
|
||||||
|
|
||||||
-- instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where
|
instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where
|
||||||
-- parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig)
|
parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig)
|
||||||
|
|
||||||
-- instance ProtocolTypeI p => FromJSON (UserProtoServers p) where
|
instance ProtocolTypeI p => FromJSON (UserProtoServers p) where
|
||||||
-- parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers)
|
parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers)
|
||||||
|
|
||||||
-- instance ProtocolTypeI p => ToJSON (UserProtoServers p) where
|
instance ProtocolTypeI p => ToJSON (UserProtoServers p) where
|
||||||
-- toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers)
|
toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers)
|
||||||
-- toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers)
|
toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers)
|
||||||
|
|
||||||
-- instance FromJSON AUserProtoServers where
|
instance FromJSON AUserProtoServers where
|
||||||
-- parseJSON v = J.withObject "AUserProtoServers" parse v
|
parseJSON v = J.withObject "AUserProtoServers" parse v
|
||||||
-- where
|
where
|
||||||
-- parse o = do
|
parse o = do
|
||||||
-- AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol"
|
AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol"
|
||||||
-- case userProtocol p of
|
case userProtocol p of
|
||||||
-- Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v
|
Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v
|
||||||
-- Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p
|
Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p
|
||||||
|
|
||||||
-- instance ToJSON AUserProtoServers where
|
instance ToJSON AUserProtoServers where
|
||||||
-- toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s
|
toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s
|
||||||
-- toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s
|
toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCS") ''RemoteCtrlSessionState)
|
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCS") ''RemoteCtrlSessionState)
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,12 @@ chatItemTs (CChatItem _ ci) = chatItemTs' ci
|
|||||||
chatItemTs' :: ChatItem c d -> UTCTime
|
chatItemTs' :: ChatItem c d -> UTCTime
|
||||||
chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs
|
chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs
|
||||||
|
|
||||||
|
chatItemCreatedAt :: CChatItem c -> UTCTime
|
||||||
|
chatItemCreatedAt (CChatItem _ ci) = chatItemCreatedAt' ci
|
||||||
|
|
||||||
|
chatItemCreatedAt' :: ChatItem c d -> UTCTime
|
||||||
|
chatItemCreatedAt' ChatItem {meta = CIMeta {createdAt}} = createdAt
|
||||||
|
|
||||||
chatItemTimed :: ChatItem c d -> Maybe CITimed
|
chatItemTimed :: ChatItem c d -> Maybe CITimed
|
||||||
chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed
|
chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20241023_chat_item_autoincrement_id :: Query
|
||||||
|
m20241023_chat_item_autoincrement_id =
|
||||||
|
[sql|
|
||||||
|
INSERT INTO sqlite_sequence (name, seq)
|
||||||
|
SELECT 'chat_items', MAX(ROWID) FROM chat_items;
|
||||||
|
|
||||||
|
PRAGMA writable_schema=1;
|
||||||
|
|
||||||
|
UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT')
|
||||||
|
WHERE name = 'chat_items' AND type = 'table';
|
||||||
|
|
||||||
|
PRAGMA writable_schema=0;
|
||||||
|
|]
|
||||||
|
|
||||||
|
down_m20241023_chat_item_autoincrement_id :: Query
|
||||||
|
down_m20241023_chat_item_autoincrement_id =
|
||||||
|
[sql|
|
||||||
|
DELETE FROM sqlite_sequence WHERE name = 'chat_items';
|
||||||
|
|
||||||
|
PRAGMA writable_schema=1;
|
||||||
|
|
||||||
|
UPDATE sqlite_master
|
||||||
|
SET sql = replace(sql, 'INTEGER PRIMARY KEY AUTOINCREMENT', 'INTEGER PRIMARY KEY')
|
||||||
|
WHERE name = 'chat_items' AND type = 'table';
|
||||||
|
|
||||||
|
PRAGMA writable_schema=0;
|
||||||
|
|]
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
|
|
||||||
module Simplex.Chat.Migrations.M20241027_server_operators where
|
|
||||||
|
|
||||||
import Database.SQLite.Simple (Query)
|
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
|
||||||
|
|
||||||
m20241027_server_operators :: Query
|
|
||||||
m20241027_server_operators =
|
|
||||||
[sql|
|
|
||||||
CREATE TABLE server_operators (
|
|
||||||
server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
server_operator_tag TEXT,
|
|
||||||
trade_name TEXT NOT NULL,
|
|
||||||
legal_name TEXT,
|
|
||||||
server_domains TEXT,
|
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
|
||||||
role_storage INTEGER NOT NULL DEFAULT 1,
|
|
||||||
role_proxy INTEGER NOT NULL DEFAULT 1,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE usage_conditions (
|
|
||||||
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
conditions_commit TEXT NOT NULL UNIQUE,
|
|
||||||
notified_at TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE operator_usage_conditions (
|
|
||||||
operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
server_operator_id INTEGER REFERENCES server_operators (server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
||||||
server_operator_tag TEXT,
|
|
||||||
conditions_commit TEXT NOT NULL,
|
|
||||||
accepted_at TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(server_operator_id);
|
|
||||||
CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(conditions_commit, server_operator_id);
|
|
||||||
|]
|
|
||||||
|
|
||||||
down_m20241027_server_operators :: Query
|
|
||||||
down_m20241027_server_operators =
|
|
||||||
[sql|
|
|
||||||
DROP INDEX idx_operator_usage_conditions_conditions_commit;
|
|
||||||
DROP INDEX idx_operator_usage_conditions_server_operator_id;
|
|
||||||
|
|
||||||
DROP TABLE operator_usage_conditions;
|
|
||||||
DROP TABLE usage_conditions;
|
|
||||||
DROP TABLE server_operators;
|
|
||||||
|]
|
|
||||||
@@ -360,7 +360,7 @@ CREATE TABLE pending_group_messages(
|
|||||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||||
);
|
);
|
||||||
CREATE TABLE chat_items(
|
CREATE TABLE chat_items(
|
||||||
chat_item_id INTEGER PRIMARY KEY,
|
chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||||
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
|
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
|
||||||
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
||||||
@@ -399,6 +399,7 @@ CREATE TABLE chat_items(
|
|||||||
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
|
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
|
||||||
via_proxy INTEGER
|
via_proxy INTEGER
|
||||||
);
|
);
|
||||||
|
CREATE TABLE sqlite_sequence(name,seq);
|
||||||
CREATE TABLE chat_item_messages(
|
CREATE TABLE chat_item_messages(
|
||||||
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
||||||
message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE,
|
message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE,
|
||||||
@@ -429,7 +430,6 @@ CREATE TABLE commands(
|
|||||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||||
);
|
);
|
||||||
CREATE TABLE sqlite_sequence(name,seq);
|
|
||||||
CREATE TABLE settings(
|
CREATE TABLE settings(
|
||||||
settings_id INTEGER PRIMARY KEY,
|
settings_id INTEGER PRIMARY KEY,
|
||||||
chat_item_ttl INTEGER,
|
chat_item_ttl INTEGER,
|
||||||
@@ -589,33 +589,6 @@ CREATE TABLE note_folders(
|
|||||||
unread_chat INTEGER NOT NULL DEFAULT 0
|
unread_chat INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
CREATE TABLE app_settings(app_settings TEXT NOT NULL);
|
CREATE TABLE app_settings(app_settings TEXT NOT NULL);
|
||||||
CREATE TABLE server_operators(
|
|
||||||
server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
server_operator_tag TEXT,
|
|
||||||
trade_name TEXT NOT NULL,
|
|
||||||
legal_name TEXT,
|
|
||||||
server_domains TEXT,
|
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
|
||||||
role_storage INTEGER NOT NULL DEFAULT 1,
|
|
||||||
role_proxy INTEGER NOT NULL DEFAULT 1,
|
|
||||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
|
||||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
|
||||||
);
|
|
||||||
CREATE TABLE usage_conditions(
|
|
||||||
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
conditions_commit TEXT NOT NULL UNIQUE,
|
|
||||||
notified_at TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
|
||||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
|
||||||
);
|
|
||||||
CREATE TABLE operator_usage_conditions(
|
|
||||||
operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE,
|
|
||||||
server_operator_tag TEXT,
|
|
||||||
conditions_commit TEXT NOT NULL,
|
|
||||||
accepted_at TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
|
||||||
);
|
|
||||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||||
display_name,
|
display_name,
|
||||||
full_name
|
full_name
|
||||||
@@ -917,10 +890,3 @@ CREATE INDEX idx_received_probes_group_member_id on received_probes(
|
|||||||
group_member_id
|
group_member_id
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id);
|
CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id);
|
||||||
CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(
|
|
||||||
server_operator_id
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(
|
|
||||||
conditions_commit,
|
|
||||||
server_operator_id
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,447 +0,0 @@
|
|||||||
{-# LANGUAGE DataKinds #-}
|
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
|
||||||
{-# LANGUAGE GADTs #-}
|
|
||||||
{-# LANGUAGE KindSignatures #-}
|
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
{-# LANGUAGE OverloadedLists #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE PatternSynonyms #-}
|
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
|
||||||
{-# LANGUAGE StandaloneDeriving #-}
|
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE TupleSections #-}
|
|
||||||
{-# LANGUAGE TypeApplications #-}
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
||||||
|
|
||||||
module Simplex.Chat.Operators where
|
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
|
||||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
|
||||||
import qualified Data.Aeson as J
|
|
||||||
import qualified Data.Aeson.Encoding as JE
|
|
||||||
import qualified Data.Aeson.TH as JQ
|
|
||||||
import Data.FileEmbed
|
|
||||||
import Data.Foldable (foldMap')
|
|
||||||
import Data.IORef
|
|
||||||
import Data.Int (Int64)
|
|
||||||
import Data.List (find, foldl')
|
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
|
||||||
import qualified Data.List.NonEmpty as L
|
|
||||||
import Data.Map.Strict (Map)
|
|
||||||
import qualified Data.Map.Strict as M
|
|
||||||
import Data.Maybe (fromMaybe, isNothing, mapMaybe)
|
|
||||||
import Data.Scientific (floatingOrInteger)
|
|
||||||
import Data.Set (Set)
|
|
||||||
import qualified Data.Set as S
|
|
||||||
import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import Data.Time (addUTCTime)
|
|
||||||
import Data.Time.Clock (UTCTime, nominalDay)
|
|
||||||
import Data.Type.Equality
|
|
||||||
import Database.SQLite.Simple.FromField (FromField (..))
|
|
||||||
import Database.SQLite.Simple.ToField (ToField (..))
|
|
||||||
import Language.Haskell.TH.Syntax (lift)
|
|
||||||
import Simplex.Chat.Operators.Conditions
|
|
||||||
import Simplex.Chat.Types.Util (textParseJSON)
|
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles)
|
|
||||||
import Simplex.Messaging.Encoding.String
|
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON)
|
|
||||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol)
|
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
|
||||||
import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8, (<$?>))
|
|
||||||
|
|
||||||
usageConditionsCommit :: Text
|
|
||||||
usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c"
|
|
||||||
|
|
||||||
previousConditionsCommit :: Text
|
|
||||||
previousConditionsCommit = "edf99fcd1d7d38d2501d19608b94c084cf00f2ac"
|
|
||||||
|
|
||||||
usageConditionsText :: Text
|
|
||||||
usageConditionsText =
|
|
||||||
$( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md")
|
|
||||||
in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|]
|
|
||||||
)
|
|
||||||
|
|
||||||
data DBStored = DBStored | DBNew
|
|
||||||
|
|
||||||
data SDBStored (s :: DBStored) where
|
|
||||||
SDBStored :: SDBStored 'DBStored
|
|
||||||
SDBNew :: SDBStored 'DBNew
|
|
||||||
|
|
||||||
deriving instance Show (SDBStored s)
|
|
||||||
|
|
||||||
class DBStoredI s where sdbStored :: SDBStored s
|
|
||||||
|
|
||||||
instance DBStoredI 'DBStored where sdbStored = SDBStored
|
|
||||||
|
|
||||||
instance DBStoredI 'DBNew where sdbStored = SDBNew
|
|
||||||
|
|
||||||
instance TestEquality SDBStored where
|
|
||||||
testEquality SDBStored SDBStored = Just Refl
|
|
||||||
testEquality SDBNew SDBNew = Just Refl
|
|
||||||
testEquality _ _ = Nothing
|
|
||||||
|
|
||||||
data DBEntityId' (s :: DBStored) where
|
|
||||||
DBEntityId :: Int64 -> DBEntityId' 'DBStored
|
|
||||||
DBNewEntity :: DBEntityId' 'DBNew
|
|
||||||
|
|
||||||
deriving instance Show (DBEntityId' s)
|
|
||||||
|
|
||||||
type DBEntityId = DBEntityId' 'DBStored
|
|
||||||
|
|
||||||
type DBNewEntity = DBEntityId' 'DBNew
|
|
||||||
|
|
||||||
data ADBEntityId = forall s. DBStoredI s => AEI (SDBStored s) (DBEntityId' s)
|
|
||||||
|
|
||||||
pattern ADBEntityId :: Int64 -> ADBEntityId
|
|
||||||
pattern ADBEntityId i = AEI SDBStored (DBEntityId i)
|
|
||||||
|
|
||||||
pattern ADBNewEntity :: ADBEntityId
|
|
||||||
pattern ADBNewEntity = AEI SDBNew DBNewEntity
|
|
||||||
|
|
||||||
data OperatorTag = OTSimplex | OTXyz
|
|
||||||
deriving (Eq, Ord, Show)
|
|
||||||
|
|
||||||
instance FromField OperatorTag where fromField = fromTextField_ textDecode
|
|
||||||
|
|
||||||
instance ToField OperatorTag where toField = toField . textEncode
|
|
||||||
|
|
||||||
instance FromJSON OperatorTag where
|
|
||||||
parseJSON = textParseJSON "OperatorTag"
|
|
||||||
|
|
||||||
instance ToJSON OperatorTag where
|
|
||||||
toJSON = J.String . textEncode
|
|
||||||
toEncoding = JE.text . textEncode
|
|
||||||
|
|
||||||
instance TextEncoding OperatorTag where
|
|
||||||
textDecode = \case
|
|
||||||
"simplex" -> Just OTSimplex
|
|
||||||
"xyz" -> Just OTXyz
|
|
||||||
_ -> Nothing
|
|
||||||
textEncode = \case
|
|
||||||
OTSimplex -> "simplex"
|
|
||||||
OTXyz -> "xyz"
|
|
||||||
|
|
||||||
-- this and other types only define instances of serialization for known DB IDs only,
|
|
||||||
-- entities without IDs cannot be serialized to JSON
|
|
||||||
instance FromField DBEntityId where fromField f = DBEntityId <$> fromField f
|
|
||||||
|
|
||||||
instance ToField DBEntityId where toField (DBEntityId i) = toField i
|
|
||||||
|
|
||||||
data UsageConditions = UsageConditions
|
|
||||||
{ conditionsId :: Int64,
|
|
||||||
conditionsCommit :: Text,
|
|
||||||
notifiedAt :: Maybe UTCTime,
|
|
||||||
createdAt :: UTCTime
|
|
||||||
}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
data UsageConditionsAction
|
|
||||||
= UCAReview {operators :: [ServerOperator], deadline :: Maybe UTCTime, showNotice :: Bool}
|
|
||||||
| UCAAccepted {operators :: [ServerOperator]}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction
|
|
||||||
usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do
|
|
||||||
let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators
|
|
||||||
if
|
|
||||||
| null enabledOperators -> Nothing
|
|
||||||
| all conditionsAccepted enabledOperators ->
|
|
||||||
let acceptedForOperators = filter conditionsAccepted operators
|
|
||||||
in Just $ UCAAccepted acceptedForOperators
|
|
||||||
| otherwise ->
|
|
||||||
let acceptForOperators = filter (not . conditionsAccepted) enabledOperators
|
|
||||||
deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt)
|
|
||||||
showNotice = isNothing notifiedAt
|
|
||||||
in Just $ UCAReview acceptForOperators deadline showNotice
|
|
||||||
|
|
||||||
conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime
|
|
||||||
conditionsRequiredOrDeadline createdAt notifiedAtOrNow =
|
|
||||||
if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt
|
|
||||||
then Just $ conditionsDeadline notifiedAtOrNow
|
|
||||||
else Nothing -- required
|
|
||||||
where
|
|
||||||
conditionsDeadline :: UTCTime -> UTCTime
|
|
||||||
conditionsDeadline = addUTCTime (31 * nominalDay)
|
|
||||||
|
|
||||||
data ConditionsAcceptance
|
|
||||||
= CAAccepted {acceptedAt :: Maybe UTCTime}
|
|
||||||
| CARequired {deadline :: Maybe UTCTime}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
type ServerOperator = ServerOperator' 'DBStored
|
|
||||||
|
|
||||||
type NewServerOperator = ServerOperator' 'DBNew
|
|
||||||
|
|
||||||
data AServerOperator = forall s. ASO (SDBStored s) (ServerOperator' s)
|
|
||||||
|
|
||||||
deriving instance Show AServerOperator
|
|
||||||
|
|
||||||
data ServerOperator' s = ServerOperator
|
|
||||||
{ operatorId :: DBEntityId' s,
|
|
||||||
operatorTag :: Maybe OperatorTag,
|
|
||||||
tradeName :: Text,
|
|
||||||
legalName :: Maybe Text,
|
|
||||||
serverDomains :: [Text],
|
|
||||||
conditionsAcceptance :: ConditionsAcceptance,
|
|
||||||
enabled :: Bool,
|
|
||||||
roles :: ServerRoles
|
|
||||||
}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
conditionsAccepted :: ServerOperator -> Bool
|
|
||||||
conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of
|
|
||||||
CAAccepted {} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
data UserOperatorServers = UserOperatorServers
|
|
||||||
{ operator :: Maybe ServerOperator,
|
|
||||||
smpServers :: [UserServer 'PSMP],
|
|
||||||
xftpServers :: [UserServer 'PXFTP]
|
|
||||||
}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
data UpdatedUserOperatorServers = UpdatedUserOperatorServers
|
|
||||||
{ operator :: Maybe ServerOperator,
|
|
||||||
smpServers :: [AUserServer 'PSMP],
|
|
||||||
xftpServers :: [AUserServer 'PXFTP]
|
|
||||||
}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
updatedServers :: UserProtocol p => UpdatedUserOperatorServers -> SProtocolType p -> [AUserServer p]
|
|
||||||
updatedServers UpdatedUserOperatorServers {smpServers, xftpServers} = \case
|
|
||||||
SPSMP -> smpServers
|
|
||||||
SPXFTP -> xftpServers
|
|
||||||
|
|
||||||
type UserServer p = UserServer' 'DBStored p
|
|
||||||
|
|
||||||
type NewUserServer p = UserServer' 'DBNew p
|
|
||||||
|
|
||||||
data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p)
|
|
||||||
|
|
||||||
deriving instance Show (AUserServer p)
|
|
||||||
|
|
||||||
data UserServer' s p = UserServer
|
|
||||||
{ serverId :: DBEntityId' s,
|
|
||||||
server :: ProtoServerWithAuth p,
|
|
||||||
preset :: Bool,
|
|
||||||
tested :: Maybe Bool,
|
|
||||||
enabled :: Bool,
|
|
||||||
deleted :: Bool
|
|
||||||
}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
data PresetOperator = PresetOperator
|
|
||||||
{ operator :: Maybe NewServerOperator,
|
|
||||||
smp :: [NewUserServer 'PSMP],
|
|
||||||
useSMP :: Int,
|
|
||||||
xftp :: [NewUserServer 'PXFTP],
|
|
||||||
useXFTP :: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p]
|
|
||||||
operatorServers p PresetOperator {smp, xftp} = case p of
|
|
||||||
SPSMP -> smp
|
|
||||||
SPXFTP -> xftp
|
|
||||||
|
|
||||||
operatorServersToUse :: UserProtocol p => SProtocolType p -> PresetOperator -> Int
|
|
||||||
operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of
|
|
||||||
SPSMP -> useSMP
|
|
||||||
SPXFTP -> useXFTP
|
|
||||||
|
|
||||||
presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p
|
|
||||||
presetServer enabled server =
|
|
||||||
UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled, deleted = False}
|
|
||||||
|
|
||||||
-- This function should be used inside DB transaction to update conditions in the database
|
|
||||||
-- it evaluates to (conditions to mark as accepted to SimpleX operator, current conditions, and conditions to add)
|
|
||||||
usageConditionsToAdd :: Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions])
|
|
||||||
usageConditionsToAdd = usageConditionsToAdd' previousConditionsCommit usageConditionsCommit
|
|
||||||
|
|
||||||
-- This function is used in unit tests
|
|
||||||
usageConditionsToAdd' :: Text -> Text -> Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions])
|
|
||||||
usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case
|
|
||||||
[]
|
|
||||||
| newUser -> (Just sourceCond, sourceCond, [sourceCond])
|
|
||||||
| otherwise -> (Just prevCond, sourceCond, [prevCond, sourceCond])
|
|
||||||
where
|
|
||||||
prevCond = conditions 1 prevCommit
|
|
||||||
sourceCond = conditions 2 sourceCommit
|
|
||||||
conds
|
|
||||||
| hasSourceCond -> (Nothing, last conds, [])
|
|
||||||
| otherwise -> (Nothing, sourceCond, [sourceCond])
|
|
||||||
where
|
|
||||||
hasSourceCond = any ((sourceCommit ==) . conditionsCommit) conds
|
|
||||||
sourceCond = conditions cId sourceCommit
|
|
||||||
cId = maximum (map conditionsId conds) + 1
|
|
||||||
where
|
|
||||||
conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt}
|
|
||||||
|
|
||||||
-- This function should be used inside DB transaction to update operators.
|
|
||||||
-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings,
|
|
||||||
-- and preserves custom operators without tags for forward compatibility.
|
|
||||||
updatedServerOperators :: NonEmpty PresetOperator -> [ServerOperator] -> [AServerOperator]
|
|
||||||
updatedServerOperators presetOps storedOps =
|
|
||||||
foldr addPreset [] presetOps
|
|
||||||
<> map (ASO SDBStored) (filter (isNothing . operatorTag) storedOps)
|
|
||||||
where
|
|
||||||
-- TODO remove domains of preset operators from custom
|
|
||||||
addPreset PresetOperator {operator} = case operator of
|
|
||||||
Nothing -> id
|
|
||||||
Just presetOp -> (storedOp' :)
|
|
||||||
where
|
|
||||||
storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of
|
|
||||||
Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} ->
|
|
||||||
ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, roles}
|
|
||||||
Nothing -> ASO SDBNew presetOp
|
|
||||||
|
|
||||||
-- This function should be used inside DB transaction to update servers.
|
|
||||||
updatedUserServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p)
|
|
||||||
updatedUserServers _ _ randomSrvs [] = L.map (AUS SDBNew) randomSrvs
|
|
||||||
updatedUserServers p presetOps randomSrvs srvs =
|
|
||||||
fromMaybe (L.map (AUS SDBNew) randomSrvs) (L.nonEmpty updatedSrvs)
|
|
||||||
where
|
|
||||||
updatedSrvs = map userServer presetSrvs <> map (AUS SDBStored) (filter customServer srvs)
|
|
||||||
storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p)
|
|
||||||
storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs
|
|
||||||
customServer :: UserServer p -> Bool
|
|
||||||
customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv)
|
|
||||||
presetSrvs :: [NewUserServer p]
|
|
||||||
presetSrvs = concatMap (operatorServers p) presetOps
|
|
||||||
presetHosts :: Set TransportHost
|
|
||||||
presetHosts = foldMap' (S.fromList . L.toList . srvHost) presetSrvs
|
|
||||||
userServer :: NewUserServer p -> AUserServer p
|
|
||||||
userServer srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs)
|
|
||||||
|
|
||||||
srvHost :: UserServer' s p -> NonEmpty TransportHost
|
|
||||||
srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv
|
|
||||||
|
|
||||||
agentServerCfgs :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p)
|
|
||||||
agentServerCfgs opDomains = L.map agentServer
|
|
||||||
where
|
|
||||||
agentServer :: UserServer' s p -> ServerCfg p
|
|
||||||
agentServer srv@UserServer {server, enabled} =
|
|
||||||
case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of
|
|
||||||
Just (_, ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles}) ->
|
|
||||||
ServerCfg {server, operator = Just opId, enabled = opEnabled && enabled, roles}
|
|
||||||
Nothing ->
|
|
||||||
ServerCfg {server, operator = Nothing, enabled, roles = allRoles}
|
|
||||||
|
|
||||||
matchingHost :: Text -> TransportHost -> Bool
|
|
||||||
matchingHost d = \case
|
|
||||||
THDomainName h -> d `T.isSuffixOf` T.pack h
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
operatorDomains :: [ServerOperator] -> [(Text, ServerOperator)]
|
|
||||||
operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) []
|
|
||||||
|
|
||||||
groupByOperator :: ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers]
|
|
||||||
groupByOperator (ops, smpSrvs, xftpSrvs) = do
|
|
||||||
ss <- mapM (\op -> (serverDomains op,) <$> newIORef (UserOperatorServers (Just op) [] [])) ops
|
|
||||||
custom <- newIORef $ UserOperatorServers Nothing [] []
|
|
||||||
mapM_ (addServer ss custom addSMP) (reverse smpSrvs)
|
|
||||||
mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs)
|
|
||||||
mapM (readIORef . snd) ss
|
|
||||||
where
|
|
||||||
addServer :: [([Text], IORef UserOperatorServers)] -> IORef UserOperatorServers -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO ()
|
|
||||||
addServer ss custom add srv =
|
|
||||||
let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss
|
|
||||||
in atomicModifyIORef'_ v $ add srv
|
|
||||||
addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers}
|
|
||||||
addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers}
|
|
||||||
|
|
||||||
data UserServersError
|
|
||||||
= USEStorageMissing {protocol :: AProtocolType}
|
|
||||||
| USEProxyMissing {protocol :: AProtocolType}
|
|
||||||
| USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: AProtoServerWithAuth, duplicateHost :: TransportHost}
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
validateUserServers :: NonEmpty UpdatedUserOperatorServers -> [UserServersError]
|
|
||||||
validateUserServers uss =
|
|
||||||
missingRolesErr SPSMP storage USEStorageMissing
|
|
||||||
<> missingRolesErr SPSMP proxy USEProxyMissing
|
|
||||||
<> missingRolesErr SPXFTP storage USEStorageMissing
|
|
||||||
<> duplicatServerErrs SPSMP
|
|
||||||
<> duplicatServerErrs SPXFTP
|
|
||||||
where
|
|
||||||
missingRolesErr :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> (AProtocolType -> UserServersError) -> [UserServersError]
|
|
||||||
missingRolesErr p roleSel err = [err (AProtocolType p) | hasRole]
|
|
||||||
where
|
|
||||||
hasRole =
|
|
||||||
any (\(AUS _ UserServer {deleted, enabled}) -> enabled && not deleted) $
|
|
||||||
concatMap (`updatedServers` p) $ filter roleEnabled (L.toList uss)
|
|
||||||
roleEnabled UpdatedUserOperatorServers {operator} =
|
|
||||||
maybe True (\ServerOperator {enabled, roles} -> enabled && roleSel roles) operator
|
|
||||||
duplicatServerErrs :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServersError]
|
|
||||||
duplicatServerErrs p = mapMaybe duplicateErr_ srvs
|
|
||||||
where
|
|
||||||
srvs =
|
|
||||||
filter (\(AUS _ UserServer {deleted}) -> not deleted) $
|
|
||||||
concatMap (`updatedServers` p) (L.toList uss)
|
|
||||||
duplicateErr_ (AUS _ srv@UserServer {server}) =
|
|
||||||
USEDuplicateServer (AProtocolType p) (AProtoServerWithAuth p server)
|
|
||||||
<$> find (`S.member` duplicateHosts) (srvHost srv)
|
|
||||||
duplicateHosts = snd $ foldl' (\acc (AUS _ srv) -> foldl' addHost acc $ srvHost srv) (S.empty, S.empty) srvs
|
|
||||||
addHost (hs, dups) h
|
|
||||||
| h `S.member` hs = (hs, S.insert h dups)
|
|
||||||
| otherwise = (S.insert h hs, dups)
|
|
||||||
|
|
||||||
instance ToJSON ADBEntityId where
|
|
||||||
toEncoding (AEI _ dbId) = toEncoding dbId
|
|
||||||
toJSON (AEI _ dbId) = toJSON dbId
|
|
||||||
|
|
||||||
instance ToJSON (DBEntityId' s) where
|
|
||||||
toEncoding = \case
|
|
||||||
DBEntityId i -> toEncoding i
|
|
||||||
DBNewEntity -> JE.null_
|
|
||||||
toJSON = \case
|
|
||||||
DBEntityId i -> toJSON i
|
|
||||||
DBNewEntity -> J.Null
|
|
||||||
|
|
||||||
instance FromJSON ADBEntityId where
|
|
||||||
parseJSON (J.Null) = pure $ AEI SDBNew DBNewEntity
|
|
||||||
parseJSON (J.Number n) = case floatingOrInteger n of
|
|
||||||
Left (_ :: Double) -> fail "bad ADBEntityId"
|
|
||||||
Right i -> pure $ AEI SDBStored (DBEntityId $ fromInteger i)
|
|
||||||
parseJSON _ = fail "bad ADBEntityId"
|
|
||||||
|
|
||||||
instance DBStoredI s => FromJSON (DBEntityId' s) where
|
|
||||||
parseJSON v = (\(AEI _ dbId) -> checkDBStored dbId) <$?> parseJSON v
|
|
||||||
|
|
||||||
checkDBStored :: forall t s s'. (DBStoredI s, DBStoredI s') => t s' -> Either String (t s)
|
|
||||||
checkDBStored x = case testEquality (sdbStored @s) (sdbStored @s') of
|
|
||||||
Just Refl -> Right x
|
|
||||||
Nothing -> Left "bad DBStored"
|
|
||||||
|
|
||||||
$(JQ.deriveJSON defaultJSON ''UsageConditions)
|
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance)
|
|
||||||
|
|
||||||
instance ToJSON (ServerOperator' s) where
|
|
||||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerOperator')
|
|
||||||
toJSON = $(JQ.mkToJSON defaultJSON ''ServerOperator')
|
|
||||||
|
|
||||||
instance DBStoredI s => FromJSON (ServerOperator' s) where
|
|
||||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator')
|
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction)
|
|
||||||
|
|
||||||
instance ProtocolTypeI p => ToJSON (UserServer' s p) where
|
|
||||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer')
|
|
||||||
toJSON = $(JQ.mkToJSON defaultJSON ''UserServer')
|
|
||||||
|
|
||||||
instance (DBStoredI s, ProtocolTypeI p) => FromJSON (UserServer' s p) where
|
|
||||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer')
|
|
||||||
|
|
||||||
instance ProtocolTypeI p => FromJSON (AUserServer p) where
|
|
||||||
parseJSON v = (AUS SDBStored <$> parseJSON v) <|> (AUS SDBNew <$> parseJSON v)
|
|
||||||
|
|
||||||
$(JQ.deriveJSON defaultJSON ''UserOperatorServers)
|
|
||||||
|
|
||||||
instance FromJSON UpdatedUserOperatorServers where
|
|
||||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''UpdatedUserOperatorServers)
|
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Simplex.Chat.Operators.Conditions where
|
|
||||||
|
|
||||||
import Data.Char (isSpace)
|
|
||||||
import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
|
|
||||||
stripFrontMatter :: Text -> Text
|
|
||||||
stripFrontMatter =
|
|
||||||
T.unlines
|
|
||||||
. dropWhile ("# " `T.isPrefixOf`) -- strip title
|
|
||||||
. dropWhile (T.all isSpace)
|
|
||||||
. dropWhile fm
|
|
||||||
. (\ls -> let ls' = dropWhile (not . fm) ls in if null ls' then ls else ls')
|
|
||||||
. dropWhile fm
|
|
||||||
. T.lines
|
|
||||||
where
|
|
||||||
fm = ("---" `T.isPrefixOf`)
|
|
||||||
@@ -7,6 +7,7 @@ module Simplex.Chat.Stats where
|
|||||||
|
|
||||||
import qualified Data.Aeson.TH as J
|
import qualified Data.Aeson.TH as J
|
||||||
import Data.List (partition)
|
import Data.List (partition)
|
||||||
|
import Data.List.NonEmpty (NonEmpty)
|
||||||
import Data.Map.Strict (Map)
|
import Data.Map.Strict (Map)
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Maybe (fromMaybe, isJust)
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
@@ -130,7 +131,7 @@ data NtfServerSummary = NtfServerSummary
|
|||||||
-- - users are passed to exclude hidden users from totalServersSummary;
|
-- - users are passed to exclude hidden users from totalServersSummary;
|
||||||
-- - if currentUser is hidden, it should be accounted in totalServersSummary;
|
-- - if currentUser is hidden, it should be accounted in totalServersSummary;
|
||||||
-- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs
|
-- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs
|
||||||
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> [NtfServer] -> PresentedServersSummary
|
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> NonEmpty SMPServer -> NonEmpty XFTPServer -> [NtfServer] -> PresentedServersSummary
|
||||||
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do
|
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do
|
||||||
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
|
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
|
||||||
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
|
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
|
||||||
|
|||||||
+345
-151
@@ -3,6 +3,7 @@
|
|||||||
{-# LANGUAGE GADTs #-}
|
{-# LANGUAGE GADTs #-}
|
||||||
{-# LANGUAGE KindSignatures #-}
|
{-# LANGUAGE KindSignatures #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PatternSynonyms #-}
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
@@ -951,33 +952,37 @@ getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagin
|
|||||||
getDirectChat db vr user contactId pagination search_ = do
|
getDirectChat db vr user contactId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
ct <- getContact db vr user contactId
|
ct <- getContact db vr user contactId
|
||||||
liftIO $ case pagination of
|
case pagination of
|
||||||
CPLast count -> getDirectChatLast_ db user ct count search
|
CPLast count -> liftIO $ getDirectChatLast_ db user ct count search
|
||||||
CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search
|
CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search
|
||||||
CPBefore beforeId count -> getDirectChatBefore_ db user ct beforeId count search
|
CPBefore beforeId count -> getDirectChatBefore_ db user ct beforeId count search
|
||||||
|
CPAround aroundId count -> getDirectChatAround_ db user ct aroundId count search
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
getDirectChatInitial_ db user ct count
|
||||||
|
|
||||||
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
|
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
|
||||||
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect)
|
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect)
|
||||||
getDirectChatLast_ db user@User {userId} ct@Contact {contactId} count search = do
|
getDirectChatLast_ db user ct count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getDirectChatItemIdsLast_
|
chatItemIds <- getDirectChatItemIdsLast_ db user ct count search
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
||||||
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
||||||
where
|
|
||||||
getDirectChatItemIdsLast_ :: IO [ChatItemId]
|
getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO [ChatItemId]
|
||||||
getDirectChatItemIdsLast_ =
|
getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, count)
|
(userId, contactId, search, count)
|
||||||
|
|
||||||
safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect)
|
safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect)
|
||||||
safeGetDirectItem db user ct currentTs itemId =
|
safeGetDirectItem db user ct currentTs itemId =
|
||||||
@@ -1021,51 +1026,96 @@ getDirectChatItemLast db user@User {userId} contactId = do
|
|||||||
(userId, contactId)
|
(userId, contactId)
|
||||||
getDirectChatItem db user contactId chatItemId
|
getDirectChatItem db user contactId chatItemId
|
||||||
|
|
||||||
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> IO (Chat 'CTDirect)
|
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
getDirectChatAfter_ db user@User {userId} ct@Contact {contactId} afterChatItemId count search = do
|
getDirectChatAfter_ db user ct@Contact {contactId} afterChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getDirectChatItemIdsAfter_
|
afterChatItem <- getDirectChatItem db user contactId afterChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getDirectChatItemIdsAfter_ db user ct afterChatItemId count search (chatItemCreatedAt afterChatItem)
|
||||||
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
||||||
pure $ Chat (DirectChat ct) chatItems stats
|
pure $ Chat (DirectChat ct) chatItems stats
|
||||||
where
|
|
||||||
getDirectChatItemIdsAfter_ :: IO [ChatItemId]
|
|
||||||
getDirectChatItemIdsAfter_ =
|
|
||||||
map fromOnly
|
|
||||||
<$> DB.query
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT chat_item_id
|
|
||||||
FROM chat_items
|
|
||||||
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
|
||||||
AND chat_item_id > ?
|
|
||||||
ORDER BY created_at ASC, chat_item_id ASC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|
||||||
(userId, contactId, search, afterChatItemId, count)
|
|
||||||
|
|
||||||
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> IO (Chat 'CTDirect)
|
getDirectChatItemIdsAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
getDirectChatBefore_ db user@User {userId} ct@Contact {contactId} beforeChatItemId count search = do
|
getDirectChatItemIdsAfter_ db User {userId} Contact {contactId} afterChatItemId count search afterChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
|
||||||
|
ORDER BY created_at ASC, chat_item_id ASC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, contactId, search, afterChatItemCreatedAt, afterChatItemCreatedAt, afterChatItemId, count)
|
||||||
|
|
||||||
|
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
|
getDirectChatBefore_ db user ct@Contact {contactId} beforeChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getDirectChatItemsIdsBefore_
|
beforeChatItem <- getDirectChatItem db user contactId beforeChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getDirectChatItemsIdsBefore_ db user ct beforeChatItemId count search (chatItemCreatedAt beforeChatItem)
|
||||||
chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) chatItemIds
|
||||||
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
pure $ Chat (DirectChat ct) (reverse chatItems) stats
|
||||||
|
|
||||||
|
getDirectChatItemsIdsBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
|
getDirectChatItemsIdsBefore_ db User {userId} Contact {contactId} beforeChatItemId count search beforeChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
|
||||||
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, contactId, search, beforeChatItemCreatedAt, beforeChatItemCreatedAt, beforeChatItemId, count)
|
||||||
|
|
||||||
|
getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
|
getDirectChatAround_ db user ct@Contact {contactId} aroundItemId count search = do
|
||||||
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
middleChatItem <- getDirectChatItem db user contactId aroundItemId
|
||||||
|
beforeIds <- liftIO $ getDirectChatItemsIdsBefore_ db user ct aroundItemId fetchCountBefore search (chatItemCreatedAt middleChatItem)
|
||||||
|
afterIds <- liftIO $ getDirectChatItemIdsAfter_ db user ct aroundItemId fetchCountAfter search (chatItemCreatedAt middleChatItem)
|
||||||
|
currentTs <- liftIO getCurrentTime
|
||||||
|
beforeChatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) beforeIds
|
||||||
|
afterChatItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) afterIds
|
||||||
|
let remainingAfter = fetchCountAfter - length afterIds
|
||||||
|
let remainingBefore = fetchCountBefore - length beforeIds
|
||||||
|
if
|
||||||
|
| remainingBefore > 0 && remainingAfter <= 0 -> do
|
||||||
|
extraAfterIds <- liftIO $ getDirectChatItemIdsAfter_ db user ct (last afterIds) remainingBefore search (chatItemCreatedAt (last afterChatItems))
|
||||||
|
extraAfterItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) extraAfterIds
|
||||||
|
pure $ Chat (DirectChat ct) (reverse beforeChatItems <> [middleChatItem] <> afterChatItems <> extraAfterItems) stats
|
||||||
|
| remainingAfter > 0 && remainingBefore <= 0 -> do
|
||||||
|
extraBeforeIds <- liftIO $ getDirectChatItemsIdsBefore_ db user ct (last beforeIds) remainingAfter search (chatItemCreatedAt (last beforeChatItems))
|
||||||
|
extraBeforeItems <- liftIO $ mapM (safeGetDirectItem db user ct currentTs) extraBeforeIds
|
||||||
|
pure $ Chat (DirectChat ct) (reverse (beforeChatItems <> extraBeforeItems) <> [middleChatItem] <> afterChatItems) stats
|
||||||
|
| otherwise ->
|
||||||
|
pure $ Chat (DirectChat ct) (reverse beforeChatItems <> [middleChatItem] <> afterChatItems) stats
|
||||||
|
|
||||||
|
getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Int -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||||
|
getDirectChatInitial_ db user@User {userId} ct@Contact {contactId} count = do
|
||||||
|
firstUnreadItemId_ <- liftIO getDirectChatMinUnreadItemId_
|
||||||
|
case firstUnreadItemId_ of
|
||||||
|
Just firstUnreadItemId -> getDirectChatAround_ db user ct firstUnreadItemId count ""
|
||||||
|
Nothing -> liftIO $ getDirectChatLast_ db user ct count ""
|
||||||
where
|
where
|
||||||
getDirectChatItemsIdsBefore_ :: IO [ChatItemId]
|
getDirectChatMinUnreadItemId_ :: IO (Maybe ChatItemId)
|
||||||
getDirectChatItemsIdsBefore_ =
|
getDirectChatMinUnreadItemId_ =
|
||||||
map fromOnly
|
fmap join . maybeFirstRow fromOnly $
|
||||||
<$> DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT MIN(chat_item_id)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND contact_id = ? AND item_status = ?
|
||||||
AND chat_item_id < ?
|
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, beforeChatItemId, count)
|
(userId, contactId, CISRcvNew)
|
||||||
|
|
||||||
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
|
getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
getGroupChat db vr user groupId pagination search_ = do
|
getGroupChat db vr user groupId pagination search_ = do
|
||||||
@@ -1075,28 +1125,32 @@ getGroupChat db vr user groupId pagination search_ = do
|
|||||||
CPLast count -> liftIO $ getGroupChatLast_ db user g count search
|
CPLast count -> liftIO $ getGroupChatLast_ db user g count search
|
||||||
CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search
|
CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search
|
||||||
CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search
|
CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search
|
||||||
|
CPAround aroundId count -> getGroupChatAround_ db user g aroundId count search
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
getGroupChatInitial_ db user g count
|
||||||
|
|
||||||
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO (Chat 'CTGroup)
|
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO (Chat 'CTGroup)
|
||||||
getGroupChatLast_ db user@User {userId} g@GroupInfo {groupId} count search = do
|
getGroupChatLast_ db user g count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getGroupChatItemIdsLast_
|
chatItemIds <- getGroupChatItemIdsLast_ db user g count search
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
chatItems <- mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
chatItems <- mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
||||||
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
||||||
where
|
|
||||||
getGroupChatItemIdsLast_ :: IO [ChatItemId]
|
getGroupChatItemIdsLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO [ChatItemId]
|
||||||
getGroupChatItemIdsLast_ =
|
getGroupChatItemIdsLast_ db User {userId} GroupInfo {groupId} count search =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY item_ts DESC, chat_item_id DESC
|
ORDER BY item_ts DESC, chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, groupId, search, count)
|
(userId, groupId, search, count)
|
||||||
|
|
||||||
safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup)
|
safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup)
|
||||||
safeGetGroupItem db user g currentTs itemId =
|
safeGetGroupItem db user g currentTs itemId =
|
||||||
@@ -1141,83 +1195,130 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do
|
|||||||
getGroupChatItem db user groupId chatItemId
|
getGroupChatItem db user groupId chatItemId
|
||||||
|
|
||||||
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
getGroupChatAfter_ db user@User {userId} g@GroupInfo {groupId} afterChatItemId count search = do
|
getGroupChatAfter_ db user g@GroupInfo {groupId} afterChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
afterChatItem <- getGroupChatItem db user groupId afterChatItemId
|
afterChatItem <- getGroupChatItem db user groupId afterChatItemId
|
||||||
chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem)
|
chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ db user g afterChatItemId count search (chatItemTs afterChatItem)
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
||||||
pure $ Chat (GroupChat g) chatItems stats
|
pure $ Chat (GroupChat g) chatItems stats
|
||||||
where
|
|
||||||
getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId]
|
getGroupChatItemIdsAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
getGroupChatItemIdsAfter_ afterChatItemTs =
|
getGroupChatItemIdsAfter_ db User {userId} GroupInfo {groupId} afterChatItemId count search afterChatItemTs =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?))
|
AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?))
|
||||||
ORDER BY item_ts ASC, chat_item_id ASC
|
ORDER BY item_ts ASC, chat_item_id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count)
|
(userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count)
|
||||||
|
|
||||||
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId count search = do
|
getGroupChatBefore_ db user g@GroupInfo {groupId} beforeChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId
|
beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId
|
||||||
chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem)
|
chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ db user g beforeChatItemId count search (chatItemTs beforeChatItem)
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds
|
||||||
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
pure $ Chat (GroupChat g) (reverse chatItems) stats
|
||||||
|
|
||||||
|
getGroupChatItemIdsBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
|
getGroupChatItemIdsBefore_ db User {userId} GroupInfo {groupId} beforeChatItemId count search beforeChatItemTs =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?))
|
||||||
|
ORDER BY item_ts DESC, chat_item_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count)
|
||||||
|
|
||||||
|
getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
|
getGroupChatAround_ db user g@GroupInfo {groupId} aroundItemId count search = do
|
||||||
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
middleChatItem <- getGroupChatItem db user groupId aroundItemId
|
||||||
|
beforeIds <- liftIO $ getGroupChatItemIdsBefore_ db user g aroundItemId fetchCountBefore search (chatItemTs middleChatItem)
|
||||||
|
afterIds <- liftIO $ getGroupChatItemIdsAfter_ db user g aroundItemId fetchCountAfter search (chatItemTs middleChatItem)
|
||||||
|
currentTs <- liftIO getCurrentTime
|
||||||
|
beforeChatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) beforeIds
|
||||||
|
afterChatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) afterIds
|
||||||
|
let remainingAfter = fetchCountAfter - length afterIds
|
||||||
|
let remainingBefore = fetchCountBefore - length beforeIds
|
||||||
|
if
|
||||||
|
| remainingBefore > 0 && remainingAfter <= 0 -> do
|
||||||
|
extraAfterIds <- liftIO $ getGroupChatItemIdsAfter_ db user g (last afterIds) remainingBefore search (chatItemTs (last afterChatItems))
|
||||||
|
extraAfterItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) extraAfterIds
|
||||||
|
pure $ Chat (GroupChat g) (reverse beforeChatItems <> [middleChatItem] <> afterChatItems <> extraAfterItems) stats
|
||||||
|
| remainingAfter > 0 && remainingBefore <= 0 -> do
|
||||||
|
extraBeforeIds <- liftIO $ getGroupChatItemIdsBefore_ db user g (last beforeIds) remainingAfter search (chatItemTs (last beforeChatItems))
|
||||||
|
extraBeforeItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) extraBeforeIds
|
||||||
|
pure $ Chat (GroupChat g) (reverse (beforeChatItems <> extraBeforeItems) <> [middleChatItem] <> afterChatItems) stats
|
||||||
|
| otherwise ->
|
||||||
|
pure $ Chat (GroupChat g) (reverse beforeChatItems <> [middleChatItem] <> afterChatItems) stats
|
||||||
|
|
||||||
|
getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Int -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||||
|
getGroupChatInitial_ db user@User {userId} g@GroupInfo {groupId} count = do
|
||||||
|
firstUnreadItemId_ <- liftIO getGroupChatMinUnreadItemId_
|
||||||
|
case firstUnreadItemId_ of
|
||||||
|
Just firstUnreadItemId -> getGroupChatAround_ db user g firstUnreadItemId count ""
|
||||||
|
Nothing -> liftIO $ getGroupChatLast_ db user g count ""
|
||||||
where
|
where
|
||||||
getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId]
|
getGroupChatMinUnreadItemId_ :: IO (Maybe ChatItemId)
|
||||||
getGroupChatItemIdsBefore_ beforeChatItemTs =
|
getGroupChatMinUnreadItemId_ =
|
||||||
map fromOnly
|
fmap join . maybeFirstRow fromOnly $
|
||||||
<$> DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT MIN(chat_item_id)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND group_id = ? AND item_status = ?
|
||||||
AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?))
|
|
||||||
ORDER BY item_ts DESC, chat_item_id DESC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|]
|
||||||
(userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count)
|
(userId, groupId, CISRcvNew)
|
||||||
|
|
||||||
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal)
|
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
getLocalChat db user folderId pagination search_ = do
|
getLocalChat db user folderId pagination search_ = do
|
||||||
let search = fromMaybe "" search_
|
let search = fromMaybe "" search_
|
||||||
nf <- getNoteFolder db user folderId
|
nf <- getNoteFolder db user folderId
|
||||||
liftIO $ case pagination of
|
case pagination of
|
||||||
CPLast count -> getLocalChatLast_ db user nf count search
|
CPLast count -> liftIO $ getLocalChatLast_ db user nf count search
|
||||||
CPAfter afterId count -> getLocalChatAfter_ db user nf afterId count search
|
CPAfter afterId count -> getLocalChatAfter_ db user nf afterId count search
|
||||||
CPBefore beforeId count -> getLocalChatBefore_ db user nf beforeId count search
|
CPBefore beforeId count -> getLocalChatBefore_ db user nf beforeId count search
|
||||||
|
CPAround aroundId count -> getLocalChatAround_ db user nf aroundId count search
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
getLocalChatInitial_ db user nf count
|
||||||
|
|
||||||
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal)
|
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal)
|
||||||
getLocalChatLast_ db user@User {userId} nf@NoteFolder {noteFolderId} count search = do
|
getLocalChatLast_ db user nf count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getLocalChatItemIdsLast_
|
chatItemIds <- getLocalChatItemIdsLast_ db user nf count search
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
||||||
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
||||||
where
|
|
||||||
getLocalChatItemIdsLast_ :: IO [ChatItemId]
|
getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO [ChatItemId]
|
||||||
getLocalChatItemIdsLast_ =
|
getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search =
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT chat_item_id
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, noteFolderId, search, count)
|
(userId, noteFolderId, search, count)
|
||||||
|
|
||||||
safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal)
|
safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal)
|
||||||
safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId =
|
safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId =
|
||||||
@@ -1245,51 +1346,105 @@ safeToLocalItem currentTs itemId = \case
|
|||||||
file = Nothing
|
file = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> IO (Chat 'CTLocal)
|
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
getLocalChatAfter_ db user@User {userId} nf@NoteFolder {noteFolderId} afterChatItemId count search = do
|
getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getLocalChatItemIdsAfter_
|
afterChatItem <- getLocalChatItem db user noteFolderId afterChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getLocalChatItemIdsAfter_ db user nf afterChatItemId count search (chatItemCreatedAt afterChatItem)
|
||||||
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
||||||
pure $ Chat (LocalChat nf) chatItems stats
|
pure $ Chat (LocalChat nf) chatItems stats
|
||||||
where
|
|
||||||
getLocalChatItemIdsAfter_ :: IO [ChatItemId]
|
|
||||||
getLocalChatItemIdsAfter_ =
|
|
||||||
map fromOnly
|
|
||||||
<$> DB.query
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT chat_item_id
|
|
||||||
FROM chat_items
|
|
||||||
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
|
||||||
AND chat_item_id > ?
|
|
||||||
ORDER BY created_at ASC, chat_item_id ASC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|
||||||
(userId, noteFolderId, search, afterChatItemId, count)
|
|
||||||
|
|
||||||
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> IO (Chat 'CTLocal)
|
getLocalChatItemIdsAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
getLocalChatBefore_ db user@User {userId} nf@NoteFolder {noteFolderId} beforeChatItemId count search = do
|
getLocalChatItemIdsAfter_ db User {userId} NoteFolder {noteFolderId} afterChatItemId count search afterChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
|
||||||
|
ORDER BY created_at ASC, chat_item_id ASC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, noteFolderId, search, afterChatItemCreatedAt, afterChatItemCreatedAt, afterChatItemId, count)
|
||||||
|
|
||||||
|
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
|
getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeChatItemId count search = do
|
||||||
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
chatItemIds <- getLocalChatItemIdsBefore_
|
beforeChatItem <- getLocalChatItem db user noteFolderId beforeChatItemId
|
||||||
currentTs <- getCurrentTime
|
chatItemIds <- liftIO $ getLocalChatItemIdsBefore_ db user nf beforeChatItemId count search (chatItemCreatedAt beforeChatItem)
|
||||||
chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
currentTs <- liftIO getCurrentTime
|
||||||
|
chatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) chatItemIds
|
||||||
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
pure $ Chat (LocalChat nf) (reverse chatItems) stats
|
||||||
|
|
||||||
|
getLocalChatItemIdsBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> UTCTime -> IO [ChatItemId]
|
||||||
|
getLocalChatItemIdsBefore_ db User {userId} NoteFolder {noteFolderId} beforeChatItemId count search beforeChatItemCreatedAt =
|
||||||
|
map fromOnly
|
||||||
|
<$> DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
||||||
|
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
|
||||||
|
ORDER BY created_at DESC, chat_item_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
|]
|
||||||
|
(userId, noteFolderId, search, beforeChatItemCreatedAt, beforeChatItemCreatedAt, beforeChatItemId, count)
|
||||||
|
|
||||||
|
getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
|
getLocalChatAround_ db user nf@NoteFolder {noteFolderId} aroundItemId count search = do
|
||||||
|
let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
middleChatItem <- getLocalChatItem db user noteFolderId aroundItemId
|
||||||
|
beforeIds <- liftIO $ getLocalChatItemIdsBefore_ db user nf aroundItemId fetchCountBefore search (chatItemCreatedAt middleChatItem)
|
||||||
|
afterIds <- liftIO $ getLocalChatItemIdsAfter_ db user nf aroundItemId fetchCountAfter search (chatItemCreatedAt middleChatItem)
|
||||||
|
currentTs <- liftIO getCurrentTime
|
||||||
|
beforeChatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) beforeIds
|
||||||
|
afterChatItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) afterIds
|
||||||
|
let remainingAfter = fetchCountAfter - length afterIds
|
||||||
|
let remainingBefore = fetchCountBefore - length beforeIds
|
||||||
|
if
|
||||||
|
| remainingBefore > 0 && remainingAfter <= 0 -> do
|
||||||
|
extraAfterIds <- liftIO $ getLocalChatItemIdsAfter_ db user nf (last afterIds) remainingBefore search (chatItemCreatedAt (last afterChatItems))
|
||||||
|
extraAfterItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) extraAfterIds
|
||||||
|
pure $ Chat (LocalChat nf) (reverse beforeChatItems <> [middleChatItem] <> afterChatItems <> extraAfterItems) stats
|
||||||
|
| remainingAfter > 0 && remainingBefore <= 0 -> do
|
||||||
|
extraBeforeIds <- liftIO $ getLocalChatItemIdsBefore_ db user nf (last beforeIds) remainingAfter search (chatItemCreatedAt (last beforeChatItems))
|
||||||
|
extraBeforeItems <- liftIO $ mapM (safeGetLocalItem db user nf currentTs) extraBeforeIds
|
||||||
|
pure $ Chat (LocalChat nf) (reverse (beforeChatItems <> extraBeforeItems) <> [middleChatItem] <> afterChatItems) stats
|
||||||
|
| otherwise ->
|
||||||
|
pure $ Chat (LocalChat nf) (reverse beforeChatItems <> [middleChatItem] <> afterChatItems) stats
|
||||||
|
|
||||||
|
getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Int -> ExceptT StoreError IO (Chat 'CTLocal)
|
||||||
|
getLocalChatInitial_ db user@User {userId} nf@NoteFolder {noteFolderId} count = do
|
||||||
|
firstUnreadItemId_ <- liftIO getLocalChatMinUnreadItemId_
|
||||||
|
case firstUnreadItemId_ of
|
||||||
|
Just firstUnreadItemId -> do
|
||||||
|
chat <- getLocalChatAround_ db user nf firstUnreadItemId count ""
|
||||||
|
let items = chatItems chat
|
||||||
|
if null items || length items == count
|
||||||
|
then pure chat
|
||||||
|
else do
|
||||||
|
let remainingCount = count - length items
|
||||||
|
let afterId = cchatItemId $ last items
|
||||||
|
after <- getLocalChatAfter_ db user nf afterId remainingCount ""
|
||||||
|
pure $ chat {chatItems = chatItems chat <> chatItems after}
|
||||||
|
Nothing -> liftIO $ getLocalChatLast_ db user nf count ""
|
||||||
where
|
where
|
||||||
getLocalChatItemIdsBefore_ :: IO [ChatItemId]
|
getLocalChatMinUnreadItemId_ :: IO (Maybe ChatItemId)
|
||||||
getLocalChatItemIdsBefore_ =
|
getLocalChatMinUnreadItemId_ =
|
||||||
map fromOnly
|
fmap join . maybeFirstRow fromOnly $
|
||||||
<$> DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT chat_item_id
|
SELECT MIN(chat_item_id)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%'
|
WHERE user_id = ? AND note_folder_id = ? AND item_status = ?
|
||||||
AND chat_item_id < ?
|
|
||||||
ORDER BY created_at DESC, chat_item_id DESC
|
|
||||||
LIMIT ?
|
|
||||||
|]
|
|]
|
||||||
(userId, noteFolderId, search, beforeChatItemId, count)
|
(userId, noteFolderId, CISRcvNew)
|
||||||
|
|
||||||
toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId)
|
toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId)
|
||||||
toChatItemRef = \case
|
toChatItemRef = \case
|
||||||
@@ -1581,6 +1736,13 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
|
|||||||
CPLast count -> liftIO $ getAllChatItemsLast_ count
|
CPLast count -> liftIO $ getAllChatItemsLast_ count
|
||||||
CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem_ afterId
|
CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem_ afterId
|
||||||
CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId
|
CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId
|
||||||
|
CPAround aroundId count -> liftIO . getAllChatItemsAround_ aroundId count . aChatItemTs =<< getAChatItem_ aroundId
|
||||||
|
CPInitial count -> do
|
||||||
|
unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
|
||||||
|
firstUnreadItemId <- liftIO getFirstUnreadItemId_
|
||||||
|
case firstUnreadItemId of
|
||||||
|
Just itemId -> liftIO . getAllChatItemsAround_ itemId count . aChatItemTs =<< getAChatItem_ itemId
|
||||||
|
Nothing -> liftIO $ getAllChatItemsLast_ count
|
||||||
mapM (uncurry (getAChatItem db vr user)) itemRefs
|
mapM (uncurry (getAChatItem db vr user)) itemRefs
|
||||||
where
|
where
|
||||||
search = fromMaybe "" search_
|
search = fromMaybe "" search_
|
||||||
@@ -1624,6 +1786,31 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, search, beforeTs, beforeTs, beforeId, count)
|
(userId, search, beforeTs, beforeTs, beforeId, count)
|
||||||
|
getChatItem chatId =
|
||||||
|
DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT chat_item_id, contact_id, group_id, note_folder_id
|
||||||
|
FROM chat_items
|
||||||
|
WHERE chat_item_id = ?
|
||||||
|
|]
|
||||||
|
(Only chatId)
|
||||||
|
getAllChatItemsAround_ aroundId count aroundTs = do
|
||||||
|
let (fetchCountBefore, fetchCountAfter) = divideFetchCountAround_ (count - 1)
|
||||||
|
itemsBefore <- getAllChatItemsBefore_ aroundId fetchCountBefore aroundTs
|
||||||
|
item <- getChatItem aroundId
|
||||||
|
itemsAfter <- getAllChatItemsAfter_ aroundId fetchCountAfter aroundTs
|
||||||
|
pure $ itemsBefore <> item <> itemsAfter
|
||||||
|
getFirstUnreadItemId_ =
|
||||||
|
fmap join . maybeFirstRow fromOnly $
|
||||||
|
DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT MIN(chat_item_id)
|
||||||
|
FROM chat_items
|
||||||
|
WHERE user_id = ? AND item_status = ?
|
||||||
|
|]
|
||||||
|
(userId, CISRcvNew)
|
||||||
|
|
||||||
getChatItemIdsByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO [ChatItemId]
|
getChatItemIdsByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO [ChatItemId]
|
||||||
getChatItemIdsByAgentMsgId db connId msgId =
|
getChatItemIdsByAgentMsgId db connId msgId =
|
||||||
@@ -2650,3 +2837,10 @@ getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, groupId, rcvMsgContentTag, sndMsgContentTag, count)
|
(userId, groupId, rcvMsgContentTag, sndMsgContentTag, count)
|
||||||
|
|
||||||
|
divideFetchCountAround_ :: Int -> (Int, Int)
|
||||||
|
divideFetchCountAround_ count =
|
||||||
|
let fetchCountEachSide = count `div` 2
|
||||||
|
fetchCountBefore = fetchCountEachSide + if count `mod` 2 /= 0 then 1 else 0
|
||||||
|
fetchCountAfter = fetchCountEachSide
|
||||||
|
in (fetchCountBefore, fetchCountAfter)
|
||||||
@@ -114,7 +114,7 @@ import Simplex.Chat.Migrations.M20240827_calls_uuid
|
|||||||
import Simplex.Chat.Migrations.M20240920_user_order
|
import Simplex.Chat.Migrations.M20240920_user_order
|
||||||
import Simplex.Chat.Migrations.M20241008_indexes
|
import Simplex.Chat.Migrations.M20241008_indexes
|
||||||
import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
|
||||||
import Simplex.Chat.Migrations.M20241027_server_operators
|
import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||||
|
|
||||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||||
@@ -229,7 +229,7 @@ schemaMigrations =
|
|||||||
("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order),
|
("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order),
|
||||||
("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes),
|
("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes),
|
||||||
("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id),
|
("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id),
|
||||||
("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators)
|
("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{-# LANGUAGE DataKinds #-}
|
|
||||||
{-# LANGUAGE DeriveAnyClass #-}
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE GADTs #-}
|
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
@@ -50,19 +47,7 @@ module Simplex.Chat.Store.Profiles
|
|||||||
getContactWithoutConnViaAddress,
|
getContactWithoutConnViaAddress,
|
||||||
updateUserAddressAutoAccept,
|
updateUserAddressAutoAccept,
|
||||||
getProtocolServers,
|
getProtocolServers,
|
||||||
getUpdateUserServers,
|
|
||||||
-- overwriteOperatorsAndServers,
|
|
||||||
overwriteProtocolServers,
|
overwriteProtocolServers,
|
||||||
insertProtocolServer,
|
|
||||||
getUpdateServerOperators,
|
|
||||||
getServerOperators,
|
|
||||||
getUserServers,
|
|
||||||
setServerOperators,
|
|
||||||
getCurrentUsageConditions,
|
|
||||||
getLatestAcceptedConditions,
|
|
||||||
setConditionsNotified,
|
|
||||||
acceptConditions,
|
|
||||||
setUserServers,
|
|
||||||
createCall,
|
createCall,
|
||||||
deleteCalls,
|
deleteCalls,
|
||||||
getCalls,
|
getCalls,
|
||||||
@@ -85,14 +70,12 @@ import Data.List.NonEmpty (NonEmpty)
|
|||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
|
||||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||||
import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..))
|
import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..))
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
import Simplex.Chat.Call
|
import Simplex.Chat.Call
|
||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Operators
|
|
||||||
import Simplex.Chat.Protocol
|
import Simplex.Chat.Protocol
|
||||||
import Simplex.Chat.Store.Direct
|
import Simplex.Chat.Store.Direct
|
||||||
import Simplex.Chat.Store.Shared
|
import Simplex.Chat.Store.Shared
|
||||||
@@ -100,7 +83,7 @@ import Simplex.Chat.Types
|
|||||||
import Simplex.Chat.Types.Preferences
|
import Simplex.Chat.Types.Preferences
|
||||||
import Simplex.Chat.Types.Shared
|
import Simplex.Chat.Types.Shared
|
||||||
import Simplex.Chat.Types.UITheme
|
import Simplex.Chat.Types.UITheme
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..))
|
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..))
|
||||||
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId)
|
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
@@ -108,7 +91,7 @@ import qualified Simplex.Messaging.Crypto as C
|
|||||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (defaultJSON)
|
import Simplex.Messaging.Parsers (defaultJSON)
|
||||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, UserProtocol)
|
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
|
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
|
||||||
|
|
||||||
@@ -532,309 +515,42 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
|||||||
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply)
|
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply)
|
||||||
_ -> (False, False, Nothing)
|
_ -> (False, False, Nothing)
|
||||||
|
|
||||||
getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO (NonEmpty (UserServer p))
|
getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p]
|
||||||
getUpdateUserServers db p presetOps randomSrvs user = do
|
getProtocolServers db User {userId} =
|
||||||
ts <- getCurrentTime
|
map toServerCfg
|
||||||
srvs <- getProtocolServers db p user
|
|
||||||
let srvs' = updatedUserServers p presetOps randomSrvs srvs
|
|
||||||
mapM (upsertServer ts) srvs'
|
|
||||||
where
|
|
||||||
upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p)
|
|
||||||
upsertServer ts (AUS _ s@UserServer {serverId}) = case serverId of
|
|
||||||
DBNewEntity -> insertProtocolServer db p user ts s
|
|
||||||
DBEntityId _ -> updateProtocolServer db p ts s $> s
|
|
||||||
|
|
||||||
getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p]
|
|
||||||
getProtocolServers db p User {userId} =
|
|
||||||
map toUserServer
|
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled
|
SELECT host, port, key_hash, basic_auth, preset, tested, enabled
|
||||||
FROM protocol_servers
|
FROM protocol_servers
|
||||||
WHERE user_id = ? AND protocol = ?
|
WHERE user_id = ? AND protocol = ?;
|
||||||
|]
|
|]
|
||||||
(userId, decodeLatin1 $ strEncode p)
|
(userId, decodeLatin1 $ strEncode protocol)
|
||||||
where
|
where
|
||||||
toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> UserServer p
|
protocol = protocolTypeI @p
|
||||||
toUserServer (serverId, host, port, keyHash, auth_, preset, tested, enabled) =
|
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p
|
||||||
let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
|
toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) =
|
||||||
in UserServer {serverId, server, preset, tested, enabled, deleted = False}
|
let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
|
||||||
|
in ServerCfg {server, preset, tested, enabled}
|
||||||
|
|
||||||
-- TODO remove
|
overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO ()
|
||||||
-- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p]
|
overwriteProtocolServers db User {userId} servers =
|
||||||
-- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do
|
|
||||||
overwriteProtocolServers :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> [UserServer p] -> ExceptT StoreError IO ()
|
|
||||||
overwriteProtocolServers db p User {userId} servers =
|
|
||||||
-- liftIO $ mapM_ (updateServerOperators_ db) operators_
|
|
||||||
checkConstraint SEUniqueID . ExceptT $ do
|
checkConstraint SEUniqueID . ExceptT $ do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, decodeLatin1 $ strEncode p)
|
DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol)
|
||||||
forM_ servers $ \UserServer {serverId, server, preset, tested, enabled} -> do
|
forM_ servers $ \ServerCfg {server, preset, tested, enabled} -> do
|
||||||
|
let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO protocol_servers
|
INSERT INTO protocol_servers
|
||||||
(server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
|
(protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
(Only serverId :. serverColumns p server :. (preset, tested, enabled, userId, currentTs, currentTs))
|
((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs))
|
||||||
pure $ Right ()
|
pure $ Right ()
|
||||||
|
|
||||||
insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p)
|
|
||||||
insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled} = do
|
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
INSERT INTO protocol_servers
|
|
||||||
(protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
|
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
||||||
|]
|
|
||||||
(serverColumns p server :. (preset, tested, enabled, userId, ts, ts))
|
|
||||||
sId <- insertedRowId db
|
|
||||||
pure (srv :: NewUserServer p) {serverId = DBEntityId sId}
|
|
||||||
|
|
||||||
updateProtocolServer :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> UTCTime -> UserServer p -> IO ()
|
|
||||||
updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled} =
|
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
UPDATE protocol_servers
|
|
||||||
SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?,
|
|
||||||
preset = ?, tested = ?, enabled = ?, updated_at = ?
|
|
||||||
WHERE smp_server_id = ?
|
|
||||||
|]
|
|
||||||
(serverColumns p server :. (preset, tested, enabled, ts, serverId))
|
|
||||||
|
|
||||||
serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text)
|
|
||||||
serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) =
|
|
||||||
let protocol = decodeLatin1 $ strEncode p
|
|
||||||
auth = safeDecodeUtf8 . unBasicAuth <$> auth_
|
|
||||||
in (protocol, host, port, keyHash, auth)
|
|
||||||
|
|
||||||
getServerOperators :: DB.Connection -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction)
|
|
||||||
getServerOperators db = do
|
|
||||||
currentConds <- getCurrentUsageConditions db
|
|
||||||
liftIO $ do
|
|
||||||
now <- getCurrentTime
|
|
||||||
latestAcceptedConds_ <- getLatestAcceptedConditions db
|
|
||||||
let getConds op = (\ca -> op {conditionsAcceptance = ca}) <$> getOperatorConditions_ db op currentConds latestAcceptedConds_ now
|
|
||||||
operators <- mapM getConds =<< getServerOperators_ db
|
|
||||||
pure (operators, usageConditionsAction operators currentConds now)
|
|
||||||
|
|
||||||
getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
|
|
||||||
getUserServers db user =
|
|
||||||
(,,)
|
|
||||||
<$> (fst <$> getServerOperators db)
|
|
||||||
<*> liftIO (getProtocolServers db SPSMP user)
|
|
||||||
<*> liftIO (getProtocolServers db SPXFTP user)
|
|
||||||
|
|
||||||
setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO ()
|
|
||||||
setServerOperators db ops = do
|
|
||||||
currentTs <- getCurrentTime
|
|
||||||
mapM_ (updateServerOperator db currentTs) ops
|
|
||||||
|
|
||||||
updateServerOperator :: DB.Connection -> UTCTime -> ServerOperator -> IO ()
|
|
||||||
updateServerOperator db currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} =
|
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
UPDATE server_operators
|
|
||||||
SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ?
|
|
||||||
WHERE server_operator_id = ?
|
|
||||||
|]
|
|
||||||
(enabled, storage, proxy, operatorId, currentTs)
|
|
||||||
|
|
||||||
getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator]
|
|
||||||
getUpdateServerOperators db presetOps newUser = do
|
|
||||||
conds <- map toUsageConditions <$> DB.query_ db usageCondsQuery
|
|
||||||
now <- getCurrentTime
|
|
||||||
let (acceptForSimplex_, currentConds, condsToAdd) = usageConditionsToAdd newUser now conds
|
|
||||||
mapM_ insertConditions condsToAdd
|
|
||||||
latestAcceptedConds_ <- getLatestAcceptedConditions db
|
|
||||||
ops <- updatedServerOperators presetOps <$> getServerOperators_ db
|
|
||||||
forM ops $ \(ASO _ op) ->
|
|
||||||
case operatorId op of
|
|
||||||
DBNewEntity -> do
|
|
||||||
op' <- insertOperator op
|
|
||||||
case (operatorTag op', acceptForSimplex_) of
|
|
||||||
(Just OTSimplex, Just cond) -> autoAcceptConditions op' cond
|
|
||||||
_ -> pure op'
|
|
||||||
DBEntityId _ -> do
|
|
||||||
updateOperator op
|
|
||||||
getOperatorConditions_ db op currentConds latestAcceptedConds_ now >>= \case
|
|
||||||
CARequired Nothing | operatorTag op == Just OTSimplex -> autoAcceptConditions op currentConds
|
|
||||||
CARequired (Just ts) | ts < now -> autoAcceptConditions op currentConds
|
|
||||||
ca -> pure op {conditionsAcceptance = ca}
|
|
||||||
where
|
where
|
||||||
insertConditions UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} =
|
protocol = decodeLatin1 $ strEncode $ protocolTypeI @p
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
INSERT INTO usage_conditions
|
|
||||||
(usage_conditions_id, conditions_commit, notified_at, created_at)
|
|
||||||
VALUES (?,?,?,?)
|
|
||||||
|]
|
|
||||||
(conditionsId, conditionsCommit, notifiedAt, createdAt)
|
|
||||||
updateOperator :: ServerOperator -> IO ()
|
|
||||||
updateOperator ServerOperator {operatorId, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} =
|
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
UPDATE server_operators
|
|
||||||
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, role_storage = ?, role_proxy = ?
|
|
||||||
WHERE server_operator_id = ?
|
|
||||||
|]
|
|
||||||
(tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy, operatorId)
|
|
||||||
insertOperator :: NewServerOperator -> IO ServerOperator
|
|
||||||
insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = do
|
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
INSERT INTO server_operators
|
|
||||||
(server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy)
|
|
||||||
VALUES (?,?,?,?,?,?,?)
|
|
||||||
|]
|
|
||||||
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy)
|
|
||||||
opId <- insertedRowId db
|
|
||||||
pure op {operatorId = DBEntityId opId}
|
|
||||||
autoAcceptConditions op UsageConditions {conditionsCommit} =
|
|
||||||
acceptConditions_ db op conditionsCommit Nothing
|
|
||||||
$> op {conditionsAcceptance = CAAccepted Nothing}
|
|
||||||
|
|
||||||
serverOperatorQuery :: Query
|
|
||||||
serverOperatorQuery =
|
|
||||||
[sql|
|
|
||||||
SELECT server_operator_id, server_operator_tag, trade_name, legal_name,
|
|
||||||
server_domains, enabled, role_storage, role_proxy
|
|
||||||
FROM server_operators
|
|
||||||
|]
|
|
||||||
|
|
||||||
getServerOperators_ :: DB.Connection -> IO [ServerOperator]
|
|
||||||
getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery
|
|
||||||
|
|
||||||
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) -> ServerOperator
|
|
||||||
toServerOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) =
|
|
||||||
ServerOperator
|
|
||||||
{ operatorId,
|
|
||||||
operatorTag,
|
|
||||||
tradeName,
|
|
||||||
legalName,
|
|
||||||
serverDomains = T.splitOn "," domains,
|
|
||||||
conditionsAcceptance = CARequired Nothing,
|
|
||||||
enabled,
|
|
||||||
roles = ServerRoles {storage, proxy}
|
|
||||||
}
|
|
||||||
|
|
||||||
getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance
|
|
||||||
getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do
|
|
||||||
case latestAcceptedConds_ of
|
|
||||||
Nothing -> pure $ CARequired Nothing -- no conditions accepted by any operator
|
|
||||||
Just UsageConditions {conditionsCommit = latestAcceptedCommit} -> do
|
|
||||||
operatorAcceptedConds_ <-
|
|
||||||
maybeFirstRow id $
|
|
||||||
DB.query
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT conditions_commit, accepted_at
|
|
||||||
FROM operator_usage_conditions
|
|
||||||
WHERE server_operator_id = ?
|
|
||||||
ORDER BY operator_usage_conditions_id DESC
|
|
||||||
LIMIT 1
|
|
||||||
|]
|
|
||||||
(Only operatorId)
|
|
||||||
pure $ case operatorAcceptedConds_ of
|
|
||||||
Just (operatorCommit, acceptedAt_)
|
|
||||||
| operatorCommit /= latestAcceptedCommit -> CARequired Nothing -- TODO should we consider this operator disabled?
|
|
||||||
| currentCommit /= latestAcceptedCommit -> CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt)
|
|
||||||
| otherwise -> CAAccepted acceptedAt_
|
|
||||||
_ -> CARequired Nothing -- no conditions were accepted for this operator
|
|
||||||
|
|
||||||
getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions
|
|
||||||
getCurrentUsageConditions db =
|
|
||||||
ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $
|
|
||||||
DB.query_ db (usageCondsQuery <> " DESC LIMIT 1")
|
|
||||||
|
|
||||||
usageCondsQuery :: Query
|
|
||||||
usageCondsQuery =
|
|
||||||
[sql|
|
|
||||||
SELECT usage_conditions_id, conditions_commit, notified_at, created_at
|
|
||||||
FROM usage_conditions
|
|
||||||
ORDER BY usage_conditions_id
|
|
||||||
|]
|
|
||||||
|
|
||||||
toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions
|
|
||||||
toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) =
|
|
||||||
UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt}
|
|
||||||
|
|
||||||
getLatestAcceptedConditions :: DB.Connection -> IO (Maybe UsageConditions)
|
|
||||||
getLatestAcceptedConditions db =
|
|
||||||
maybeFirstRow toUsageConditions $
|
|
||||||
DB.query_
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT usage_conditions_id, conditions_commit, notified_at, created_at
|
|
||||||
FROM usage_conditions
|
|
||||||
WHERE conditions_commit = (
|
|
||||||
SELECT conditions_commit
|
|
||||||
FROM operator_usage_conditions
|
|
||||||
ORDER BY accepted_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
|]
|
|
||||||
|
|
||||||
setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO ()
|
|
||||||
setConditionsNotified db condId notifiedAt =
|
|
||||||
DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, condId)
|
|
||||||
|
|
||||||
acceptConditions :: DB.Connection -> Int64 -> NonEmpty Int64 -> UTCTime -> ExceptT StoreError IO ()
|
|
||||||
acceptConditions db condId opIds acceptedAt = do
|
|
||||||
UsageConditions {conditionsCommit} <- getUsageConditionsById_ db condId
|
|
||||||
operators <- mapM getServerOperator_ opIds
|
|
||||||
let ts = Just acceptedAt
|
|
||||||
liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts
|
|
||||||
where
|
|
||||||
getServerOperator_ opId =
|
|
||||||
ExceptT $ firstRow toServerOperator (SEOperatorNotFound opId) $
|
|
||||||
DB.query db (serverOperatorQuery <> " WHERE operator_id = ?") (Only opId)
|
|
||||||
|
|
||||||
acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO ()
|
|
||||||
acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt =
|
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
INSERT INTO operator_usage_conditions
|
|
||||||
(server_operator_id, server_operator_tag, conditions_commit, accepted_at)
|
|
||||||
VALUES (?,?,?,?)
|
|
||||||
|]
|
|
||||||
(operatorId, operatorTag, conditionsCommit, acceptedAt)
|
|
||||||
|
|
||||||
getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions
|
|
||||||
getUsageConditionsById_ db conditionsId =
|
|
||||||
ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $
|
|
||||||
DB.query
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
SELECT usage_conditions_id, conditions_commit, notified_at, created_at
|
|
||||||
FROM usage_conditions
|
|
||||||
WHERE usage_conditions_id = ?
|
|
||||||
|]
|
|
||||||
(Only conditionsId)
|
|
||||||
|
|
||||||
setUserServers :: DB.Connection -> User -> NonEmpty UpdatedUserOperatorServers -> ExceptT StoreError IO ()
|
|
||||||
setUserServers db user@User {userId} userServers = checkConstraint SEUniqueID $ liftIO $ do
|
|
||||||
ts <- getCurrentTime
|
|
||||||
forM_ userServers $ \UpdatedUserOperatorServers {operator, smpServers, xftpServers} -> do
|
|
||||||
mapM_ (updateServerOperator db ts) operator
|
|
||||||
mapM_ (upsertOrDelete SPSMP ts) smpServers
|
|
||||||
mapM_ (upsertOrDelete SPXFTP ts) xftpServers
|
|
||||||
where
|
|
||||||
upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> UTCTime -> AUserServer p -> IO ()
|
|
||||||
upsertOrDelete p ts (AUS _ s@UserServer {serverId, deleted}) = case serverId of
|
|
||||||
DBNewEntity -> void $ insertProtocolServer db p user ts s
|
|
||||||
DBEntityId srvId
|
|
||||||
| deleted -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, False)
|
|
||||||
| otherwise -> updateProtocolServer db p ts s
|
|
||||||
|
|
||||||
createCall :: DB.Connection -> User -> Call -> UTCTime -> IO ()
|
createCall :: DB.Connection -> User -> Call -> UTCTime -> IO ()
|
||||||
createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do
|
createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do
|
||||||
|
|||||||
@@ -127,8 +127,6 @@ data StoreError
|
|||||||
| SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId}
|
| SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId}
|
||||||
| SERemoteCtrlDuplicateCA
|
| SERemoteCtrlDuplicateCA
|
||||||
| SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId}
|
| SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId}
|
||||||
| SEOperatorNotFound {serverOperatorId :: Int64}
|
|
||||||
| SEUsageConditionsNotFound
|
|
||||||
deriving (Show, Exception)
|
deriving (Show, Exception)
|
||||||
|
|
||||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError)
|
$(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedLists #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Simplex.Chat.Terminal where
|
module Simplex.Chat.Terminal where
|
||||||
@@ -14,15 +13,15 @@ import qualified Data.Text as T
|
|||||||
import Data.Text.Encoding (encodeUtf8)
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
import Database.SQLite.Simple (SQLError (..))
|
import Database.SQLite.Simple (SQLError (..))
|
||||||
import qualified Database.SQLite.Simple as DB
|
import qualified Database.SQLite.Simple as DB
|
||||||
import Simplex.Chat (_defaultNtfServers, defaultChatConfig, operatorSimpleXChat)
|
import Simplex.Chat (defaultChatConfig)
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Chat.Core
|
import Simplex.Chat.Core
|
||||||
import Simplex.Chat.Help (chatWelcome)
|
import Simplex.Chat.Help (chatWelcome)
|
||||||
import Simplex.Chat.Operators
|
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Terminal.Input
|
import Simplex.Chat.Terminal.Input
|
||||||
import Simplex.Chat.Terminal.Output
|
import Simplex.Chat.Terminal.Output
|
||||||
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||||
|
import Simplex.Messaging.Agent.Env.SQLite (presetServerCfg)
|
||||||
import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig)
|
import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig)
|
||||||
import Simplex.Messaging.Util (raceAny_)
|
import Simplex.Messaging.Util (raceAny_)
|
||||||
import System.IO (hFlush, hSetEcho, stdin, stdout)
|
import System.IO (hFlush, hSetEcho, stdin, stdout)
|
||||||
@@ -30,24 +29,20 @@ import System.IO (hFlush, hSetEcho, stdin, stdout)
|
|||||||
terminalChatConfig :: ChatConfig
|
terminalChatConfig :: ChatConfig
|
||||||
terminalChatConfig =
|
terminalChatConfig =
|
||||||
defaultChatConfig
|
defaultChatConfig
|
||||||
{ presetServers =
|
{ defaultServers =
|
||||||
PresetServers
|
DefaultAgentServers
|
||||||
{ operators =
|
{ smp =
|
||||||
[ PresetOperator
|
L.fromList $
|
||||||
{ operator = Just operatorSimpleXChat,
|
map
|
||||||
smp =
|
(presetServerCfg True)
|
||||||
map
|
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
|
||||||
(presetServer True)
|
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion",
|
||||||
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
|
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion"
|
||||||
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion",
|
],
|
||||||
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion"
|
useSMP = 3,
|
||||||
],
|
ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"],
|
||||||
useSMP = 3,
|
xftp = L.map (presetServerCfg True) defaultXFTPServers,
|
||||||
xftp = map (presetServer True) $ L.toList defaultXFTPServers,
|
useXFTP = L.length defaultXFTPServers,
|
||||||
useXFTP = 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
ntf = _defaultNtfServers,
|
|
||||||
netCfg =
|
netCfg =
|
||||||
defaultNetworkConfig
|
defaultNetworkConfig
|
||||||
{ smpProxyMode = SPMUnknown,
|
{ smpProxyMode = SPMUnknown,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Data.Maybe (fromMaybe)
|
|||||||
import Data.Time.Clock (getCurrentTime)
|
import Data.Time.Clock (getCurrentTime)
|
||||||
import Data.Time.LocalTime (getCurrentTimeZone)
|
import Data.Time.LocalTime (getCurrentTimeZone)
|
||||||
import Network.Socket
|
import Network.Socket
|
||||||
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString)
|
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), DefaultAgentServers (DefaultAgentServers, netCfg), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString)
|
||||||
import Simplex.Chat.Core
|
import Simplex.Chat.Core
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Terminal
|
import Simplex.Chat.Terminal
|
||||||
@@ -56,7 +56,7 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer
|
|||||||
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
|
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
|
||||||
|
|
||||||
welcome :: ChatConfig -> ChatOpts -> IO ()
|
welcome :: ChatConfig -> ChatOpts -> IO ()
|
||||||
welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} =
|
welcome ChatConfig {defaultServers = DefaultAgentServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} =
|
||||||
mapM_
|
mapM_
|
||||||
putStrLn
|
putStrLn
|
||||||
[ versionString versionNumber,
|
[ versionString versionNumber,
|
||||||
|
|||||||
+25
-36
@@ -53,7 +53,7 @@ import Simplex.Chat.Types.Shared
|
|||||||
import Simplex.Chat.Types.UITheme
|
import Simplex.Chat.Types.UITheme
|
||||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
|
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerCfg (..))
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||||
import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..))
|
import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..))
|
||||||
@@ -95,16 +95,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRChats chats -> viewChats ts tz chats
|
CRChats chats -> viewChats ts tz chats
|
||||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat]
|
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat]
|
||||||
CRApiParsedMarkdown ft -> [viewJSON ft]
|
CRApiParsedMarkdown ft -> [viewJSON ft]
|
||||||
-- CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView
|
CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView
|
||||||
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
||||||
CRTestOperator _ -> []
|
|
||||||
CRTestUsageConditionsAction _ -> []
|
|
||||||
CRTestConditionsAcceptance _ -> []
|
|
||||||
CRTestServerRoles _ -> []
|
|
||||||
CRServerOperators {} -> []
|
|
||||||
CRUserServers {} -> []
|
|
||||||
CRUserServersValidation _ -> []
|
|
||||||
CRUsageConditions {} -> []
|
|
||||||
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
|
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
|
||||||
CRNetworkConfig cfg -> viewNetworkConfig cfg
|
CRNetworkConfig cfg -> viewNetworkConfig cfg
|
||||||
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
|
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
|
||||||
@@ -1217,27 +1209,27 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho
|
|||||||
"profile is " <> if isJust viewPwdHash then "hidden" else "visible"
|
"profile is " <> if isJust viewPwdHash then "hidden" else "visible"
|
||||||
]
|
]
|
||||||
|
|
||||||
-- viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString]
|
viewUserServers :: AUserProtoServers -> Bool -> [StyledString]
|
||||||
-- viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView =
|
viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) testView =
|
||||||
-- customServers
|
customServers
|
||||||
-- <> if testView
|
<> if testView
|
||||||
-- then []
|
then []
|
||||||
-- else
|
else
|
||||||
-- [ "",
|
[ "",
|
||||||
-- "use " <> highlight (srvCmd <> " test <srv>") <> " to test " <> pName <> " server connection",
|
"use " <> highlight (srvCmd <> " test <srv>") <> " to test " <> pName <> " server connection",
|
||||||
-- "use " <> highlight (srvCmd <> " <srv1[,srv2,...]>") <> " to configure " <> pName <> " servers",
|
"use " <> highlight (srvCmd <> " <srv1[,srv2,...]>") <> " to configure " <> pName <> " servers",
|
||||||
-- "use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets"
|
"use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets"
|
||||||
-- ]
|
]
|
||||||
-- <> case p of
|
<> case p of
|
||||||
-- SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"]
|
SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"]
|
||||||
-- SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"]
|
SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"]
|
||||||
-- where
|
where
|
||||||
-- srvCmd = "/" <> strEncode p
|
srvCmd = "/" <> strEncode p
|
||||||
-- pName = protocolName p
|
pName = protocolName p
|
||||||
-- customServers =
|
customServers =
|
||||||
-- if null protoServers
|
if null protoServers
|
||||||
-- then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers
|
then ("no " <> pName <> " servers saved, using presets: ") : viewServers presetServers
|
||||||
-- else viewServers operators protoServers
|
else viewServers protoServers
|
||||||
|
|
||||||
protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString
|
protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString
|
||||||
protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode
|
protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode
|
||||||
@@ -1334,11 +1326,8 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
|
|||||||
["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo]
|
["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo]
|
||||||
<> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo]
|
<> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo]
|
||||||
|
|
||||||
-- viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString]
|
viewServers :: ProtocolTypeI p => NonEmpty (ServerCfg p) -> [StyledString]
|
||||||
-- viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList
|
viewServers = map (plain . B.unpack . strEncode . (\ServerCfg {server} -> server)) . L.toList
|
||||||
-- where
|
|
||||||
-- ops :: Map (Maybe DBEntityId) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators
|
|
||||||
-- viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")"
|
|
||||||
|
|
||||||
viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString
|
viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString
|
||||||
viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo
|
viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import ChatTests.Utils
|
|||||||
import Control.Concurrent (forkIO, killThread, threadDelay)
|
import Control.Concurrent (forkIO, killThread, threadDelay)
|
||||||
import Control.Exception (finally)
|
import Control.Exception (finally)
|
||||||
import Control.Monad (forM_)
|
import Control.Monad (forM_)
|
||||||
import qualified Data.Text as T
|
import Directory.Events (viewName)
|
||||||
import qualified Directory.Events as DE
|
|
||||||
import Directory.Options
|
import Directory.Options
|
||||||
import Directory.Service
|
import Directory.Service
|
||||||
import Directory.Store
|
import Directory.Store
|
||||||
@@ -28,7 +27,7 @@ import Test.Hspec hiding (it)
|
|||||||
directoryServiceTests :: SpecWith FilePath
|
directoryServiceTests :: SpecWith FilePath
|
||||||
directoryServiceTests = do
|
directoryServiceTests = do
|
||||||
it "should register group" testDirectoryService
|
it "should register group" testDirectoryService
|
||||||
it "should suspend and resume group, send message to owner" testSuspendResume
|
it "should suspend and resume group" testSuspendResume
|
||||||
it "should delete group registration" testDeleteGroup
|
it "should delete group registration" testDeleteGroup
|
||||||
it "should change initial member role" testSetRole
|
it "should change initial member role" testSetRole
|
||||||
it "should join found group via link" testJoinGroup
|
it "should join found group via link" testJoinGroup
|
||||||
@@ -68,7 +67,6 @@ mkDirectoryOpts :: FilePath -> [KnownContact] -> DirectoryOpts
|
|||||||
mkDirectoryOpts tmp superUsers =
|
mkDirectoryOpts tmp superUsers =
|
||||||
DirectoryOpts
|
DirectoryOpts
|
||||||
{ coreOptions = testCoreOpts {dbFilePrefix = tmp </> serviceDbPrefix},
|
{ coreOptions = testCoreOpts {dbFilePrefix = tmp </> serviceDbPrefix},
|
||||||
adminUsers = [],
|
|
||||||
superUsers,
|
superUsers,
|
||||||
directoryLog = Just $ tmp </> "directory_service.log",
|
directoryLog = Just $ tmp </> "directory_service.log",
|
||||||
serviceName = "SimpleX-Directory",
|
serviceName = "SimpleX-Directory",
|
||||||
@@ -79,9 +77,6 @@ mkDirectoryOpts tmp superUsers =
|
|||||||
serviceDbPrefix :: FilePath
|
serviceDbPrefix :: FilePath
|
||||||
serviceDbPrefix = "directory_service"
|
serviceDbPrefix = "directory_service"
|
||||||
|
|
||||||
viewName :: String -> String
|
|
||||||
viewName = T.unpack . DE.viewName . T.pack
|
|
||||||
|
|
||||||
testDirectoryService :: HasCallStack => FilePath -> IO ()
|
testDirectoryService :: HasCallStack => FilePath -> IO ()
|
||||||
testDirectoryService tmp =
|
testDirectoryService tmp =
|
||||||
withDirectoryService tmp $ \superUser dsLink ->
|
withDirectoryService tmp $ \superUser dsLink ->
|
||||||
@@ -116,7 +111,7 @@ testDirectoryService tmp =
|
|||||||
-- putStrLn "*** update profile so that it has link"
|
-- putStrLn "*** update profile so that it has link"
|
||||||
updateGroupProfile bob welcomeWithLink
|
updateGroupProfile bob welcomeWithLink
|
||||||
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
|
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
|
||||||
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
|
bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
approvalRequested superUser welcomeWithLink (1 :: Int)
|
approvalRequested superUser welcomeWithLink (1 :: Int)
|
||||||
-- putStrLn "*** update profile so that it still has link"
|
-- putStrLn "*** update profile so that it still has link"
|
||||||
let welcomeWithLink' = "Welcome! " <> welcomeWithLink
|
let welcomeWithLink' = "Welcome! " <> welcomeWithLink
|
||||||
@@ -144,7 +139,7 @@ testDirectoryService tmp =
|
|||||||
-- putStrLn "*** update profile so that it has link again"
|
-- putStrLn "*** update profile so that it has link again"
|
||||||
updateGroupProfile bob welcomeWithLink'
|
updateGroupProfile bob welcomeWithLink'
|
||||||
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
|
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
|
||||||
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
|
bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
approvalRequested superUser welcomeWithLink' (1 :: Int)
|
approvalRequested superUser welcomeWithLink' (1 :: Int)
|
||||||
superUser #> "@SimpleX-Directory /pending"
|
superUser #> "@SimpleX-Directory /pending"
|
||||||
superUser <# "SimpleX-Directory> > /pending"
|
superUser <# "SimpleX-Directory> > /pending"
|
||||||
@@ -212,17 +207,6 @@ testSuspendResume tmp =
|
|||||||
superUser <## " Group listing resumed!"
|
superUser <## " Group listing resumed!"
|
||||||
bob <# "SimpleX-Directory> The group ID 1 (privacy) is listed in the directory again!"
|
bob <# "SimpleX-Directory> The group ID 1 (privacy) is listed in the directory again!"
|
||||||
groupFound bob "privacy"
|
groupFound bob "privacy"
|
||||||
superUser #> "@SimpleX-Directory privacy"
|
|
||||||
groupFoundN_ (Just 1) 2 superUser "privacy"
|
|
||||||
superUser #> "@SimpleX-Directory /link 1:privacy"
|
|
||||||
superUser <# "SimpleX-Directory> > /link 1:privacy"
|
|
||||||
superUser <## " The link to join the group ID 1 (privacy):"
|
|
||||||
superUser <##. "https://simplex.chat/contact"
|
|
||||||
superUser <## "New member role: member"
|
|
||||||
superUser #> "@SimpleX-Directory /owner 1:privacy hello there"
|
|
||||||
superUser <# "SimpleX-Directory> > /owner 1:privacy hello there"
|
|
||||||
superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)"
|
|
||||||
bob <# "SimpleX-Directory> hello there"
|
|
||||||
|
|
||||||
testDeleteGroup :: HasCallStack => FilePath -> IO ()
|
testDeleteGroup :: HasCallStack => FilePath -> IO ()
|
||||||
testDeleteGroup tmp =
|
testDeleteGroup tmp =
|
||||||
@@ -666,7 +650,7 @@ testRegOwnerRemovedLink tmp =
|
|||||||
bob <## "description changed to:"
|
bob <## "description changed to:"
|
||||||
bob <## welcomeWithLink
|
bob <## welcomeWithLink
|
||||||
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
|
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
|
||||||
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
|
bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
cath <## "bob updated group #privacy:"
|
cath <## "bob updated group #privacy:"
|
||||||
cath <## "description changed to:"
|
cath <## "description changed to:"
|
||||||
cath <## welcomeWithLink
|
cath <## welcomeWithLink
|
||||||
@@ -708,7 +692,7 @@ testAnotherOwnerRemovedLink tmp =
|
|||||||
bob <## "description changed to:"
|
bob <## "description changed to:"
|
||||||
bob <## (welcomeWithLink <> " - welcome!")
|
bob <## (welcomeWithLink <> " - welcome!")
|
||||||
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
|
bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
|
||||||
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
|
bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
cath <## "bob updated group #privacy:"
|
cath <## "bob updated group #privacy:"
|
||||||
cath <## "description changed to:"
|
cath <## "description changed to:"
|
||||||
cath <## (welcomeWithLink <> " - welcome!")
|
cath <## (welcomeWithLink <> " - welcome!")
|
||||||
@@ -790,7 +774,7 @@ testDuplicateProhibitWhenUpdated tmp =
|
|||||||
cath ##> "/gp privacy security Security"
|
cath ##> "/gp privacy security Security"
|
||||||
cath <## "changed to #security (Security)"
|
cath <## "changed to #security (Security)"
|
||||||
cath <# "SimpleX-Directory> Thank you! The group link for ID 2 (security) is added to the welcome message."
|
cath <# "SimpleX-Directory> Thank you! The group link for ID 2 (security) is added to the welcome message."
|
||||||
cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
|
cath <## "You will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2
|
notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2
|
||||||
approveRegistration superUser cath "security" 2
|
approveRegistration superUser cath "security" 2
|
||||||
groupFound bob "security"
|
groupFound bob "security"
|
||||||
@@ -1051,7 +1035,7 @@ updateProfileWithLink u n welcomeWithLink ugId = do
|
|||||||
u <## "description changed to:"
|
u <## "description changed to:"
|
||||||
u <## welcomeWithLink
|
u <## welcomeWithLink
|
||||||
u <# ("SimpleX-Directory> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.")
|
u <# ("SimpleX-Directory> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.")
|
||||||
u <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
|
u <## "You will be notified once the group is added to the directory - it may take up to 24 hours."
|
||||||
|
|
||||||
notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
|
notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
|
||||||
notifySuperUser su u n fn welcomeWithLink gId = do
|
notifySuperUser su u n fn welcomeWithLink gId = do
|
||||||
@@ -1128,13 +1112,10 @@ groupFoundN count u name = do
|
|||||||
groupFoundN' count u name
|
groupFoundN' count u name
|
||||||
|
|
||||||
groupFoundN' :: Int -> TestCC -> String -> IO ()
|
groupFoundN' :: Int -> TestCC -> String -> IO ()
|
||||||
groupFoundN' = groupFoundN_ Nothing
|
groupFoundN' count u name = do
|
||||||
|
|
||||||
groupFoundN_ :: Maybe Int -> Int -> TestCC -> String -> IO ()
|
|
||||||
groupFoundN_ shownId_ count u name = do
|
|
||||||
u <# ("SimpleX-Directory> > " <> name)
|
u <# ("SimpleX-Directory> > " <> name)
|
||||||
u <## " Found 1 group(s)."
|
u <## " Found 1 group(s)."
|
||||||
u <#. ("SimpleX-Directory> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name)
|
u <#. ("SimpleX-Directory> " <> name)
|
||||||
u <## "Welcome message:"
|
u <## "Welcome message:"
|
||||||
u <##. "Link to join the group "
|
u <##. "Link to join the group "
|
||||||
u <## (show count <> " members")
|
u <## (show count <> " members")
|
||||||
|
|||||||
+7
-19
@@ -25,10 +25,9 @@ import Data.Maybe (isNothing)
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Network.Socket
|
import Network.Socket
|
||||||
import Simplex.Chat
|
import Simplex.Chat
|
||||||
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), PresetServers (..), defaultSimpleNetCfg)
|
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg)
|
||||||
import Simplex.Chat.Core
|
import Simplex.Chat.Core
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Operators (PresetOperator (..), presetServer)
|
|
||||||
import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion)
|
import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion)
|
||||||
import Simplex.Chat.Store
|
import Simplex.Chat.Store
|
||||||
import Simplex.Chat.Store.Profiles
|
import Simplex.Chat.Store.Profiles
|
||||||
@@ -95,8 +94,8 @@ testCoreOpts =
|
|||||||
{ dbFilePrefix = "./simplex_v1",
|
{ dbFilePrefix = "./simplex_v1",
|
||||||
dbKey = "",
|
dbKey = "",
|
||||||
-- dbKey = "this is a pass-phrase to encrypt the database",
|
-- dbKey = "this is a pass-phrase to encrypt the database",
|
||||||
smpServers = [],
|
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"],
|
||||||
xftpServers = [],
|
xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"],
|
||||||
simpleNetCfg = defaultSimpleNetCfg,
|
simpleNetCfg = defaultSimpleNetCfg,
|
||||||
logLevel = CLLImportant,
|
logLevel = CLLImportant,
|
||||||
logConnections = False,
|
logConnections = False,
|
||||||
@@ -150,18 +149,6 @@ testCfg :: ChatConfig
|
|||||||
testCfg =
|
testCfg =
|
||||||
defaultChatConfig
|
defaultChatConfig
|
||||||
{ agentConfig = testAgentCfg,
|
{ agentConfig = testAgentCfg,
|
||||||
presetServers =
|
|
||||||
(presetServers defaultChatConfig)
|
|
||||||
{ operators =
|
|
||||||
[ PresetOperator
|
|
||||||
{ operator = Just operatorSimpleXChat,
|
|
||||||
smp = map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"],
|
|
||||||
useSMP = 1,
|
|
||||||
xftp = map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"],
|
|
||||||
useXFTP = 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
showReceipts = False,
|
showReceipts = False,
|
||||||
testView = True,
|
testView = True,
|
||||||
tbqSize = 16
|
tbqSize = 16
|
||||||
@@ -436,10 +423,11 @@ smpServerCfg =
|
|||||||
ServerConfig
|
ServerConfig
|
||||||
{ transports = [(serverPort, transport @TLS, False)],
|
{ transports = [(serverPort, transport @TLS, False)],
|
||||||
tbqSize = 1,
|
tbqSize = 1,
|
||||||
msgStoreType = AMSType SMSMemory,
|
-- serverTbqSize = 1,
|
||||||
msgQueueQuota = 16,
|
msgQueueQuota = 16,
|
||||||
maxJournalMsgCount = 24,
|
msgStoreType = AMSType SMSMemory,
|
||||||
maxJournalStateLines = 4,
|
maxJournalMsgCount = 1000,
|
||||||
|
maxJournalStateLines = 1000,
|
||||||
queueIdBytes = 12,
|
queueIdBytes = 12,
|
||||||
msgIdBytes = 6,
|
msgIdBytes = 6,
|
||||||
storeLogFile = Nothing,
|
storeLogFile = Nothing,
|
||||||
|
|||||||
+62
-11
@@ -25,7 +25,7 @@ import Database.SQLite.Simple (Only (..))
|
|||||||
import Simplex.Chat.AppSettings (defaultAppSettings)
|
import Simplex.Chat.AppSettings (defaultAppSettings)
|
||||||
import qualified Simplex.Chat.AppSettings as AS
|
import qualified Simplex.Chat.AppSettings as AS
|
||||||
import Simplex.Chat.Call
|
import Simplex.Chat.Call
|
||||||
import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..))
|
import Simplex.Chat.Controller (ChatConfig (..), DefaultAgentServers (..))
|
||||||
import Simplex.Chat.Messages (ChatItemId)
|
import Simplex.Chat.Messages (ChatItemId)
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Protocol (supportedChatVRange)
|
import Simplex.Chat.Protocol (supportedChatVRange)
|
||||||
@@ -66,6 +66,7 @@ chatDirectTests = do
|
|||||||
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
|
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
|
||||||
it "should send multiline message" testMultilineMessage
|
it "should send multiline message" testMultilineMessage
|
||||||
it "send large message" testLargeMessage
|
it "send large message" testLargeMessage
|
||||||
|
it "initial chat pagination" testChatPaginationInitial
|
||||||
describe "batch send messages" $ do
|
describe "batch send messages" $ do
|
||||||
it "send multiple messages api" testSendMulti
|
it "send multiple messages api" testSendMulti
|
||||||
it "send multiple timed messages" testSendMultiTimed
|
it "send multiple timed messages" testSendMultiTimed
|
||||||
@@ -210,6 +211,7 @@ testAddContact = versionTestMatrix2 runTestAddContact
|
|||||||
-- pagination
|
-- pagination
|
||||||
alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")])
|
alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")])
|
||||||
alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")])
|
alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")])
|
||||||
|
alice #$> ("/_get chat @2 around=" <> itemId 2 <> " count=3", chat, [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")])
|
||||||
-- search
|
-- search
|
||||||
alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")])
|
alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")])
|
||||||
-- read messages
|
-- read messages
|
||||||
@@ -332,8 +334,8 @@ testRetryConnectingClientTimeout tmp = do
|
|||||||
{ quotaExceededTimeout = 1,
|
{ quotaExceededTimeout = 1,
|
||||||
messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
|
messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
|
||||||
},
|
},
|
||||||
presetServers =
|
defaultServers =
|
||||||
let def@PresetServers {netCfg} = presetServers testCfg
|
let def@DefaultAgentServers {netCfg} = defaultServers testCfg
|
||||||
in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}}
|
in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}}
|
||||||
}
|
}
|
||||||
opts' =
|
opts' =
|
||||||
@@ -360,6 +362,49 @@ testMarkReadDirect = testChat2 aliceProfile bobProfile $ \alice bob -> do
|
|||||||
let itemIds = intercalate "," $ map show [i - 3 .. i]
|
let itemIds = intercalate "," $ map show [i - 3 .. i]
|
||||||
bob #$> ("/_read chat items @2 " <> itemIds, id, "ok")
|
bob #$> ("/_read chat items @2 " <> itemIds, id, "ok")
|
||||||
|
|
||||||
|
testChatPaginationInitial :: HasCallStack => FilePath -> IO ()
|
||||||
|
testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
-- Wait, otherwise ids are going to be wrong.
|
||||||
|
threadDelay 1000000
|
||||||
|
|
||||||
|
-- Send messages from alice to bob
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("@bob " <> show n)
|
||||||
|
|
||||||
|
-- Bob receives the messages.
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("alice> " <> show n)
|
||||||
|
|
||||||
|
-- All messages are unread for bob, should return area around unread
|
||||||
|
bob #$> ("/_get chat @2 initial=3", chat, [(0, "Audio/video calls: enabled"), (0, "1"), (0, "2")])
|
||||||
|
|
||||||
|
-- Read next 2 items
|
||||||
|
let itemIds = intercalate "," $ map itemId [1 .. 2]
|
||||||
|
bob #$> ("/_read chat items @2 " <> itemIds, id, "ok")
|
||||||
|
bob #$> ("/_get chat @2 initial=3", chat, [(0, "2"), (0, "3"), (0, "4")])
|
||||||
|
|
||||||
|
-- Read all items
|
||||||
|
bob #$> ("/_read chat @2", id, "ok")
|
||||||
|
bob #$> ("/_get chat @2 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")])
|
||||||
|
bob #$> ("/_get chat @2 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")])
|
||||||
|
|
||||||
|
-- Clear chat, send a few extra message and assert page size is consistent
|
||||||
|
bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("@bob " <> show n)
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("alice> " <> show n)
|
||||||
|
|
||||||
|
bob #$> ("/_get chat @2 initial=5", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")])
|
||||||
|
let newItemIds = intercalate "," $ map itemId [11 .. 12] -- Read, 1, 2
|
||||||
|
bob #$> ("/_read chat items @2 " <> newItemIds, id, "ok")
|
||||||
|
bob #$> ("/_get chat @2 initial=5", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")])
|
||||||
|
let allButLastId = intercalate "," $ map itemId [13 .. 19] -- Read all but last
|
||||||
|
bob #$> ("/_read chat items @2 " <> allButLastId, id, "ok")
|
||||||
|
bob #$> ("/_get chat @2 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")])
|
||||||
|
where
|
||||||
|
opts =
|
||||||
|
testOpts
|
||||||
|
{ markRead = False
|
||||||
|
}
|
||||||
|
|
||||||
testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO ()
|
testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO ()
|
||||||
testDuplicateContactsSeparate =
|
testDuplicateContactsSeparate =
|
||||||
testChat2 aliceProfile bobProfile $
|
testChat2 aliceProfile bobProfile $
|
||||||
@@ -791,7 +836,7 @@ testDirectMessageDelete =
|
|||||||
alice @@@ [("@bob", lastChatFeature)]
|
alice @@@ [("@bob", lastChatFeature)]
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
||||||
|
|
||||||
-- alice: msg id 1
|
-- alice: msg id 3
|
||||||
bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice")
|
bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice")
|
||||||
bob <# "@alice [edited] > hello 🙂"
|
bob <# "@alice [edited] > hello 🙂"
|
||||||
bob <## " hey alice"
|
bob <## " hey alice"
|
||||||
@@ -806,12 +851,12 @@ testDirectMessageDelete =
|
|||||||
alice @@@ [("@bob", "hey alice [marked deleted]")]
|
alice @@@ [("@bob", "hey alice [marked deleted]")]
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")])
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")])
|
||||||
|
|
||||||
-- alice: deletes msg id 1 that was broadcast deleted by bob
|
-- alice: deletes msg id 3 that was broadcast deleted by bob
|
||||||
alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted")
|
alice #$> ("/_delete item @2 " <> itemId 3 <> " internal", id, "message deleted")
|
||||||
alice @@@ [("@bob", lastChatFeature)]
|
alice @@@ [("@bob", lastChatFeature)]
|
||||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
alice #$> ("/_get chat @2 count=100", chat, chatFeatures)
|
||||||
|
|
||||||
-- alice: msg id 1, bob: msg id 3 (quoting message alice deleted locally)
|
-- alice: msg id 4, bob: msg id 3 (quoting message alice deleted locally)
|
||||||
bob `send` "> @alice (hello 🙂) do you receive my messages?"
|
bob `send` "> @alice (hello 🙂) do you receive my messages?"
|
||||||
bob <# "@alice > hello 🙂"
|
bob <# "@alice > hello 🙂"
|
||||||
bob <## " do you receive my messages?"
|
bob <## " do you receive my messages?"
|
||||||
@@ -819,14 +864,14 @@ testDirectMessageDelete =
|
|||||||
alice <## " do you receive my messages?"
|
alice <## " do you receive my messages?"
|
||||||
alice @@@ [("@bob", "do you receive my messages?")]
|
alice @@@ [("@bob", "do you receive my messages?")]
|
||||||
alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))])
|
alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))])
|
||||||
alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "cannot delete this item")
|
alice #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "cannot delete this item")
|
||||||
|
|
||||||
-- alice: msg id 2, bob: msg id 4
|
-- alice: msg id 5, bob: msg id 4
|
||||||
bob #> "@alice how are you?"
|
bob #> "@alice how are you?"
|
||||||
alice <# "bob> how are you?"
|
alice <# "bob> how are you?"
|
||||||
|
|
||||||
-- alice: deletes msg id 2
|
-- alice: deletes msg id 5
|
||||||
alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted")
|
alice #$> ("/_delete item @2 " <> itemId 5 <> " internal", id, "message deleted")
|
||||||
|
|
||||||
-- bob: marks deleted msg id 4 (that alice deleted locally)
|
-- bob: marks deleted msg id 4 (that alice deleted locally)
|
||||||
bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted")
|
bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted")
|
||||||
@@ -2340,6 +2385,12 @@ testUserPrivacy =
|
|||||||
"bob> Voice messages: enabled",
|
"bob> Voice messages: enabled",
|
||||||
"bob> Audio/video calls: enabled"
|
"bob> Audio/video calls: enabled"
|
||||||
]
|
]
|
||||||
|
alice ##> "/_get items around=11 count=3"
|
||||||
|
alice
|
||||||
|
<##? [ "bob> Message reactions: enabled",
|
||||||
|
"bob> Voice messages: enabled",
|
||||||
|
"bob> Audio/video calls: enabled"
|
||||||
|
]
|
||||||
alice ##> "/_get items after=12 count=10"
|
alice ##> "/_get items after=12 count=10"
|
||||||
alice
|
alice
|
||||||
<##? [ "@bob hello",
|
<##? [ "@bob hello",
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
|
||||||
{-# LANGUAGE NumericUnderscores #-}
|
{-# LANGUAGE NumericUnderscores #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PostfixOperators #-}
|
{-# LANGUAGE PostfixOperators #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
||||||
|
|
||||||
module ChatTests.Groups where
|
module ChatTests.Groups where
|
||||||
|
|
||||||
@@ -38,6 +36,7 @@ chatGroupTests = do
|
|||||||
describe "chat groups" $ do
|
describe "chat groups" $ do
|
||||||
describe "add contacts, create group and send/receive messages" testGroupMatrix
|
describe "add contacts, create group and send/receive messages" testGroupMatrix
|
||||||
it "mark multiple messages as read" testMarkReadGroup
|
it "mark multiple messages as read" testMarkReadGroup
|
||||||
|
it "initial chat pagination" testChatPaginationInitial
|
||||||
it "v1: add contacts, create group and send/receive messages" testGroup
|
it "v1: add contacts, create group and send/receive messages" testGroup
|
||||||
it "v1: add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
|
it "v1: add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
|
||||||
it "send large message" testGroupLargeMessage
|
it "send large message" testGroupLargeMessage
|
||||||
@@ -346,6 +345,7 @@ testGroupShared alice bob cath checkMessages directConnections = do
|
|||||||
-- so we take into account group event items as well as sent group invitations in direct chats
|
-- so we take into account group event items as well as sent group invitations in direct chats
|
||||||
alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")])
|
alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")])
|
||||||
alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")])
|
alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")])
|
||||||
|
alice #$> ("/_get chat #1 around=" <> msgItem2 <> " count=6", chat, [(0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) -- expecting only 4 items since there is only 1 item after
|
||||||
alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")])
|
alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")])
|
||||||
bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
|
bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
|
||||||
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")])
|
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")])
|
||||||
@@ -376,6 +376,51 @@ testMarkReadGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do
|
|||||||
let itemIds = intercalate "," $ map show [i - 3 .. i]
|
let itemIds = intercalate "," $ map show [i - 3 .. i]
|
||||||
bob #$> ("/_read chat items #1 " <> itemIds, id, "ok")
|
bob #$> ("/_read chat items #1 " <> itemIds, id, "ok")
|
||||||
|
|
||||||
|
testChatPaginationInitial :: HasCallStack => FilePath -> IO ()
|
||||||
|
testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do
|
||||||
|
createGroup2 "team" alice bob
|
||||||
|
-- Wait, otherwise ids are going to be wrong.
|
||||||
|
threadDelay 1000000
|
||||||
|
lastEventId <- (read :: String -> Int) <$> lastItemId bob
|
||||||
|
let groupItemId n = show $ lastEventId + n
|
||||||
|
|
||||||
|
-- Send messages from alice to bob
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("#team " <> show n)
|
||||||
|
|
||||||
|
-- Bob receives the messages.
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("#team alice> " <> show n)
|
||||||
|
|
||||||
|
-- All messages are unread for bob, should return area around unread
|
||||||
|
bob #$> ("/_get chat #1 initial=3", chat, [(0, "connected"), (0, "1"), (0, "2")])
|
||||||
|
|
||||||
|
-- Read next 2 items
|
||||||
|
let itemIds = intercalate "," $ map groupItemId [1 .. 2]
|
||||||
|
bob #$> ("/_read chat items #1 " <> itemIds, id, "ok")
|
||||||
|
bob #$> ("/_get chat #1 initial=3", chat, [(0, "2"), (0, "3"), (0, "4")])
|
||||||
|
|
||||||
|
-- Read all items
|
||||||
|
bob #$> ("/_read chat #1", id, "ok")
|
||||||
|
bob #$> ("/_get chat #1 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")])
|
||||||
|
bob #$> ("/_get chat #1 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")])
|
||||||
|
|
||||||
|
-- Clear chat, send a few extra message and assert page size is consistent
|
||||||
|
bob #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("#team " <> show n)
|
||||||
|
forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("#team alice> " <> show n)
|
||||||
|
|
||||||
|
bob #$> ("/_get chat #1 initial=5", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")])
|
||||||
|
let newItemIds = intercalate "," $ map groupItemId [11 .. 12] -- Read, 1, 2
|
||||||
|
bob #$> ("/_read chat items #1 " <> newItemIds, id, "ok")
|
||||||
|
bob #$> ("/_get chat #1 initial=5", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")])
|
||||||
|
let allButLastId = intercalate "," $ map groupItemId [13 .. 19] -- Read all but last
|
||||||
|
bob #$> ("/_read chat items #1 " <> allButLastId, id, "ok")
|
||||||
|
bob #$> ("/_get chat #1 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")])
|
||||||
|
where
|
||||||
|
opts =
|
||||||
|
testOpts
|
||||||
|
{ markRead = False
|
||||||
|
}
|
||||||
|
|
||||||
testGroupLargeMessage :: HasCallStack => FilePath -> IO ()
|
testGroupLargeMessage :: HasCallStack => FilePath -> IO ()
|
||||||
testGroupLargeMessage =
|
testGroupLargeMessage =
|
||||||
testChat2 aliceProfile bobProfile $
|
testChat2 aliceProfile bobProfile $
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
|||||||
alice ##> "/chats"
|
alice ##> "/chats"
|
||||||
|
|
||||||
alice /* "ahoy!"
|
alice /* "ahoy!"
|
||||||
alice ##> "/_update item *1 1 text Greetings."
|
alice ##> "/_update item *1 2 text Greetings."
|
||||||
alice ##> "/tail *"
|
alice ##> "/tail *"
|
||||||
alice <# "* Greetings."
|
alice <# "* Greetings."
|
||||||
|
|
||||||
@@ -102,6 +102,10 @@ testChatPagination tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
|||||||
|
|
||||||
alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")])
|
alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")])
|
||||||
|
alice #$> ("/_get chat *1 around=2 count=1", chat, [(1, "memento mori")])
|
||||||
|
alice #$> ("/_get chat *1 around=2 count=3", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock")])
|
||||||
|
alice #$> ("/_get chat *1 around=3 count=10", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
||||||
|
alice #$> ("/_get chat *1 around=4 count=3", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")])
|
alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")])
|
alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||||
alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")])
|
alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")])
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PostfixOperators #-}
|
{-# LANGUAGE PostfixOperators #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
||||||
|
|
||||||
module ChatTests.Profiles where
|
module ChatTests.Profiles where
|
||||||
|
|
||||||
|
|||||||
+20
-33
@@ -1,64 +1,51 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
|
||||||
{-# LANGUAGE GADTs #-}
|
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE StandaloneDeriving #-}
|
{-# LANGUAGE StandaloneDeriving #-}
|
||||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
||||||
|
|
||||||
module RandomServers where
|
module RandomServers where
|
||||||
|
|
||||||
import Control.Monad (replicateM)
|
import Control.Monad (replicateM)
|
||||||
import Data.Foldable (foldMap')
|
|
||||||
import Data.List (sortOn)
|
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
|
||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
import Data.Monoid (Sum (..))
|
import Simplex.Chat (cfgServers, cfgServersToUse, defaultChatConfig, randomServers)
|
||||||
import Simplex.Chat (defaultChatConfig, randomPresetServers)
|
import Simplex.Chat.Controller (ChatConfig (..))
|
||||||
import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..))
|
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..))
|
||||||
import Simplex.Chat.Operators (DBEntityId' (..), NewUserServer, UserServer' (..), operatorServers, operatorServersToUse)
|
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..))
|
|
||||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol)
|
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol)
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
randomServersTests :: Spec
|
randomServersTests :: Spec
|
||||||
randomServersTests = describe "choosig random servers" $ do
|
randomServersTests = describe "choosig random servers" $ do
|
||||||
it "should choose 4 + 3 random SMP servers and keep the rest disabled" testRandomSMPServers
|
it "should choose 4 random SMP servers and keep the rest disabled" testRandomSMPServers
|
||||||
it "should choose 3 + 3 random XFTP servers and keep the rest disabled" testRandomXFTPServers
|
it "should keep all 6 XFTP servers" testRandomXFTPServers
|
||||||
|
|
||||||
deriving instance Eq ServerRoles
|
deriving instance Eq (ServerCfg p)
|
||||||
|
|
||||||
deriving instance Eq (DBEntityId' s)
|
|
||||||
|
|
||||||
deriving instance Eq (UserServer' s p)
|
|
||||||
|
|
||||||
testRandomSMPServers :: IO ()
|
testRandomSMPServers :: IO ()
|
||||||
testRandomSMPServers = do
|
testRandomSMPServers = do
|
||||||
[srvs1, srvs2, srvs3] <-
|
[srvs1, srvs2, srvs3] <-
|
||||||
replicateM 3 $
|
replicateM 3 $
|
||||||
checkEnabled SPSMP 7 False =<< randomPresetServers SPSMP (presetServers defaultChatConfig)
|
checkEnabled SPSMP 4 False =<< randomServers SPSMP defaultChatConfig
|
||||||
(srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures
|
(srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures
|
||||||
|
|
||||||
testRandomXFTPServers :: IO ()
|
testRandomXFTPServers :: IO ()
|
||||||
testRandomXFTPServers = do
|
testRandomXFTPServers = do
|
||||||
[srvs1, srvs2, srvs3] <-
|
[srvs1, srvs2, srvs3] <-
|
||||||
replicateM 3 $
|
replicateM 3 $
|
||||||
checkEnabled SPXFTP 6 False =<< randomPresetServers SPXFTP (presetServers defaultChatConfig)
|
checkEnabled SPXFTP 6 True =<< randomServers SPXFTP defaultChatConfig
|
||||||
(srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures
|
(srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` True
|
||||||
|
|
||||||
checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> NonEmpty (NewUserServer p) -> IO [NewUserServer p]
|
checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> (L.NonEmpty (ServerCfg p), [ServerCfg p]) -> IO [ServerCfg p]
|
||||||
checkEnabled p n allUsed srvs = do
|
checkEnabled p n allUsed (srvs, _) = do
|
||||||
let srvs' = sortOn server' $ L.toList srvs
|
let def = defaultServers defaultChatConfig
|
||||||
PresetServers {operators = presetOps} = presetServers defaultChatConfig
|
cfgSrvs = L.sortWith server' $ cfgServers p def
|
||||||
presetSrvs = sortOn server' $ concatMap (operatorServers p) presetOps
|
toUse = cfgServersToUse p def
|
||||||
Sum toUse = foldMap' (Sum . operatorServersToUse p) presetOps
|
srvs == cfgSrvs `shouldBe` allUsed
|
||||||
srvs' == presetSrvs `shouldBe` allUsed
|
L.map enable srvs `shouldBe` L.map enable cfgSrvs
|
||||||
map enable srvs' `shouldBe` map enable presetSrvs
|
let enbldSrvs = L.filter (\ServerCfg {enabled} -> enabled) srvs
|
||||||
let enbldSrvs = filter (\UserServer {enabled} -> enabled) srvs'
|
|
||||||
toUse `shouldBe` n
|
toUse `shouldBe` n
|
||||||
length enbldSrvs `shouldBe` n
|
length enbldSrvs `shouldBe` n
|
||||||
pure enbldSrvs
|
pure enbldSrvs
|
||||||
where
|
where
|
||||||
server' UserServer {server = ProtoServerWithAuth srv _} = srv
|
server' ServerCfg {server = ProtoServerWithAuth srv _} = srv
|
||||||
enable :: forall p. NewUserServer p -> NewUserServer p
|
enable :: forall p. ServerCfg p -> ServerCfg p
|
||||||
enable srv = (srv :: NewUserServer p) {enabled = False}
|
enable srv = (srv :: ServerCfg p) {enabled = False}
|
||||||
|
|||||||
+3
-1
@@ -102,7 +102,9 @@ skipComparisonForDownMigrations =
|
|||||||
-- table and indexes move down to the end of the file
|
-- table and indexes move down to the end of the file
|
||||||
"20231215_recreate_msg_deliveries",
|
"20231215_recreate_msg_deliveries",
|
||||||
-- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file
|
-- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file
|
||||||
"20240313_drop_agent_ack_cmd_id"
|
"20240313_drop_agent_ack_cmd_id",
|
||||||
|
-- on down migration chat_item_autoincrement_id makes sequence table creation move down on the file
|
||||||
|
"20241023_chat_item_autoincrement_id"
|
||||||
]
|
]
|
||||||
|
|
||||||
getSchema :: FilePath -> FilePath -> IO String
|
getSchema :: FilePath -> FilePath -> IO String
|
||||||
|
|||||||
Reference in New Issue
Block a user