Horrible working prototype
This commit is contained in:
parent
379ed1bed1
commit
92c0da036d
8 changed files with 89 additions and 9 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
GoogleService-Info.plist
|
GoogleService-Info.plist
|
||||||
ntfy.xcodeproj/xcuserdata
|
ntfy.xcodeproj/xcuserdata
|
||||||
|
ntfy.xcodeproj/project.xcworkspace/xcuserdata/
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
9474F217283531A300CDE4DD /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; };
|
9474F217283531A300CDE4DD /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; };
|
||||||
94867143283EC9960093C7A4 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94867142283EC9950093C7A4 /* Actions.swift */; };
|
94867143283EC9960093C7A4 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94867142283EC9950093C7A4 /* Actions.swift */; };
|
||||||
94867144283ECD370093C7A4 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94867142283EC9950093C7A4 /* Actions.swift */; };
|
94867144283ECD370093C7A4 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94867142283EC9950093C7A4 /* Actions.swift */; };
|
||||||
|
94867145284058C60093C7A4 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F20E283326C500CDE4DD /* ApiService.swift */; };
|
||||||
94A3F7C8283734D900C48E79 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */; };
|
94A3F7C8283734D900C48E79 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */; };
|
||||||
94A3F7CA28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; };
|
94A3F7CA28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; };
|
||||||
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; };
|
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; };
|
||||||
|
|
@ -374,6 +375,7 @@
|
||||||
9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */,
|
9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */,
|
||||||
94CD196B283E666900973B93 /* EmojiManager.swift in Sources */,
|
94CD196B283E666900973B93 /* EmojiManager.swift in Sources */,
|
||||||
94867144283ECD370093C7A4 /* Actions.swift in Sources */,
|
94867144283ECD370093C7A4 /* Actions.swift in Sources */,
|
||||||
|
94867145284058C60093C7A4 /* ApiService.swift in Sources */,
|
||||||
9474F2052831D51500CDE4DD /* Store.swift in Sources */,
|
9474F2052831D51500CDE4DD /* Store.swift in Sources */,
|
||||||
9474F2062831D73C00CDE4DD /* ntfy.xcdatamodeld in Sources */,
|
9474F2062831D73C00CDE4DD /* ntfy.xcdatamodeld in Sources */,
|
||||||
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */,
|
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ struct Message: Decodable {
|
||||||
var tags: [String]?
|
var tags: [String]?
|
||||||
var actions: [Action]?
|
var actions: [Action]?
|
||||||
var click: String?
|
var click: String?
|
||||||
|
var pollId: String?
|
||||||
|
|
||||||
func toUserInfo() -> [AnyHashable: Any] {
|
func toUserInfo() -> [AnyHashable: Any] {
|
||||||
// This should mimic the way that the ntfy server encodes a message.
|
// This should mimic the way that the ntfy server encodes a message.
|
||||||
|
|
@ -85,7 +86,8 @@ struct Message: Decodable {
|
||||||
"priority": String(priority ?? 3),
|
"priority": String(priority ?? 3),
|
||||||
"tags": tags?.joined(separator: ",") ?? "",
|
"tags": tags?.joined(separator: ",") ?? "",
|
||||||
"actions": Actions.shared.encode(actions),
|
"actions": Actions.shared.encode(actions),
|
||||||
"click": click ?? ""
|
"click": click ?? "",
|
||||||
|
"poll_id": pollId ?? ""
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +105,7 @@ struct Message: Decodable {
|
||||||
let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
|
let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
|
||||||
let actions = userInfo["actions"] as? String
|
let actions = userInfo["actions"] as? String
|
||||||
let click = userInfo["click"] as? String
|
let click = userInfo["click"] as? String
|
||||||
|
let pollId = userInfo["poll_id"] as? String
|
||||||
return Message(
|
return Message(
|
||||||
id: id,
|
id: id,
|
||||||
time: timeInt,
|
time: timeInt,
|
||||||
|
|
@ -112,7 +115,8 @@ struct Message: Decodable {
|
||||||
priority: priority,
|
priority: priority,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
actions: Actions.shared.parse(actions),
|
actions: Actions.shared.parse(actions),
|
||||||
click: click
|
click: click,
|
||||||
|
pollId: pollId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ extension Subscription {
|
||||||
}
|
}
|
||||||
|
|
||||||
func topicName() -> String {
|
func topicName() -> String {
|
||||||
return topic ?? "<unknown>"
|
return topic ?? "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlHash() -> String {
|
||||||
|
return topicHash(baseUrl: baseUrl ?? "?", topic: topic ?? "?")
|
||||||
}
|
}
|
||||||
|
|
||||||
func notificationCount() -> Int {
|
func notificationCount() -> Int {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ struct SubscriptionManager {
|
||||||
|
|
||||||
func subscribe(baseUrl: String, topic: String) {
|
func subscribe(baseUrl: String, topic: String) {
|
||||||
Log.d(tag, "Subscribing to \(topicUrl(baseUrl: baseUrl, topic: topic))")
|
Log.d(tag, "Subscribing to \(topicUrl(baseUrl: baseUrl, topic: topic))")
|
||||||
Messaging.messaging().subscribe(toTopic: topic)
|
if baseUrl == Config.appBaseUrl {
|
||||||
|
Messaging.messaging().subscribe(toTopic: topic)
|
||||||
|
} else {
|
||||||
|
Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic))
|
||||||
|
}
|
||||||
let subscription = store.saveSubscription(baseUrl: baseUrl, topic: topic)
|
let subscription = store.saveSubscription(baseUrl: baseUrl, topic: topic)
|
||||||
poll(subscription)
|
poll(subscription)
|
||||||
}
|
}
|
||||||
|
|
@ -17,8 +21,12 @@ struct SubscriptionManager {
|
||||||
func unsubscribe(_ subscription: Subscription) {
|
func unsubscribe(_ subscription: Subscription) {
|
||||||
Log.d(tag, "Unsubscribing from \(subscription.urlString())")
|
Log.d(tag, "Unsubscribing from \(subscription.urlString())")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let topic = subscription.topic {
|
if let baseUrl = subscription.baseUrl, let topic = subscription.topic {
|
||||||
Messaging.messaging().unsubscribe(fromTopic: topic)
|
if baseUrl == Config.appBaseUrl {
|
||||||
|
Messaging.messaging().unsubscribe(fromTopic: topic)
|
||||||
|
} else {
|
||||||
|
Messaging.messaging().unsubscribe(fromTopic: topicHash(baseUrl: baseUrl, topic: topic))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
store.delete(subscription: subscription)
|
store.delete(subscription: subscription)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,34 @@ class ApiService {
|
||||||
static let shared = ApiService()
|
static let shared = ApiService()
|
||||||
|
|
||||||
func poll(subscription: Subscription, completionHandler: @escaping ([Message]?, Error?) -> Void) {
|
func poll(subscription: Subscription, completionHandler: @escaping ([Message]?, Error?) -> Void) {
|
||||||
guard let url = URL(string: subscription.urlString()) else { return }
|
guard let url = URL(string: subscription.urlString()) else {
|
||||||
|
// FIXME
|
||||||
|
return
|
||||||
|
}
|
||||||
let since = subscription.lastNotificationId ?? "all"
|
let since = subscription.lastNotificationId ?? "all"
|
||||||
let urlString = "\(url)/json?poll=1&since=\(since)"
|
let urlString = "\(url)/json?poll=1&since=\(since)"
|
||||||
|
|
||||||
Log.d(tag, "Polling from \(urlString)")
|
Log.d(tag, "Polling from \(urlString)")
|
||||||
fetchJsonData(urlString: urlString, completionHandler: completionHandler)
|
fetchJsonData(urlString: urlString, completionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func poll(subscription: Subscription, messageId: String, completionHandler: @escaping (Message?, Error?) -> Void) {
|
||||||
|
let url = URL(string: "\(subscription.urlString())/json?poll=1&id=\(messageId)")!
|
||||||
|
Log.d(tag, "Polling single message from \(url)")
|
||||||
|
|
||||||
|
URLSession.shared.dataTask(with: URLRequest(url: url)) { (data, response, error) in
|
||||||
|
if let error = error {
|
||||||
|
completionHandler(nil, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let message = try JSONDecoder().decode(Message.self, from: data!)
|
||||||
|
completionHandler(message, nil)
|
||||||
|
} catch {
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
func publish(
|
func publish(
|
||||||
subscription: Subscription,
|
subscription: Subscription,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
func topicUrl(baseUrl: String, topic: String) -> String {
|
func topicUrl(baseUrl: String, topic: String) -> String {
|
||||||
return "\(baseUrl)/\(topic)"
|
return "\(baseUrl)/\(topic)"
|
||||||
|
|
@ -10,6 +11,12 @@ func topicShortUrl(baseUrl: String, topic: String) -> String {
|
||||||
.replacingOccurrences(of: "https://", with: "")
|
.replacingOccurrences(of: "https://", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func topicHash(baseUrl: String, topic: String) -> String {
|
||||||
|
let data = Data(topicUrl(baseUrl: baseUrl, topic: topic).utf8)
|
||||||
|
let digest = SHA256.hash(data: data)
|
||||||
|
return digest.compactMap { String(format: "%02x", $0)}.joined()
|
||||||
|
}
|
||||||
|
|
||||||
func parseAllTags(_ tags: String?) -> [String] {
|
func parseAllTags(_ tags: String?) -> [String] {
|
||||||
return (tags?.components(separatedBy: ",") ?? [])
|
return (tags?.components(separatedBy: ",") ?? [])
|
||||||
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
/// This app extension is responsible for persisting the incoming notification to the data store (Core Data). It will eventually be the entity that
|
/// This app extension is responsible for persisting the incoming notification to the data store (Core Data). It will eventually be the entity that
|
||||||
/// fetches notification content from selfhosted servers (when a "poll request" is received). This is not implemented yet.
|
/// fetches notification content from selfhosted servers (when a "poll request" is received). This is not implemented yet.
|
||||||
|
|
@ -22,11 +23,37 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
if let bestAttemptContent = bestAttemptContent {
|
if let bestAttemptContent = bestAttemptContent {
|
||||||
let store = Store.shared
|
let store = Store.shared
|
||||||
let userInfo = bestAttemptContent.userInfo
|
let userInfo = bestAttemptContent.userInfo
|
||||||
|
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
|
||||||
|
let topic = userInfo["topic"] as? String ?? ""
|
||||||
guard let message = Message.from(userInfo: userInfo) else {
|
guard let message = Message.from(userInfo: userInfo) else {
|
||||||
Log.w(Store.tag, "Message cannot be parsed from userInfo", userInfo)
|
Log.w(Store.tag, "Message cannot be parsed from userInfo", userInfo)
|
||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if message.event == "poll_request" {
|
||||||
|
let subscription = store.getSubscriptions()?.first { $0.urlHash() == topic }
|
||||||
|
guard let subscription = subscription, let pollId = message.pollId else {
|
||||||
|
Log.w(tag, "Cannot find subscription", message)
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
ApiService.shared.poll(subscription: subscription, messageId: pollId) { message, error in
|
||||||
|
guard let message = message else {
|
||||||
|
Log.w(self.tag, "Error fetching message", error)
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bestAttemptContent.title = message.title ?? subscription.urlString()
|
||||||
|
bestAttemptContent.body = message.message ?? ""
|
||||||
|
contentHandler(bestAttemptContent)
|
||||||
|
//semaphore.signal()
|
||||||
|
}
|
||||||
|
//semaphore.wait(timeout: .distantFuture)
|
||||||
|
Thread.sleep(forTimeInterval: 5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if message.event != "message" {
|
if message.event != "message" {
|
||||||
Log.w(tag, "Irrelevant message received", message)
|
Log.w(tag, "Irrelevant message received", message)
|
||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
|
|
@ -34,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only handle "message" events
|
// Only handle "message" events
|
||||||
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
|
|
||||||
let topic = userInfo["topic"] as? String ?? ""
|
|
||||||
guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: topic) else {
|
guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: topic) else {
|
||||||
Log.w(tag, "Subscription for topic \(topic) unknown")
|
Log.w(tag, "Subscription for topic \(topic) unknown")
|
||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
|
|
@ -115,4 +140,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleMessage() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePollRequest() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue