Horrible working prototype

This commit is contained in:
Philipp Heckel 2022-05-27 20:45:17 -04:00
parent 379ed1bed1
commit 92c0da036d
8 changed files with 89 additions and 9 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.DS_Store
GoogleService-Info.plist
ntfy.xcodeproj/xcuserdata
ntfy.xcodeproj/project.xcworkspace/xcuserdata/

View file

@ -34,6 +34,7 @@
9474F217283531A300CDE4DD /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; };
94867143283EC9960093C7A4 /* 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 */; };
94A3F7CA28386B2100C48E79 /* 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 */,
94CD196B283E666900973B93 /* EmojiManager.swift in Sources */,
94867144283ECD370093C7A4 /* Actions.swift in Sources */,
94867145284058C60093C7A4 /* ApiService.swift in Sources */,
9474F2052831D51500CDE4DD /* Store.swift in Sources */,
9474F2062831D73C00CDE4DD /* ntfy.xcdatamodeld in Sources */,
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */,

View file

@ -71,6 +71,7 @@ struct Message: Decodable {
var tags: [String]?
var actions: [Action]?
var click: String?
var pollId: String?
func toUserInfo() -> [AnyHashable: Any] {
// This should mimic the way that the ntfy server encodes a message.
@ -85,7 +86,8 @@ struct Message: Decodable {
"priority": String(priority ?? 3),
"tags": tags?.joined(separator: ",") ?? "",
"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 actions = userInfo["actions"] as? String
let click = userInfo["click"] as? String
let pollId = userInfo["poll_id"] as? String
return Message(
id: id,
time: timeInt,
@ -112,7 +115,8 @@ struct Message: Decodable {
priority: priority,
tags: tags,
actions: Actions.shared.parse(actions),
click: click
click: click,
pollId: pollId
)
}
}

View file

@ -10,7 +10,11 @@ extension Subscription {
}
func topicName() -> String {
return topic ?? "<unknown>"
return topic ?? "?"
}
func urlHash() -> String {
return topicHash(baseUrl: baseUrl ?? "?", topic: topic ?? "?")
}
func notificationCount() -> Int {

View file

@ -9,7 +9,11 @@ struct SubscriptionManager {
func subscribe(baseUrl: String, topic: String) {
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)
poll(subscription)
}
@ -17,8 +21,12 @@ struct SubscriptionManager {
func unsubscribe(_ subscription: Subscription) {
Log.d(tag, "Unsubscribing from \(subscription.urlString())")
DispatchQueue.main.async {
if let topic = subscription.topic {
Messaging.messaging().unsubscribe(fromTopic: topic)
if let baseUrl = subscription.baseUrl, let topic = subscription.topic {
if baseUrl == Config.appBaseUrl {
Messaging.messaging().unsubscribe(fromTopic: topic)
} else {
Messaging.messaging().unsubscribe(fromTopic: topicHash(baseUrl: baseUrl, topic: topic))
}
}
store.delete(subscription: subscription)
}

View file

@ -5,7 +5,10 @@ class ApiService {
static let shared = ApiService()
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 urlString = "\(url)/json?poll=1&since=\(since)"
@ -13,6 +16,24 @@ class ApiService {
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(
subscription: Subscription,
message: String,

View file

@ -1,4 +1,5 @@
import Foundation
import CryptoKit
func topicUrl(baseUrl: String, topic: String) -> String {
return "\(baseUrl)/\(topic)"
@ -10,6 +11,12 @@ func topicShortUrl(baseUrl: String, topic: String) -> String {
.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] {
return (tags?.components(separatedBy: ",") ?? [])
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }

View file

@ -1,5 +1,6 @@
import UserNotifications
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
/// 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 {
let store = Store.shared
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 {
Log.w(Store.tag, "Message cannot be parsed from userInfo", userInfo)
contentHandler(request.content)
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" {
Log.w(tag, "Irrelevant message received", message)
contentHandler(request.content)
@ -34,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension {
}
// 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 {
Log.w(tag, "Subscription for topic \(topic) unknown")
contentHandler(request.content)
@ -115,4 +140,12 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(bestAttemptContent)
}
}
func handleMessage() {
}
func handlePollRequest() {
}
}