iOS refactor push notifications to store data or send to JS for processing (#7128)

This commit is contained in:
Elias Nahum
2023-02-15 17:07:54 +02:00
committed by GitHub
parent ee13679f38
commit 153c2f7c8d
43 changed files with 2468 additions and 1182 deletions

View File

@@ -1,7 +1,7 @@
import Foundation
extension ImageCache {
func image(for userId: String, updatedAt: Double, onServer serverUrl: String) -> Data? {
func image(for userId: String, updatedAt: Double, forServer serverUrl: String) -> Data? {
lock.lock(); defer { lock.unlock() }
let key = "\(serverUrl)-\(userId)-\(updatedAt)" as NSString
if let image = imageCache.object(forKey: key) as? Data {

View File

@@ -6,9 +6,9 @@ extension ImageCache {
keysCache.removeAllObjects()
}
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, onServer serverUrl: String ) {
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, forServer serverUrl: String ) {
guard let data = data else {
return removeImage(for: userId, onServer: serverUrl)
return removeImage(for: userId, forServer: serverUrl)
}
lock.lock(); defer { lock.unlock() }
@@ -18,7 +18,7 @@ extension ImageCache {
keysCache.setObject(imageKey, forKey: cacheKey)
}
func removeImage(for userId: String, onServer serverUrl: String) {
func removeImage(for userId: String, forServer serverUrl: String) {
lock.lock(); defer { lock.unlock() }
let cacheKey = "\(serverUrl)-\(userId)" as NSString
if let key = keysCache.object(forKey: cacheKey) {

View File

@@ -1,11 +1,11 @@
import Foundation
protocol ImageCacheType: class {
func image(for userId: String, updatedAt: Double, onServer serverUrl: String) -> Data?
protocol ImageCacheType: AnyObject {
func image(for userId: String, updatedAt: Double, forServer serverUrl: String) -> Data?
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, onServer serverUrl: String )
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, forServer serverUrl: String )
func removeImage(for userId: String, onServer serverUrl: String)
func removeImage(for userId: String, forServer serverUrl: String)
func removeAllImages()
}

View File

@@ -0,0 +1,109 @@
import Foundation
public struct CategoriesWithOrder: Codable {
let order: [String]
let categories: [Category]
public enum CategoriesWithOrderKeys: String, CodingKey {
case order, categories
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CategoriesWithOrderKeys.self)
order = values.decodeIfPresent(forKey: .order, defaultValue: [String]())
categories = (try? values.decode([Category].self, forKey: .categories)) ?? [Category]()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CategoriesWithOrderKeys.self)
try container.encode(self.order, forKey: .order)
try container.encode(self.categories, forKey: .categories)
}
}
public struct Category: Codable {
let id: String
let channelIds: [String]
let collapsed: Bool
let displayName: String
let muted: Bool
let sortOrder: Int
let sorting: String
let teamId: String
let type: String
let userId: String
public enum CategoryKeys: String, CodingKey {
case id, collapsed, muted, sorting, type
case channelIds = "channel_ids"
case displayName = "display_name"
case sortOrder = "sort_order"
case teamId = "team_id"
case userId = "user_id"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CategoryKeys.self)
id = try values.decode(String.self, forKey: .id)
teamId = try values.decode(String.self, forKey: .teamId)
userId = try values.decode(String.self, forKey: .userId)
channelIds = values.decodeIfPresent(forKey: .channelIds, defaultValue: [String]())
collapsed = false
displayName = values.decodeIfPresent(forKey: .displayName, defaultValue: "")
muted = values.decodeIfPresent(forKey: .muted, defaultValue: false)
sortOrder = values.decodeIfPresent(forKey: .sortOrder, defaultValue: 0)
sorting = values.decodeIfPresent(forKey: .sorting, defaultValue: "recent")
type = values.decodeIfPresent(forKey: .type, defaultValue: "custom")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CategoryKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.channelIds, forKey: .channelIds)
try container.encode(self.collapsed, forKey: .collapsed)
try container.encode(self.displayName, forKey: .displayName)
try container.encode(self.muted, forKey: .muted)
try container.encode(self.sortOrder, forKey: .sortOrder)
try container.encode(self.sorting, forKey: .sorting)
try container.encode(self.teamId, forKey: .teamId)
try container.encode(self.type, forKey: .type)
try container.encode(self.userId, forKey: .userId)
}
}
public struct CategoryChannel: Codable {
let id: String
let categoryId: String
let channelId: String
let sortOrder: Int
public enum CategoryChannelKeys: String, CodingKey {
case id
case channelId = "channel_id"
case categoryId = "category_id"
case sortOrder = "sort_order"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CategoryChannelKeys.self)
id = try values.decode(String.self, forKey: .id)
channelId = try values.decode(String.self, forKey: .channelId)
categoryId = try values.decode(String.self, forKey: .categoryId)
sortOrder = values.decodeIfPresent(forKey: .sortOrder, defaultValue: 0)
}
public init(id: String, categoryId: String, channelId: String, sortOrder: Int = 0) {
self.id = id
self.categoryId = categoryId
self.channelId = channelId
self.sortOrder = sortOrder
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CategoryChannelKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.channelId, forKey: .channelId)
try container.encode(self.categoryId, forKey: .categoryId)
try container.encode(self.sortOrder, forKey: .sortOrder)
}
}

View File

@@ -0,0 +1,100 @@
import Foundation
public struct Channel: Codable {
let id: String
let createAt: Double
let creatorId: String
let deleteAt: Double
var displayName: String = ""
let extraUpdateAt: Double
let groupConstrained: Bool
let header: String
let lastPostAt: Double
let lastRootPostAt: Double
let name: String
let policyId: String
let props: String
let purpose: String
let schemeId: String
let shared: Bool
let teamId: String
let totalMsgCount: Int
let totalMsgCountRoot: Int
let type: String
let updateAt: Double
public enum ChannelKeys: String, CodingKey {
case id
case createAt = "create_at"
case creatorId = "creator_id"
case deleteAt = "delete_at"
case displayName = "display_name"
case extraUpdateAt = "extra_update_at"
case groupConstrained = "group_constrained"
case header
case lastPostAt = "last_post_at"
case lastRootPostAt = "last_root_post_at"
case name
case policyId = "policy_id"
case props
case purpose
case schemeId = "scheme_id"
case shared
case teamId = "team_id"
case totalMsgCount = "total_msg_count"
case totalMsgCountRoot = "total_msg_count_root"
case type
case updateAt = "update_at"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ChannelKeys.self)
id = try values.decode(String.self, forKey: .id)
creatorId = values.decodeIfPresent(forKey: .creatorId, defaultValue: "")
createAt = values.decodeIfPresent(forKey: .createAt, defaultValue: 0)
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
displayName = values.decodeIfPresent(forKey: .displayName, defaultValue: "")
extraUpdateAt = values.decodeIfPresent(forKey: .extraUpdateAt, defaultValue: 0)
groupConstrained = values.decodeIfPresent(forKey: .groupConstrained, defaultValue: false)
header = values.decodeIfPresent(forKey: .header, defaultValue: "")
lastPostAt = values.decodeIfPresent(forKey: .lastPostAt, defaultValue: 0)
lastRootPostAt = values.decodeIfPresent(forKey: .lastRootPostAt, defaultValue: 0)
name = values.decodeIfPresent(forKey: .name, defaultValue: "")
policyId = values.decodeIfPresent(forKey: .policyId, defaultValue: "")
let propsData = try? values.decode([String:Any].self, forKey: .props)
props = Database.default.json(from: propsData) ?? "{}"
purpose = values.decodeIfPresent(forKey: .purpose, defaultValue: "")
schemeId = values.decodeIfPresent(forKey: .schemeId, defaultValue: "")
shared = values.decodeIfPresent(forKey: .shared, defaultValue: false)
teamId = values.decodeIfPresent(forKey: .teamId, defaultValue: "")
totalMsgCount = values.decodeIfPresent(forKey: .totalMsgCount, defaultValue: 0)
totalMsgCountRoot = values.decodeIfPresent(forKey: .totalMsgCountRoot, defaultValue: 0)
type = values.decodeIfPresent(forKey: .type, defaultValue: "O")
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ChannelKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.creatorId, forKey: .creatorId)
try container.encode(self.createAt, forKey: .createAt)
try container.encode(self.deleteAt, forKey: .deleteAt)
try container.encode(self.displayName, forKey: .displayName)
try container.encode(self.extraUpdateAt, forKey: .extraUpdateAt)
try container.encode(self.groupConstrained, forKey: .groupConstrained)
try container.encode(self.header, forKey: .header)
try container.encode(self.lastPostAt, forKey: .lastPostAt)
try container.encode(self.lastRootPostAt, forKey: .lastRootPostAt)
try container.encode(self.name, forKey: .name)
try container.encode(self.policyId, forKey: .policyId)
try container.encode(self.props, forKey: .props)
try container.encode(self.purpose, forKey: .purpose)
try container.encode(self.schemeId, forKey: .schemeId)
try container.encode(self.shared, forKey: .shared)
try container.encode(self.teamId, forKey: .teamId)
try container.encode(self.totalMsgCount, forKey: .totalMsgCount)
try container.encode(self.totalMsgCountRoot, forKey: .totalMsgCountRoot)
try container.encode(self.type, forKey: .type)
try container.encode(self.updateAt, forKey: .updateAt)
}
}

View File

@@ -0,0 +1,85 @@
import Foundation
public struct ChannelMember: Codable {
let id: String
let explicitRoles: String
let lastUpdateAt: Double
let lastViewedAt: Double
let mentionCount: Int
let mentionCountRoot: Int
let msgCount: Int
let msgCountRoot: Int
let notifyProps: String
let roles: String
let schemeAdmin: Bool
let schemeGuest: Bool
let schemeUser: Bool
let urgentMentionCount: Int
let userId: String
var internalMsgCount: Int
var internalMsgCountRoot: Int
public enum ChannelMemberKeys: String, CodingKey {
case internalMsgCount, internalMsgCountRoot
case id = "channel_id"
case explicitRoles = "explicit_roles"
case lastUpdateAt = "last_update_at"
case lastViewedAt = "last_viewed_at"
case mentionCount = "mention_count"
case mentionCountRoot = "mention_count_root"
case msgCount = "msg_count"
case msgCountRoot = "msg_count_root"
case notifyProps = "notify_props"
case roles
case schemeAdmin = "scheme_admin"
case schemeGuest = "scheme_guest"
case schemeUser = "scheme_user"
case urgentMentionCount = "urgent_mention_count"
case userId = "user_id"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ChannelMemberKeys.self)
id = try values.decode(String.self, forKey: .id)
userId = try values.decode(String.self, forKey: .userId)
explicitRoles = values.decodeIfPresent(forKey: .explicitRoles, defaultValue: "")
lastUpdateAt = values.decodeIfPresent(forKey: .lastUpdateAt, defaultValue: 0)
lastViewedAt = values.decodeIfPresent(forKey: .lastViewedAt, defaultValue: 0)
mentionCount = values.decodeIfPresent(forKey: .mentionCount, defaultValue: 0)
mentionCountRoot = values.decodeIfPresent(forKey: .mentionCountRoot, defaultValue: 0)
msgCount = values.decodeIfPresent(forKey: .msgCount, defaultValue: 0)
msgCountRoot = values.decodeIfPresent(forKey: .msgCountRoot, defaultValue: 0)
let propsData = try values.decode([String:Any].self, forKey: .notifyProps)
notifyProps = Database.default.json(from: propsData) ?? "{}"
roles = values.decodeIfPresent(forKey: .roles, defaultValue: "")
schemeAdmin = values.decodeIfPresent(forKey: .schemeAdmin, defaultValue: false)
schemeGuest = values.decodeIfPresent(forKey: .schemeGuest, defaultValue: false)
schemeUser = values.decodeIfPresent(forKey: .schemeUser, defaultValue: true)
urgentMentionCount = values.decodeIfPresent(forKey: .urgentMentionCount, defaultValue: 0)
internalMsgCount = 0
internalMsgCountRoot = 0
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ChannelMemberKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.explicitRoles, forKey: .explicitRoles)
try container.encode(self.lastUpdateAt, forKey: .lastUpdateAt)
try container.encode(self.lastViewedAt, forKey: .lastViewedAt)
try container.encode(self.mentionCount, forKey: .mentionCount)
try container.encode(self.mentionCountRoot, forKey: .mentionCountRoot)
try container.encode(self.msgCount, forKey: .msgCount)
try container.encode(self.msgCountRoot, forKey: .msgCountRoot)
try container.encode(self.notifyProps, forKey: .notifyProps)
try container.encode(self.roles, forKey: .roles)
try container.encode(self.schemeAdmin, forKey: .schemeAdmin)
try container.encode(self.schemeGuest, forKey: .schemeGuest)
try container.encode(self.schemeUser, forKey: .schemeUser)
try container.encode(self.urgentMentionCount, forKey: .urgentMentionCount)
try container.encode(self.userId, forKey: .userId)
try container.encode(self.internalMsgCount, forKey: .internalMsgCount)
try container.encode(self.internalMsgCountRoot, forKey: .internalMsgCountRoot)
}
}

View File

@@ -0,0 +1,103 @@
import Foundation
public struct Post: Codable {
let id: String
let createAt: Double
let updateAt: Double
let editAt: Double
let deleteAt: Double
let isPinned: Bool
let userId: String
let channelId: String
let rootId: String
let originalId: String
let message: String
let type: String
let props: String
let pendingPostId: String
let metadata: String
var prevPostId: String
// CRT
let participants: [User]?
let lastReplyAt: Double
let replyCount: Int
let isFollowing: Bool
public enum PostKeys: String, CodingKey {
case id, message, type, props, metadata, participants
case createAt = "create_at"
case updateAt = "update_at"
case deleteAt = "delete_at"
case editAt = "edit_at"
case isPinned = "is_pinned"
case userId = "user_id"
case channelId = "channel_id"
case rootId = "root_id"
case originalId = "original_id"
case pendingPostId = "pending_post_id"
case prevPostId = "previous_post_id"
// CRT
case lastReplyAt = "last_reply_at"
case replyCount = "reply_count"
case isFollowing = "is_following"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: PostKeys.self)
prevPostId = ""
id = try values.decode(String.self, forKey: .id)
channelId = try values.decode(String.self, forKey: .channelId)
userId = try values.decode(String.self, forKey: .userId)
createAt = values.decodeIfPresent(forKey: .createAt, defaultValue: 0)
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
editAt = values.decodeIfPresent(forKey: .editAt, defaultValue: 0)
isPinned = values.decodeIfPresent(forKey: .isPinned, defaultValue: false)
rootId = values.decodeIfPresent(forKey: .rootId, defaultValue: "")
originalId = values.decodeIfPresent(forKey: .originalId, defaultValue: "")
message = values.decodeIfPresent(forKey: .message, defaultValue: "")
type = values.decodeIfPresent(forKey: .type, defaultValue: "")
pendingPostId = values.decodeIfPresent(forKey: .pendingPostId, defaultValue: "")
lastReplyAt = values.decodeIfPresent(forKey: .lastReplyAt, defaultValue: 0)
replyCount = values.decodeIfPresent(forKey: .replyCount, defaultValue: 0)
isFollowing = values.decodeIfPresent(forKey: .isFollowing, defaultValue: false)
participants = (try? values.decodeIfPresent([User].self, forKey: .participants)) ?? nil
if let meta = try? values.decode([String:Any].self, forKey: .metadata) {
metadata = Database.default.json(from: meta) ?? "{}"
} else {
metadata = "{}"
}
if let propsData = try? values.decode([String:Any].self, forKey: .props) {
props = Database.default.json(from: propsData) ?? "{}"
} else {
props = "{}"
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: PostKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.createAt, forKey: .createAt)
try container.encode(self.updateAt, forKey: .updateAt)
try container.encode(self.editAt, forKey: .editAt)
try container.encode(self.deleteAt, forKey: .deleteAt)
try container.encode(self.isPinned, forKey: .isPinned)
try container.encode(self.userId, forKey: .userId)
try container.encode(self.channelId, forKey: .channelId)
try container.encode(self.rootId, forKey: .rootId)
try container.encode(self.originalId, forKey: .originalId)
try container.encode(self.message, forKey: .message)
try container.encode(self.type, forKey: .type)
try container.encode(self.props, forKey: .props)
try container.encode(self.pendingPostId, forKey: .pendingPostId)
try container.encode(self.metadata, forKey: .metadata)
try container.encode(self.prevPostId, forKey: .prevPostId)
try container.encodeIfPresent(self.participants, forKey: .participants)
try container.encode(self.lastReplyAt, forKey: .lastReplyAt)
try container.encode(self.replyCount, forKey: .replyCount)
try container.encode(self.isFollowing, forKey: .isFollowing)
}
}

