Skip to content

Commit

Permalink
Merge pull request #1207 from planetary-social/bugfix/upload
Browse files Browse the repository at this point in the history
Fix uploading photos and videos to nostr.build
  • Loading branch information
joshuatbrown authored May 31, 2024
2 parents 3ccf8d9 + 57950dd commit 6572ce2
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 24 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Added feedback to the copy button in Settings
- Fixed a crash on logout
- Added feedback to the copy button in Settings.
- Fixed an issue where photos and videos could not be uploaded.
- Fixed a crash on logout.

## [0.1.15] - 2024-05-29Z

Expand Down
34 changes: 26 additions & 8 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
035729BD2BE416BD005FEE85 /* EventObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035729BB2BE416BD005FEE85 /* EventObservationTests.swift */; };
035729CA2BE4173E005FEE85 /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94BC09A2A0AC74A0098F6F1 /* PreviewData.swift */; };
035729CB2BE41770005FEE85 /* ContentWarningController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0357299A2BE415E5005FEE85 /* ContentWarningController.swift */; };
0373CE862C090C070027C856 /* NostrBuildResponseJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373CE852C090C070027C856 /* NostrBuildResponseJSON.swift */; };
0373CE8C2C090C8A0027C856 /* nostr_build_response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0373CE8B2C090C8A0027C856 /* nostr_build_response.json */; };
0373CE8D2C090D110027C856 /* NostrBuildResponseJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373CE852C090C070027C856 /* NostrBuildResponseJSON.swift */; };
0373CE8F2C090D4D0027C856 /* NostrBuildResponseJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373CE8E2C090D4D0027C856 /* NostrBuildResponseJSONTests.swift */; };
0373CE992C0910250027C856 /* XCTestCase+JSONData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373CE982C0910250027C856 /* XCTestCase+JSONData.swift */; };
0378409D2BB4A2B600E5E901 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0378409C2BB4A2B600E5E901 /* PrivacyInfo.xcprivacy */; };
2D06BB9D2AE249D70085F509 /* ThreadRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */; };
2D4010A22AD87DF300F93AD4 /* KnownFollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */; };
Expand Down Expand Up @@ -473,6 +478,10 @@
035729B52BE416A6005FEE85 /* GiftWrapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiftWrapperTests.swift; sourceTree = "<group>"; };
035729B62BE416A6005FEE85 /* ReportPublisherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportPublisherTests.swift; sourceTree = "<group>"; };
035729BB2BE416BD005FEE85 /* EventObservationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventObservationTests.swift; sourceTree = "<group>"; };
0373CE852C090C070027C856 /* NostrBuildResponseJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrBuildResponseJSON.swift; sourceTree = "<group>"; };
0373CE8B2C090C8A0027C856 /* nostr_build_response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nostr_build_response.json; sourceTree = "<group>"; };
0373CE8E2C090D4D0027C856 /* NostrBuildResponseJSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrBuildResponseJSONTests.swift; sourceTree = "<group>"; };
0373CE982C0910250027C856 /* XCTestCase+JSONData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+JSONData.swift"; sourceTree = "<group>"; };
0378409C2BB4A2B600E5E901 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
03AB2F7D2BF6609500B73DB1 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadRootView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -857,6 +866,7 @@
035729A32BE4167E005FEE85 /* EventTests.swift */,
035729A42BE4167E005FEE85 /* FollowTests.swift */,
035729A52BE4167E005FEE85 /* KeyPairTests.swift */,
0373CE8E2C090D4D0027C856 /* NostrBuildResponseJSONTests.swift */,
035729A62BE4167E005FEE85 /* NoteParserTests.swift */,
035729A72BE4167E005FEE85 /* ReportTests.swift */,
035729A82BE4167E005FEE85 /* SHA256KeyTests.swift */,
Expand Down Expand Up @@ -1054,15 +1064,17 @@
C97797B7298AA1600046BD25 /* Service */ = {
isa = PBXGroup;
children = (
65D066982BD558690011C5CD /* DirectMessageWrapper.swift */,
C9646EA029B7A22C007239A4 /* Analytics.swift */,
C9C2B77E29E0731600548B4A /* AsyncTimer.swift */,
C9ADB13C29929B540075E7F8 /* Bech32.swift */,
C9B71DC12A9003670031ED9F /* CrashReporting.swift */,
A34E439829A522F20057AFCB /* CurrentUser.swift */,
C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */,
C9B678DA29EEBF3B00303F33 /* DependencyInjection.swift */,
65D066982BD558690011C5CD /* DirectMessageWrapper.swift */,
C9BAB09A2996FBA10003A84E /* EventProcessor.swift */,
DC5F20422A6ED75C00F8D73F /* FileStorageAPI.swift */,
C959DB752BD01DF4008F3627 /* GiftWrapper.swift */,
A3B943CE299AE00100A15A08 /* KeyChain.swift */,
C9F64D8B29ED840700563F2B /* LogHelper.swift */,
5BC0D9CB2B867B9D005D6980 /* NamesAPI.swift */,
Expand All @@ -1073,7 +1085,6 @@
5B8B77182A1FDA3C004FC675 /* TLV.swift */,
C9A0DAF729C92F4500466635 /* UNSAPI.swift */,
C98CA9052B14FA8500929141 /* Relay */,
C959DB752BD01DF4008F3627 /* GiftWrapper.swift */,
);
path = Service;
sourceTree = "<group>";
Expand Down Expand Up @@ -1125,9 +1136,10 @@
isa = PBXGroup;
children = (
C9FC1E622B61ACE300A3A6FB /* CoreDataTestCase.swift */,
C9BD919A2B61C4FB00FDA083 /* RawNostrID+Random.swift */,
C91400232B2A3894009B13B4 /* SQLiteStoreTestCase.swift */,
C9C9444129F6F0E2002F2C7A /* XCTest+Eventually.swift */,
C9BD919A2B61C4FB00FDA083 /* RawNostrID+Random.swift */,
0373CE982C0910250027C856 /* XCTestCase+JSONData.swift */,
);
path = "Test Helpers";
sourceTree = "<group>";
Expand Down Expand Up @@ -1260,6 +1272,7 @@
isa = PBXGroup;
children = (
C9BD91882B61BBEF00FDA083 /* bad_contact_list.json */,
0373CE8B2C090C8A0027C856 /* nostr_build_response.json */,
C9DEC005298947900078B43A /* sample_data.json */,
CD27177529A7C8B200AE8888 /* sample_replies.json */,
C94B2D172B17F5EC002104B6 /* sample_repost.json */,
Expand All @@ -1279,20 +1292,21 @@
5B503F612A291A1A0098805A /* JSONRelayMetadata.swift */,
C9F84C26298DC98800C6714D /* KeyPair.swift */,
C930055E2A6AF8320098CA9E /* LoadingContent.swift */,
0373CE852C090C070027C856 /* NostrBuildResponseJSON.swift */,
C9F0BB6E29A50437000547FC /* NostrConstants.swift */,
5B6EB48D29EDBE0E006E750C /* NoteParser.swift */,
5BFBB28A2BD9D79F002E909F /* URLParser.swift */,
C9AC31AC2A55E0BD00A94E5A /* NotificationViewModel.swift */,
C9CF23162A38A58B00EBEC31 /* ParseQueue.swift */,
C9F0BB6A29A503D6000547FC /* PublicKey.swift */,
C96D39262B61B6D200D3D0A1 /* RawNostrID.swift */,
C9C2B78429E073E300548B4A /* RelaySubscription.swift */,
C9E37E142A1E8143003D4B0A /* ReportTarget.swift */,
C94A5E172A72C84200B6EC5D /* ReportCategory.swift */,
C9E37E142A1E8143003D4B0A /* ReportTarget.swift */,
C992B3292B3613CC00704A9C /* SubscriptionCancellable.swift */,
5BFBB28A2BD9D79F002E909F /* URLParser.swift */,
659B27232BD9CB4500BEA6CC /* VerifiableEvent.swift */,
030742C32B4769F90073839D /* CoreData */,
C936B4572A4C7B7C00DF1EB9 /* Nos.xcdatamodeld */,
C96D39262B61B6D200D3D0A1 /* RawNostrID.swift */,
659B27232BD9CB4500BEA6CC /* VerifiableEvent.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -1347,7 +1361,6 @@
C9F75AD52A041FF7005BBE45 /* ExpirationTimePicker.swift */,
C9EE3E5F2A0538B7008A7491 /* ExpirationTimeButton.swift */,
DC5F203E2A6AE24200F8D73F /* ImagePickerButton.swift */,
DC5F20422A6ED75C00F8D73F /* FileStorageAPI.swift */,
C93F045D2B9B7A7000AD5872 /* ReplyPreview.swift */,
C9B597642BBC8300002EC76A /* ImagePickerUIViewController.swift */,
);
Expand Down Expand Up @@ -1673,6 +1686,7 @@
C9DEC006298947900078B43A /* sample_data.json in Resources */,
C94B2D182B17F5EC002104B6 /* sample_repost.json in Resources */,
C987F84929BA951E00B44E7A /* ClarityCity-BlackItalic.otf in Resources */,
0373CE8C2C090C8A0027C856 /* nostr_build_response.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1792,6 +1806,7 @@
C9E8C1132B081E9C002D46B0 /* UNSNameView.swift in Sources */,
A351E1A229BA92240009B7F6 /* ProfileEditView.swift in Sources */,
C9DFA969299BEC33006929C1 /* CardStyle.swift in Sources */,
0373CE862C090C070027C856 /* NostrBuildResponseJSON.swift in Sources */,
C95D68AD299E721700429F86 /* ProfileView.swift in Sources */,
C942566929B66A2800C4202C /* Date+Elapsed.swift in Sources */,
5B8C96B029DB2E1100B73AEC /* SearchTextFieldObserver.swift in Sources */,
Expand Down Expand Up @@ -1949,6 +1964,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0373CE8D2C090D110027C856 /* NostrBuildResponseJSON.swift in Sources */,
0320C1152BFE63DC00C4C080 /* MockRelaySubscriptionManager.swift in Sources */,
035729CB2BE41770005FEE85 /* ContentWarningController.swift in Sources */,
CD09A76229A5220E0063464F /* AppController.swift in Sources */,
Expand Down Expand Up @@ -1990,6 +2006,7 @@
5B098DC72BDAF77400500A1B /* AttributedString+Links.swift in Sources */,
035729B02BE4167E005FEE85 /* NoteParserTests.swift in Sources */,
C9FC1E632B61ACE300A3A6FB /* CoreDataTestCase.swift in Sources */,
0373CE992C0910250027C856 /* XCTestCase+JSONData.swift in Sources */,
C9E37E162A1E8143003D4B0A /* ReportTarget.swift in Sources */,
035729BA2BE416A6005FEE85 /* ReportPublisherTests.swift in Sources */,
0320C0FB2BFE43A600C4C080 /* RelayServiceTests.swift in Sources */,
Expand Down Expand Up @@ -2050,6 +2067,7 @@
035729AE2BE4167E005FEE85 /* FollowTests.swift in Sources */,
5BFBB2952BD9D7EB002E909F /* URLParserTests.swift in Sources */,
C91400252B2A3ABF009B13B4 /* SQLiteStoreTestCase.swift in Sources */,
0373CE8F2C090D4D0027C856 /* NostrBuildResponseJSONTests.swift in Sources */,
C9DEC04E29894BED0078B43A /* Author+CoreDataClass.swift in Sources */,
C9ADB14229951CB10075E7F8 /* NSManagedObject+Nos.swift in Sources */,
035729AB2BE4167E005FEE85 /* AuthorTests.swift in Sources */,
Expand Down
31 changes: 31 additions & 0 deletions Nos/Models/NostrBuildResponseJSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

/// The response JSON that's returned from the nostr.build API.
struct NostrBuildResponseJSON: Codable {
let status: NostrBuildResponseStatus?
let message: String?
let data: [NostrBuildResponseDataItem]?
}

enum NostrBuildResponseStatus: String, Codable {
case success
case unknown

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let statusString = try container.decode(String.self)
switch statusString {
case "success":
self = .success
default:
self = .unknown
}
}
}

