forked from Ivasoft/mattermost-mobile
[Gekidou] iOS - Fetch and store data on push notification receipt
This commit is contained in:
@@ -16,3 +16,9 @@ extension Date {
|
||||
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
extension StringProtocol {
|
||||
public subscript(offset: Int) -> Character {
|
||||
self[index(startIndex, offsetBy: offset)]
|
||||
}
|
||||
}
|
||||
|
||||
300
ios/Gekidou/Sources/Gekidou/JSONDecode+Extension.swift
Normal file
300
ios/Gekidou/Sources/Gekidou/JSONDecode+Extension.swift
Normal file
@@ -0,0 +1,300 @@
|
||||
//
|
||||
// JSONDecode+Extension.swift
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Inspired by https://gist.github.com/loudmouth/332e8d89d8de2c1eaf81875cfcd22e24
|
||||
// Used to decode dictionaries and array of dictionaries
|
||||
|
||||
struct JSONCodingKeys: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
self.init(stringValue: "\(intValue)")
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyedDecodingContainer {
|
||||
|
||||
func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] {
|
||||
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
|
||||
return try container.decode(type)
|
||||
}
|
||||
|
||||
func decodeIfPresent(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any]? {
|
||||
guard contains(key) else {
|
||||
return nil
|
||||
}
|
||||
return try decode(type, forKey: key)
|
||||
}
|
||||
|
||||
func decode(_ type: [Any].Type, forKey key: K) throws -> [Any] {
|
||||
var container = try self.nestedUnkeyedContainer(forKey: key)
|
||||
return try container.decode(type)
|
||||
}
|
||||
|
||||
func decode(_ type: [[String: Any]].Type, forKey key: K) throws -> [[String: Any]] {
|
||||
var container = try self.nestedUnkeyedContainer(forKey: key)
|
||||
return try container.decode(type)
|
||||
}
|
||||
|
||||
func decodeIfPresent(_ type: [Any].Type, forKey key: K) throws -> [Any]? {
|
||||
guard contains(key) else {
|
||||
return nil
|
||||
}
|
||||
return try decode(type, forKey: key)
|
||||
}
|
||||
|
||||
func decode(_ type: [String: Any].Type) throws -> [String: Any] {
|
||||
var dictionary = [String: Any]()
|
||||
|
||||
for key in allKeys {
|
||||
if let boolValue = try? decode(Bool.self, forKey: key) {
|
||||
dictionary[key.stringValue] = boolValue
|
||||
} else if let intValue = try? decode(Int.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(Int8.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(Int16.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(Int32.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(Int64.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(UInt.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(UInt8.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(UInt16.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(UInt32.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let intValue = try? decode(UInt64.self, forKey: key) {
|
||||
dictionary[key.stringValue] = intValue
|
||||
} else if let doubleValue = try? decode(Float.self, forKey: key) {
|
||||
dictionary[key.stringValue] = doubleValue
|
||||
} else if let doubleValue = try? decode(Double.self, forKey: key) {
|
||||
dictionary[key.stringValue] = doubleValue
|
||||
} else if let stringValue = try? decode(String.self, forKey: key) {
|
||||
dictionary[key.stringValue] = stringValue
|
||||
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
|
||||
dictionary[key.stringValue] = nestedDictionary
|
||||
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
|
||||
dictionary[key.stringValue] = nestedArray
|
||||
} else if let value = try? decodeNil(forKey: key), value {
|
||||
//saving NSNull values in a dictionary will produce unexpected results for users, just skip
|
||||
}
|
||||
}
|
||||
return dictionary
|
||||
}
|
||||
|
||||
func decodeIfPresent<T: Decodable>(forKey key: K, defaultValue: T) -> T {
|
||||
do {
|
||||
//below will throw
|
||||
return try self.decodeIfPresent(T.self, forKey: key) ?? defaultValue
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UnkeyedDecodingContainer {
|
||||
|
||||
mutating func decode(_ type: [[String: Any]].Type) throws -> [[String: Any]] {
|
||||
var array: [[String: Any]] = []
|
||||
while isAtEnd == false {
|
||||
if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
|
||||
array.append(nestedDictionary)
|
||||
}
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
mutating func decode(_ type: [Any].Type) throws -> [Any] {
|
||||
var array: [Any] = []
|
||||
while isAtEnd == false {
|
||||
if let value = try? decode(Bool.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Int.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Int8.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Int16.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Int32.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Int64.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(UInt.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(UInt16.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(UInt32.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(UInt64.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Float.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(Double.self) {
|
||||
array.append(value)
|
||||
} else if let value = try? decode(String.self) {
|
||||
array.append(value)
|
||||
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
|
||||
array.append(nestedDictionary)
|
||||
} else if let nestedArray = try? decodeNestedArray(Array<Any>.self) {
|
||||
array.append(nestedArray)
|
||||
} else if let value = try? decodeNil(), value {
|
||||
array.append(NSNull()) //unavoidable, but should be fine. We return [Any]. An overload to return homegenous array would be nice.
|
||||
} else {
|
||||
//if the right type is not found, it will get stuck in an infinite loop, throw, we can't handle it
|
||||
throw EncodingError.invalidValue("<UNKNOWN TYPE>", EncodingError.Context(codingPath: codingPath, debugDescription: "<UNKNOWN TYPE>"))
|
||||
}
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
mutating func decodeNestedArray(_ type: [Any].Type) throws -> [Any] {
|
||||
// throws: `CocoaError.coderTypeMismatch` if the encountered stored value is not an unkeyed container.
|
||||
var nestedContainer = try self.nestedUnkeyedContainer()
|
||||
return try nestedContainer.decode(Array<Any>.self)
|
||||
}
|
||||
|
||||
mutating func decode(_ type: [String: Any].Type) throws -> [String: Any] {
|
||||
// throws: `CocoaError.coderTypeMismatch` if the encountered stored value is not a keyed container.
|
||||
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
|
||||
return try nestedContainer.decode(type)
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyedEncodingContainerProtocol where Key == JSONCodingKeys {
|
||||
|
||||
mutating func encode(_ value: [String: Any]) throws {
|
||||
for (key, value) in value {
|
||||
let key = JSONCodingKeys(stringValue: key)
|
||||
switch value {
|
||||
case let value as Bool:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Int:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Int8:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Int16:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Int32:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Int64:
|
||||
try encode(value, forKey: key)
|
||||
case let value as UInt:
|
||||
try encode(value, forKey: key)
|
||||
case let value as UInt8:
|
||||
try encode(value, forKey: key)
|
||||
case let value as UInt16:
|
||||
try encode(value, forKey: key)
|
||||
case let value as UInt32:
|
||||
try encode(value, forKey: key)
|
||||
case let value as UInt64:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Float:
|
||||
try encode(value, forKey: key)
|
||||
case let value as Double:
|
||||
try encode(value, forKey: key)
|
||||
case let value as String:
|
||||
try encode(value, forKey: key)
|
||||
case let value as [String: Any]:
|
||||
try encode(value, forKey: key)
|
||||
case let value as [Any]:
|
||||
try encode(value, forKey: key)
|
||||
case is NSNull:
|
||||
try encodeNil(forKey: key)
|
||||
case Optional<Any>.none:
|
||||
try encodeNil(forKey: key)
|
||||
default:
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [key], debugDescription: "Invalid JSON value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyedEncodingContainerProtocol {
|
||||
mutating func encode(_ value: [String: Any]?, forKey key: Key) throws {
|
||||
guard let value = value else { return }
|
||||
|
||||
var container = nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
|
||||
try container.encode(value)
|
||||
}
|
||||
|
||||
mutating func encode(_ value: [Any]?, forKey key: Key) throws {
|
||||
guard let value = value else { return }
|
||||
|
||||
var container = nestedUnkeyedContainer(forKey: key)
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension UnkeyedEncodingContainer {
|
||||
|
||||
mutating func encode(_ value: [Any]) throws {
|
||||
for (index, value) in value.enumerated() {
|
||||
switch value {
|
||||
case let value as Bool:
|
||||
try encode(value)
|
||||
case let value as Int:
|
||||
try encode(value)
|
||||
case let value as Int8:
|
||||
try encode(value)
|
||||
case let value as Int16:
|
||||
try encode(value)
|
||||
case let value as Int32:
|
||||
try encode(value)
|
||||
case let value as Int64:
|
||||
try encode(value)
|
||||
case let value as UInt:
|
||||
try encode(value)
|
||||
case let value as UInt8:
|
||||
try encode(value)
|
||||
case let value as UInt16:
|
||||
try encode(value)
|
||||
case let value as UInt32:
|
||||
try encode(value)
|
||||
case let value as UInt64:
|
||||
try encode(value)
|
||||
case let value as Float:
|
||||
try encode(value)
|
||||
case let value as Double:
|
||||
try encode(value)
|
||||
case let value as String:
|
||||
try encode(value)
|
||||
case let value as [String: Any]:
|
||||
try encode(value)
|
||||
case let value as [Any]:
|
||||
try encodeNestedArray(value)
|
||||
case is NSNull:
|
||||
try encodeNil()
|
||||
case Optional<Any>.none:
|
||||
try encodeNil()
|
||||
default:
|
||||
let keys = JSONCodingKeys(intValue: index).map({ [ $0 ] }) ?? []
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + keys, debugDescription: "Invalid JSON value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func encode(_ value: [String: Any]) throws {
|
||||
var container = nestedContainer(keyedBy: JSONCodingKeys.self)
|
||||
try container.encode(value)
|
||||
}
|
||||
|
||||
mutating func encodeNestedArray(_ value: [Any]) throws {
|
||||
var container = nestedUnkeyedContainer()
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// Network+Channels.swift
|
||||
//
|
||||
//
|
||||
// Created by Miguel Alatzar on 8/27/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Network {
|
||||
public func fetchChannel(withId channelId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/channels/\(channelId)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchChannelMembership(withChannelId channelId: String, withUserId userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/channels/\(channelId)/members/\(userId)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// Network+Teams.swift
|
||||
//
|
||||
//
|
||||
// Created by Miguel Alatzar on 8/27/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Network {
|
||||
public func fetchTeam(withId teamId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/teams/\(teamId)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchTeamMembership(withTeamId teamId: String, withUserId userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/teams/\(teamId)/members/\(userId)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
26
ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift
Normal file
26
ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Network+Channels.swift
|
||||
//
|
||||
//
|
||||
// Created by Miguel Alatzar on 8/27/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Network {
|
||||
public func fetchUsers(byIds userIds: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/users/ids"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
let data = try? JSONSerialization.data(withJSONObject: userIds, options: [])
|
||||
|
||||
return request(url, withMethod: "POST", withBody: data, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchUsers(byUsernames usernames: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "users/usernames"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
let data = try? JSONSerialization.data(withJSONObject: usernames, options: [])
|
||||
|
||||
return request(url, withMethod: "POST", withBody: data, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import SQLite
|
||||
|
||||
public struct AckNotification: Codable {
|
||||
let type: String
|
||||
@@ -18,12 +19,22 @@ public struct AckNotification: Codable {
|
||||
let platform = "ios"
|
||||
|
||||
public enum AckNotificationKeys: String, CodingKey {
|
||||
case type
|
||||
case type = "type"
|
||||
case id = "ack_id"
|
||||
case postId = "post_id"
|
||||
case serverUrl = "server_url"
|
||||
case isIdLoaded = "id_loaded"
|
||||
case platform = "platform"
|
||||
}
|
||||
|
||||
public enum AckNotificationRequestKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "ack_id"
|
||||
case postId = "post_id"
|
||||
case serverUrl = "server_url"
|
||||
case isIdLoaded = "is_id_loaded"
|
||||
|
||||
case receivedAt = "received_at"
|
||||
case platform = "platform"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -42,6 +53,25 @@ public struct AckNotification: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
extension AckNotification {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: AckNotificationRequestKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(postId, forKey: .postId)
|
||||
try container.encode(receivedAt, forKey: .receivedAt)
|
||||
try container.encode(platform, forKey: .platform)
|
||||
try container.encode(type, forKey: .type)
|
||||
try container.encode(isIdLoaded, forKey: .isIdLoaded)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func removePrefix(_ prefix: String) -> String {
|
||||
guard self.hasPrefix(prefix) else { return self }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
}
|
||||
|
||||
extension Network {
|
||||
@objc public func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: userInfo),
|
||||
@@ -50,6 +80,18 @@ extension Network {
|
||||
}
|
||||
}
|
||||
|
||||
private func matchUsername(in message: String) -> [String] {
|
||||
let specialMentions = Set(["all", "here", "channel"])
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: "\\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)", options: [.caseInsensitive])
|
||||
let results = regex.matches(in: message, range: _NSRange(message.startIndex..., in: message))
|
||||
return results.map{ String(message[Range($0.range, in: message)!]).removePrefix("@") }.filter{ !specialMentions.contains($0)}
|
||||
} catch let error {
|
||||
print("invalid regex: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func postNotificationReceipt(_ ackNotification: AckNotification, completionHandler: @escaping ResponseHandler) {
|
||||
do {
|
||||
let jsonData = try JSONEncoder().encode(ackNotification)
|
||||
@@ -64,69 +106,70 @@ extension Network {
|
||||
}
|
||||
|
||||
public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: ((UNNotificationContent) -> Void)?) {
|
||||
// TODO: All DB writes should be made in a single transaction
|
||||
|
||||
let operation = BlockOperation {
|
||||
let group = DispatchGroup()
|
||||
var channel: Channel? = nil
|
||||
var channelMembership: ChannelMembership?
|
||||
|
||||
let teamId = notification.userInfo["team_id"] as! String?
|
||||
|
||||
let channelId = notification.userInfo["channel_id"] as! String
|
||||
let serverUrl = notification.userInfo["server_url"] as! String
|
||||
let currentUserId = try! Database.default.queryCurrentUserId(serverUrl)
|
||||
|
||||
if let teamId = teamId {
|
||||
if try! !Database.default.hasMyTeam(withId: teamId, withServerUrl: serverUrl) {
|
||||
group.enter()
|
||||
self.fetchTeam(withId: teamId, withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
let team = try! JSONDecoder().decode(Team.self, from: data)
|
||||
try! Database.default.insertTeam(team, serverUrl)
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.fetchTeamMembership(withTeamId: teamId, withUserId: currentUserId, withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
let teamMembership = try! JSONDecoder().decode(TeamMembership.self, from: data)
|
||||
if teamMembership.user_id == currentUserId {
|
||||
try! Database.default.insertMyTeam(teamMembership, serverUrl)
|
||||
}
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
let currentUser = try! Database.default.queryCurrentUser(serverUrl)
|
||||
let currentUsername = currentUser?[Expression<String>("username")]
|
||||
|
||||
|
||||
group.enter()
|
||||
self.fetchChannel(withId: channelId, withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
channel = try! JSONDecoder().decode(Channel.self, from: data)
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.fetchChannelMembership(withChannelId: channelId, withUserId: currentUserId, withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
channelMembership = try! JSONDecoder().decode(ChannelMembership.self, from: data)
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
var postData: PostData? = nil
|
||||
var userIdsToLoad: Set<String> = Set()
|
||||
var usernamesToLoad: Set<String> = Set()
|
||||
var users: Set<User> = Set()
|
||||
|
||||
group.enter()
|
||||
let since = try! Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl)
|
||||
self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
let postData = try! JSONDecoder().decode(PostData.self, from: data)
|
||||
if postData.posts.count > 0 {
|
||||
try! Database.default.handlePostData(postData, channelId, serverUrl, since != nil)
|
||||
postData = try! JSONDecoder().decode(PostData.self, from: data)
|
||||
if postData?.posts.count ?? 0 > 0 {
|
||||
var authorIds: Set<String> = Set()
|
||||
var usernames: Set<String> = Set()
|
||||
postData!.posts.forEach{post in
|
||||
if (post.user_id != currentUserId) {
|
||||
authorIds.insert(post.user_id)
|
||||
}
|
||||
self.matchUsername(in: post.message).forEach{
|
||||
if ($0 != currentUsername) {
|
||||
usernames.insert($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authorIds.count > 0) {
|
||||
let existingIds = try! Database.default.queryUsers(byIds: authorIds, withServerUrl: serverUrl)
|
||||
userIdsToLoad = authorIds.filter { !existingIds.contains($0) }
|
||||
if (userIdsToLoad.count > 0) {
|
||||
group.enter()
|
||||
self.fetchUsers(byIds: Array(userIdsToLoad), withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
let usersData = try! JSONDecoder().decode([User].self, from: data)
|
||||
usersData.forEach { users.insert($0) }
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usernames.count > 0) {
|
||||
let existingUsernames = try! Database.default.queryUsers(byUsernames: usernames, withServerUrl: serverUrl)
|
||||
usernamesToLoad = usernames.filter{ !existingUsernames.contains($0)}
|
||||
if (usernamesToLoad.count > 0) {
|
||||
group.enter()
|
||||
|
||||
self.fetchUsers(byUsernames: Array(usernamesToLoad), withServerUrl: serverUrl) { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
let usersData = try! JSONDecoder().decode([User].self, from: data)
|
||||
usersData.forEach { users.insert($0) }
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +178,17 @@ extension Network {
|
||||
|
||||
group.wait()
|
||||
|
||||
try! Database.default.handleChannelAndMembership(channel, channelMembership, serverUrl)
|
||||
group.enter()
|
||||
if (postData != nil) {
|
||||
let db = try! Database.default.getDatabaseForServer(serverUrl)
|
||||
try! db.transaction {
|
||||
try! Database.default.handlePostData(db, postData!, channelId, since != nil)
|
||||
if (users.count > 0) {
|
||||
try! Database.default.insertUsers(db, users)
|
||||
}
|
||||
}
|
||||
}
|
||||
group.leave()
|
||||
|
||||
if let contentHandler = contentHandler {
|
||||
contentHandler(notification)
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
//
|
||||
// Database+Channels.swift
|
||||
//
|
||||
//
|
||||
// Created by Miguel Alatzar on 8/27/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
public struct Channel: Codable {
|
||||
let id: String
|
||||
let create_at: Int64
|
||||
let update_at: Int64
|
||||
let delete_at: Int64
|
||||
let team_id: String
|
||||
let type: String
|
||||
let display_name: String
|
||||
let name: String
|
||||
let creator_id: String
|
||||
let header: String
|
||||
let purpose: String
|
||||
let total_msg_count: Int64
|
||||
}
|
||||
|
||||
public struct ChannelMembership: Codable {
|
||||
let channel_id: String
|
||||
let user_id: String
|
||||
let roles: String
|
||||
let last_viewed_at: Int64
|
||||
let mention_count: Int64
|
||||
let msg_count: Int64
|
||||
let notify_props: [String:String]
|
||||
}
|
||||
|
||||
extension Database {
|
||||
public func queryChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Row? {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let idCol = Expression<String>("id")
|
||||
let query = channelTable.where(idCol == channelId)
|
||||
|
||||
return try db.pluck(query)
|
||||
}
|
||||
|
||||
public func queryMyChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Row? {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let idCol = Expression<String>("id")
|
||||
let query = myChannelTable.where(idCol == channelId)
|
||||
|
||||
return try db.pluck(query)
|
||||
}
|
||||
|
||||
public func insertChannel(_ channel: Channel, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createChannelSetter(from: channel)
|
||||
let query = channelTable.insert(or: .replace, setter)
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
public func insertChannelInfo(_ channel: Channel, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createChannelInfoSetter(from: channel)
|
||||
let query = channelInfoTable.insert(or: .replace, setter)
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
public func insertMyChannel(_ channelMembership: ChannelMembership, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createMyChannelSetter(from: channelMembership)
|
||||
let query = myChannelTable.insert(or: .replace, setter)
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
public func insertMyChannelSettings(_ channelMembership: ChannelMembership, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createMyChannelSettingsSetter(from: channelMembership)
|
||||
let query = myChannelSettingsTable.insert(or: .replace, setter)
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
public func insertChannelMembership(_ channelMembership: ChannelMembership, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createChannelMembershipSetter(from: channelMembership)
|
||||
let query = channelMembershipTable.insert(or: .replace, setter)
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
public func handleChannelAndMembership(_ channel: Channel?, _ channelMembership: ChannelMembership?, _ serverUrl: String) throws {
|
||||
if let channel = channel {
|
||||
try insertChannel(channel, serverUrl)
|
||||
try insertChannelInfo(channel, serverUrl)
|
||||
}
|
||||
|
||||
if let channelMembership = channelMembership {
|
||||
try insertMyChannel(channelMembership, serverUrl)
|
||||
try insertMyChannelSettings(channelMembership, serverUrl)
|
||||
try insertChannelMembership(channelMembership, serverUrl)
|
||||
}
|
||||
|
||||
try updateMyChannelMessageCount(channel, channelMembership, serverUrl)
|
||||
}
|
||||
|
||||
private func updateMyChannelMessageCount(_ channel: Channel?, _ channelMembership: ChannelMembership?, _ serverUrl: String) throws {
|
||||
if let channel = channel, let channelMembership = channelMembership {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let messageCount = channel.total_msg_count - channelMembership.msg_count
|
||||
let idCol = Expression<String>("id")
|
||||
let messageCountCol = Expression<Int64>("message_count")
|
||||
let query = myChannelTable
|
||||
.where(idCol == channel.id)
|
||||
.update(messageCountCol <- messageCount)
|
||||
|
||||
try db.run(query)
|
||||
}
|
||||
}
|
||||
|
||||
private func createChannelSetter(from channel: Channel) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let createAt = Expression<Int64>("create_at")
|
||||
let updateAt = Expression<Int64>("update_at")
|
||||
let deleteAt = Expression<Int64>("delete_at")
|
||||
let teamId = Expression<String>("team_id")
|
||||
let type = Expression<String>("type")
|
||||
let displayName = Expression<String>("display_name")
|
||||
let name = Expression<String>("name")
|
||||
let creatorId = Expression<String>("creator_id")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channel.id)
|
||||
setter.append(createAt <- channel.create_at)
|
||||
setter.append(updateAt <- channel.update_at)
|
||||
setter.append(deleteAt <- channel.delete_at)
|
||||
setter.append(teamId <- channel.team_id)
|
||||
setter.append(type <- channel.type)
|
||||
setter.append(displayName <- channel.display_name)
|
||||
setter.append(name <- channel.name)
|
||||
setter.append(creatorId <- channel.creator_id)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createChannelInfoSetter(from channel: Channel) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let header = Expression<String>("header")
|
||||
let purpose = Expression<String>("purpose")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channel.id)
|
||||
setter.append(header <- channel.header)
|
||||
setter.append(purpose <- channel.purpose)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createMyChannelSetter(from channelMembership: ChannelMembership) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let roles = Expression<String>("roles")
|
||||
let lastViewedAt = Expression<Int64>("last_viewed_at")
|
||||
let mentionsCount = Expression<Int64>("mentions_count")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channelMembership.channel_id)
|
||||
setter.append(roles <- channelMembership.roles)
|
||||
setter.append(lastViewedAt <- channelMembership.last_viewed_at)
|
||||
setter.append(mentionsCount <- channelMembership.mention_count)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createMyChannelSettingsSetter(from channelMembership: ChannelMembership) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let notifyProps = Expression<String>("notify_props")
|
||||
|
||||
let notifyPropsJSON = try! JSONSerialization.data(withJSONObject: channelMembership.notify_props, options: [])
|
||||
let notifyPropsString = String(data: notifyPropsJSON, encoding: String.Encoding.utf8)!
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channelMembership.channel_id)
|
||||
setter.append(notifyProps <- notifyPropsString)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createChannelMembershipSetter(from channelMembership: ChannelMembership) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let channelId = Expression<String>("channel_id")
|
||||
let userId = Expression<String>("user_id")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channelMembership.channel_id)
|
||||
setter.append(channelId <- channelMembership.channel_id)
|
||||
setter.append(userId <- channelMembership.user_id)
|
||||
|
||||
return setter
|
||||
}
|
||||
}
|
||||
@@ -8,132 +8,6 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
public struct EmbedMedia: Codable {
|
||||
let type: String?
|
||||
let url: String?
|
||||
let secure_url: String?
|
||||
let width: Int64?
|
||||
let height: Int64?
|
||||
}
|
||||
|
||||
public struct EmbedData: Codable {
|
||||
let type: String?
|
||||
let url: String?
|
||||
let title: String?
|
||||
let description: String?
|
||||
let determiner: String?
|
||||
let site_name: String?
|
||||
let locale: String?
|
||||
let locales_alternate: String?
|
||||
let images: [EmbedMedia]?
|
||||
let audios: [EmbedMedia]?
|
||||
let videos: [EmbedMedia]?
|
||||
}
|
||||
|
||||
public struct Embed: Codable {
|
||||
let type: String?
|
||||
let url: String?
|
||||
let data: EmbedData?
|
||||
}
|
||||
|
||||
public struct Emoji: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
}
|
||||
|
||||
public struct File: Codable {
|
||||
let id: String
|
||||
let post_id: String
|
||||
let name: String
|
||||
let `extension`: String
|
||||
let size: Int64
|
||||
let mime_type: String
|
||||
let width: Int64
|
||||
let height: Int64
|
||||
let local_path: String?
|
||||
let mini_preview: String?
|
||||
}
|
||||
|
||||
public struct Reaction: Codable {
|
||||
let user_id: String
|
||||
let post_id: String
|
||||
let emoji_name: String
|
||||
let create_at: Int64
|
||||
}
|
||||
|
||||
public struct Image: Codable {
|
||||
let width: Int64
|
||||
let height: Int64
|
||||
let format: String
|
||||
let frame_count: Int64
|
||||
}
|
||||
|
||||
public struct PostMetadata: Codable {
|
||||
let embeds: [Embed]?
|
||||
let images: [String:Image]?
|
||||
var emojis: [Emoji]?
|
||||
var files: [File]?
|
||||
var reactions: [Reaction]?
|
||||
}
|
||||
|
||||
public struct PostPropsAddChannelMember: Codable {
|
||||
let post_id: String
|
||||
let usernames: String
|
||||
let not_in_channel_usernames: String
|
||||
let user_ids: String
|
||||
let not_in_channel_user_ids: String
|
||||
let not_in_groups_usernames: String
|
||||
let not_in_groups_user_ids: String
|
||||
}
|
||||
|
||||
public struct PostPropsAttachment: Codable {
|
||||
let id: Int64
|
||||
let fallback: String
|
||||
let color: String
|
||||
let pretext: String
|
||||
let author_name: String
|
||||
let author_link: String
|
||||
let author_icon: String
|
||||
let title: String
|
||||
let title_link: String
|
||||
let text: String
|
||||
let image_url: String
|
||||
let thumb_url: String
|
||||
let footer: String
|
||||
let footer_icon: String
|
||||
|
||||
// TODO:
|
||||
// fields
|
||||
// timestamp
|
||||
// actions
|
||||
}
|
||||
|
||||
public struct PostProps: Codable {
|
||||
let userId: String?
|
||||
let username: String?
|
||||
let addedUserId: String?
|
||||
let removedUserId: String?
|
||||
let removedUsername: String?
|
||||
let deleteBy: String?
|
||||
let old_header: String?
|
||||
let new_header: String?
|
||||
let old_purpose: String?
|
||||
let new_purpose: String?
|
||||
let old_displayname: String?
|
||||
let new_displayname: String?
|
||||
let mentionHighlightDisabled: Bool?
|
||||
let disable_group_highlight: Bool?
|
||||
let override_username: Bool?
|
||||
let from_webhook: Bool?
|
||||
let override_icon_url: Bool?
|
||||
let override_icon_emoji: Bool?
|
||||
let add_channel_member: PostPropsAddChannelMember?
|
||||
let attachments: [PostPropsAttachment]?
|
||||
|
||||
// TODO:
|
||||
// appBindings
|
||||
}
|
||||
|
||||
public struct Post: Codable {
|
||||
let id: String
|
||||
let create_at: Int64
|
||||
@@ -147,21 +21,62 @@ public struct Post: Codable {
|
||||
let original_id: String
|
||||
let message: String
|
||||
let type: String
|
||||
var props: PostProps?
|
||||
let hashtag: String?
|
||||
let pending_post_id: String?
|
||||
let reply_count: Int64
|
||||
let file_ids: [String]?
|
||||
let metadata: PostMetadata?
|
||||
let last_reply_at: Int64?
|
||||
let failed: Bool?
|
||||
let ownPost: Bool?
|
||||
let participants: [String]?
|
||||
var prev_post_id: String?
|
||||
let props: String
|
||||
let pending_post_id: String
|
||||
let metadata: String
|
||||
var prev_post_id: String
|
||||
|
||||
public enum PostKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case create_at = "create_at"
|
||||
case update_at = "update_at"
|
||||
case delete_at = "delete_at"
|
||||
case edit_at = "edit_at"
|
||||
case is_pinned = "is_pinned"
|
||||
case user_id = "user_id"
|
||||
case channel_id = "channel_id"
|
||||
case root_id = "root_id"
|
||||
case original_id = "original_id"
|
||||
case message = "message"
|
||||
case type = "type"
|
||||
case props = "props"
|
||||
case pending_post_id = "pending_post_id"
|
||||
case metadata = "metadata"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: PostKeys.self)
|
||||
prev_post_id = ""
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
create_at = try values.decode(Int64.self, forKey: .create_at)
|
||||
update_at = try values.decode(Int64.self, forKey: .update_at)
|
||||
delete_at = try values.decode(Int64.self, forKey: .delete_at)
|
||||
edit_at = try values.decode(Int64.self, forKey: .edit_at)
|
||||
is_pinned = try values.decode(Bool.self, forKey: .is_pinned)
|
||||
user_id = try values.decode(String.self, forKey: .user_id)
|
||||
channel_id = try values.decode(String.self, forKey: .channel_id)
|
||||
root_id = try values.decode(String.self, forKey: .root_id)
|
||||
original_id = try values.decode(String.self, forKey: .original_id)
|
||||
message = try values.decode(String.self, forKey: .message)
|
||||
let meta = try values.decode([String:Any].self, forKey: .metadata)
|
||||
metadata = Database.default.json(from: meta) ?? "{}"
|
||||
type = try values.decode(String.self, forKey: .type)
|
||||
pending_post_id = try values.decode(String.self, forKey: .pending_post_id)
|
||||
let propsData = try values.decode([String:Any].self, forKey: .props)
|
||||
props = Database.default.json(from: propsData) ?? "{}"
|
||||
}
|
||||
}
|
||||
|
||||
struct MetadataSetters {
|
||||
let postMetadataSetters: [[Setter]]
|
||||
let metadata: String
|
||||
let reactionSetters: [[Setter]]
|
||||
let fileSetters: [[Setter]]
|
||||
let emojiSetters: [[Setter]]
|
||||
}
|
||||
|
||||
struct PostSetters {
|
||||
let id: String
|
||||
let postSetters: [Setter]
|
||||
let reactionSetters: [[Setter]]
|
||||
let fileSetters: [[Setter]]
|
||||
let emojiSetters: [[Setter]]
|
||||
@@ -228,32 +143,31 @@ extension Database {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
public func handlePostData(_ postData: PostData, _ channelId: String, _ serverUrl: String, _ usedSince: Bool = false) throws {
|
||||
try insertAndDeletePosts(postData.posts, channelId, serverUrl)
|
||||
try handlePostsInChannel(postData, channelId, serverUrl, usedSince)
|
||||
try handlePostMetadata(postData.posts, channelId, serverUrl)
|
||||
try handlePostsInThread(postData.posts, serverUrl)
|
||||
public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false) throws {
|
||||
let sortedChainedPosts = chainAndSortPosts(postData)
|
||||
try insertOrUpdatePosts(db, sortedChainedPosts, channelId)
|
||||
try handlePostsInChannel(db, channelId, postData, usedSince)
|
||||
try handlePostsInThread(db, postData.posts)
|
||||
}
|
||||
|
||||
private func handlePostsInChannel(_ postData: PostData, _ channelId: String, _ serverUrl: String, _ usedSince: Bool = false) throws {
|
||||
let sortedChainedPosts = chainAndSortPosts(postData)
|
||||
let earliest = sortedChainedPosts.first!.create_at
|
||||
let latest = sortedChainedPosts.last!.create_at
|
||||
private func handlePostsInChannel(_ db: Connection, _ channelId: String, _ postData: PostData, _ usedSince: Bool = false) throws {
|
||||
let firstId = postData.order.first
|
||||
let lastId = postData.order.last
|
||||
let earliest = postData.posts.first(where: { $0.id == lastId})!.create_at
|
||||
let latest = postData.posts.first(where: { $0.id == firstId})!.create_at
|
||||
|
||||
if usedSince {
|
||||
try updatePostsInChannelLatestOnly(latest, channelId, serverUrl)
|
||||
try updatePostsInChannelLatestOnly(db, channelId, latest)
|
||||
} else {
|
||||
let updated = try updatePostsInChannelEarliestAndLatest(earliest, latest, channelId, serverUrl)
|
||||
let updated = try updatePostsInChannelEarliestAndLatest(db, channelId, earliest, latest)
|
||||
if (!updated) {
|
||||
try insertPostsInChannel(earliest, latest, channelId, serverUrl)
|
||||
try insertPostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePostsInThread(_ posts: [Post], _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let postsInThreadSetters = try createPostsInThreadSetters(from: posts, withServerUrl: serverUrl)
|
||||
private func handlePostsInThread(_ db: Connection, _ posts: [Post]) throws {
|
||||
let postsInThreadSetters = try createPostsInThreadSetters(db, from: posts)
|
||||
if !postsInThreadSetters.isEmpty {
|
||||
let insertQuery = postsInThreadTable.insertMany(or: .replace, postsInThreadSetters)
|
||||
try db.run(insertQuery)
|
||||
@@ -262,44 +176,45 @@ extension Database {
|
||||
|
||||
private func chainAndSortPosts(_ postData: PostData) -> [Post] {
|
||||
let order = postData.order
|
||||
let prevPostId = postData.prev_post_id
|
||||
let posts = postData.posts
|
||||
|
||||
return order.enumerated().reduce([Post]()) { (chainedPosts: [Post], current) in
|
||||
let index = current.0
|
||||
let postId = current.1
|
||||
var prevPostId = ""
|
||||
|
||||
return posts.sorted(by: {$0.create_at < $1.create_at}).enumerated().map { (index, post) in
|
||||
var modified = post
|
||||
if (index == 0) {
|
||||
modified.prev_post_id = postData.prev_post_id
|
||||
} else {
|
||||
modified.prev_post_id = prevPostId
|
||||
}
|
||||
|
||||
var post = posts.first(where: {$0.id == postId})!
|
||||
post.prev_post_id = index == order.count - 1 ?
|
||||
prevPostId :
|
||||
order[index + 1]
|
||||
|
||||
return chainedPosts + [post]
|
||||
}.sorted(by: { $0.create_at < $1.create_at })
|
||||
if (order.contains(post.id)) {
|
||||
prevPostId = post.id
|
||||
}
|
||||
|
||||
return modified
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePostsInChannelLatestOnly(_ latest: Int64, _ channelId: String, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
private func updatePostsInChannelLatestOnly(_ db: Connection, _ channelId: String, _ latest: Int64) throws {
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
let query = postsInChannelTable
|
||||
.where(channelIdCol == channelId)
|
||||
.order(latestCol.desc)
|
||||
.limit(1)
|
||||
.update(latestCol <- latest)
|
||||
.update(latestCol <- latest, statusCol <- "updated")
|
||||
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
private func updatePostsInChannelEarliestAndLatest(_ earliest: Int64, _ latest: Int64, _ channelId: String, _ serverUrl: String) throws -> Bool {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
private func updatePostsInChannelEarliestAndLatest(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws -> Bool {
|
||||
let idCol = Expression<String>("id")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
let query = postsInChannelTable
|
||||
.where(channelIdCol == channelId && (earliestCol <= earliest || latestCol >= latest))
|
||||
@@ -313,7 +228,7 @@ extension Database {
|
||||
|
||||
let updateQuery = postsInChannelTable
|
||||
.filter(idCol == recordId)
|
||||
.update(earliestCol <- min(earliest, recordEarliest), latestCol <- max(latest, recordLatest))
|
||||
.update(earliestCol <- min(earliest, recordEarliest), latestCol <- max(latest, recordLatest), statusCol <- "updated")
|
||||
|
||||
try db.run(updateQuery)
|
||||
|
||||
@@ -323,20 +238,20 @@ extension Database {
|
||||
return false
|
||||
}
|
||||
|
||||
private func insertPostsInChannel(_ earliest: Int64, _ latest: Int64, _ channelId: String, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let rowIdCol = Expression<Int64>("rowid")
|
||||
private func insertPostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
let id = generateId()
|
||||
|
||||
let query = postsInChannelTable
|
||||
.insert(channelIdCol <- channelId, earliestCol <- earliest, latestCol <- latest)
|
||||
let newRecordId = try db.run(query)
|
||||
.insert(idCol <- id, channelIdCol <- channelId, earliestCol <- earliest, latestCol <- latest, statusCol <- "created")
|
||||
try db.run(query)
|
||||
|
||||
let deleteQuery = postsInChannelTable
|
||||
.where(rowIdCol != newRecordId &&
|
||||
.where(idCol != id &&
|
||||
channelIdCol == channelId &&
|
||||
earliestCol >= earliest &&
|
||||
latestCol <= latest)
|
||||
@@ -345,54 +260,33 @@ extension Database {
|
||||
try db.run(deleteQuery)
|
||||
}
|
||||
|
||||
private func insertAndDeletePosts(_ posts: [Post], _ channelId: String, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
private func insertOrUpdatePosts(_ db: Connection, _ posts: [Post], _ channelId: String) throws {
|
||||
let setters = createPostSetters(from: posts)
|
||||
let insertQuery = postTable.insertMany(or: .replace, setters)
|
||||
try db.run(insertQuery)
|
||||
|
||||
let deleteIds = posts.reduce([String]()) { (accumulated, post) in
|
||||
if let deleteId = post.pending_post_id {
|
||||
return accumulated + [deleteId]
|
||||
for setter in setters {
|
||||
let insertPost = postTable.insert(or: .replace, setter.postSetters)
|
||||
try db.run(insertPost)
|
||||
|
||||
if !setter.emojiSetters.isEmpty {
|
||||
let insertEmojis = emojiTable.insertMany(or: .ignore, setter.emojiSetters)
|
||||
try db.run(insertEmojis)
|
||||
}
|
||||
|
||||
if !setter.fileSetters.isEmpty {
|
||||
let insertFiles = fileTable.insertMany(or: .ignore, setter.fileSetters)
|
||||
try db.run(insertFiles)
|
||||
}
|
||||
|
||||
if !setter.reactionSetters.isEmpty {
|
||||
let postIdCol = Expression<String>("post_id")
|
||||
let deletePreviousReactions = reactionTable.where(postIdCol == setter.id).delete()
|
||||
try db.run(deletePreviousReactions)
|
||||
let insertReactions = reactionTable.insertMany(setter.reactionSetters)
|
||||
try db.run(insertReactions)
|
||||
}
|
||||
|
||||
return accumulated
|
||||
}
|
||||
let id = Expression<String>("id")
|
||||
let deleteQuery = postTable
|
||||
.filter(deleteIds.contains(id))
|
||||
.delete()
|
||||
try db.run(deleteQuery)
|
||||
}
|
||||
|
||||
private func handlePostMetadata(_ posts: [Post], _ channelId: String, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setters = createPostMetadataSetters(from: posts)
|
||||
|
||||
if !setters.postMetadataSetters.isEmpty {
|
||||
let insertQuery = postMetadataTable.insertMany(or: .replace, setters.postMetadataSetters)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
if !setters.reactionSetters.isEmpty {
|
||||
let insertQuery = reactionTable.insertMany(or: .replace, setters.reactionSetters)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
if !setters.fileSetters.isEmpty {
|
||||
let insertQuery = fileTable.insertMany(or: .replace, setters.fileSetters)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
if !setters.emojiSetters.isEmpty {
|
||||
let insertQuery = emojiTable.insertMany(or: .replace, setters.emojiSetters)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
}
|
||||
|
||||
private func createPostSetters(from posts: [Post]) -> [[Setter]] {
|
||||
private func createPostSetters(from posts: [Post]) -> [PostSetters] {
|
||||
let id = Expression<String>("id")
|
||||
let createAt = Expression<Int64>("create_at")
|
||||
let updateAt = Expression<Int64>("update_at")
|
||||
@@ -404,21 +298,18 @@ extension Database {
|
||||
let rootId = Expression<String>("root_id")
|
||||
let originalId = Expression<String>("original_id")
|
||||
let message = Expression<String>("message")
|
||||
let metadata = Expression<String>("metadata")
|
||||
let type = Expression<String>("type")
|
||||
// let hashtag = Expression<String?>("hashtag")
|
||||
let pendingPostId = Expression<String?>("pending_post_id")
|
||||
// let replyCount = Expression<Int64>("reply_count")
|
||||
// let fileIds = Expression<String?>("file_ids")
|
||||
// let lastReplyAt = Expression<Int64?>("last_reply_at")
|
||||
// let failed = Expression<Bool?>("failed")
|
||||
// let ownPost = Expression<Bool?>("ownPost")
|
||||
let prevPostId = Expression<String?>("previous_post_id")
|
||||
// let participants = Expression<String?>("participants")
|
||||
let props = Expression<String?>("props")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
var postsSetters: [PostSetters] = []
|
||||
|
||||
var setters = [[Setter]]()
|
||||
for post in posts {
|
||||
var setter = [Setter]()
|
||||
let metadataSetters = createPostMetadataSetters(from: post)
|
||||
setter.append(id <- post.id)
|
||||
setter.append(createAt <- post.create_at)
|
||||
setter.append(updateAt <- post.update_at)
|
||||
@@ -430,32 +321,28 @@ extension Database {
|
||||
setter.append(rootId <- post.root_id)
|
||||
setter.append(originalId <- post.original_id)
|
||||
setter.append(message <- post.message)
|
||||
setter.append(metadata <- metadataSetters.metadata)
|
||||
setter.append(type <- post.type)
|
||||
// setter.append(hashtag <- post.hashtag)
|
||||
setter.append(pendingPostId <- post.pending_post_id)
|
||||
// setter.append(replyCount <- post.reply_count)
|
||||
// setter.append(fileIds <- json(from: post.file_ids))
|
||||
// setter.append(lastReplyAt <- post.last_reply_at)
|
||||
// setter.append(failed <- post.failed)
|
||||
// setter.append(ownPost <- post.ownPost)
|
||||
setter.append(prevPostId <- post.prev_post_id)
|
||||
// setter.append(participants <- json(from: post.participants))
|
||||
setter.append(props <- post.props)
|
||||
setter.append(statusCol <- "created")
|
||||
|
||||
if let postProps = post.props {
|
||||
let propsJSON = try! JSONEncoder().encode(postProps)
|
||||
let propsString = String(data: propsJSON, encoding: String.Encoding.utf8)
|
||||
setter.append(props <- propsString)
|
||||
}
|
||||
|
||||
setters.append(setter)
|
||||
let postSetter = PostSetters(
|
||||
id: post.id,
|
||||
postSetters: setter,
|
||||
reactionSetters: metadataSetters.reactionSetters,
|
||||
fileSetters: metadataSetters.fileSetters,
|
||||
emojiSetters: metadataSetters.emojiSetters
|
||||
)
|
||||
postsSetters.append(postSetter)
|
||||
}
|
||||
|
||||
return setters
|
||||
return postsSetters
|
||||
}
|
||||
|
||||
private func createPostMetadataSetters(from posts: [Post]) -> MetadataSetters {
|
||||
private func createPostMetadataSetters(from post: Post) -> MetadataSetters {
|
||||
let id = Expression<String>("id")
|
||||
let data = Expression<String>("data")
|
||||
let userId = Expression<String>("user_id")
|
||||
let postId = Expression<String>("post_id")
|
||||
let emojiName = Expression<String>("emoji_name")
|
||||
@@ -468,85 +355,86 @@ extension Database {
|
||||
let height = Expression<Int64>("height")
|
||||
let localPath = Expression<String?>("local_path")
|
||||
let imageThumbnail = Expression<String?>("image_thumbnail")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
var postMetadataSetters = [[Setter]]()
|
||||
var metadataString = "{}"
|
||||
var reactionSetters = [[Setter]]()
|
||||
var fileSetters = [[Setter]]()
|
||||
var emojiSetters = [[Setter]]()
|
||||
|
||||
for post in posts {
|
||||
if var metadata = post.metadata {
|
||||
// Reaction setters
|
||||
if let reactions = metadata.reactions {
|
||||
for reaction in reactions {
|
||||
let json = try? JSONSerialization.jsonObject(with: post.metadata.data(using: .utf8)!, options: [])
|
||||
if var metadata = json as? [String: Any] {
|
||||
// Reaction setters
|
||||
if let reactions = metadata["reactions"] as? [Any] {
|
||||
for reaction in reactions {
|
||||
if let r = reaction as? [String: Any] {
|
||||
var reactionSetter = [Setter]()
|
||||
reactionSetter.append(userId <- reaction.user_id)
|
||||
reactionSetter.append(postId <- reaction.post_id)
|
||||
reactionSetter.append(emojiName <- reaction.emoji_name)
|
||||
reactionSetter.append(createAt <- reaction.create_at)
|
||||
|
||||
reactionSetter.append(id <- generateId())
|
||||
reactionSetter.append(userId <- r["user_id"] as! String)
|
||||
reactionSetter.append(postId <- r["post_id"] as! String)
|
||||
reactionSetter.append(emojiName <- r["emoji_name"] as! String)
|
||||
reactionSetter.append(createAt <- r["create_at"] as! Int64)
|
||||
reactionSetter.append(statusCol <- "created")
|
||||
|
||||
reactionSetters.append(reactionSetter)
|
||||
}
|
||||
|
||||
metadata.reactions = nil
|
||||
}
|
||||
|
||||
// File setters
|
||||
if let files = metadata.files {
|
||||
for file in files {
|
||||
metadata.removeValue(forKey: "reactions")
|
||||
}
|
||||
|
||||
// File setters
|
||||
if let files = metadata["files"] as? [Any] {
|
||||
for file in files {
|
||||
if let f = file as? [String: Any] {
|
||||
var fileSetter = [Setter]()
|
||||
fileSetter.append(id <- file.id)
|
||||
fileSetter.append(postId <- file.post_id)
|
||||
fileSetter.append(name <- file.name)
|
||||
fileSetter.append(ext <- file.`extension`)
|
||||
fileSetter.append(size <- file.size)
|
||||
fileSetter.append(mimeType <- file.mime_type)
|
||||
fileSetter.append(width <- file.width)
|
||||
fileSetter.append(height <- file.height)
|
||||
fileSetter.append(localPath <- file.local_path)
|
||||
fileSetter.append(imageThumbnail <- file.mini_preview)
|
||||
|
||||
fileSetter.append(id <- f["id"] as! String)
|
||||
fileSetter.append(postId <- f["post_id"] as! String)
|
||||
fileSetter.append(name <- f["name"] as! String)
|
||||
fileSetter.append(ext <- f["extension"] as! String)
|
||||
fileSetter.append(size <- f["size"] as! Int64)
|
||||
fileSetter.append(mimeType <- f["mime_type"] as! String)
|
||||
fileSetter.append(width <- f["width"] as! Int64)
|
||||
fileSetter.append(height <- f["height"] as! Int64)
|
||||
fileSetter.append(localPath <- "")
|
||||
fileSetter.append(imageThumbnail <- f["mini_preview"] as? String)
|
||||
fileSetter.append(statusCol <- "created")
|
||||
|
||||
fileSetters.append(fileSetter)
|
||||
}
|
||||
|
||||
metadata.files = nil
|
||||
}
|
||||
|
||||
// Emoji setters
|
||||
if let emojis = metadata.emojis {
|
||||
for emoji in emojis {
|
||||
var emojiSetter = [Setter]()
|
||||
emojiSetter.append(id <- emoji.id)
|
||||
emojiSetter.append(name <- emoji.name)
|
||||
|
||||
emojiSetters.append(emojiSetter)
|
||||
}
|
||||
|
||||
metadata.emojis = nil
|
||||
}
|
||||
|
||||
// Metadata setter
|
||||
var metadataSetter = [Setter]()
|
||||
|
||||
metadataSetter.append(id <- post.id)
|
||||
|
||||
let dataJSON = try! JSONEncoder().encode(metadata)
|
||||
let dataString = String(data: dataJSON, encoding: String.Encoding.utf8)!
|
||||
metadataSetter.append(data <- dataString)
|
||||
|
||||
postMetadataSetters.append(metadataSetter)
|
||||
metadata.removeValue(forKey: "files")
|
||||
}
|
||||
|
||||
// Emoji setters
|
||||
if let emojis = metadata["emojis"] as? [Any] {
|
||||
for emoji in emojis {
|
||||
if let e = emoji as? [String: Any] {
|
||||
var emojiSetter = [Setter]()
|
||||
emojiSetter.append(id <- e["id"] as! String)
|
||||
emojiSetter.append(name <- e["name"] as! String)
|
||||
emojiSetter.append(statusCol <- "created")
|
||||
|
||||
emojiSetters.append(emojiSetter)
|
||||
}
|
||||
}
|
||||
|
||||
metadata.removeValue(forKey: "emojis")
|
||||
}
|
||||
|
||||
// Remaining Metadata
|
||||
|
||||
let dataJSON = try! JSONSerialization.data(withJSONObject: metadata, options: [])
|
||||
metadataString = String(data: dataJSON, encoding: String.Encoding.utf8)!
|
||||
}
|
||||
|
||||
return MetadataSetters(postMetadataSetters: postMetadataSetters,
|
||||
return MetadataSetters(metadata: metadataString,
|
||||
reactionSetters: reactionSetters,
|
||||
fileSetters: fileSetters,
|
||||
emojiSetters: emojiSetters)
|
||||
}
|
||||
|
||||
private func createPostsInThreadSetters(from posts: [Post], withServerUrl serverUrl: String) throws -> [[Setter]] {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
private func createPostsInThreadSetters(_ db: Connection, from posts: [Post]) throws -> [[Setter]] {
|
||||
var setters = [[Setter]]()
|
||||
var postsInThread = [String: [Post]]()
|
||||
|
||||
@@ -562,6 +450,7 @@ extension Database {
|
||||
let rootIdCol = Expression<String>("root_id")
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
for (rootId, posts) in postsInThread {
|
||||
let sortedPosts = posts.sorted(by: { $0.create_at < $1.create_at })
|
||||
@@ -579,13 +468,15 @@ extension Database {
|
||||
let updateQuery = postsInThreadTable
|
||||
.where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest)
|
||||
.update(earliestCol <- min(earliest, rowEarliest),
|
||||
latestCol <- max(latest, rowLatest))
|
||||
latestCol <- max(latest, rowLatest), statusCol <- "updated")
|
||||
try db.run(updateQuery)
|
||||
} else {
|
||||
var setter = [Setter]()
|
||||
setter.append(Expression<String>("id") <- generateId())
|
||||
setter.append(rootIdCol <- rootId)
|
||||
setter.append(earliestCol <- earliest)
|
||||
setter.append(latestCol <- latest)
|
||||
setter.append(statusCol <- "created")
|
||||
|
||||
setters.append(setter)
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// Database+Teams.swift
|
||||
//
|
||||
//
|
||||
// Created by Miguel Alatzar on 8/27/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
public struct Team: Codable {
|
||||
let id: String
|
||||
let update_at: Int64
|
||||
let display_name: String
|
||||
let name: String
|
||||
let description: String
|
||||
let type: String
|
||||
let allowed_domains: String
|
||||
let allow_open_invite: Bool
|
||||
let policy_id: String
|
||||
}
|
||||
|
||||
public struct TeamMembership: Codable {
|
||||
let team_id: String
|
||||
let user_id: String
|
||||
let roles: String
|
||||
let delete_at: Int64
|
||||
let scheme_user: Bool
|
||||
let scheme_admin: Bool
|
||||
let explicit_roles: String
|
||||
}
|
||||
|
||||
extension Database {
|
||||
public func hasMyTeam(withId teamId: String, withServerUrl serverUrl: String) throws -> Bool {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let idCol = Expression<String>("id")
|
||||
let query = myTeamTable.where(idCol == teamId)
|
||||
|
||||
if let _ = try db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func insertTeam(_ team: Team, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createTeamSetter(from: team)
|
||||
let insertQuery = teamTable.insert(or: .replace, setter)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
public func insertMyTeam(_ teamMembership: TeamMembership, _ serverUrl: String) throws {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let setter = createMyTeamSetter(from: teamMembership)
|
||||
let insertQuery = myTeamTable.insert(or: .replace, setter)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
private func createTeamSetter(from team: Team) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let updateAt = Expression<Int64>("update_at")
|
||||
let displayName = Expression<String>("display_name")
|
||||
let name = Expression<String>("name")
|
||||
let description = Expression<String>("description")
|
||||
let type = Expression<String>("type")
|
||||
let allowedDomains = Expression<String>("allowed_domains")
|
||||
let isAllowOpenInvite = Expression<Bool>("is_allow_open_invite")
|
||||
let policyId = Expression<String>("policy_id")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- team.id)
|
||||
setter.append(updateAt <- team.update_at)
|
||||
setter.append(displayName <- team.display_name)
|
||||
setter.append(name <- team.name)
|
||||
setter.append(description <- team.description)
|
||||
setter.append(type <- team.type)
|
||||
setter.append(allowedDomains <- team.allowed_domains)
|
||||
setter.append(isAllowOpenInvite <- team.allow_open_invite)
|
||||
|
||||
// TODO: policyId is not yet in the Team table
|
||||
// setter.append(policyId <- team.policy_id)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createMyTeamSetter(from teamMembership: TeamMembership) -> [Setter] {
|
||||
let teamId = Expression<String>("team_id")
|
||||
let roles = Expression<String>("roles")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(teamId <- teamMembership.team_id)
|
||||
setter.append(roles <- teamMembership.roles)
|
||||
|
||||
return setter
|
||||
}
|
||||
}
|
||||
177
ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift
Normal file
177
ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Elias Nahum on 16-09-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
public struct User: Codable, Hashable {
|
||||
let id: String
|
||||
let auth_service: String
|
||||
let update_at: Int64
|
||||
let delete_at: Int64
|
||||
let email: String
|
||||
let first_name: String
|
||||
let is_bot: Bool
|
||||
let is_guest: Bool
|
||||
let last_name: String
|
||||
let last_picture_update: Int64
|
||||
let locale: String
|
||||
let nickname: String
|
||||
let position: String
|
||||
let roles: String
|
||||
let status: String
|
||||
let username: String
|
||||
let notify_props: String
|
||||
let props: String
|
||||
let timezone: String
|
||||
|
||||
public enum UserKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case auth_service = "auth_service"
|
||||
case update_at = "update_at"
|
||||
case delete_at = "delete_at"
|
||||
case email = "email"
|
||||
case first_name = "first_name"
|
||||
case is_bot = "is_bot"
|
||||
case last_name = "last_name"
|
||||
case last_picture_update = "last_picture_update"
|
||||
case locale = "locale"
|
||||
case nickname = "nickname"
|
||||
case position = "position"
|
||||
case roles = "roles"
|
||||
case username = "username"
|
||||
case notify_props = "notify_props"
|
||||
case props = "props"
|
||||
case timezone = "timezone"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: UserKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
auth_service = try container.decode(String.self, forKey: .auth_service)
|
||||
update_at = try container.decode(Int64.self, forKey: .update_at)
|
||||
delete_at = try container.decode(Int64.self, forKey: .delete_at)
|
||||
email = try container.decode(String.self, forKey: .email)
|
||||
first_name = try container.decode(String.self, forKey: .first_name)
|
||||
is_bot = container.contains(.is_bot) ? try container.decode(Bool.self, forKey: .is_bot) : false
|
||||
roles = try container.decode(String.self, forKey: .roles)
|
||||
is_guest = roles.contains("system_guest")
|
||||
last_name = try container.decode(String.self, forKey: .last_name)
|
||||
last_picture_update = try container.decode(Int64.self, forKey: .last_picture_update)
|
||||
locale = try container.decode(String.self, forKey: .locale)
|
||||
nickname = try container.decode(String.self, forKey: .nickname)
|
||||
position = try container.decode(String.self, forKey: .position)
|
||||
status = "offline"
|
||||
username = try container.decode(String.self, forKey: .username)
|
||||
|
||||
let notifyPropsData = try container.decodeIfPresent([String: String].self, forKey: .notify_props)
|
||||
if (notifyPropsData != nil) {
|
||||
notify_props = Database.default.json(from: notifyPropsData) ?? "{}"
|
||||
} else {
|
||||
notify_props = "{}"
|
||||
}
|
||||
|
||||
let propsData = try container.decodeIfPresent([String: String].self, forKey: .props)
|
||||
if (propsData != nil) {
|
||||
props = Database.default.json(from: propsData) ?? "{}"
|
||||
} else {
|
||||
props = "{}"
|
||||
}
|
||||
|
||||
let timezoneData = try container.decodeIfPresent([String: String].self, forKey: .timezone)
|
||||
if (timezoneData != nil) {
|
||||
timezone = Database.default.json(from: timezoneData) ?? "{}"
|
||||
} else {
|
||||
timezone = "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Database {
|
||||
public func queryUsers(byIds: Set<String>, withServerUrl: String) throws -> Set<String> {
|
||||
let db = try getDatabaseForServer(withServerUrl)
|
||||
|
||||
var result: Set<String> = Set()
|
||||
let idCol = Expression<String>("id")
|
||||
for user in try db.prepare(
|
||||
userTable.select(idCol).filter(byIds.contains(idCol))
|
||||
) {
|
||||
result.insert(user[idCol])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func queryUsers(byUsernames: Set<String>, withServerUrl: String) throws -> Set<String> {
|
||||
let db = try getDatabaseForServer(withServerUrl)
|
||||
|
||||
var result: Set<String> = Set()
|
||||
let usernameCol = Expression<String>("username")
|
||||
for user in try db.prepare(
|
||||
userTable.select(usernameCol).filter(byUsernames.contains(usernameCol))
|
||||
) {
|
||||
result.insert(user[usernameCol])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func insertUsers(_ db: Connection, _ users: Set<User>) throws {
|
||||
let setters = createUserSettedrs(from: users)
|
||||
let insertQuery = userTable.insertMany(or: .replace, setters)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
private func createUserSettedrs(from users: Set<User>) -> [[Setter]] {
|
||||
let id = Expression<String>("id")
|
||||
let authService = Expression<String>("auth_service")
|
||||
let updateAt = Expression<Int64>("update_at")
|
||||
let deleteAt = Expression<Int64>("delete_at")
|
||||
let email = Expression<String>("email")
|
||||
let firstName = Expression<String>("first_name")
|
||||
let isBot = Expression<Bool>("is_bot")
|
||||
let isGuest = Expression<Bool>("is_guest")
|
||||
let lastName = Expression<String>("last_name")
|
||||
let lastPictureUpdate = Expression<Int64>("last_picture_update")
|
||||
let locale = Expression<String>("locale")
|
||||
let nickname = Expression<String>("nickname")
|
||||
let position = Expression<String>("position")
|
||||
let roles = Expression<String>("roles")
|
||||
let status = Expression<String>("status")
|
||||
let username = Expression<String>("username")
|
||||
let notifyProps = Expression<String>("notify_props")
|
||||
let props = Expression<String>("props")
|
||||
let timezone = Expression<String>("timezone")
|
||||
|
||||
var setters = [[Setter]]()
|
||||
for user in users {
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- user.id)
|
||||
setter.append(authService <- user.auth_service)
|
||||
setter.append(updateAt <- user.update_at)
|
||||
setter.append(deleteAt <- user.delete_at)
|
||||
setter.append(email <- user.email)
|
||||
setter.append(firstName <- user.first_name)
|
||||
setter.append(isBot <- user.is_bot)
|
||||
setter.append(isGuest <- user.is_guest)
|
||||
setter.append(lastName <- user.last_name)
|
||||
setter.append(lastPictureUpdate <- user.last_picture_update)
|
||||
setter.append(locale <- user.locale)
|
||||
setter.append(nickname <- user.nickname)
|
||||
setter.append(position <- user.position)
|
||||
setter.append(roles <- user.roles)
|
||||
setter.append(status <- user.status)
|
||||
setter.append(username <- user.username)
|
||||
setter.append(notifyProps <- user.notify_props)
|
||||
setter.append(props <- user.props)
|
||||
setter.append(timezone <- user.timezone)
|
||||
setters.append(setter)
|
||||
}
|
||||
|
||||
return setters
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ public class Database: NSObject {
|
||||
internal var reactionTable = Table("Reaction")
|
||||
internal var fileTable = Table("File")
|
||||
internal var emojiTable = Table("CustomEmoji")
|
||||
internal var userTable = Table("User")
|
||||
|
||||
@objc public static let `default` = Database()
|
||||
|
||||
@@ -77,10 +78,28 @@ public class Database: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
public func generateId() -> String {
|
||||
let alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
let alphabetLenght = alphabet.count
|
||||
let idLenght = 16
|
||||
var id = ""
|
||||
|
||||
for _ in 1...(idLenght / 2) {
|
||||
let random = floor(drand48() * Double(alphabetLenght) * Double(alphabetLenght))
|
||||
let firstIndex = Int(floor(random / Double(alphabetLenght)))
|
||||
let lastIndex = Int(random) % alphabetLenght
|
||||
id += String(alphabet[firstIndex])
|
||||
id += String(alphabet[lastIndex])
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
public func getOnlyServerUrl() throws -> String {
|
||||
let db = try Connection(DEFAULT_DB_PATH)
|
||||
let url = Expression<String>("url")
|
||||
let query = serversTable.select(url)
|
||||
let lastActiveAt = Expression<Int64>("last_active_at")
|
||||
let query = serversTable.select(url).filter(lastActiveAt > 0)
|
||||
|
||||
var serverUrl: String?
|
||||
for result in try db.prepare(query) {
|
||||
@@ -126,7 +145,20 @@ public class Database: NSObject {
|
||||
throw DatabaseError.NoResults(query.asSQL())
|
||||
}
|
||||
|
||||
private func json(from object:Any?) -> String? {
|
||||
internal func queryCurrentUser(_ serverUrl: String) throws -> Row? {
|
||||
let currentUserId = try queryCurrentUserId(serverUrl)
|
||||
let idCol = Expression<String>("id")
|
||||
let query = userTable.where(idCol == currentUserId)
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
if let result = try db.pluck(query) {
|
||||
return result
|
||||
}
|
||||
|
||||
throw DatabaseError.NoResults(query.asSQL())
|
||||
}
|
||||
|
||||
internal func json(from object:Any?) -> String? {
|
||||
guard let object = object, let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; };
|
||||
4953BF602368AE8600593328 /* SwimeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4953BF5F2368AE8600593328 /* SwimeProxy.swift */; };
|
||||
49AE36FF26D4455800EF4E52 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE36FE26D4455800EF4E52 /* SwiftPackageProductDependency */; };
|
||||
49AE370126D4455D00EF4E52 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* SwiftPackageProductDependency */; };
|
||||
49AE370526D5CD7800EF4E52 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370426D5CD7800EF4E52 /* SwiftPackageProductDependency */; };
|
||||
49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE36FE26D4455800EF4E52 /* Gekidou */; };
|
||||
49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; };
|
||||
49AE370526D5CD7800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370426D5CD7800EF4E52 /* Gekidou */; };
|
||||
49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
|
||||
531BEBC72513E93C00BC05B1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; };
|
||||
536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; };
|
||||
@@ -168,7 +168,6 @@
|
||||
7F240ADA220E089300637665 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||
7F240ADC220E094A00637665 /* TeamsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamsViewController.swift; sourceTree = "<group>"; };
|
||||
7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = "<group>"; };
|
||||
7F292AA51E8ABB1100A450A3 /* splash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = splash.png; path = SplashScreenResource/splash.png; sourceTree = "<group>"; };
|
||||
7F325D6DAAF1047EB948EFF7 /* Pods-Mattermost-MattermostTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = "<group>"; };
|
||||
7F54ABFAE6CE4A6DB11D1ED7 /* Roboto-BlackItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-BlackItalic.ttf"; path = "../assets/fonts/Roboto-BlackItalic.ttf"; sourceTree = "<group>"; };
|
||||
@@ -220,7 +219,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
49AE370526D5CD7800EF4E52 /* BuildFile in Frameworks */,
|
||||
49AE370526D5CD7800EF4E52 /* Gekidou in Frameworks */,
|
||||
49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */,
|
||||
6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */,
|
||||
);
|
||||
@@ -230,7 +229,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
49AE36FF26D4455800EF4E52 /* BuildFile in Frameworks */,
|
||||
49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */,
|
||||
7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -239,7 +238,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
49AE370126D4455D00EF4E52 /* BuildFile in Frameworks */,
|
||||
49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */,
|
||||
7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -309,7 +308,6 @@
|
||||
7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */,
|
||||
7FEB10991F61019C0039A015 /* MattermostManaged.h */,
|
||||
7FEB109A1F61019C0039A015 /* MattermostManaged.m */,
|
||||
7F292AA51E8ABB1100A450A3 /* splash.png */,
|
||||
7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */,
|
||||
7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */,
|
||||
7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */,
|
||||
@@ -476,7 +474,7 @@
|
||||
);
|
||||
name = Mattermost;
|
||||
packageProductDependencies = (
|
||||
49AE370426D5CD7800EF4E52 /* SwiftPackageProductDependency */,
|
||||
49AE370426D5CD7800EF4E52 /* Gekidou */,
|
||||
);
|
||||
productName = "Hello World";
|
||||
productReference = 13B07F961A680F5B00A75B9A /* Mattermost.app */;
|
||||
@@ -497,7 +495,7 @@
|
||||
);
|
||||
name = MattermostShare;
|
||||
packageProductDependencies = (
|
||||
49AE36FE26D4455800EF4E52 /* SwiftPackageProductDependency */,
|
||||
49AE36FE26D4455800EF4E52 /* Gekidou */,
|
||||
);
|
||||
productName = MattermostShare;
|
||||
productReference = 7F240A19220D3A2300637665 /* MattermostShare.appex */;
|
||||
@@ -518,7 +516,7 @@
|
||||
);
|
||||
name = NotificationService;
|
||||
packageProductDependencies = (
|
||||
49AE370026D4455D00EF4E52 /* SwiftPackageProductDependency */,
|
||||
49AE370026D4455D00EF4E52 /* Gekidou */,
|
||||
);
|
||||
productName = NotificationService;
|
||||
productReference = 7F581D32221ED5C60099E66B /* NotificationService.appex */;
|
||||
@@ -1250,15 +1248,15 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
49AE36FE26D4455800EF4E52 /* SwiftPackageProductDependency */ = {
|
||||
49AE36FE26D4455800EF4E52 /* Gekidou */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Gekidou;
|
||||
};
|
||||
49AE370026D4455D00EF4E52 /* SwiftPackageProductDependency */ = {
|
||||
49AE370026D4455D00EF4E52 /* Gekidou */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Gekidou;
|
||||
};
|
||||
49AE370426D5CD7800EF4E52 /* SwiftPackageProductDependency */ = {
|
||||
49AE370426D5CD7800EF4E52 /* Gekidou */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Gekidou;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#import <UMReactNativeAdapter/UMModuleRegistryAdapter.h>
|
||||
#import <ReactNativeNavigation/ReactNativeNavigation.h>
|
||||
#import <UploadAttachments/UploadAttachments-Swift.h>
|
||||
#import <UploadAttachments/MattermostBucket.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <RNHWKeyboardEvent.h>
|
||||
|
||||
@@ -28,6 +29,7 @@
|
||||
NSString* const NOTIFICATION_MESSAGE_ACTION = @"message";
|
||||
NSString* const NOTIFICATION_CLEAR_ACTION = @"clear";
|
||||
NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge";
|
||||
MattermostBucket* bucket = nil;
|
||||
|
||||
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
|
||||
os_log(OS_LOG_DEFAULT, "Mattermost will attach session from handleEventsForBackgroundURLSession!! identifier=%{public}@", identifier);
|
||||
@@ -48,6 +50,10 @@ NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge";
|
||||
{
|
||||
self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]];
|
||||
|
||||
if (bucket == nil) {
|
||||
bucket = [[MattermostBucket alloc] init];
|
||||
}
|
||||
|
||||
// Clear keychain on first run in case of reinstallation
|
||||
if (![[NSUserDefaults standardUserDefaults] objectForKey:@"FirstRun"]) {
|
||||
|
||||
@@ -184,4 +190,21 @@ RNHWKeyboardEvent *hwKeyEvent = nil;
|
||||
NSString *selected = sender.input;
|
||||
[hwKeyEvent sendHWKeyEvent:@"shift-enter"];
|
||||
}
|
||||
|
||||
-(void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
[bucket setPreference:@"ApplicationIsForeground" value:@"true"];
|
||||
}
|
||||
|
||||
-(void)applicationWillResignActive:(UIApplication *)application {
|
||||
[bucket setPreference:@"ApplicationIsForeground" value:@"false"];
|
||||
}
|
||||
|
||||
-(void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
[bucket setPreference:@"ApplicationIsForeground" value:@"false"];
|
||||
}
|
||||
|
||||
-(void)applicationWillTerminate:(UIApplication *)application {
|
||||
[bucket setPreference:@"ApplicationIsForeground" value:@"false"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -73,8 +73,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: contentHandler)
|
||||
|
||||
if (MattermostBucket.init().getPreference("ApplicationIsForeground") as? String != "true") {
|
||||
Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: contentHandler)
|
||||
} else if let contentHandler = contentHandler {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
7FABE0F7221466F900D0F595 /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0F6221466F900D0F595 /* UploadManager.swift */; };
|
||||
7FABE0FA2214674200D0F595 /* StoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0F82214674200D0F595 /* StoreManager.m */; };
|
||||
7FABE0FC2214800F00D0F595 /* UploadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0FB2214800F00D0F595 /* UploadSession.swift */; };
|
||||
7FD4146126F3C663001A7F12 /* MattermostBucket.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE04D2213818A00D0F595 /* MattermostBucket.h */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -28,6 +29,7 @@
|
||||
dstPath = "include/$(PRODUCT_NAME)";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
7FD4146126F3C663001A7F12 /* MattermostBucket.h in CopyFiles */,
|
||||
7F80232C229C91AD0034D6D4 /* MMMConstants.h in CopyFiles */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
Reference in New Issue
Block a user