View File

@@ -0,0 +1,30 @@
import Foundation
public struct PostResponse: Codable {
let order: [String]
let posts: [String:Post]
let nextPostId: String
let prevPostId: String
public enum PostResponseKeys: String, CodingKey {
case order, posts
case nextPostId = "next_post_id"
case prevPostId = "prev_post_id"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: PostResponseKeys.self)
order = values.decodeIfPresent(forKey: .order, defaultValue: [String]())
nextPostId = values.decodeIfPresent(forKey: .nextPostId, defaultValue: "")
prevPostId = values.decodeIfPresent(forKey: .prevPostId, defaultValue: "")
posts = (try? values.decode([String:Post].self, forKey: .posts)) ?? [String:Post]()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: PostResponseKeys.self)
try container.encode(self.order, forKey: .order)
try container.encode(self.posts, forKey: .posts)
try container.encode(self.nextPostId, forKey: .nextPostId)
try container.encode(self.prevPostId, forKey: .prevPostId)
}
}

View File

@@ -0,0 +1,66 @@
import Foundation
public struct PostThread: Codable {
let id: String
var lastReplyAt: Double
var lastViewedAt: Double
let replyCount: Int
var unreadReplies: Int
var unreadMentions: Int
let post: Post?
let participants: [User]
let isFollowing: Bool
let deleteAt: Double
public enum PostThreadKeys: String, CodingKey {
case id, post, participants
case lastReplyAt = "last_reply_at"
case lastViewedAt = "last_viewed_at"
case replyCount = "reply_count"
case unreadReplies = "unread_replies"
case unreadMentions = "unread_mentions"
case isFollowing = "is_following"
case deleteAt = "delete_at"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: PostThreadKeys.self)
id = try values.decode(String.self, forKey: .id)
post = values.decodeIfPresent(forKey: .post, defaultValue: nil)
participants = values.decodeIfPresent(forKey: .participants, defaultValue: [User]())
lastReplyAt = values.decodeIfPresent(forKey: .lastReplyAt, defaultValue: 0)
lastViewedAt = values.decodeIfPresent(forKey: .lastViewedAt, defaultValue: 0)
replyCount = values.decodeIfPresent(forKey: .replyCount, defaultValue: 0)
unreadReplies = values.decodeIfPresent(forKey: .unreadReplies, defaultValue: 0)
unreadMentions = values.decodeIfPresent(forKey: .unreadMentions, defaultValue: 0)
isFollowing = values.decodeIfPresent(forKey: .isFollowing, defaultValue: false)
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
}
public init(from post: Post) {
id = post.id
replyCount = post.replyCount
participants = post.participants ?? [User]()
isFollowing = post.isFollowing
deleteAt = post.deleteAt
lastReplyAt = 0
lastViewedAt = 0
unreadReplies = 0
unreadMentions = 0
self.post = post
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: PostThreadKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.lastReplyAt, forKey: .lastReplyAt)
try container.encode(self.lastViewedAt, forKey: .lastViewedAt)
try container.encode(self.replyCount, forKey: .replyCount)
try container.encode(self.unreadReplies, forKey: .unreadReplies)
try container.encode(self.unreadMentions, forKey: .unreadMentions)
try container.encodeIfPresent(self.post, forKey: .post)
try container.encode(self.participants, forKey: .participants)
try container.encode(self.isFollowing, forKey: .isFollowing)
try container.encode(self.deleteAt, forKey: .deleteAt)
}
}

View File

@@ -0,0 +1,88 @@
import Foundation
public struct Team: Codable {
let id: String
let allowOpenInvite: Bool
let allowedDomains: String
let cloudLimitsArchived: Bool
let companyName: String
let createAt: Double
let deleteAt: Double
let description: String
let displayName: String
let email: String
let groupConstrained: Bool
let inviteId: String
let lastTeamIconUpdate: Double
let name: String
let policyId: String
let schemeId: String?
let type: String
let updateAt: Double
public enum TeamKeys: String, CodingKey {
case id
case allowOpenInvite = "allow_open_invite"
case allowedDomains = "allowed_domains"
case cloudLimitsArchived = "cloud_limits_archive"
case companyName = "company_name"
case createAt = "create_at"
case deleteAt = "delete_at"
case description
case displayName = "display_name"
case email
case groupConstrained = "group_constrained"
case inviteId = "invite_id"
case lastTeamIconUpdate = "last_team_icon_update"
case name
case policyId = "policy_id"
case schemeId = "scheme_id"
case type
case updateAt = "update_at"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: TeamKeys.self)
id = try values.decode(String.self, forKey: .id)
allowOpenInvite = values.decodeIfPresent(forKey: .allowOpenInvite, defaultValue: true)
allowedDomains = values.decodeIfPresent(forKey: .allowedDomains, defaultValue: "")
cloudLimitsArchived = values.decodeIfPresent(forKey: .cloudLimitsArchived, defaultValue: false)
companyName = values.decodeIfPresent(forKey: .companyName, defaultValue: "")
createAt = values.decodeIfPresent(forKey: .createAt, defaultValue: 0)
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
description = values.decodeIfPresent(forKey: .description, defaultValue: "")
displayName = values.decodeIfPresent(forKey: .displayName, defaultValue: "")
email = values.decodeIfPresent(forKey: .email, defaultValue: "")
groupConstrained = values.decodeIfPresent(forKey: .groupConstrained, defaultValue: false)
inviteId = values.decodeIfPresent(forKey: .inviteId, defaultValue: "")
lastTeamIconUpdate = values.decodeIfPresent(forKey: .lastTeamIconUpdate, defaultValue: 0)
name = values.decodeIfPresent(forKey: .name, defaultValue: "")
policyId = values.decodeIfPresent(forKey: .policyId, defaultValue: "")
schemeId = values.decodeIfPresent(forKey: .schemeId, defaultValue: "")
type = values.decodeIfPresent(forKey: .type, defaultValue: "O")
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: TeamKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.allowOpenInvite, forKey: .allowOpenInvite)
try container.encode(self.allowedDomains, forKey: .allowedDomains)
try container.encode(self.cloudLimitsArchived, forKey: .cloudLimitsArchived)
try container.encode(self.companyName, forKey: .companyName)
try container.encode(self.createAt, forKey: .createAt)
try container.encode(self.deleteAt, forKey: .deleteAt)
try container.encode(self.description, forKey: .description)
try container.encode(self.displayName, forKey: .displayName)
try container.encode(self.email, forKey: .email)
try container.encode(self.groupConstrained, forKey: .groupConstrained)
try container.encode(self.inviteId, forKey: .inviteId)
try container.encode(self.lastTeamIconUpdate, forKey: .lastTeamIconUpdate)
try container.encode(self.name, forKey: .name)
try container.encode(self.policyId, forKey: .policyId)
try container.encodeIfPresent(self.schemeId, forKey: .schemeId)
try container.encode(self.type, forKey: .type)
try container.encode(self.updateAt, forKey: .updateAt)
}
}

View File

@@ -0,0 +1,43 @@
import Foundation
public struct TeamMember: Codable {
let id: String
let explicitRoles: String
let roles: String
let schemeAdmin: Bool
let schemeGuest: Bool
let schemeUser: Bool
let userId: String
public enum TeamMemberKeys: String, CodingKey {
case id = "team_id"
case explicitRoles = "explicit_roles"
case roles
case schemeAdmin = "scheme_admin"
case schemeGuest = "scheme_guest"
case schemeUser = "scheme_user"
case userId = "user_id"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: TeamMemberKeys.self)
id = try values.decode(String.self, forKey: .id)
userId = try values.decode(String.self, forKey: .userId)
explicitRoles = values.decodeIfPresent(forKey: .explicitRoles, defaultValue: "")
roles = values.decodeIfPresent(forKey: .roles, defaultValue: "")
schemeAdmin = values.decodeIfPresent(forKey: .schemeAdmin, defaultValue: false)
schemeGuest = values.decodeIfPresent(forKey: .schemeGuest, defaultValue: false)
schemeUser = values.decodeIfPresent(forKey: .schemeUser, defaultValue: true)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: TeamMemberKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.explicitRoles, forKey: .explicitRoles)
try container.encode(self.roles, forKey: .roles)
try container.encode(self.schemeAdmin, forKey: .schemeAdmin)
try container.encode(self.schemeGuest, forKey: .schemeGuest)
try container.encode(self.schemeUser, forKey: .schemeUser)
try container.encode(self.userId, forKey: .userId)
}
}

View File

@@ -0,0 +1,98 @@
import Foundation
public struct User: Codable, Hashable {
let id: String
let authService: String
let updateAt: Double
let deleteAt: Double
let email: String
let firstName: String
let isBot: Bool
let isGuest: Bool
let lastName: String
let lastPictureUpdate: Double
let locale: String
let nickname: String
let position: String
let roles: String
let status: String
let username: String
let notifyProps: String
let props: String
let timezone: String
public enum UserKeys: String, CodingKey {
case id, email, locale, nickname, position, roles, username, props, timezone, status
case authService = "auth_service"
case updateAt = "update_at"
case deleteAt = "delete_at"
case firstName = "first_name"
case isBot = "is_bot"
case lastName = "last_name"
case lastPictureUpdate = "last_picture_update"
case notifyProps = "notify_props"
case isGuest = "is_guest"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: UserKeys.self)
id = try values.decode(String.self, forKey: .id)
username = try values.decode(String.self, forKey: .username)
authService = values.decodeIfPresent(forKey: .authService, defaultValue: "")
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
email = values.decodeIfPresent(forKey: .email, defaultValue: "")
firstName = values.decodeIfPresent(forKey: .firstName, defaultValue: "")
isBot = values.decodeIfPresent(forKey: .isBot, defaultValue: false)
roles = values.decodeIfPresent(forKey: .roles, defaultValue: "")
lastName = values.decodeIfPresent(forKey: .lastName, defaultValue: "")
lastPictureUpdate = values.decodeIfPresent(forKey: .lastPictureUpdate, defaultValue: 0)
locale = values.decodeIfPresent(forKey: .locale, defaultValue: "en")
nickname = values.decodeIfPresent(forKey: .nickname, defaultValue: "")
position = values.decodeIfPresent(forKey: .position, defaultValue: "")
isGuest = roles.contains("system_guest")
status = "offline"
if let notifyPropsData = try? values.decodeIfPresent([String: String].self, forKey: .notifyProps) {
notifyProps = Database.default.json(from: notifyPropsData) ?? "{}"
} else {
notifyProps = "{}"
}
if let propsData = try? values.decodeIfPresent([String: String].self, forKey: .props) {
props = Database.default.json(from: propsData) ?? "{}"
} else {
props = "{}"
}
if let timezoneData = try? values.decodeIfPresent([String: String].self, forKey: .timezone) {
timezone = Database.default.json(from: timezoneData) ?? "{}"
} else {
timezone = "{}"
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.authService, forKey: .authService)
try container.encode(self.updateAt, forKey: .updateAt)
try container.encode(self.deleteAt, forKey: .deleteAt)
try container.encode(self.email, forKey: .email)
try container.encode(self.firstName, forKey: .firstName)
try container.encode(self.isBot, forKey: .isBot)
try container.encode(self.isGuest, forKey: .isGuest)
try container.encode(self.lastName, forKey: .lastName)
try container.encode(self.lastPictureUpdate, forKey: .lastPictureUpdate)
try container.encode(self.locale, forKey: .locale)
try container.encode(self.nickname, forKey: .nickname)
try container.encode(self.position, forKey: .position)
try container.encode(self.roles, forKey: .roles)
try container.encode(self.status, forKey: .status)
try container.encode(self.username, forKey: .username)
try container.encode(self.notifyProps, forKey: .notifyProps)
try container.encode(self.props, forKey: .props)
try container.encode(self.timezone, forKey: .timezone)
}
}

View File

@@ -16,9 +16,3 @@ 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,16 @@
import Foundation
public typealias CategoriesHandler = (_ categoriesWithOrder: CategoriesWithOrder?) -> Void
extension Network {
public func fetchCategories(withTeamId teamId: String, forServerUrl serverUrl: String, completionHandler: @escaping CategoriesHandler) {
var categoriesWithOrder: CategoriesWithOrder?
let channelUrl = buildApiUrl(serverUrl, "/users/me/teams/\(teamId)/channels/categories")
request(channelUrl, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
if let data = data {
categoriesWithOrder = try? JSONDecoder().decode(CategoriesWithOrder.self, from: data)
}
completionHandler(categoriesWithOrder)
}
}
}

View File

@@ -0,0 +1,92 @@
import Foundation
extension Network {
public func fetchMyChannel(withId channelId: String, forServerUrl serverUrl: String, completionHandler: @escaping ((_ channel: Channel?, _ myChannel: ChannelMember?, _ profiles: [User]?) -> Void)) {
let group = DispatchGroup()
var channel: Channel? = nil
var tempChannel: Channel? = nil
var myChannel: ChannelMember? = nil
var profiles: [User]? = nil
group.enter()
let channelUrl = buildApiUrl(serverUrl, "/channels/\(channelId)")
request(channelUrl, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
if let data = data {
tempChannel = try? JSONDecoder().decode(Channel.self, from: data)
}
group.leave()
}
group.enter()
let myChannelUrl = buildApiUrl(serverUrl, "/channels/\(channelId)/members/me")
request(myChannelUrl, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
if let data = data {
myChannel = try? JSONDecoder().decode(ChannelMember.self, from: data)
}
group.leave()
}
group.notify(queue: .main) {
if let tempChannel = tempChannel,
(tempChannel.type == "D" || tempChannel.type == "G")
&& !Database.default.queryChannelExists(withId: channelId, forServerUrl: serverUrl) {
let displayNameSetting = Database.default.getTeammateDisplayNameSetting(serverUrl)
Network.default.fetchProfiles(inChannelId: channelId, forServerUrl: serverUrl) {[weak self] data, response, error in
if let data = data,
let currentUserId = try? Database.default.queryCurrentUserId(serverUrl),
let users = try? JSONDecoder().decode([User].self, from: data) {
if !users.isEmpty {
profiles = users.filter{ $0.id != currentUserId}
if tempChannel.type == "D",
let profiles = profiles,
let user = profiles.first,
let displayName = self?.displayUsername(user, displayNameSetting) {
var chan = tempChannel
chan.displayName = displayName
channel = chan
} else if let profiles = profiles {
let locale = Database.default.getCurrentUserLocale(serverUrl)
if let displayName = self?.displayGroupMessageName(profiles, locale: locale, displayNameSetting: displayNameSetting) {
var chan = tempChannel
chan.displayName = displayName
channel = chan
}
}
}
}
completionHandler(channel, myChannel, profiles)
}
} else {
channel = tempChannel
completionHandler(channel, myChannel, profiles)
}
}
}
private func displayUsername(_ user: User, _ displayNameSetting: String) -> String {
switch (displayNameSetting) {
case "nickname_full_name":
if !user.nickname.isEmpty {
return user.nickname
}
return "\(user.firstName) \(user.lastName)".trimmingCharacters(in: .whitespacesAndNewlines)
case "full_name":
return "\(user.firstName) \(user.lastName)".trimmingCharacters(in: .whitespacesAndNewlines)
default:
return user.username
}
}
private func displayGroupMessageName(_ users: [User], locale: String, displayNameSetting: String) -> String {
var names = [String]()
for user in users {
names.append(displayUsername(user, displayNameSetting))
}
let sorted = names.sorted { (lhs: String, rhs: String) -> Bool in
return lhs.compare(rhs, options: [.caseInsensitive], locale: Locale(identifier: locale)) == .orderedAscending
}
return sorted.joined(separator: ", ").trimmingCharacters(in: .whitespaces)
}
}

View File