struct NostrBuildResponseDataItem: Codable {
let url: String?
let type: String?
let mime: String?
let thumbnail: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ protocol FileStorageAPI {
}

class NostrBuildFileStorageAPI: FileStorageAPI {

static let uploadURL = URL(string: "https://nostr.build/api/upload/ios.php")!
static let uploadURL = URL(string: "https://nostr.build/api/v2/upload/files")!
static let paramName = "fileToUpload"


var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}

func upload(fileAt fileURL: URL) async throws -> URL {
let fileName = fileURL.lastPathComponent
let fileData = try Data(contentsOf: fileURL)
Expand Down Expand Up @@ -37,29 +42,37 @@ class NostrBuildFileStorageAPI: FileStorageAPI {
data.append(trailerData)

let (responseData, _) = try await URLSession.shared.upload(for: request, from: data)
let responseString = try JSONSerialization.jsonObject(with: responseData, options: .allowFragments) as? String
guard let responseString else {
throw FileStorageAPIError.unexpectedOutputType
}
guard let url = URL(string: responseString) else {
throw FileStorageAPIError.uploadFailed(responseString)

do {
let response = try decoder.decode(NostrBuildResponseJSON.self, from: responseData)
guard let urlString = response.data?.first?.url else {
throw FileStorageAPIError.uploadFailed(response.message ?? String(describing: response))
}
guard let url = URL(string: urlString) else {
throw FileStorageAPIError.invalidResponseURL(urlString)
}
return url
} catch {
throw FileStorageAPIError.unexpectedAPIResponse(responseData)
}
return url
}
}

enum FileStorageAPIError: Error {
case unexpectedOutputType
case invalidResponseURL(String)
case unexpectedAPIResponse(Data)
case uploadFailed(String)
case errorEncodingHeaderOrFooter
case errorEncodingImage

var localizedDescription: String {
switch self {
case .unexpectedOutputType:
return "unexpected API output type"
case .invalidResponseURL(let string):
return "invalid URL: \(string)"
case .unexpectedAPIResponse(let data):
return "unexpected API response:\n\(String(describing: String(data: data, encoding: .utf8)))"
case .uploadFailed(let message):
return "upload failed with message: '\(message)'"
return "upload failed: \(message)"
case .errorEncodingHeaderOrFooter:
return "error encoding multipart header or footer"
case .errorEncodingImage:
Expand Down
31 changes: 31 additions & 0 deletions NosTests/Fixtures/nostr_build_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"status": "success",
"message": "Upload successful.",
"data": [
{
"input_name": "APIv2",
"name": "d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"sha256": "",
"original_sha256": "d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1",
"type": "picture",
"mime": "image/jpeg",
"size": 526284,
"blurhash": "L9C;-XQS.9roDg^,D$-pHSD$%NnM",
"dimensions": {
"width": 4032,
"height": 3024
},
"dimensionsString": "4032x3024",
"url": "https://image.nostr.build/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"thumbnail": "https://image.nostr.build/thumb/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"responsive": {
"240p": "https://image.nostr.build/resp/240p/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"360p": "https://image.nostr.build/resp/360p/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"480p": "https://image.nostr.build/resp/480p/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"720p": "https://image.nostr.build/resp/720p/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg",
"1080p": "https://image.nostr.build/resp/1080p/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg"
},
"metadata": []
}
]
}
30 changes: 30 additions & 0 deletions NosTests/Model/NostrBuildResponseJSONTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import XCTest

class NostrBuildResponseJSONTests: XCTestCase {
/// Verifies that we can properly decode a response frome the nostr.build API.
func testDecoding() throws {
// Arrange
let jsonData = try jsonData(filename: "nostr_build_response")
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// Act
let subject = try decoder.decode(NostrBuildResponseJSON.self, from: jsonData)

// Assert
XCTAssertEqual(subject.status, .success)
XCTAssertEqual(subject.message, "Upload successful.")

let photo = try XCTUnwrap(subject.data?.first)
XCTAssertEqual(
photo.url,
"https://image.nostr.build/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg"
)
XCTAssertEqual(
photo.thumbnail,
"https://image.nostr.build/thumb/d9a71e94e59ab36ce13dd6998dcdb34527acdd2781cb6b91858ea0a8ca8557b1.jpeg"
)
XCTAssertEqual(photo.mime, "image/jpeg")
XCTAssertEqual(photo.type, "picture")
}
}
8 changes: 8 additions & 0 deletions NosTests/Test Helpers/XCTestCase+JSONData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import XCTest

extension XCTestCase {
func jsonData(filename: String) throws -> Data {
let url = try XCTUnwrap(Bundle.current.url(forResource: filename, withExtension: "json"))
return try Data(contentsOf: url)
}
}

0 comments on commit 6572ce2

Please sign in to comment.