[Gekidou] iOS - Fetch and store data on push notification receipt

This commit is contained in:
Elias Nahum
2021-09-20 17:59:42 -03:00
parent 1c26f14fdb
commit 9ed616afa9
15 changed files with 892 additions and 732 deletions

View File

@@ -16,3 +16,9 @@ extension Date {
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
}
}
extension StringProtocol {
public subscript(offset: Int) -> Character {
self[index(startIndex, offsetBy: offset)]
}
}

View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View 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)
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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
}
}

View 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
}
}

View File

@@ -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
}

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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() {

View File

@@ -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;