@@ -0,0 +1,24 @@
import Foundation
extension Network: URLSessionDelegate, URLSessionTaskDelegate {
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
var credential: URLCredential? = nil
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
let authMethod = challenge.protectionSpace.authenticationMethod
if authMethod == NSURLAuthenticationMethodClientCertificate {
let host = task.currentRequest!.url!.host!
if let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) {
credential = URLCredential(identity: identity,
certificates: [certificate],
persistence: URLCredential.Persistence.permanent)
}
disposition = .useCredential
}
completionHandler(disposition, credential)
}
}

View File

@@ -1,64 +0,0 @@
import Foundation
public struct ChannelMemberData: Codable {
let channel_id: String
let mention_count: Int
let mention_count_root: Int
let user_id: String
let roles: String
let last_viewed_at: Int64
let last_update_at: Int64
public enum ChannelMemberKeys: String, CodingKey {
case channel_id, mention_count, mention_count_root, user_id, roles, last_viewed_at, last_update_at
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ChannelMemberKeys.self)
channel_id = try container.decode(String.self, forKey: .channel_id)
mention_count = try container.decode(Int.self, forKey: .mention_count)
let mentions_root = try? container.decode(Int?.self, forKey: .mention_count_root)
mention_count_root = mentions_root ?? 0
user_id = try container.decode(String.self, forKey: .user_id)
roles = try container.decode(String.self, forKey: .roles)
last_update_at = try container.decode(Int64.self, forKey: .last_update_at)
last_viewed_at = try container.decode(Int64.self, forKey: .last_viewed_at)
}
}
public struct ThreadData: Codable {
let id: String
let last_reply_at: Int64
let last_viewed_at: Int64
let reply_count: Int
let unread_replies: Int
let unread_mentions: Int
public enum ThreadKeys: String, CodingKey {
case id, last_reply_at, last_viewed_at, reply_count, unread_replies, unread_mentions
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ThreadKeys.self)
id = try container.decode(String.self, forKey: .id)
last_viewed_at = try container.decode(Int64.self, forKey: .last_viewed_at)
last_reply_at = try container.decode(Int64.self, forKey: .last_reply_at)
reply_count = try container.decode(Int.self, forKey: .reply_count)
unread_replies = try container.decode(Int.self, forKey: .unread_replies)
unread_mentions = try container.decode(Int.self, forKey: .unread_mentions)
}
}
extension Network {
public func fetchChannelMentions(channelId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let endpoint = "/channels/\(channelId)/members/me"
let url = buildApiUrl(serverUrl, endpoint)
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
}
public func fetchThreadMentions(teamId: String, threadId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let endpoint = "/users/me/teams/\(teamId)/threads/\(threadId)"
let url = buildApiUrl(serverUrl, endpoint)
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
}
}

View File

@@ -7,49 +7,9 @@
import Foundation
let POST_CHUNK_SIZE = 60
public struct PostData: Codable {
let order: [String]
let posts: [Post]
let next_post_id: String
let prev_post_id: String
public enum PostDataKeys: String, CodingKey {
case order, posts, next_post_id, prev_post_id
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostDataKeys.self)
order = try container.decode([String].self, forKey: .order)
next_post_id = try container.decode(String.self, forKey: .next_post_id)
prev_post_id = try container.decode(String.self, forKey: .prev_post_id)
let decodedPosts = try container.decode([String:Post].self, forKey: .posts)
posts = Array(decodedPosts.values)
}
}
public typealias PostsHandler = (_ postResponse: PostResponse?, _ threads: [PostThread]?, _ users: [User]?) -> Void
extension Network {
public func fetchPostsForChannel(withId channelId: String, withSince since: Int64?, withServerUrl serverUrl: String, withIsCRTEnabled isCRTEnabled: Bool, withRootId rootId: String, completionHandler: @escaping ResponseHandler) {
let additionalParams = isCRTEnabled ? "&collapsedThreads=true&collapsedThreadsExtended=true" : ""
let endpoint: String
if (isCRTEnabled && !rootId.isEmpty) {
let queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
endpoint = "/posts/\(rootId)/thread\(queryParams)\(additionalParams)"
} else {
let queryParams = since == nil ?
"?page=0&per_page=\(POST_CHUNK_SIZE)" :
"?since=\(since!)"
endpoint = "/channels/\(channelId)/posts\(queryParams)\(additionalParams)"
}
let url = buildApiUrl(serverUrl, endpoint)
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
}
public func createPost(serverUrl: String, channelId: String, message: String, fileIds: [String], completionHandler: @escaping ResponseHandler) {
do {
if !message.isEmpty || !fileIds.isEmpty {
@@ -66,8 +26,8 @@ extension Network {
url,
withMethod: "POST",
withBody: data,
withHeaders: headers,
withServerUrl: serverUrl,
andHeaders: headers,
forServerUrl: serverUrl,
completionHandler: completionHandler
)
}
@@ -75,4 +35,131 @@ extension Network {
}
}
public func fetchPosts(forChannelId channelId: String, andRootId rootId: String, havingCRTEnabled isCRTEnabled: Bool, withAlreadyLoadedProfiles loadedProfiles: [User], forServerUrl serverUrl: String, completionHandler: @escaping PostsHandler) {
let additionalParams = isCRTEnabled ? "&collapsedThreads=true&collapsedThreadsExtended=true" : ""
let receivingThreads = isCRTEnabled && !rootId.isEmpty
let endpoint: String
let alreadyLoadedUserIds = loadedProfiles.map { $0.id }
var postResponse: PostResponse? = nil
if receivingThreads {
let since = rootId.isEmpty ? nil : Database.default.queryLastPostInThread(withRootId: rootId, forServerUrl: serverUrl)
let queryParams = since == nil ? "?perPage=60&fromCreateAt=0&direction=up" : "?fromCreateAt=\(Int64(since!))&direction=down"
endpoint = "/posts/\(rootId)/thread\(queryParams)\(additionalParams)"
} else {
let since = Database.default.queryPostsSinceForChannel(withId: channelId, forServerUrl: serverUrl)
let queryParams = since == nil ? "?page=0&per_page=60" : "?since=\(Int64(since!))"
endpoint = "/channels/\(channelId)/posts\(queryParams)\(additionalParams)"
}
let url = buildApiUrl(serverUrl, endpoint)
request(url, usingMethod: "GET", forServerUrl: serverUrl) {data, response, error in
if let data = data {
postResponse = try? JSONDecoder().decode(PostResponse.self, from: data)
}
DispatchQueue.main.async {
self.processPostsFetched(postResponse, andAlreadyLoadedProfilesIds: alreadyLoadedUserIds,
usingCRT: isCRTEnabled, forServerUrl: serverUrl,
completionHandler: completionHandler)
}
}
}
private func processPostsFetched(_ postResponse: PostResponse?, andAlreadyLoadedProfilesIds alreadyLoadedUserIds: [String],
usingCRT isCRTEnabled: Bool, forServerUrl serverUrl: String, completionHandler: @escaping PostsHandler) {
guard let currentUserRow = try? Database.default.queryCurrentUser(serverUrl),
let currentUser = Database.default.getUserFromRow(currentUserRow)
else {
completionHandler(nil, nil, nil)
return
}
var users: [User]? = nil
var threads: [PostThread]? = nil
var threadParticipantUserIds: Set<String> = Set() // Used to exclude the "userIds" present in the thread participants
var threadParticipantUsernames: Set<String> = Set() // Used to exclude the "usernames" present in the thread participants
var threadParticipantUsers = [String: User]() // All unique users from thread participants are stored here
var userIdsToLoad: Set<String> = Set()
var usernamesToLoad: Set<String> = Set()
if let postsWithKeys = postResponse?.posts {
let posts = Array(postsWithKeys.values)
for post in posts {
let authorId = post.userId
let message = post.message
if isCRTEnabled && post.rootId.isEmpty {
if threads == nil {
threads = [PostThread]()
}
threads?.append(PostThread(from: post))
}
if let participants = post.participants {
for participant in participants {
let userId = participant.id
let username = participant.username
if userId != currentUser.id && !alreadyLoadedUserIds.contains(userId) && !threadParticipantUserIds.contains(userId) {
threadParticipantUserIds.insert(userId)
if threadParticipantUsers[userId] == nil {
threadParticipantUsers[userId] = participant
}
}
if !username.isEmpty && username != currentUser.username && !threadParticipantUsernames.contains(username) {
threadParticipantUsernames.insert(username)
}
}
}
if (authorId != currentUser.id && !alreadyLoadedUserIds.contains(authorId) && !threadParticipantUserIds.contains(authorId) && !userIdsToLoad.contains(authorId)) {
userIdsToLoad.insert(authorId)
}
if !message.isEmpty {
for username in self.matchUsername(in: message) {
if username != currentUser.username && !threadParticipantUsernames.contains(username) && !usernamesToLoad.contains(username) {
usernamesToLoad.insert(username)
}
}
}
}
if !threadParticipantUsers.isEmpty || !usernamesToLoad.isEmpty || !userIdsToLoad.isEmpty {
users = [User]()
}
if !usernamesToLoad.isEmpty || !userIdsToLoad.isEmpty,
let profiles = Network.default.fetchNeededUsers(userIds: userIdsToLoad, usernames: usernamesToLoad, forServerUrl: serverUrl),
!profiles.isEmpty {
users?.append(contentsOf: profiles)
}
if !threadParticipantUsers.isEmpty {
let storedParticipantsById = Database.default.queryUsers(byIds: threadParticipantUserIds, forServerUrl: serverUrl)
let storedParticipantsByUsername = Database.default.queryUsers(byUsernames: threadParticipantUsernames, forServerUrl: serverUrl)
let participantUsers = Array(threadParticipantUsers.values).filter{ !storedParticipantsById.contains($0.id) && !storedParticipantsByUsername.contains($0.username) }
if !participantUsers.isEmpty {
users?.append(contentsOf: participantUsers)
}
}
}
completionHandler(postResponse, threads, users)
}
private func matchUsername(in message: String) -> [String] {
let specialMentions = Set(["all", "here", "channel"])
if 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))
if !results.isEmpty {
let username = results.map({ String(message[Range($0.range, in: message)!]).removePrefix("@") }).filter({ !specialMentions.contains($0)})
return username
}
}
return []
}
}

View File

@@ -0,0 +1,36 @@
import Foundation
extension Network {
public func fetchTeamIfNeeded(withId teamId: String, forServerUrl serverUrl: String, completionHandler: @escaping ((_ team: Team?, _ myTeam: TeamMember?) -> Void)) {
let group = DispatchGroup()
var team: Team? = nil
var myTeam: TeamMember? = nil
if !Database.default.queryTeamExists(withId: teamId, forServerUrl: serverUrl) {
group.enter()
let url = buildApiUrl(serverUrl, "/teams/\(teamId)")
request(url, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
if let data = data {
team = try? JSONDecoder().decode(Team.self, from: data)
}
group.leave()
}
}
if !Database.default.queryMyTeamExists(withId: teamId, forServerUrl: serverUrl) {
group.enter()
let url = buildApiUrl(serverUrl, "/teams/\(teamId)/members/me")
request(url, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
if let data = data {
myTeam = try? JSONDecoder().decode(TeamMember.self, from: data)
}
group.leave()
}
}
group.notify(queue: .main) {
completionHandler(team, myTeam)
}
}
}

View File

@@ -0,0 +1,22 @@
import Foundation
public typealias ThreadResponse = (_ threads: PostThread?) -> Void
extension Network {
public func fetchThread(byId threadId: String, belongingToTeamId teamId: String, forServerUrl serverUrl: String, completionHandler: @escaping ThreadResponse) {
var thread: PostThread? = nil
guard let currentUserId = try? Database.default.queryCurrentUserId(serverUrl),
let threadTeamId = teamId.isEmpty ? Database.default.queryCurrentTeamId(serverUrl) : teamId
else {
completionHandler(nil)
return
}
let url = buildApiUrl(serverUrl, "/users/\(currentUserId)/teams/\(threadTeamId)/threads/\(threadId)")
request(url, usingMethod: "GET", forServerUrl: serverUrl) {data, response, error in
if let data = data {
thread = try? JSONDecoder().decode(PostThread.self, from: data)
}
completionHandler(thread)
}
}
}

View File

