Initial attachments, tested with picture only so far

This commit is contained in:
Andrew Cope 2022-02-20 12:18:40 -05:00
parent 954c773500
commit 545f0414ab
8 changed files with 283 additions and 6 deletions

View file

@ -31,12 +31,33 @@ class NotificationService: UNNotificationServiceExtension {
let notificationTopic = userInfo["topic"] as? String,
let notificationTimestamp = userInfo["time"] as? String,
let notiticationTimestampInt = Int64(notificationTimestamp),
let notificationTitle = userInfo["title"] as? String,
let notificationMessage = userInfo["message"] as? String {
print("Attempting to create notification")
let notificationTitle = userInfo["title"] as? String ?? ""
let notificationPriority = Int(userInfo["priority"] as? String ?? "")
let notificationTags = userInfo["tags"] as? String ?? ""
let attachmentName = userInfo["attachment_name"] as? String ?? "attachment.bin"
let attachmentType = userInfo["attachment_type"] as? String ?? ""
let attachmentSize = Int64(userInfo["attachment_size"] as? String ?? "0") ?? 0
let attachmentUrl = userInfo["attachment_url"] as? String ?? ""
let attachmentExpires = Int64(userInfo["attachment_expires"] as? String ?? "0") ?? 0
if let subscription = Database.current.getSubscription(topic: notificationTopic) {
var attachment: NtfyAttachment? = nil
if !attachmentUrl.isEmpty {
attachment = NtfyAttachment(
id: 0,
name: attachmentName,
type: attachmentType,
size: attachmentSize,
expires: attachmentExpires,
url: attachmentUrl,
contentUrl: ""
)
}
let ntfyNotification = NtfyNotification(
id: notificationId,
subscriptionId: subscription.id,
@ -44,7 +65,8 @@ class NotificationService: UNNotificationServiceExtension {
title: notificationTitle,
message: notificationMessage,
priority: Int(notificationPriority ?? 3),
tags: notificationTags.components(separatedBy: ",")
tags: notificationTags.components(separatedBy: ","),
attachment: attachment
)
ntfyNotification.save()
print("Created notification")

View file

@ -0,0 +1,85 @@
//
// NtfyAttachment.swift
// ntfy.sh
//
// Created by Andrew Cope on 2/20/22.
//
import Foundation
class NtfyAttachment {
var id: Int64!
var name: String
var type: String
var size: Int64
var expires: Int64
var url: String
var contentUrl: String
init(id: Int64, name: String, type: String = "", size: Int64 = 0, expires: Int64 = 0, url: String = "", contentUrl: String = "") {
self.id = id
self.name = name
self.type = type
self.size = size
self.expires = expires
self.url = url
self.contentUrl = contentUrl
}
func save() {
Database.current.updateAttachment(attachment: self)
}
func sizeString() -> String {
if (self.size < 1000) { return "\(self.size) B" }
let exp = Int(log2(Double(self.size)) / log2(1000.0))
let unit = ["KB", "MB", "GB", "TB", "PB", "EB"][exp - 1]
let number = Double(self.size) / pow(1000, Double(exp))
return String(format: "%.1f %@", number, unit)
}
func isDownloaded() -> Bool {
return !contentUrl.isEmpty
}
func expiresString() -> String {
return "Expires \(self.expires)"
}
func download() {
print("Attempting attachment download")
guard let attachmentUrl = URL(string: self.url) else { return }
print("Attachment URL: \(attachmentUrl)")
URLSession.shared.downloadTask(with: attachmentUrl) { (data, response, error) in
print("Attachment download complete")
print("Response: \(response)")
print("Data: \(data)")
if let error = error {
print("Download attachment error: \(error)")
return
}
if let response = response,
let data = data {
let fileManager = FileManager.default
// Get the App Group path, which is accessed by both the app and the notification service extension
if let path = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.ntfy") {
guard let fileUrl = URL(string: "\(path)/downloads/\(response.hash)") else { return }
do {
let parentPath = fileUrl.deletingLastPathComponent()
if !fileManager.fileExists(atPath: parentPath.path) {
try fileManager.createDirectory(atPath: parentPath.path, withIntermediateDirectories: true, attributes: nil)
}
try fileManager.moveItem(at: data.absoluteURL, to: fileUrl)
self.contentUrl = fileUrl.path
self.save()
print(self.contentUrl)
print("Attachment saved to \(fileUrl.path)")
} catch {
print("Error saving attachment: \(error)")
}
}
}
}.resume()
}
}

View file

@ -17,12 +17,13 @@ class NtfyNotification: Identifiable, Decodable {
var message: String
var priority: Int
var tags: [String]
var attachment: NtfyAttachment?
// Object Properties
var emojiTags: [String] = []
var nonEmojiTags: [String] = []
init(id: String, subscriptionId: Int64, timestamp: Int64, title: String, message: String, priority: Int = 3, tags: [String] = []) {
init(id: String, subscriptionId: Int64, timestamp: Int64, title: String, message: String, priority: Int = 3, tags: [String] = [], attachment: NtfyAttachment?) {
// Initialize values
self.id = id
self.subscriptionId = subscriptionId
@ -31,6 +32,7 @@ class NtfyNotification: Identifiable, Decodable {
self.message = message
self.priority = priority
self.tags = tags
self.attachment = attachment
// Set notification tags
self.setTags()

View file

@ -7,7 +7,7 @@
import Foundation
class ApiService {
class ApiService: NSObject {
static let shared = ApiService()
func poll(subscription: NtfySubscription, completionHandler: @escaping ([NtfyNotification]?, Error?) -> Void) {
@ -32,6 +32,20 @@ class ApiService {
}.resume()
}
/*func checkAuth(baseUrl: String, topic: String, user: NtfyUser?) -> Bool {
guard let url = URL(string: "\(baseUrl)/\(topic)/auth") else { return false }
var request = URLRequest(url: url)
if user != nil {
let credential = URLCredential(user: user!.username, password: user!.password, persistence: URLCredential.Persistence.none)
request
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
}
return false
}*/
private func fetchJsonData<T: Decodable>(urlString: String, completionHandler: @escaping ([T]?, Error?) -> ()) {
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in

View file

@ -35,6 +35,17 @@ class Database {
let notification_message = Expression<String>("message")
let notification_priority = Expression<Int>("priority")
let notification_tags = Expression<String>("tags")
let notification_attachment_id = Expression<Int64>("attachmentId")
// Attachments Table
let attachments = Table("Attachments")
var attachment_id = Expression<Int64>("id")
let attachment_name = Expression<String>("name")
let attachment_type = Expression<String>("type")
let attachment_size = Expression<Int64>("size")
let attachment_expires = Expression<Int64>("expires")
let attachment_url = Expression<String>("url")
let attachment_content_url = Expression<String>("contentUrl")
// Initialize
init() {
@ -61,6 +72,18 @@ class Database {
table.column(notification_message)
table.column(notification_priority)
table.column(notification_tags)
table.column(notification_attachment_id)
})
// Initialize Attachments Table
try db?.run(attachments.create(ifNotExists: true) { table in
table.column(attachment_id, primaryKey: .autoincrement)
table.column(attachment_name)
table.column(attachment_type)
table.column(attachment_size)
table.column(attachment_expires)
table.column(attachment_url)
table.column(attachment_content_url)
})
}
} catch {
@ -149,6 +172,21 @@ class Database {
}
if let result = try db?.prepare(query) {
for line in result {
var attachment: NtfyAttachment? = nil
let attachmentId = try line.get(notification_attachment_id)
if attachmentId != 0 {
if let attachmentResult = try db?.pluck(attachments.filter(attachment_id == attachmentId)) {
attachment = NtfyAttachment(
id: try attachmentResult.get(attachment_id),
name: try attachmentResult.get(attachment_name),
type: try attachmentResult.get(attachment_type),
size: try attachmentResult.get(attachment_size),
expires: try attachmentResult.get(attachment_expires),
url: try attachmentResult.get(attachment_url),
contentUrl: try attachmentResult.get(attachment_content_url)
)
}
}
list.append(
NtfyNotification(
id: try line.get(notification_id),
@ -157,7 +195,8 @@ class Database {
title: try line.get(notification_title),
message: try line.get(notification_message),
priority: try line.get(notification_priority),
tags: try line.get(notification_tags).components(separatedBy: ",")
tags: try line.get(notification_tags).components(separatedBy: ","),
attachment: attachment
)
)
}
@ -171,6 +210,10 @@ class Database {
func addNotification(notification: NtfyNotification) -> NtfyNotification {
do {
var attachmentId: Int64 = 0
if notification.attachment != nil {
attachmentId = addAttachment(attachment: notification.attachment!) ?? 0
}
try db?.run(notifications.insert(
notification_id <- notification.id,
notification_subscription_id <- notification.subscriptionId,
@ -178,7 +221,8 @@ class Database {
notification_title <- notification.title,
notification_message <- notification.message,
notification_priority <- notification.priority,
notification_tags <- notification.tags.joined(separator: ",")
notification_tags <- notification.tags.joined(separator: ","),
notification_attachment_id <- attachmentId
))
} catch let Result.error(message, code, _) where code == SQLITE_CONSTRAINT {
// Likely means that the notification already exists
@ -220,4 +264,31 @@ class Database {
print(error.localizedDescription)
}
}
func addAttachment(attachment: NtfyAttachment) -> Int64? {
do {
return try db?.run(attachments.insert(
attachment_name <- attachment.name,
attachment_type <- attachment.type,
attachment_size <- attachment.size,
attachment_expires <- attachment.expires,
attachment_url <- attachment.url,
attachment_content_url <- attachment.contentUrl
))
} catch {
print("Error saving attachment: \(error)")
return nil
}
}
func updateAttachment(attachment: NtfyAttachment) {
do {
let dbAttachment = attachments.filter(attachment_id == attachment.id)
try db?.run(dbAttachment.update(
attachment_content_url <- attachment.contentUrl
))
} catch {
print("Error updating attachment: \(error)")
}
}
}

View file

@ -0,0 +1,62 @@
//
// NotificationAttachmentView.swift
// ntfy.sh
//
// Created by Andrew Cope on 2/20/22.
//
import Foundation
import SwiftUI
import UIKit
struct NotificationAttachmentView: View {
let attachment: NtfyAttachment
@State private var isAttachmentOpen = false
var body: some View {
if attachment.isDownloaded() {
if let imageUrl = UIImage(contentsOfFile: attachment.contentUrl) {
let image = Image(uiImage: imageUrl)
.resizable()
.scaledToFit()
ZStack {
image
}
.onTapGesture {
isAttachmentOpen.toggle()
}
.fullScreenCover(isPresented: $isAttachmentOpen, onDismiss: {
// Dismiss logic here
}, content: {
VStack {
image
}
.onTapGesture {
isAttachmentOpen.toggle()
}
})
}
} else {
HStack {
// TODO: Replace paperclip here with mimetype icon
Image(systemName: "paperclip")
VStack(alignment: .leading) {
Text(attachment.name)
.font(.footnote)
HStack {
Text(attachment.sizeString())
.font(.footnote)
.foregroundColor(.gray)
Text("Not downloaded")
.font(.footnote)
.foregroundColor(.gray)
Text(attachment.expiresString())
.font(.footnote)
.foregroundColor(.gray)
}
}
}
}
}
}

View file

@ -42,7 +42,18 @@ struct NotificationRow: View {
.font(.subheadline)
.foregroundColor(.gray)
}
if let attachment = notification.attachment {
Spacer()
NotificationAttachmentView(attachment: attachment)
}
}
.padding(.all, 4)
.onTapGesture {
if let attachment = notification.attachment {
if !attachment.isDownloaded() {
attachment.download()
}
}
}
}
}

View file

@ -23,6 +23,9 @@
80386E9927935CC9009B0480 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 80386E9827935CC9009B0480 /* FirebaseMessaging */; };
80386E9F27936087009B0480 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80386E9E27936087009B0480 /* AppDelegate.swift */; };
80386EA92793A7AC009B0480 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 80386EA82793A7AC009B0480 /* SQLite */; };
8079FF4C27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */; };
8079FF4D27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */; };
8079FF4F27C29D3300FB3D18 /* NotificationAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079FF4E27C29D3300FB3D18 /* NotificationAttachmentView.swift */; };
80856C7F27BDE0A7008AC8B8 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80856C7E27BDE0A7008AC8B8 /* ApiService.swift */; };
80856C8027BDE0A7008AC8B8 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80856C7E27BDE0A7008AC8B8 /* ApiService.swift */; };
8086EB242794630800C3628A /* AddSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8086EB232794630800C3628A /* AddSubscriptionView.swift */; };
@ -80,6 +83,8 @@
80386E9C27935EE9009B0480 /* ntfy-sh-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ntfy-sh-Info.plist"; sourceTree = "<group>"; };
80386E9E27936087009B0480 /* AppDelegate.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; tabWidth = 4; usesTabs = 0; };
80386EA0279363A2009B0480 /* ntfy.sh.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ntfy.sh.entitlements; sourceTree = "<group>"; };
8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfyAttachment.swift; sourceTree = "<group>"; };
8079FF4E27C29D3300FB3D18 /* NotificationAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAttachmentView.swift; sourceTree = "<group>"; };
8081A7F427B4CB67004A8986 /* FEATURE_PARITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FEATURE_PARITY.md; sourceTree = "<group>"; };
80856C7E27BDE0A7008AC8B8 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = "<group>"; };
8086EB232794630800C3628A /* AddSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSubscriptionView.swift; sourceTree = "<group>"; };
@ -216,6 +221,7 @@
80A313FC2793C42000F1A639 /* NotificationRow.swift */,
8086EB232794630800C3628A /* AddSubscriptionView.swift */,
8086EB2527946FCE00C3628A /* ContentView.swift */,
8079FF4E27C29D3300FB3D18 /* NotificationAttachmentView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -233,6 +239,7 @@
children = (
80A313F42793B1CF00F1A639 /* NtfySubscription.swift */,
80A313F62793B56800F1A639 /* NtfyNotification.swift */,
8079FF4B27C2874A00FB3D18 /* NtfyAttachment.swift */,
);
path = Models;
sourceTree = "<group>";
@ -353,6 +360,7 @@
buildActionMask = 2147483647;
files = (
802D626C27B1A37700DDD3AF /* Database.swift in Sources */,
8079FF4D27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */,
80ED61BF27BCA36A00FCEA36 /* Configuration.swift in Sources */,
802D626D27B1A37900DDD3AF /* NtfyNotification.swift in Sources */,
800FA49627B19CA0005D05B9 /* NotificationService.swift in Sources */,
@ -375,8 +383,10 @@
8086EB242794630800C3628A /* AddSubscriptionView.swift in Sources */,
80A313FD2793C42000F1A639 /* NotificationRow.swift in Sources */,
80A313FB2793C2EA00F1A639 /* SubscriptionDetail.swift in Sources */,
8079FF4C27C2874A00FB3D18 /* NtfyAttachment.swift in Sources */,
80A313F92793C0D800F1A639 /* SubscriptionRow.swift in Sources */,
80386E802793585B009B0480 /* AppMain.swift in Sources */,
8079FF4F27C29D3300FB3D18 /* NotificationAttachmentView.swift in Sources */,
80A313F72793B56800F1A639 /* NtfyNotification.swift in Sources */,
80856C7F27BDE0A7008AC8B8 /* ApiService.swift in Sources */,
80A313F52793B1CF00F1A639 /* NtfySubscription.swift in Sources */,