@@ -8,26 +8,71 @@
import Foundation
extension Network {
public func fetchUsers(byIds userIds: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
public func fetchNeededUsers(userIds: Set<String>, usernames: Set<String>, forServerUrl serverUrl: String) -> [User]? {
let group = DispatchGroup()
var users: [User]? = nil
if !userIds.isEmpty || !usernames.isEmpty {
// remove existing users in the database
users = [User]()
let storedUserIds = Database.default.queryUsers(byIds: userIds, forServerUrl: serverUrl)
if !(userIds.filter{ !storedUserIds.contains($0) }).isEmpty {
group.enter()
DispatchQueue.global(qos: .default).async {
self.fetchUsers(byIds: Array(userIds), forServerUrl: serverUrl) {data, response, error in
if let data = data,
let profiles = try? JSONDecoder().decode([User].self, from: data) {
users?.append(contentsOf: profiles)
}
group.leave()
}
}
}
let storedUsernames = Database.default.queryUsers(byUsernames: usernames, forServerUrl: serverUrl)
if !(usernames.filter{ !storedUsernames.contains($0) }).isEmpty {
group.enter()
DispatchQueue.global(qos: .default).async {
self.fetchUsers(byUsernames: Array(usernames), forServerUrl: serverUrl) {data, response, error in
if let data = data,
let profiles = try? JSONDecoder().decode([User].self, from: data) {
users?.append(contentsOf: profiles)
}
group.leave()
}
}
}
}
group.wait()
return users
}
public func fetchUsers(byIds userIds: [String], forServerUrl 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)
return request(url, withMethod: "POST", withBody: data, andHeaders: nil, forServerUrl: serverUrl, completionHandler: completionHandler)
}
public func fetchUsers(byUsernames usernames: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
public func fetchUsers(byUsernames usernames: [String], forServerUrl 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)
return request(url, withMethod: "POST", withBody: data, andHeaders: nil, forServerUrl: serverUrl, completionHandler: completionHandler)
}
public func fetchUserProfilePicture(userId: String, lastUpdateAt: Double, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
public func fetchProfiles(inChannelId channelId: String, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let endpoint = "/users?in_channel=\(channelId)&page=0&per_page=8&sort="
let url = buildApiUrl(serverUrl, endpoint)
request(url, usingMethod: "GET", forServerUrl: serverUrl, completionHandler: completionHandler)
}
public func fetchUserProfilePicture(userId: String, lastUpdateAt: Double, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let endpoint = "/users/\(userId)/image?lastPictureUpdate=\(lastUpdateAt)"
let url = buildApiUrl(serverUrl, endpoint)
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
return request(url, usingMethod: "GET", forServerUrl: serverUrl, completionHandler: completionHandler)
}
}

View File

@@ -41,7 +41,7 @@ public class Network: NSObject {
return (response as? HTTPURLResponse)?.statusCode == 200
}
internal func buildURLRequest(for url: URL, withMethod method: String, withBody body: Data?, withHeaders headers: [String:String]?, withServerUrl serverUrl: String) -> URLRequest {
internal func buildURLRequest(for url: URL, usingMethod method: String, withBody body: Data?, andHeaders headers: [String:String]?, forServerUrl serverUrl: String) -> URLRequest {
let request = NSMutableURLRequest(url: url)
request.httpMethod = method
@@ -62,12 +62,12 @@ public class Network: NSObject {
return request as URLRequest
}
internal func request(_ url: URL, withMethod method: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
return request(url, withMethod: method, withBody: nil, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler)
internal func request(_ url: URL, usingMethod method: String, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
return request(url, withMethod: method, withBody: nil, andHeaders: nil, forServerUrl: serverUrl, completionHandler: completionHandler)
}
internal func request(_ url: URL, withMethod method: String, withBody body: Data?, withHeaders headers: [String:String]?, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let urlRequest = buildURLRequest(for: url, withMethod: method, withBody: body, withHeaders: headers, withServerUrl: serverUrl)
internal func request(_ url: URL, withMethod method: String, withBody body: Data?, andHeaders headers: [String:String]?, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let urlRequest = buildURLRequest(for: url, usingMethod: method, withBody: body, andHeaders: headers, forServerUrl: serverUrl)
let task = session!.dataTask(with: urlRequest) { data, response, error in
completionHandler(data, response, error)
@@ -76,26 +76,3 @@ public class Network: NSObject {
task.resume()
}
}
extension Network: URLSessionDelegate, URLSessionTaskDelegate {
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
var credential: URLCredential? = nil
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
let authMethod = challenge.protectionSpace.authenticationMethod
if authMethod == NSURLAuthenticationMethodClientCertificate {
let host = task.currentRequest!.url!.host!
if let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) {
credential = URLCredential(identity: identity,
certificates: [certificate],
persistence: URLCredential.Persistence.permanent)
}
disposition = .useCredential
}
completionHandler(disposition, credential)
}
}

View File

@@ -1,347 +0,0 @@
import Foundation
import UserNotifications
import SQLite
import os.log
public struct AckNotification: Codable {
let type: String
let id: String
let postId: String?
public let serverUrl: String
public let isIdLoaded: Bool
let receivedAt:Int
let platform = "ios"
public enum AckNotificationKeys: String, CodingKey {
case type = "type"
case id = "ack_id"
case postId = "post_id"
case server_id = "server_id"
case isIdLoaded = "id_loaded"
case platform = "platform"
}
public enum AckNotificationRequestKeys: String, CodingKey {
case type = "type"
case id = "id"
case postId = "post_id"
case isIdLoaded = "is_id_loaded"
case receivedAt = "received_at"
case platform = "platform"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AckNotificationKeys.self)
id = try container.decode(String.self, forKey: .id)
type = try container.decode(String.self, forKey: .type)
postId = try? container.decode(String.self, forKey: .postId)
if container.contains(.isIdLoaded) {
isIdLoaded = (try? container.decode(Bool.self, forKey: .isIdLoaded)) == true
} else {
isIdLoaded = false
}
receivedAt = Date().millisecondsSince1970
if let decodedIdentifier = try? container.decode(String.self, forKey: .server_id) {
serverUrl = try Database.default.getServerUrlForServer(decodedIdentifier)
} else {
serverUrl = try Database.default.getOnlyServerUrl()
}
}
}
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))
}
func toUrlSafeBase64Encode() -> String {
return Data(
self.replacingOccurrences(of: "/\\+/g", with: "-", options: .regularExpression)
.replacingOccurrences(of: "/\\//g", with: "_", options: .regularExpression)
.utf8
).base64EncodedString()
}
}
extension Network {
@objc public func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
if let jsonData = try? JSONSerialization.data(withJSONObject: userInfo),
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
postNotificationReceipt(ackNotification, completionHandler: {_, _, _ in})
}
}
private func matchUsername(in message: String) -> [String] {
let specialMentions = Set(["all", "here", "channel"])
if 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))
if !results.isEmpty {
let username = results.map({ String(message[Range($0.range, in: message)!]).removePrefix("@") }).filter({ !specialMentions.contains($0)})
return username
}
}
return []
}
public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?, completionHandler: @escaping (_ data: Data?) -> Void) {
var updatedAt: Double = 0
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200 && error == nil) {
ImageCache.default.insertImage(data, for: senderId, updatedAt: updatedAt, onServer: serverUrl)
completionHandler(data)
} else {
os_log(
OSLogType.default,
"Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@",
httpResponse.statusCode,
(error?.localizedDescription ?? "")
)
}
}
}
if let overrideUrl = overrideIconUrl,
let url = URL(string: overrideUrl) {
request(url, withMethod: "GET", withServerUrl: "", completionHandler: processResponse)
} else {
if let lastUpdateAt = Database.default.getUserLastPictureAt(for: senderId, withServerUrl: serverUrl) {
updatedAt = lastUpdateAt
}
if let image = ImageCache.default.image(for: senderId, updatedAt: updatedAt, onServer: serverUrl) {
os_log(OSLogType.default, "Mattermost Notifications: cached image")
completionHandler(image)
} else {
ImageCache.default.removeImage(for: senderId, onServer: serverUrl)
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
fetchUserProfilePicture(userId: senderId, lastUpdateAt: updatedAt, withServerUrl: serverUrl, completionHandler: processResponse)
}
}
}
public func postNotificationReceipt(_ ackNotification: AckNotification, completionHandler: @escaping ResponseHandler) {
do {
let jsonData = try JSONEncoder().encode(ackNotification)
let headers = ["Content-Type": "application/json; charset=utf-8"]
let endpoint = "/notifications/ack"
let url = buildApiUrl(ackNotification.serverUrl, endpoint)
request(url, withMethod: "POST", withBody: jsonData, withHeaders: headers, withServerUrl: ackNotification.serverUrl, completionHandler: completionHandler)
} catch {
}
}
public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: ((UNNotificationContent) -> Void)?) {
let operation = BlockOperation {
let group = DispatchGroup()
let channelId = notification.userInfo["channel_id"] as? String
let rootId = notification.userInfo["root_id"] as? String ?? ""
let serverUrl = notification.userInfo["server_url"] as? String
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
guard let serverUrl = serverUrl,
let channelId = channelId
else { return }
let currentUser = try? Database.default.queryCurrentUser(serverUrl)
let currentUserId = currentUser?[Expression<String>("id")]
let currentUsername = currentUser?[Expression<String>("username")]
var postData: PostData? = nil
var myChannelData: ChannelMemberData? = nil
var threadData: ThreadData? = nil
var threads: [Post] = []
var userIdsToLoad: Set<String> = Set()
var usernamesToLoad: Set<String> = Set()
var users: Set<User> = Set()
if isCRTEnabled && !rootId.isEmpty {
// Fetch the thread mentions
let teamId = Gekidou.Database.default.queryTeamIdForChannel(withId: channelId, withServerUrl: serverUrl) ?? ""
if !teamId.isEmpty {
group.enter()
self.fetchThreadMentions(teamId: teamId, threadId: rootId, withServerUrl: serverUrl, completionHandler: {data, response, error in
if self.responseOK(response), let data = data {
threadData = try? JSONDecoder().decode(ThreadData.self, from: data)
}
group.leave()
})
}
} else {
// Fetch the channel mentions
group.enter()
self.fetchChannelMentions(channelId: channelId, withServerUrl: serverUrl, completionHandler: { data, response, error in
if self.responseOK(response), let data = data {
myChannelData = try? JSONDecoder().decode(ChannelMemberData.self, from: data)
}
group.leave()
})
}
group.enter()
let since = try? Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl)
self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl, withIsCRTEnabled: isCRTEnabled, withRootId: rootId) { data, response, error in
if self.responseOK(response), let data = data,
let jsonData = try? JSONDecoder().decode(PostData.self, from: data) {
postData = jsonData
if jsonData.posts.count > 0 {
var authorIds: Set<String> = Set()
var usernames: Set<String> = Set()
var threadParticipantUserIds: Set<String> = Set() // Used to exclude the "userIds" present in the thread participants
var threadParticipantUsernames: Set<String> = Set() // Used to exclude the "usernames" present in the thread participants
var threadParticipantUsers = [String: User]() // All unique users from thread participants are stored here
jsonData.posts.forEach{post in
if (currentUserId != nil && post.user_id != currentUserId) {
authorIds.insert(post.user_id)
}
self.matchUsername(in: post.message).forEach{
if ($0 != currentUsername) {
usernames.insert($0)
}
}
if (isCRTEnabled) {
// Add root post as a thread
let rootId = post.root_id
if (rootId.isEmpty) {
threads.append(post)
}
let participants = post.participants ?? []
if (participants.count > 0) {
participants.forEach { participant in
let userId = participant.id
if (userId != currentUserId) {
threadParticipantUserIds.insert(userId)
if (threadParticipantUsers[userId] == nil) {
threadParticipantUsers[userId] = participant
}
}
let username = participant.username
if (username != "" && username != currentUsername) {
threadParticipantUsernames.insert(username)
}
}
}
}
}
if (authorIds.count > 0) {
if let existingIds = try? Database.default.queryUsers(byIds: authorIds, withServerUrl: serverUrl) {
userIdsToLoad = authorIds.filter { !existingIds.contains($0) }
// Filter the users found in the thread participants list
if (threadParticipantUserIds.count > 0) {
userIdsToLoad = userIdsToLoad.filter{ !threadParticipantUserIds.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) {
if let existingUsernames = try? Database.default.queryUsers(byUsernames: usernames, withServerUrl: serverUrl) {
usernamesToLoad = usernames.filter{ !existingUsernames.contains($0)}
// Filter the users found in the thread participants list
if (threadParticipantUsernames.count > 0) {
usernamesToLoad = usernamesToLoad.filter{ !threadParticipantUsernames.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()
}
}
}
}
if (threadParticipantUserIds.count > 0) {
if let existingThreadParticipantUserIds = try? Database.default.queryUsers(byIds: threadParticipantUserIds, withServerUrl: serverUrl) {
threadParticipantUsers.forEach { (userId: String, user: User) in
if (!existingThreadParticipantUserIds.contains(userId)) {
users.insert(user)
}
}
}
}
}
}
group.leave()
}
group.wait()
group.enter()
if let data = postData,
let posts = data.posts as [Post]?,
let db = try? Database.default.getDatabaseForServer(serverUrl),
posts.count > 0 {
let receivingThreads = isCRTEnabled && !rootId.isEmpty
try? db.transaction {
try? Database.default.handlePostData(db, data, channelId, since != nil, receivingThreads)
if threads.count > 0 {
try? Database.default.handleThreads(db, threads)
}
if users.count > 0 {
try? Database.default.insertUsers(db, users)
}
if let myChannel = myChannelData {
try? Database.default.handleMyChannelMentions(db, myChannel, withCRTEnabled: isCRTEnabled)
}
if let threads = threadData {
try? Database.default.handleThreadMentions(db, threads)
}
}
}
group.leave()
if let contentHandler = contentHandler {
// Get the total mentions from all databases and set the badge icon
notification.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
contentHandler(notification)
}
}
queue.addOperation(operation)
}
}

View File

@@ -0,0 +1,58 @@
import Foundation
public struct AckNotification: Codable {
let type: String
let id: String
let postId: String?
public let serverUrl: String
public let isIdLoaded: Bool
let receivedAt:Int
let platform = "ios"
public enum AckNotificationKeys: String, CodingKey {
case type = "type"
case id = "ack_id"
case postId = "post_id"
case server_id = "server_id"
case isIdLoaded = "id_loaded"
case platform = "platform"
}
public enum AckNotificationRequestKeys: String, CodingKey {
case type = "type"
case id = "id"
case postId = "post_id"
case isIdLoaded = "is_id_loaded"
case receivedAt = "received_at"
case platform = "platform"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AckNotificationKeys.self)
id = try container.decode(String.self, forKey: .id)
type = try container.decode(String.self, forKey: .type)
postId = try? container.decode(String.self, forKey: .postId)
if container.contains(.isIdLoaded) {
isIdLoaded = (try? container.decode(Bool.self, forKey: .isIdLoaded)) == true
} else {
isIdLoaded = false
}
receivedAt = Date().millisecondsSince1970
if let decodedIdentifier = try? container.decode(String.self, forKey: .server_id) {
serverUrl = try Database.default.getServerUrlForServer(decodedIdentifier)
} else {
serverUrl = try Database.default.getOnlyServerUrl()
}
}
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)
}
}

View File

@@ -0,0 +1,37 @@
import Foundation
extension PushNotification {
func addChannelToDefaultCategoryIfNeeded(_ channel: Channel, forServerUrl serverUrl: String) -> [CategoryChannel]? {
var categoryChannels = [CategoryChannel]()
if channel.type == "D" || channel.type == "G" {
if let teamIds = Database.default.queryAllMyTeamIds(serverUrl) {
for teamId in teamIds {
if let item = categoryChannelForTeam(channelId: channel.id, teamId: teamId, type: "direct_messages", forServerUrl: serverUrl) {
categoryChannels.append(item)
}
}
return categoryChannels
}
} else if let item = categoryChannelForTeam(channelId: channel.id, teamId: channel.teamId, type: "channels", forServerUrl: serverUrl) {
categoryChannels.append(item)
return categoryChannels
}
return nil
}
private func categoryChannelForTeam(channelId: String, teamId: String, type: String, forServerUrl serverUrl: String) -> CategoryChannel? {
guard !teamId.isEmpty,
let categoryId = Database.default.queryCategoryId(inTeamId: teamId, type: type, forServerUrl: serverUrl) else { return nil }
let cc = Database.default.queryCategoryChannelId(inCategoryId: categoryId, channelId: channelId, forServerUrl: serverUrl)
if cc == nil {
return CategoryChannel(
id: "\(teamId)_\(channelId)",
categoryId: categoryId,
channelId: channelId
)
}
return nil
}
}

View File

@@ -0,0 +1,206 @@
import Foundation
import os.log
import SQLite
import UserNotifications
public struct PushNotificationData: Encodable {
public var categories: CategoriesWithOrder? = nil
public var categoryChannels: [CategoryChannel]? = nil
public var channel: Channel? = nil
public var myChannel: ChannelMember? = nil
public var team: Team? = nil
public var myTeam: TeamMember? = nil
public var posts: PostResponse? = nil
public var users = [User]()
public var threads: [PostThread]? = nil
public enum PushNotificationDataKeys: String, CodingKey {
case categories, categoryChannels, channel, myChannel, team, myTeam, posts, users, threads
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: PushNotificationDataKeys.self)
try container.encodeIfPresent(self.categories, forKey: .categories)
try container.encodeIfPresent(self.categoryChannels, forKey: .categoryChannels)
try container.encodeIfPresent(self.channel, forKey: .channel)
try container.encodeIfPresent(self.myChannel, forKey: .myChannel)
try container.encodeIfPresent(self.team, forKey: .team)
try container.encodeIfPresent(self.myTeam, forKey: .myTeam)
try container.encodeIfPresent(self.posts, forKey: .posts)
try container.encode(self.users, forKey: .users)
try container.encodeIfPresent(self.threads, forKey: .threads)
}
}
extension PushNotification {
public func fetchDataForPushNotification(_ notification: [AnyHashable:Any], withContentHandler contentHander: @escaping ((_ data: PushNotificationData) -> Void)) {
let operation = BlockOperation {
os_log(OSLogType.default, "Mattermost Notifications: Fetch notification data.")
let fetchGroup = DispatchGroup()
let teamId = notification["team_id"] as? String ?? ""
let channelId = notification["channel_id"] as? String
let postId = notification["post_id"] as? String
let rootId = notification["root_id"] as? String ?? ""
var serverUrl = notification["server_url"] as? String
let serverId = notification["server_id"] as? String
let isCRTEnabled = notification["is_crt_enabled"] as? Bool ?? false
if let serverId = serverId,
serverUrl == nil {
serverUrl = try? Database.default.getServerUrlForServer(serverId)
}
guard let serverUrl = serverUrl,
let channelId = channelId,
let _ = postId
else { return }
var notificationData = PushNotificationData()
if !teamId.isEmpty {
fetchGroup.enter()
Network.default.fetchTeamIfNeeded(withId: teamId, forServerUrl: serverUrl) { team, myTeam in
notificationData.team = team
notificationData.myTeam = myTeam
fetchGroup.leave()
}
}
fetchGroup.enter()
Network.default.fetchMyChannel(withId: channelId, forServerUrl: serverUrl) { channel, myChannel, profiles in
notificationData.channel = channel
notificationData.myChannel = myChannel
if let profiles = profiles {
notificationData.users.append(contentsOf: profiles)
}
fetchGroup.leave()
}
if let _ = notificationData.myTeam,
!teamId.isEmpty {
fetchGroup.enter()
Network.default.fetchCategories(withTeamId: teamId, forServerUrl: serverUrl) { categoriesWithOrder in
if let categoriesWithOrder = categoriesWithOrder {
notificationData.categories = categoriesWithOrder
}
fetchGroup.leave()
}
} else if let channel = notificationData.channel {
if let categoryChannels = self.addChannelToDefaultCategoryIfNeeded(channel, forServerUrl: serverUrl) {
notificationData.categoryChannels = categoryChannels
}
}
fetchGroup.enter()
Network.default.fetchPosts(forChannelId: channelId, andRootId: rootId, havingCRTEnabled: isCRTEnabled, withAlreadyLoadedProfiles: notificationData.users, forServerUrl: serverUrl) { postResponse, threads, users in
notificationData.posts = postResponse
notificationData.threads = threads
if let users = users {
notificationData.users.append(contentsOf: users)
}
fetchGroup.leave()
}
fetchGroup.notify(queue: DispatchQueue.main) {
if isCRTEnabled && !rootId.isEmpty {
Network.default.fetchThread(byId: rootId, belongingToTeamId: teamId, forServerUrl: serverUrl) { thread in
if let thread = thread {
if notificationData.threads == nil {
notificationData.threads = [thread]
}
if let threads = notificationData.threads,
let index = threads.firstIndex(where: { $0.id == thread.id }) {
var copy = threads[index]
copy.unreadMentions = thread.unreadMentions
copy.unreadReplies = thread.unreadReplies
copy.lastReplyAt = thread.lastReplyAt
copy.lastViewedAt = thread.lastViewedAt
notificationData.threads?[index] = copy
}
}
contentHander(notificationData);
}
} else {
contentHander(notificationData);
}
}
}
queue.addOperation(operation)
}
public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: @escaping ((UNNotificationContent) -> Void)) {
guard let serverUrl = notification.userInfo["server_url"] as? String,
let channelId = notification.userInfo["channel_id"] as? String
else {
contentHandler(notification)
return
}
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
let rootId = notification.userInfo["root_id"] as? String ?? ""
let teamId = notification.userInfo["team_id"] as? String ?? ""
fetchDataForPushNotification(notification.userInfo) { data in
if let db = try? Database.default.getDatabaseForServer(serverUrl) {
try? db.transaction {
let receivingThreads = isCRTEnabled && !rootId.isEmpty
if let team = data.team {
try? Database.default.insertTeam(db, team)
}
if let myTeam = data.myTeam {
try? Database.default.insertMyTeam(db, myTeam)
}
if let categories = data.categories {
try? Database.default.insertCategoriesWithChannels(db, categories.categories)
}
if let categoryChannels = data.categoryChannels,
!categoryChannels.isEmpty {
try? Database.default.insertChannelToDefaultCategory(db, categoryChannels)
}
if let channel = data.channel,
!Database.default.queryChannelExists(withId: channel.id, forServerUrl: serverUrl) {
try? Database.default.insertChannel(db, channel)
}
if var myChannel = data.myChannel {
var lastFetchedAt: Double = 0
if let postResponse = data.posts, !receivingThreads {
let posts = Array(postResponse.posts.values)
if let fetchedAt = posts.map({max($0.createAt, $0.updateAt, $0.deleteAt)}).max() {
lastFetchedAt = fetchedAt
}
}
var lastPostAt: Double = 0
if let channel = data.channel {
lastPostAt = isCRTEnabled ? channel.lastRootPostAt : channel.lastPostAt
myChannel.internalMsgCount = channel.totalMsgCount - myChannel.msgCount
myChannel.internalMsgCountRoot = channel.totalMsgCountRoot - myChannel.msgCountRoot
}
try? Database.default.insertOrUpdateMyChannel(db, myChannel, isCRTEnabled, lastFetchedAt, lastPostAt)
}
if let posts = data.posts {
try? Database.default.handlePostData(db, posts, channelId, receivingThreads)
}
if let threads = data.threads {
try? Database.default.handleThreads(db, threads, forTeamId: teamId)
}
if !data.users.isEmpty {
try? Database.default.insertUsers(db, data.users)
}
}
}
notification.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
contentHandler(notification)
}
}
}

View File

@@ -0,0 +1,43 @@
import Foundation
import os.log
extension PushNotification {
public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?, completionHandler: @escaping (_ data: Data?) -> Void) {
var updatedAt: Double = 0
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200 && error == nil) {
ImageCache.default.insertImage(data, for: senderId, updatedAt: updatedAt, forServer: serverUrl)
completionHandler(data)
} else {
os_log(
OSLogType.default,
"Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@",
httpResponse.statusCode,
(error?.localizedDescription ?? "")
)
}
}
}
if let overrideUrl = overrideIconUrl,
let url = URL(string: overrideUrl) {
Network.default.request(url, usingMethod: "GET", forServerUrl: "", completionHandler: processResponse)
} else {
if let lastUpdateAt = Database.default.getUserLastPictureAt(for: senderId, forServerUrl: serverUrl) {
updatedAt = lastUpdateAt
}
if let image = ImageCache.default.image(for: senderId, updatedAt: updatedAt, forServer: serverUrl) {
os_log(OSLogType.default, "Mattermost Notifications: cached image")
completionHandler(image)
} else {
ImageCache.default.removeImage(for: senderId, forServer: serverUrl)
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
Network.default.fetchUserProfilePicture(
userId: senderId, lastUpdateAt: updatedAt,
forServerUrl: serverUrl, completionHandler: processResponse
)
}
}
}
}

View File

@@ -0,0 +1,118 @@
import Foundation
import UserNotifications
import os.log
public class PushNotification: NSObject {
@objc public static let `default` = PushNotification()
let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0]
private var retryIndex = 0
let queue = OperationQueue()
public override init() {
queue.maxConcurrentOperationCount = 1
}
@objc public func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
let notification = UNMutableNotificationContent()
notification.userInfo = userInfo
postNotificationReceipt(notification, completionHandler: {_ in})
}
public func postNotificationReceipt(_ notification: UNMutableNotificationContent, completionHandler: @escaping (_ notification: UNMutableNotificationContent?) -> Void) {
if let jsonData = try? JSONSerialization.data(withJSONObject: notification.userInfo),
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
postNotificationReceiptWithRetry(ackNotification) { data in
os_log(
OSLogType.default,
"Mattermost Notifications: process receipt response for serverUrl %{public}@",
ackNotification.serverUrl
)
guard let data = data else {
os_log(
OSLogType.default,
"Mattermost Notifications: process receipt response for serverUrl %{public}@ does not contain data",
ackNotification.serverUrl
)
completionHandler(notification)
return
}
notification.userInfo["server_url"] = ackNotification.serverUrl
if let idLoadedNotification = self.parseIdLoadedNotification(data) {
if let body = idLoadedNotification["message"] as? String {
notification.body = body
}
if let title = idLoadedNotification["channel_name"] as? String {
notification.title = title
}
for (key, value) in idLoadedNotification {
notification.userInfo[key] = value
}
}
completionHandler(notification)
}
return
}
os_log(OSLogType.default, "Mattermost Notifications: Could not parse ACK notification")
completionHandler(nil)
}
private func postNotificationReceiptWithRetry(_ ackNotification: AckNotification, completionHandler: @escaping (_ data: Data?) -> Void) {
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")
completionHandler(nil)
return
}
do {
let jsonData = try JSONEncoder().encode(ackNotification)
let headers = ["Content-Type": "application/json; charset=utf-8"]
let endpoint = "/notifications/ack"
let url = Network.default.buildApiUrl(ackNotification.serverUrl, endpoint)
Network.default.request(
url, withMethod: "POST", withBody: jsonData,
andHeaders: headers, forServerUrl: ackNotification.serverUrl) { data, response, error in
if (error != nil && ackNotification.isIdLoaded) {
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
self.retryIndex += 1
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {[weak self] in
os_log(
OSLogType.default,
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
String(describing: self?.retryIndex)
)
self?.postNotificationReceiptWithRetry(ackNotification, completionHandler: completionHandler)
})
return
}
completionHandler(data)
}
} catch {
os_log(OSLogType.default, "Mattermost Notifications: receipt failed %{public}@", error.localizedDescription)
}
}
private func parseIdLoadedNotification(_ data: Data?) -> [AnyHashable:Any]? {
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
{
os_log(OSLogType.default, "Mattermost Notifications: parsed json response")
var userInfo = [AnyHashable:Any]()
let userInfoKeys = ["channel_name", "team_id", "sender_id", "sender_name", "root_id", "override_username", "override_icon_url", "from_webhook", "message"]
for key in userInfoKeys {
if let value = json[key] as? String {
userInfo[key] = value
}
}
return userInfo
}
return nil
}
}

View File

@@ -0,0 +1,111 @@
import Foundation
import SQLite
extension Database {
public func queryCategoryId(inTeamId teamId: String, type: String, forServerUrl serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let teamIdCol = Expression<String>("team_id")
let typeCol = Expression<String>("type")
let query = categoryTable.where(teamIdCol == teamId && typeCol == type)
if let result = try? db.pluck(query) {
return try? result.get(idCol)
}
}
return nil
}
public func queryCategoryChannelId(inCategoryId categoryId: String, channelId: String, forServerUrl serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let categoryIdCol = Expression<String>("category_id")
let channelIdCol = Expression<String>("channel_id")
let query = categoryChannelTable.where(categoryIdCol == categoryId && channelIdCol == channelId)
if let result = try? db.pluck(query) {
return try? result.get(idCol)
}
}
return nil
}
public func insertCategoriesWithChannels(_ db: Connection, _ categoriesWithChannels: [Category]) throws {
let categories = createCategoriesSetter(from: categoriesWithChannels)
let categoryChannels = createCategoryChannelsSetter(from: categoriesWithChannels)
try db.run(categoryTable.insertMany(or: .replace, categories))
try db.run(categoryChannelTable.insertMany(or: .replace, categoryChannels))
}
public func insertChannelToDefaultCategory(_ db: Connection, _ categoryChannels: [CategoryChannel]) throws {
let categoryId = Expression<String>("category_id")
for cc in categoryChannels {
let count = (try? db.scalar(categoryChannelTable.where(categoryId == cc.categoryId).count)) ?? 0
let setter = createCategoryChannelsSetter(from: cc, index: count > 0 ? count + 1 : 0)
try db.run(categoryChannelTable.insert(or: .replace, setter))
}
}
private func createCategoriesSetter(from categories: [Category]) -> [[Setter]] {
let id = Expression<String>("id")
let collapsed = Expression<Bool>("collapsed")
let displayName = Expression<String>("display_name")
let muted = Expression<Bool>("muted")
let sortOrder = Expression<Int>("sort_order")
let sorting = Expression<String>("sorting")
let teamId = Expression<String>("team_id")
let type = Expression<String>("type")
var setters = [[Setter]]()
for category in categories {
var setter = [Setter]()
setter.append(id <- category.id)
setter.append(collapsed <- category.collapsed)
setter.append(displayName <- category.displayName)
setter.append(muted <- category.muted)
setter.append(sortOrder <- category.sortOrder / 10)
setter.append(sorting <- category.sorting)
setter.append(teamId <- category.teamId)
setter.append(type <- category.type)
setters.append(setter)
}
return setters
}
private func createCategoryChannelsSetter(from categories: [Category]) -> [[Setter]] {
let id = Expression<String>("id")
let categoryId = Expression<String>("category_id")
let channelId = Expression<String>("channel_id")
let sortOrder = Expression<Int>("sort_order")
var setters = [[Setter]]()
for category in categories {
for (index, chId) in category.channelIds.enumerated() {
var setter = [Setter]()
setter.append(id <- "\(category.teamId)_\(chId)")
setter.append(categoryId <- category.id)
setter.append(channelId <- chId)
setter.append(sortOrder <- index)
setters.append(setter)
}
}
return setters
}
private func createCategoryChannelsSetter(from categoryChannel: CategoryChannel, index: Int = 0) -> [Setter] {
let id = Expression<String>("id")
let categoryId = Expression<String>("category_id")
let channelId = Expression<String>("channel_id")
let sortOrder = Expression<Int>("sort_order")
var setter = [Setter]()
setter.append(id <- categoryChannel.id)
setter.append(categoryId <- categoryChannel.categoryId)
setter.append(channelId <- categoryChannel.channelId)
setter.append(sortOrder <- index)
return setter
}
}

View File

@@ -40,6 +40,27 @@ extension Database {
}
}
public func queryChannelExists(withId channelId: String, forServerUrl serverUrl: String) -> Bool {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let query = channelTable.where(idCol == channelId)
if let _ = try? db.pluck(query) {
return true
}
}
return false
}
public func hasMyChannel(_ db: Connection, channelId: String) -> Bool {
let idCol = Expression<String>("id")
let query = myChannelTable.where(idCol == channelId)
if let _ = try? db.pluck(query) {
return true
}
return false
}
public func getCurrentChannelWithTeam<T: Codable>(_ serverUrl: String) -> T? {
do {
let channelId = try queryCurrentChannelId(serverUrl)
@@ -186,4 +207,131 @@ extension Database {
return []
}
}
public func insertChannel(_ db: Connection, _ channel: Channel) throws {
let setter = createChannelSetter(from: channel)
try db.run(channelTable.insert(or: .replace, setter))
let channelInfo = createChannelInfoSetter(from: channel)
try db.run(channelInfoTable.insert(or: .replace, channelInfo))
}
public func insertOrUpdateMyChannel(_ db: Connection, _ myChannel: ChannelMember, _ isCRTEnabled: Bool, _ lastFetchedAt: Double, _ lastPostAt: Double) throws {
let idCol = Expression<String>("id")
let messageCountCol = Expression<Int>("message_count")
let mentionsCol = Expression<Int>("mentions_count")
let isUnreadCol = Expression<Bool>("is_unread")
let lastFetchedAtCol = Expression<Double>("last_fetched_at")
let lastPostAtCol = Expression<Double>("last_post_at")
let mentionsCount = isCRTEnabled ? myChannel.mentionCountRoot : myChannel.mentionCount
let messageCount = isCRTEnabled ? myChannel.internalMsgCountRoot : myChannel.internalMsgCount
let isUnread = messageCount > 0
if hasThread(db, threadId: myChannel.id) {
let updateQuery = myChannelTable.where(idCol == myChannel.id)
.update(
messageCountCol <- messageCount,
mentionsCol <- mentionsCount,
isUnreadCol <- isUnreadCol,
lastPostAtCol <- lastPostAt,
lastFetchedAtCol <- lastFetchedAt
)
let _ = try db.run(updateQuery)
} else {
let rolesCol = Expression<String>("roles")
let manuallyUnreadCol = Expression<Bool>("manually_unread")
let lastViewedAtCol = Expression<Double>("last_viewed_at")
let viewedAtCol = Expression<Double>("viewed_at")
let setter: [Setter] = [
idCol <- myChannel.id,
mentionsCol <- mentionsCount,
messageCountCol <- messageCount,
lastPostAtCol <- lastPostAt,
lastViewedAtCol <- myChannel.lastViewedAt,
viewedAtCol <- 0,
lastFetchedAtCol <- lastFetchedAt,
isUnreadCol <- isUnread,
manuallyUnreadCol <- false,
rolesCol <- myChannel.roles,
]
let _ = try db.run(myChannelTable.insert(or: .replace, setter))
try insertMyChannelSettings(db, myChannel)
try insertChannelMember(db, myChannel)
}
}
private func insertMyChannelSettings(_ db: Connection, _ myChannel: ChannelMember) throws {
let id = Expression<String>("id")
let notifyProps = Expression<String>("notify_props")
let setter: [Setter] = [
id <- myChannel.id,
notifyProps <- myChannel.notifyProps,
]
let _ = try db.run(myChannelSettingsTable.insert(or: .replace, setter))
}
private func insertChannelMember(_ db: Connection, _ member: ChannelMember) throws {
let id = Expression<String>("id")
let channelId = Expression<String>("channel_id")
let userId = Expression<String>("user_id")
let schemeAdmin = Expression<Bool>("scheme_admin")
let setter: [Setter] = [
id <- "\(member.id)-\(member.userId)",
channelId <- member.id,
userId <- member.userId,
schemeAdmin <- member.schemeAdmin,
]
let _ = try db.run(channelMembershipTable.insert(or: .replace, setter))
}
private func createChannelSetter(from channel: Channel) -> [Setter] {
let id = Expression<String>("id")
let createAt = Expression<Double>("create_at")
let deleteAt = Expression<Double>("delete_at")
let updateAt = Expression<Double>("update_at")
let creatorId = Expression<String>("creator_id")
let displayName = Expression<String>("display_name")
let name = Expression<String>("name")
let teamId = Expression<String>("team_id")
let type = Expression<String>("type")
let isGroupConstrained = Expression<Bool>("group_constrained")
let shared = Expression<Bool>("shared")
var setter = [Setter]()
setter.append(id <- channel.id)
setter.append(createAt <- channel.createAt)
setter.append(deleteAt <- channel.deleteAt)
setter.append(updateAt <- channel.updateAt)
setter.append(creatorId <- channel.creatorId)
setter.append(displayName <- channel.displayName)
setter.append(name <- channel.name)
setter.append(teamId <- channel.teamId)
setter.append(type <- channel.type)
setter.append(isGroupConstrained <- channel.groupConstrained)
setter.append(shared <- channel.shared)
return setter
}
private func createChannelInfoSetter(from channel: Channel) -> [Setter] {
let id = Expression<String>("id")
let header = Expression<String>("header")
let purpose = Expression<String>("purpose")
let guestCount = Expression<Int>("guest_count")
let memberCount = Expression<Int>("member_count")
let pinnedPostCount = Expression<Int>("pinned_post_count")
var setter = [Setter]()
setter.append(id <- channel.id)
setter.append(header <- channel.header)
setter.append(purpose <- channel.purpose)
setter.append(guestCount <- 0)
setter.append(memberCount <- 0)
setter.append(pinnedPostCount <- 0)
return setter
}
}

View File

@@ -2,26 +2,6 @@ import Foundation
import SQLite
extension Database {
public func hasMyChannel(_ db: Connection, channelId: String) -> Bool {
let idCol = Expression<String>("id")
let query = myChannelTable.where(idCol == channelId)
if let _ = try? db.pluck(query) {
return true
}
return false
}
public func hasThread(_ db: Connection, threadId: String) -> Bool {
let idCol = Expression<String>("id")
let query = threadTable.where(idCol == threadId)
if let _ = try? db.pluck(query) {
return true
}
return false
}
public func getTotalMentions() -> Int {
let serverUrls = getAllActiveServerUrls()
var mentions = 0
@@ -58,85 +38,6 @@ extension Database {
return Int(mentions ?? 0)
}
public func handleMyChannelMentions(_ db: Connection, _ channelMemberData: ChannelMemberData, withCRTEnabled crtEnabled: Bool) throws {
let idCol = Expression<String>("id")
let mentionsCol = Expression<Int>("mentions_count")
let isUnreadCol = Expression<Bool>("is_unread")
let mentions = crtEnabled ? channelMemberData.mention_count_root : channelMemberData.mention_count
if hasMyChannel(db, channelId: channelMemberData.channel_id) {
let updateQuery = myChannelTable
.where(idCol == channelMemberData.channel_id)
.update(mentionsCol <- mentions,
isUnreadCol <- true
)
let _ = try db.run(updateQuery)
} else {
let msgCol = Expression<Int>("message_count")
let lastPostAtCol = Expression<Int64>("last_post_at")
let lastViewedAtCol = Expression<Int64>("last_viewed_at")
let viewedAtCol = Expression<Int64>("viewed_at")
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
let manuallyUnreadCol = Expression<Bool>("manually_unread")
let rolesCol = Expression<String>("roles")
let statusCol = Expression<String>("status")
let setters: [Setter] = [
idCol <- channelMemberData.channel_id,
mentionsCol <- mentions,
msgCol <- mentions,
lastPostAtCol <- channelMemberData.last_update_at,
lastViewedAtCol <- channelMemberData.last_viewed_at,
viewedAtCol <- 0,
lastFetchedAtCol <- 0,
isUnreadCol <- true,
manuallyUnreadCol <- false,
rolesCol <- channelMemberData.roles,
statusCol <- "created"
]
let insertQuery = myChannelTable.insert(setters)
let _ = try db.run(insertQuery)
}
}
public func handleThreadMentions(_ db: Connection, _ threadData: ThreadData) throws {
let idCol = Expression<String>("id")
let unreadMentionsCol = Expression<Int>("unread_mentions")
if hasThread(db, threadId: threadData.id) {
let updateQuery = threadTable
.where(idCol == threadData.id)
.update(unreadMentionsCol <- threadData.unread_mentions)
let _ = try db.run(updateQuery)
} else {
let lastReplyAtCol = Expression<Int64>("last_reply_at")
let lastViewedAtCol = Expression<Int64>("last_viewed_at")
let viewedAtCol = Expression<Int64>("viewed_at")
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
let isFollowingCol = Expression<Bool>("is_following")
let unreadRepliesCol = Expression<Int>("unread_replies")
let replyCountCol = Expression<Int>("reply_count")
let statusCol = Expression<String>("status")
let setters: [Setter] = [
idCol <- threadData.id,
unreadMentionsCol <- threadData.unread_mentions,
lastReplyAtCol <- threadData.last_reply_at,
lastViewedAtCol <- threadData.last_viewed_at,
viewedAtCol <- 0,
lastFetchedAtCol <- 0,
isFollowingCol <- true,
unreadRepliesCol <- threadData.unread_replies,
replyCountCol <- threadData.reply_count,
statusCol <- "created"
]
let insertQuery = threadTable.insert(setters)
let _ = try db.run(insertQuery)
}
}
public func resetMyChannelMentions(_ serverUrl: String, _ channelId: String) throws {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")

View File

@@ -8,80 +8,6 @@
import Foundation
import SQLite
public struct Post: Codable {
let id: String
let create_at: Int64
let update_at: Int64
let edit_at: Int64
let delete_at: Int64
let is_pinned: Bool
let user_id: String
let channel_id: String
let root_id: String
let original_id: String
let message: String
let type: String
let props: String
let pending_post_id: String
let metadata: String
var prev_post_id: String
// CRT
let participants: [User]?
let last_reply_at: Int64
let reply_count: Int
let is_following: Bool
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"
// CRT
case participants = "participants"
case last_reply_at = "last_reply_at"
case reply_count = "reply_count"
case is_following = "is_following"
}
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) ?? "{}"
// CRT
participants = try values.decodeIfPresent([User].self, forKey: .participants) ?? []
last_reply_at = try values.decodeIfPresent(Int64.self, forKey: .last_reply_at) ?? 0
reply_count = try values.decodeIfPresent(Int.self, forKey: .reply_count) ?? 0
is_following = try values.decodeIfPresent(Bool.self, forKey: .is_following) ?? false
}
}
struct MetadataSetters {
let metadata: String
let reactionSetters: [[Setter]]
@@ -104,110 +30,99 @@ struct ThreadSetters {
}
extension Database {
public func queryLastPostCreateAt(withId channelId: String, withServerUrl serverUrl: String) throws -> Int64? {
let db = try getDatabaseForServer(serverUrl)
let earliestCol = Expression<Int64>("earliest")
let latestCol = Expression<Int64>("latest")
let channelIdCol = Expression<String>("channel_id")
let earliestLatestQuery = postsInChannelTable
.select(earliestCol, latestCol)
.where(channelIdCol == channelId)
.order(latestCol.desc)
.limit(1)
var earliest: Int64?
var latest: Int64?
if let result = try? db.pluck(earliestLatestQuery) {
earliest = try? result.get(earliestCol)
latest = try? result.get(latestCol)
} else {
return nil
public func queryLastPostInThread(withRootId rootId: String, forServerUrl serverUrl: String) -> Double? {
if let db = try? getDatabaseForServer(serverUrl) {
let createAtCol = Expression<Double>("create_at")
let rootIdCol = Expression<String>("root_id")
let query = postTable
.select(createAtCol)
.where(rootIdCol == rootId)
.order(createAtCol.desc)
.limit(1)
if let result = try? db.pluck(query) {
return try? result.get(createAtCol)
}
}
let createAtCol = Expression<Int64>("create_at")
let deleteAtCol = Expression<Int64>("delete_at")
var postQuery = postTable
.select(createAtCol)
.where(channelIdCol == channelId && deleteAtCol == 0)
if let earliest = earliest, let latest = latest {
postQuery = postQuery.filter(earliest...latest ~= createAtCol)
}
postQuery = postQuery.order(createAtCol.desc).limit(1)
if let result = try db.pluck(postQuery) {
return try result.get(createAtCol)
return nil
}
public func queryLastPostCreateAt(withId channelId: String, forServerUrl serverUrl: String) -> Double? {
if let db = try? getDatabaseForServer(serverUrl) {
let earliestCol = Expression<Double>("earliest")
let latestCol = Expression<Double>("latest")
let channelIdCol = Expression<String>("channel_id")
let earliestLatestQuery = postsInChannelTable
.select(earliestCol, latestCol)
.where(channelIdCol == channelId)
.order(latestCol.desc)
.limit(1)
var earliest: Double?
var latest: Double?
if let result = try? db.pluck(earliestLatestQuery) {
earliest = try? result.get(earliestCol)
latest = try? result.get(latestCol)
} else {
return nil
}
let createAtCol = Expression<Double>("create_at")
let deleteAtCol = Expression<Double>("delete_at")
var postQuery = postTable
.select(createAtCol)
.where(channelIdCol == channelId && deleteAtCol == 0)
if let earliest = earliest, let latest = latest {
postQuery = postQuery.filter(earliest...latest ~= createAtCol)
}
postQuery = postQuery.order(createAtCol.desc).limit(1)
if let result = try? db.pluck(postQuery) {
return try? result.get(createAtCol)
}
}
return nil
}
public func queryPostsSinceForChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Int64? {
let db = try getDatabaseForServer(serverUrl)
let idCol = Expression<String>("id")
let lastFetchedAtColAsDouble = Expression<Double?>("last_fetched_at")
let lastFetchedAtColAsInt64 = Expression<Int64?>("last_fetched_at")
let query = myChannelTable.where(idCol == channelId)
if let result = try? db.pluck(query) {
let lastFetchAtInt64 = result[lastFetchedAtColAsInt64]
if lastFetchAtInt64 != nil,
lastFetchAtInt64! > 0 {
return lastFetchAtInt64
}
if let last = result[lastFetchedAtColAsDouble],
last > 0 {
return Int64(last)
public func queryPostsSinceForChannel(withId channelId: String, forServerUrl serverUrl: String) -> Double? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let lastFetchedAtColAsDouble = Expression<Double?>("last_fetched_at")
let query = myChannelTable.where(idCol == channelId)
if let result = try? db.pluck(query) {
if let last = result[lastFetchedAtColAsDouble],
last > 0 {
return last
}
}
return queryLastPostCreateAt(withId: channelId, forServerUrl: serverUrl)
}
return try queryLastPostCreateAt(withId: channelId, withServerUrl: serverUrl)
return nil
}
private func updateMyChannelLastFetchedAt(_ db: Connection, _ channelId: String, _ latest: Int64) throws {
let idCol = Expression<String>("id")
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
let statusCol = Expression<String>("_status")
let query = myChannelTable
.where(idCol == channelId)
.update(lastFetchedAtCol <- latest, statusCol <- "updated")
try db.run(query)
}
public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false, _ receivingThreads: Bool = false) throws {
public func handlePostData(_ db: Connection, _ postData: PostResponse, _ channelId: String, _ receivingThreads: Bool = false) throws {
let sortedChainedPosts = chainAndSortPosts(postData)
try insertOrUpdatePosts(db, sortedChainedPosts, channelId)
let sortedAndNotDeletedPosts = sortedChainedPosts.filter({$0.delete_at == 0})
let sortedAndNotDeletedPosts = sortedChainedPosts.filter({$0.deleteAt == 0})
if (!receivingThreads) {
if !sortedAndNotDeletedPosts.isEmpty {
let earliest = sortedAndNotDeletedPosts.first!.create_at
let latest = sortedAndNotDeletedPosts.last!.create_at
try handlePostsInChannel(db, channelId, earliest, latest, usedSince)
let earliest = sortedAndNotDeletedPosts.first!.createAt
let latest = sortedAndNotDeletedPosts.last!.createAt
try handlePostsInChannel(db, channelId, earliest, latest)
}
let lastFetchedAt = postData.posts.map({max($0.create_at, $0.update_at, $0.delete_at)}).max()
try updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt ?? 0)
}
try handlePostsInThread(db, postData.posts)
try handlePostsInThread(db, Array(postData.posts.values))
}
public func handleThreads(_ db: Connection, _ threads: [Post]) throws {
try insertThreads(db, threads)
}
private func handlePostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64, _ usedSince: Bool = false) throws {
if usedSince {
try? updatePostsInChannelLatestOnly(db, channelId, latest)
} else {
let updated = try updatePostsInChannelEarliestAndLatest(db, channelId, earliest, latest)
if (!updated) {
try? insertPostsInChannel(db, channelId, earliest, latest)
}
private func handlePostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Double, _ latest: Double) throws {
let updated = try updatePostsInChannelEarliestAndLatest(db, channelId, earliest, latest)
if (!updated) {
try insertPostsInChannel(db, channelId, earliest, latest)
}
}
@@ -219,17 +134,17 @@ extension Database {
}
}
private func chainAndSortPosts(_ postData: PostData) -> [Post] {
private func chainAndSortPosts(_ postData: PostResponse) -> [Post] {
let order = postData.order
let posts = postData.posts
let posts = Array(postData.posts.values)
var prevPostId = ""
return posts.sorted(by: {$0.create_at < $1.create_at}).enumerated().map { (index, post) in
return posts.sorted(by: {$0.createAt < $1.createAt}).enumerated().map { (index, post) in
var modified = post
if (index == 0) {
modified.prev_post_id = postData.prev_post_id
modified.prevPostId = postData.prevPostId
} else {
modified.prev_post_id = prevPostId
modified.prevPostId = prevPostId
}
if (order.contains(post.id)) {
@@ -240,25 +155,11 @@ extension Database {
}
}
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, statusCol <- "updated")
try db.run(query)
}
private func updatePostsInChannelEarliestAndLatest(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws -> Bool {
private func updatePostsInChannelEarliestAndLatest(_ db: Connection, _ channelId: String, _ earliest: Double, _ latest: Double) throws -> Bool {
let idCol = Expression<String>("id")
let channelIdCol = Expression<String>("channel_id")
let earliestCol = Expression<Int64>("earliest")
let latestCol = Expression<Int64>("latest")
let earliestCol = Expression<Double>("earliest")
let latestCol = Expression<Double>("latest")
let statusCol = Expression<String>("_status")
let query = postsInChannelTable
@@ -283,11 +184,11 @@ extension Database {
return false
}
private func insertPostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws {
private func insertPostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Double, _ latest: Double) throws {
let idCol = Expression<String>("id")
let channelIdCol = Expression<String>("channel_id")
let earliestCol = Expression<Int64>("earliest")
let latestCol = Expression<Int64>("latest")
let earliestCol = Expression<Double>("earliest")
let latestCol = Expression<Double>("latest")
let statusCol = Expression<String>("_status")
let id = generateId()
@@ -331,29 +232,12 @@ extension Database {
}
}
private func insertThreads(_ db: Connection, _ posts: [Post]) throws {
let setters = try createThreadSetters(db, from: posts)
for setter in setters {
let insertThread = threadTable.insert(or: .replace, setter.threadSetters)
try db.run(insertThread)
let threadIdCol = Expression<String>("thread_id")
let deletePreviousThreadParticipants = threadParticipantTable.where(threadIdCol == setter.id).delete()
try db.run(deletePreviousThreadParticipants)
if !setter.threadParticipantSetters.isEmpty {
let insertThreadParticipants = threadParticipantTable.insertMany(setter.threadParticipantSetters)
try db.run(insertThreadParticipants)
}
}
}
private func createPostSetters(from posts: [Post]) -> [PostSetters] {
let id = Expression<String>("id")
let createAt = Expression<Int64>("create_at")
let updateAt = Expression<Int64>("update_at")
let editAt = Expression<Int64>("edit_at")
let deleteAt = Expression<Int64>("delete_at")
let createAt = Expression<Double>("create_at")
let updateAt = Expression<Double>("update_at")
let editAt = Expression<Double>("edit_at")
let deleteAt = Expression<Double>("delete_at")
let isPinned = Expression<Bool>("is_pinned")
let userId = Expression<String>("user_id")
let channelId = Expression<String>("channel_id")
@@ -373,20 +257,20 @@ extension Database {
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)
setter.append(editAt <- post.edit_at)
setter.append(deleteAt <- post.delete_at)
setter.append(isPinned <- post.is_pinned)
setter.append(userId <- post.user_id)
setter.append(channelId <- post.channel_id)
setter.append(rootId <- post.root_id)
setter.append(originalId <- post.original_id)
setter.append(createAt <- post.createAt)
setter.append(updateAt <- post.updateAt)
setter.append(editAt <- post.editAt)
setter.append(deleteAt <- post.deleteAt)
setter.append(isPinned <- post.isPinned)
setter.append(userId <- post.userId)
setter.append(channelId <- post.channelId)
setter.append(rootId <- post.rootId)
setter.append(originalId <- post.originalId)
setter.append(message <- post.message)
setter.append(metadata <- metadataSetters.metadata)
setter.append(type <- post.type)
setter.append(pendingPostId <- post.pending_post_id)
setter.append(prevPostId <- post.prev_post_id)
setter.append(pendingPostId <- post.pendingPostId)
setter.append(prevPostId <- post.prevPostId)
setter.append(props <- post.props)
setter.append(statusCol <- "created")
@@ -408,13 +292,13 @@ extension Database {
let userId = Expression<String>("user_id")
let postId = Expression<String>("post_id")
let emojiName = Expression<String>("emoji_name")
let createAt = Expression<Int64>("create_at")
let createAt = Expression<Double>("create_at")
let name = Expression<String>("name")
let ext = Expression<String>("extension")
let size = Expression<Int64>("size")
let size = Expression<Double>("size")
let mimeType = Expression<String>("mime_type")
let width = Expression<Int64>("width")
let height = Expression<Int64>("height")
let width = Expression<Double>("width")
let height = Expression<Double>("height")
let localPath = Expression<String?>("local_path")
let imageThumbnail = Expression<String?>("image_thumbnail")
let statusCol = Expression<String>("_status")
@@ -435,7 +319,7 @@ extension Database {
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(createAt <- r["create_at"] as! Double)
reactionSetter.append(statusCol <- "created")
reactionSetters.append(reactionSetter)
@@ -453,10 +337,10 @@ extension Database {
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(size <- f["size"] as! Double)
fileSetter.append(mimeType <- f["mime_type"] as! String)
fileSetter.append(width <- (f["width"] as? Int64 ?? 0))
fileSetter.append(height <- (f["height"] as? Int64 ?? 0))
fileSetter.append(width <- (f["width"] as? Double ?? 0))
fileSetter.append(height <- (f["height"] as? Double ?? 0))
fileSetter.append(localPath <- "")
fileSetter.append(imageThumbnail <- (f["mini_preview"] as? String ?? ""))
fileSetter.append(statusCol <- "created")
@@ -496,118 +380,52 @@ extension Database {
emojiSetters: emojiSetters)
}
private func createThreadSetters(_ db: Connection, from posts: [Post]) throws -> [ThreadSetters] {
let id = Expression<String>("id")
let lastReplyAt = Expression<Int64>("last_reply_at")
let replyCount = Expression<Int>("reply_count")
let isFollowing = Expression<Bool>("is_following")
let statusCol = Expression<String>("_status")
let lastFetchAtCol = Expression<Int64>("last_fetched_at")
var threadsSetters: [ThreadSetters] = []
for post in posts {
let query = threadTable
.select(id)
.where(id == post.id)
if let _ = try? db.pluck(query) {
let updateQuery = threadTable
.where(id == post.id)
.update(lastReplyAt <- post.last_reply_at,
replyCount <- post.reply_count,
isFollowing <- post.is_following,
statusCol <- "updated"
)
try db.run(updateQuery)
} else {
var setter = [Setter]()
setter.append(id <- post.id)
setter.append(lastReplyAt <- post.last_reply_at)
setter.append(replyCount <- post.reply_count)
setter.append(isFollowing <- post.is_following)
setter.append(lastFetchAtCol <- 0)
setter.append(statusCol <- "created")
let threadSetter = ThreadSetters(
id: post.id,
threadSetters: setter,
threadParticipantSetters: createThreadParticipantSetters(from: post)
)
threadsSetters.append(threadSetter)
}
}
return threadsSetters
}
private func createThreadParticipantSetters(from post: Post) -> [[Setter]] {
var participantSetters = [[Setter]]()
let id = Expression<String>("id")
let userId = Expression<String>("user_id")
let threadId = Expression<String>("thread_id")
let statusCol = Expression<String>("_status")
for p in post.participants ?? [] {
var participantSetter = [Setter]()
participantSetter.append(id <- generateId() as String)
participantSetter.append(userId <- p.id)
participantSetter.append(threadId <- post.id)
participantSetter.append(statusCol <- "created")
participantSetters.append(participantSetter)
}
return participantSetters
}
private func createPostsInThreadSetters(_ db: Connection, from posts: [Post]) throws -> [[Setter]] {
var setters = [[Setter]]()
var postsInThread = [String: [Post]]()
for post in posts {
if !post.root_id.isEmpty && post.delete_at == 0 {
var threadPosts = postsInThread[post.root_id] ?? [Post]()
if !post.rootId.isEmpty && post.deleteAt == 0 {
var threadPosts = postsInThread[post.rootId] ?? [Post]()
threadPosts.append(post)
postsInThread.updateValue(threadPosts, forKey: post.root_id)
postsInThread.updateValue(threadPosts, forKey: post.rootId)
}
}
let rootIdCol = Expression<String>("root_id")
let earliestCol = Expression<Int64>("earliest")
let latestCol = Expression<Int64>("latest")
let earliestCol = Expression<Double>("earliest")
let latestCol = Expression<Double>("latest")
let statusCol = Expression<String>("_status")
for (rootId, posts) in postsInThread {
let sortedPosts = posts.sorted(by: { $0.create_at < $1.create_at })
let earliest = sortedPosts.first!.create_at
let latest = sortedPosts.last!.create_at
let query = postsInThreadTable
.where(rootIdCol == rootId)
.order(latestCol.desc)
.limit(1)
if let row = try? db.pluck(query) {
let rowEarliest = try row.get(earliestCol)
let rowLatest = try row.get(latestCol)
let sortedPosts = posts.sorted(by: { $0.createAt < $1.createAt })
if let earliest = sortedPosts.first?.createAt,
let latest = sortedPosts.last?.createAt {
let updateQuery = postsInThreadTable
.where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest)
.update(earliestCol <- min(earliest, rowEarliest),
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)
let query = postsInThreadTable
.where(rootIdCol == rootId)
.order(latestCol.desc)
.limit(1)
if let row = try? db.pluck(query) {
let rowEarliest = try row.get(earliestCol)
let rowLatest = try row.get(latestCol)
let updateQuery = postsInThreadTable
.where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest)
.update(earliestCol <- min(earliest, rowEarliest),
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

@@ -0,0 +1,26 @@
import Foundation
import SQLite
extension Database {
public func getTeammateDisplayNameSetting(_ serverUrl: String) -> String {
do {
if let displayName = geConfigDisplayNameSetting(serverUrl) {
return displayName
}
let db = try getDatabaseForServer(serverUrl)
let category = Expression<String>("category")
let name = Expression<String>("name")
let value = Expression<String>("value")
let query = preferenceTable.select(value).filter(category == "display_settings" && name == "name_format")
if let result = try db.pluck(query) {
let val = try result.get(value)
return val
}
} catch {
// do nothing
}
return "username"
}
}

View File

@@ -11,19 +11,41 @@ import SQLite
extension Database {
public func getConfig(_ serverUrl: String, _ key: String) -> String? {
do {
let db = try getDatabaseForServer(serverUrl)
if let db = try? getDatabaseForServer(serverUrl) {
let id = Expression<String>("id")
let value = Expression<String>("value")
let query = configTable.select(value).filter(id == key)
if let result = try db.pluck(query) {
let val = try result.get(value)
return val
if let result = try? db.pluck(query) {
return try? result.get(value)
}
return nil
} catch {
return nil
}
return nil
}
public func getLicense(_ serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let id = Expression<String>("id")
let value = Expression<String>("value")
let query = systemTable.select(value).filter(id == "license")
if let result = try? db.pluck(query) {
return try? result.get(value)
}
}
return nil
}
public func geConfigDisplayNameSetting(_ serverUrl: String) -> String? {
let licenseValue = getLicense(serverUrl)
guard let licenseData = licenseValue?.data(using: .utf8),
let license = try? JSONSerialization.jsonObject(with: licenseData) as? Dictionary<String,String>,
let lockDisplayName = getConfig(serverUrl, "LockTeammateNameDisplay")
else { return nil }
let displayName = getConfig(serverUrl, "TeammateNameDisplay") ?? "full_name"
let licenseLock = license["LockTeammateNameDisplay"] ?? "false"
if licenseLock == "true" && lockDisplayName == "true" {
return displayName
}
return nil
}
}

View File

@@ -2,13 +2,12 @@ import Foundation
import SQLite
extension Database {
internal func queryCurrentTeamId(_ serverUrl: String) -> String? {
public func queryCurrentTeamId(_ serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let valueCol = Expression<String>("value")
let query = systemTable.where(idCol == "currentTeamId")
if let result = try? db.pluck(query) {
if let result = try? db.pluck(systemTable.where(idCol == "currentTeamId")) {
return try? result.get(valueCol).replacingOccurrences(of: "\"", with: "")
}
}
@@ -16,7 +15,7 @@ extension Database {
return nil
}
public func queryTeamIdForChannel(withId channelId: String, withServerUrl serverUrl: String) -> String? {
public func queryTeamIdForChannel(withId channelId: String, forServerUrl serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let teamIdCol = Expression<String?>("team_id")
@@ -33,4 +32,106 @@ extension Database {
return nil
}
public func queryTeamExists(withId teamId: String, forServerUrl serverUrl: String) -> Bool {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let query = teamTable.where(idCol == teamId)
if let _ = try? db.pluck(query) {
return true
}
}
return false
}
public func queryMyTeamExists(withId teamId: String, forServerUrl serverUrl: String) -> Bool {
if 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 queryAllMyTeamIds(_ serverUrl: String) -> [String]? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
if let myTeams = try? db.prepare(myTeamTable.select(idCol)) {
return myTeams.map { try! $0.get(idCol) }
}
}
return nil
}
public func insertTeam(_ db: Connection, _ team: Team) throws {
let setter = createTeamSetter(from: team)
let insertQuery = teamTable.insert(or: .replace, setter)
try db.run(insertQuery)
}
public func insertMyTeam(_ db: Connection, _ member: TeamMember) throws {
let myTeam = createMyTeamSetter(from: member)
let teamMember = createTeamMemberSetter(from: member)
try db.run(myTeamTable.insert(or: .replace, myTeam))
try db.run(teamMembershipTable.insert(or: .replace, teamMember))
}
private func createTeamSetter(from team: Team) -> [Setter] {
let id = Expression<String>("id")
let isAllowOpenInvite = Expression<Bool>("is_allow_open_invite")
let updateAt = Expression<Double>("update_at")
let description = Expression<String>("description")
let displayName = Expression<String>("display_name")
let isGroupeConstrained = Expression<Bool>("is_group_constrained")
let lastTeamIconUpdatedAt = Expression<Double>("last_team_icon_updated_at")
let name = Expression<String>("name")
let type = Expression<String>("type")
let allowedDomains = Expression<String>("allowed_domains")
let inviteId = Expression<String>("invite_id")
let setter: [Setter] = [
id <- team.id,
isAllowOpenInvite <- team.allowOpenInvite,
updateAt <- team.updateAt,
description <- team.description,
displayName <- team.displayName,
isGroupeConstrained <- team.groupConstrained,
lastTeamIconUpdatedAt <- team.lastTeamIconUpdate,
name <- team.name,
type <- team.type,
allowedDomains <- team.allowedDomains,
inviteId <- team.inviteId,
]
return setter
}
private func createMyTeamSetter(from member: TeamMember) -> [Setter] {
let id = Expression<String>("id")
let roles = Expression<String>("roles")
var setter = [Setter]()
setter.append(id <- member.id)
setter.append(roles <- member.roles)
return setter
}
private func createTeamMemberSetter(from member: TeamMember) -> [Setter] {
let id = Expression<String>("id")
let teamId = Expression<String>("team_id")
let userId = Expression<String>("user_id")
let schemeAdmin = Expression<Bool>("scheme_admin")
let setter: [Setter] = [
id <- "\(member.id)-\(member.userId)",
teamId <- member.id,
userId <- member.userId,
schemeAdmin <- member.schemeAdmin,
]
return setter
}
}

View File

@@ -0,0 +1,188 @@
import Foundation
import SQLite
extension Database {
public func hasThread(_ db: Connection, threadId: String) -> Bool {
let idCol = Expression<String>("id")
let query = threadTable.where(idCol == threadId)
if let _ = try? db.pluck(query) {
return true
}
return false
}
public func getTeamThreadSync(_ db: Connection, teamId: String) -> Row? {
let idCol = Expression<String>("id")
let query = teamThreadsSyncTable.where(idCol == teamId)
return try? db.pluck(query)
}
public func handleThreads(_ db: Connection, _ threads: [PostThread], forTeamId teamId: String) throws {
var teamIds = [String]()
if teamId.isEmpty {
let idCol = Expression<String>("id")
if let myTeams = try? db.prepare(myTeamTable.select(idCol)) {
if let ids = try? myTeams.map({ try $0.get(idCol) }) {
teamIds.append(contentsOf: ids)
}
}
} else {
teamIds.append(teamId)
}
for thread in threads {
handleThread(db, thread, forTeamIds: teamIds)
}
handleTeamThreadSync(db, threads, forTeamIds: teamIds)
}
public func handleThread(_ db: Connection, _ thread: PostThread, forTeamIds teamIds: [String]) {
if hasThread(db, threadId: thread.id) {
try? updateThread(db, thread)
} else {
try? insertThread(db, thread)
}
try? syncParticipants(db, thread)
if thread.isFollowing {
for teamId in teamIds {
try? handleThreadInTeam(db, thread, forTeamId: teamId)
}
}
}
private func insertThread(_ db: Connection, _ thread: PostThread) throws {
let id = Expression<String>("id")
let isFollowing = Expression<Bool>("is_following")
let lastViewedAt = Expression<Double>("last_viewed_at")
let lastReplyAt = Expression<Double>("last_reply_at")
let unreadReplies = Expression<Int>("unread_replies")
let unreadMentions = Expression<Int>("unread_mentions")
let replyCount = Expression<Int>("reply_count")
let setter: [Setter] = [
id <- thread.id,
isFollowing <- thread.isFollowing,
lastViewedAt <- thread.lastViewedAt,
lastReplyAt <- thread.lastReplyAt,
unreadReplies <- thread.unreadReplies,
unreadMentions <- thread.unreadMentions,
replyCount <- thread.replyCount
]
let _ = try db.run(threadTable.insert(or: .replace, setter))
}
private func updateThread(_ db: Connection, _ thread: PostThread) throws {
let id = Expression<String>("id")
let isFollowing = Expression<Bool>("is_following")
let lastViewedAt = Expression<Double>("last_viewed_at")
let lastReplyAt = Expression<Double>("last_reply_at")
let unreadReplies = Expression<Int>("unread_replies")
let unreadMentions = Expression<Int>("unread_mentions")
let replyCount = Expression<Int>("reply_count")
let setter: [Setter] = [
isFollowing <- thread.isFollowing,
lastViewedAt <- thread.lastViewedAt,
lastReplyAt <- thread.lastReplyAt,
unreadReplies <- thread.unreadReplies,
unreadMentions <- thread.unreadMentions,
replyCount <- thread.replyCount
]
let _ = try db.run(threadTable.where(id == thread.id).update(setter))
}
private func syncParticipants(_ db: Connection, _ thread: PostThread) throws {
let threadIdCol = Expression<String>("thread_id")
let deletePreviousThreadParticipants = threadParticipantTable.where(threadIdCol == thread.id).delete()
try db.run(deletePreviousThreadParticipants)
let setters = createThreadParticipantSetters(from: thread)
if !setters.isEmpty {
try db.run(threadParticipantTable.insertMany(setters))
}
}
private func handleThreadInTeam(_ db: Connection, _ thread: PostThread, forTeamId teamId: String) throws {
let idCol = Expression<String>("id")
let threadIdCol = Expression<String>("thread_id")
let teamIdCol = Expression<String>("team_id")
let existing = try? db.pluck(threadsInTeamTable.where(threadIdCol == thread.id && teamIdCol == teamId))
if existing == nil {
let setter: [Setter] = [
idCol <- generateId(),
threadIdCol <- thread.id,
teamIdCol <- teamId,
]
let _ = try db.run(threadsInTeamTable.insert(or: .replace, setter))
}
}
private func handleTeamThreadSync(_ db: Connection, _ threads: [PostThread], forTeamIds teamIds: [String]) {
let sortedList = threads.filter({ $0.isFollowing }).sorted(by: { $0.lastReplyAt < $1.lastReplyAt}).map{ $0.lastReplyAt }
if let earliest = sortedList.first,
let latest = sortedList.last {
for teamId in teamIds {
if let existing = getTeamThreadSync(db, teamId: teamId) {
try? updateTeamThreadSync(db, forTeamId: teamId, starting: earliest, ending: latest, currentRow: existing)
} else {
try? insertTeamThreadSync(db, forTeamId: teamId, starting: earliest, ending: latest)
}
}
}
}
private func insertTeamThreadSync(_ db: Connection, forTeamId teamId: String, starting earliest: Double, ending latest: Double) throws {
let idCol = Expression<String>("id")
let earliestCol = Expression<Double>("earliest")
let latestCol = Expression<Double>("latest")
let setter: [Setter] = [
idCol <- teamId,
earliestCol <- earliest,
latestCol <- latest,
]
let _ = try db.run(teamThreadsSyncTable.insert(or: .replace, setter))
}
private func updateTeamThreadSync(_ db: Connection, forTeamId teamId: String, starting earliest: Double, ending latest: Double, currentRow existing: Row) throws {
let idCol = Expression<String>("id")
let earliestCol = Expression<Double>("earliest")
let latestCol = Expression<Double>("latest")
let existingEarliest = (try? existing.get(earliestCol)) ?? 0
let storeEarliest = min(earliest, existingEarliest)
let existingLatest = (try? existing.get(latestCol)) ?? 0
let storeLatest = max(latest, existingLatest)
let _ = try db.run(
teamThreadsSyncTable.where(idCol == teamId)
.update(earliestCol <- storeEarliest, latestCol <- storeLatest)
)
}
private func createThreadParticipantSetters(from thread: PostThread) -> [[Setter]] {
var participantSetters = [[Setter]]()
let id = Expression<String>("id")
let userId = Expression<String>("user_id")
let threadId = Expression<String>("thread_id")
for p in thread.participants {
var participantSetter = [Setter]()
participantSetter.append(id <- generateId() as String)
participantSetter.append(userId <- p.id)
participantSetter.append(threadId <- thread.id)
participantSetters.append(participantSetter)
}
return participantSetters
}
}

View File

@@ -9,90 +9,19 @@
import Foundation
import SQLite
public struct User: Codable, Hashable {
let id: String
let auth_service: String
let update_at: Double
let delete_at: Double
let email: String
let first_name: String
let is_bot: Bool
let is_guest: Bool
let last_name: String
let last_picture_update: Double
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.decodeIfPresent(Double.self, forKey: .update_at)) ?? 0
delete_at = (try? container.decodeIfPresent(Double.self, forKey: .delete_at)) ?? 0
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.decodeIfPresent(Double.self, forKey: .last_picture_update)) ?? 0
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 = "{}"
extension Database {
public func getUserFromRow(_ row: Row) -> User? {
do {
let decoder = row.decoder()
let _ = try decoder.container(keyedBy: User.UserKeys.self)
return try User(from: decoder)
} catch {
print(error.localizedDescription)
}
let timezoneData = try? container.decodeIfPresent([String: String].self, forKey: .timezone)
if (timezoneData != nil) {
timezone = Database.default.json(from: timezoneData) ?? "{}"
} else {
timezone = "{}"
}
return nil
}
}
extension Database {
public func queryCurrentUserId(_ serverUrl: String) throws -> String {
let db = try getDatabaseForServer(serverUrl)
@@ -120,8 +49,17 @@ extension Database {
throw DatabaseError.NoResults(query.expression.description)
}
public func getUserLastPictureAt(for userId: String, withServerUrl serverUrl: String) -> Double? {
let idCol = Expression<String>("id")
public func getCurrentUserLocale(_ serverUrl: String) -> String {
if let user = try? queryCurrentUser(serverUrl) {
if let locale = try? user.get(Expression<String>("locale")) {
return locale
}
}
return "en"
}
public func getUserLastPictureAt(for userId: String, forServerUrl serverUrl: String) -> Double? {
var updateAt: Double?
do {
let db = try getDatabaseForServer(serverUrl)
@@ -129,7 +67,7 @@ extension Database {
let stmtString = "SELECT * FROM User WHERE id='\(userId)'"
let results: [User] = try db.prepareRowIterator(stmtString).map {try $0.decode()}
updateAt = results.first?.last_picture_update
updateAt = results.first?.lastPictureUpdate
} catch {
return nil
@@ -138,51 +76,56 @@ extension Database {
return updateAt
}
public func queryUsers(byIds: Set<String>, withServerUrl: String) throws -> Set<String> {
let db = try getDatabaseForServer(withServerUrl)
public func queryUsers(byIds userIds: Set<String>, forServerUrl serverUrl: String) -> Set<String> {
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])
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
if let users = try? db.prepare(
userTable.select(idCol).filter(userIds.contains(idCol))
) {
for user in users {
result.insert(user[idCol])
}
}
}
return result
}
public func queryUsers(byUsernames: Set<String>, withServerUrl: String) throws -> Set<String> {
let db = try getDatabaseForServer(withServerUrl)
public func queryUsers(byUsernames usernames: Set<String>, forServerUrl serverUrl: String) -> Set<String> {
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])
if let db = try? getDatabaseForServer(serverUrl) {
let usernameCol = Expression<String>("username")
if let users = try? db.prepare(
userTable.select(usernameCol).filter(usernames.contains(usernameCol))
) {
for user in users {
result.insert(user[usernameCol])
}
}
}
return result
}
public func insertUsers(_ db: Connection, _ users: Set<User>) throws {
let setters = createUserSettedrs(from: users)
public func insertUsers(_ db: Connection, _ users: [User]) throws {
let setters = createUserSetters(from: users)
let insertQuery = userTable.insertMany(or: .replace, setters)
try db.run(insertQuery)
}
private func createUserSettedrs(from users: Set<User>) -> [[Setter]] {
private func createUserSetters(from users: [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 updateAt = Expression<Double>("update_at")
let deleteAt = Expression<Double>("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 lastPictureUpdate = Expression<Double>("last_picture_update")
let locale = Expression<String>("locale")
let nickname = Expression<String>("nickname")
let position = Expression<String>("position")
@@ -197,22 +140,22 @@ extension Database {
for user in users {
var setter = [Setter]()
setter.append(id <- user.id)
setter.append(authService <- user.auth_service)
setter.append(updateAt <- Int64(user.update_at))
setter.append(deleteAt <- Int64(user.delete_at))
setter.append(authService <- user.authService)
setter.append(updateAt <- user.updateAt)
setter.append(deleteAt <- user.deleteAt)
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 <- Int64(user.last_picture_update))
setter.append(firstName <- user.firstName)
setter.append(isBot <- user.isBot)
setter.append(isGuest <- user.isGuest)
setter.append(lastName <- user.lastName)
setter.append(lastPictureUpdate <- user.lastPictureUpdate)
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(notifyProps <- user.notifyProps)
setter.append(props <- user.props)
setter.append(timezone <- user.timezone)
setters.append(setter)

View File

@@ -43,6 +43,7 @@ public class Database: NSObject {
internal var systemTable = Table("System")
internal var teamTable = Table("Team")
internal var myTeamTable = Table("MyTeam")
internal var teamMembershipTable = Table("TeamMembership")
internal var channelTable = Table("Channel")
internal var channelInfoTable = Table("ChannelInfo")
internal var channelMembershipTable = Table("ChannelMembership")
@@ -58,7 +59,12 @@ public class Database: NSObject {
internal var userTable = Table("User")
internal var threadTable = Table("Thread")
internal var threadParticipantTable = Table("ThreadParticipant")
internal var threadsInTeamTable = Table("ThreadsInTeam")
internal var teamThreadsSyncTable = Table("TeamThreadsSync")
internal var configTable = Table("Config")
internal var preferenceTable = Table("Preference")
internal var categoryTable = Table("Category")
internal var categoryChannelTable = Table("CategoryChannel")
@objc public static let `default` = Database()

View File

@@ -0,0 +1,16 @@
import Foundation
extension String {
func removePrefix(_ prefix: String) -> String {
guard self.hasPrefix(prefix) else { return self }
return String(self.dropFirst(prefix.count))
}
func toUrlSafeBase64Encode() -> String {
return Data(
self.replacingOccurrences(of: "/\\+/g", with: "-", options: .regularExpression)
.replacingOccurrences(of: "/\\//g", with: "_", options: .regularExpression)
.utf8
).base64EncodedString()
}
}

View File

@@ -13,7 +13,14 @@ import Gekidou
@objc public static let `default` = GekidouWrapper()
@objc func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
Network.default.postNotificationReceipt(userInfo)
PushNotification.default.postNotificationReceipt(userInfo)
}
@objc func fetchDataForPushNotification(_ notification: [AnyHashable:Any], withContentHandler contentHander: @escaping ((_ data: Data?) -> Void)) {
PushNotification.default.fetchDataForPushNotification(notification, withContentHandler: { data in
let jsonData = try? JSONEncoder().encode(data)
contentHander(jsonData)
})
}
@objc func attachSession(_ id: String, completionHandler: @escaping () -> Void) {

View File

@@ -48,6 +48,7 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
}
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error: nil];
[[GekidouWrapper default] setPreference:@"true" forKey:@"ApplicationIsRunning"];
[RNNotifications startMonitorNotifications];
@@ -91,10 +92,22 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
// When rootId is nil, clear channel's root post notifications or else clear all thread notifications
[[NotificationHelper default] clearChannelOrThreadNotificationsWithUserInfo:userInfo];
[[GekidouWrapper default] postNotificationReceipt:userInfo];
[RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler];
return;
}
if (state != UIApplicationStateActive || isClearAction) {
[RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler];
if (state != UIApplicationStateActive) {
[[GekidouWrapper default] fetchDataForPushNotification:userInfo withContentHandler:^(NSData * _Nullable data) {
NSMutableDictionary *notification = [userInfo mutableCopy];
NSError *jsonError;
if (data != nil) {
id json = [NSJSONSerialization JSONObjectWithData:data options:NULL error:&jsonError];
if (!jsonError) {
[notification setObject:json forKey:@"data"];
}
}
[RNNotifications didReceiveBackgroundNotification:notification withCompletionHandler:completionHandler];
}];
} else {
completionHandler(UIBackgroundFetchResultNewData);
}
@@ -130,6 +143,7 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
-(void)applicationWillTerminate:(UIApplication *)application {
[[GekidouWrapper default] setPreference:@"false" forKey:@"ApplicationIsForeground"];
[[GekidouWrapper default] setPreference:@"false" forKey:@"ApplicationIsRunning"];
}
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {

View File

@@ -91,7 +91,7 @@ class ShareViewModel: ObservableObject {
return
}
Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: 0, withServerUrl: serverUrl, completionHandler: {data, response, error in
Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: 0, forServerUrl: serverUrl, completionHandler: {data, response, error in
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
debugPrint("Error while fetching image \(String(describing: (response as? HTTPURLResponse)?.statusCode))")
return

View File

@@ -4,12 +4,8 @@ import Intents
import os.log
class NotificationService: UNNotificationServiceExtension {
let preferences = Gekidou.Preferences.default
let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0]
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var retryIndex = 0
override init() {
super.init()
@@ -20,68 +16,66 @@ class NotificationService: UNNotificationServiceExtension {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent,
let jsonData = try? JSONSerialization.data(withJSONObject: bestAttemptContent.userInfo),
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
fetchReceipt(ackNotification)
} else {
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
sendMessageIntent(notification: request.content)
}
}
func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent) {
bestAttemptContent.userInfo["server_url"] = serverUrl
os_log(
OSLogType.default,
"Mattermost Notifications: process receipt response for serverUrl %{public}@",
serverUrl
)
let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any]
os_log(
OSLogType.default,
"Mattermost Notifications: parsed json response %{public}@",
String(describing: json != nil)
)
if let json = json {
if let message = json["message"] as? String {
bestAttemptContent.body = message
}
if let channelName = json["channel_name"] as? String {
bestAttemptContent.title = channelName
}
let userInfoKeys = ["channel_name", "team_id", "sender_id", "sender_name", "root_id", "override_username", "override_icon_url", "from_webhook", "message"]
for key in userInfoKeys {
if let value = json[key] as? String {
bestAttemptContent.userInfo[key] = value
if let bestAttemptContent = bestAttemptContent {
PushNotification.default.postNotificationReceipt(bestAttemptContent, completionHandler: {[weak self] notification in
if let notification = notification {
self?.bestAttemptContent = notification
if (Gekidou.Preferences.default.object(forKey: "ApplicationIsRunning") as? String != "true") {
PushNotification.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {notification in
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
self?.sendMessageIntent()
})
} else {
bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent")
self?.sendMessageIntent()
}
return
}
}
}
if (preferences.object(forKey: "ApplicationIsForeground") as? String != "true") {
Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {[weak self] notification in
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
self?.sendMessageIntent(notification: bestAttemptContent)
os_log(OSLogType.default, "Mattermost Notifications: notification receipt seems to be empty, will call sendMessageIntent")
self?.sendMessageIntent()
})
} else {
bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent")
sendMessageIntent(notification: bestAttemptContent)
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
sendMessageIntent()
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
os_log(OSLogType.default, "Mattermost Notifications: service extension time expired")
if let bestAttemptContent = bestAttemptContent {
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
sendMessageIntent(notification: bestAttemptContent)
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
sendMessageIntent()
}
private func sendMessageIntent() {
guard let notification = bestAttemptContent else { return }
if #available(iOSApplicationExtension 15.0, *) {
let overrideUsername = notification.userInfo["override_username"] as? String
let senderId = notification.userInfo["sender_id"] as? String
let sender = overrideUsername ?? senderId
guard let serverUrl = notification.userInfo["server_url"] as? String,
let sender = sender
else {
os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification")
self.contentHandler?(notification)
return
}
let overrideIconUrl = notification.userInfo["override_icon_url"] as? String
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, sender)
PushNotification.default.fetchProfileImageSync(serverUrl, senderId: sender, overrideIconUrl: overrideIconUrl) {[weak self] data in
self?.sendMessageIntentCompletion(data)
}
}
}
func sendMessageIntentCompletion(_ notification: UNNotificationContent, _ avatarData: Data?) {
private func sendMessageIntentCompletion(_ avatarData: Data?) {
guard let notification = bestAttemptContent else { return }
if #available(iOSApplicationExtension 15.0, *),
let imgData = avatarData,
let channelId = notification.userInfo["channel_id"] as? String {
@@ -139,79 +133,4 @@ class NotificationService: UNNotificationServiceExtension {
self.contentHandler?(notification)
}
}
func sendMessageIntent(notification: UNNotificationContent) {
if #available(iOSApplicationExtension 15.0, *) {
let overrideUsername = notification.userInfo["override_username"] as? String
let senderId = notification.userInfo["sender_id"] as? String
let sender = overrideUsername ?? senderId
guard let serverUrl = notification.userInfo["server_url"] as? String,
let sender = sender
else {
os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification")
self.contentHandler?(notification)
return
}
let overrideIconUrl = notification.userInfo["override_icon_url"] as? String
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, sender)
Network.default.fetchProfileImageSync(serverUrl, senderId: sender, overrideIconUrl: overrideIconUrl) {[weak self] data in
self?.sendMessageIntentCompletion(notification, data)
}
}
}
func fetchReceipt(_ ackNotification: AckNotification) -> Void {
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")
sendMessageIntent(notification: bestAttemptContent!)
return
}
Network.default.postNotificationReceipt(ackNotification) {data, response, error in
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
os_log(
OSLogType.default,
"Mattermost Notifications: notification receipt failed with status %{public}@. Will call sendMessageIntent",
String(describing: httpResponse.statusCode)
)
self.sendMessageIntent(notification: self.bestAttemptContent!)
return
}
guard let data = data, error == nil else {
if (ackNotification.isIdLoaded) {
// Receipt retrieval failed. Kick off retries.
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {
os_log(
OSLogType.default,
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
String(describing: self.retryIndex)
)
self.fetchReceipt(ackNotification)
})
self.retryIndex += 1
}
return
}
self.processResponse(serverUrl: ackNotification.serverUrl, data: data, bestAttemptContent: self.bestAttemptContent!)
}
}
}
extension Date {
var millisecondsSince1970: Int {
return Int((self.timeIntervalSince1970 * 1000.0).rounded())
}
init(milliseconds: Int) {
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
}
}