diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/ReadRedditCommentFile.swift b/Sources/SwiftNLP/1. Data Collection/Pushshift API/ReadRedditCommentFile.swift similarity index 100% rename from Sources/SwiftNLP/1. Data Collection/Reddit API/ReadRedditCommentFile.swift rename to Sources/SwiftNLP/1. Data Collection/Pushshift API/ReadRedditCommentFile.swift diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/RedditCommentData.swift b/Sources/SwiftNLP/1. Data Collection/Pushshift API/RedditCommentData.swift similarity index 100% rename from Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/RedditCommentData.swift rename to Sources/SwiftNLP/1. Data Collection/Pushshift API/RedditCommentData.swift diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/RedditContainer.swift b/Sources/SwiftNLP/1. Data Collection/Pushshift API/RedditContainer.swift similarity index 100% rename from Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/RedditContainer.swift rename to Sources/SwiftNLP/1. Data Collection/Pushshift API/RedditContainer.swift diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/RedditSubmissionData.swift b/Sources/SwiftNLP/1. Data Collection/Pushshift API/RedditSubmissionData.swift similarity index 100% rename from Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/RedditSubmissionData.swift rename to Sources/SwiftNLP/1. Data Collection/Pushshift API/RedditSubmissionData.swift diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/readRedditSubmissionData.swift b/Sources/SwiftNLP/1. Data Collection/Pushshift API/readRedditSubmissionData.swift similarity index 100% rename from Sources/SwiftNLP/1. Data Collection/Reddit API/readRedditSubmissionData.swift rename to Sources/SwiftNLP/1. Data Collection/Pushshift API/readRedditSubmissionData.swift diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Comment.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Comment.swift index 2a06d6942b4aafe823dee7d6ef39b543b3548e60..3a9408f988559cb59248a7e63ff021f5f0ee489b 100644 --- a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Comment.swift +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Comment.swift @@ -21,11 +21,9 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. +// TODO: use coding keys to refactor these fields as more Swifty public struct Comment: RedditDataItem { -// public static func == (lhs: Comment, rhs: Comment) -> Bool { -// return lhs.id == rhs.id -// } - + public let author: String? public let author_created_utc: Int32? public let author_flair_css_class: String? @@ -46,5 +44,5 @@ public struct Comment: RedditDataItem { public let score_hidden: Bool? public let subreddit: String? public let subreddit_id: String? - //let replies: Listing? + public let replies: String? } diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing + Codable.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing + Codable.swift index 7ca1d6e29a0d1435124f7cffefbf936a25a938aa..259163eb577ec427b9648df68bcf5b6b1cfb4044 100644 --- a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing + Codable.swift +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing + Codable.swift @@ -26,6 +26,9 @@ extension ListingDataItem { case .link: data = try container.decode(Submission.self, forKey: .data) + case .subreddit: + data = try container.decode(Subreddit.self, forKey: .data) + case .more: //debugPrint("FOUND MORE") data = try container.decode(RessidtListingMore.self, forKey: .data) diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing.swift index 4e3fbc8a71dca25a182ffbefe1359f1cbe475f65..077e5cd25caa201260813546598a5e6a757522ca 100644 --- a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing.swift +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Listing.swift @@ -28,6 +28,7 @@ struct Listing: Codable { let data: ListingData } +// TODO: use coding keys to refactor these fields as more Swifty struct ListingData: Codable { let after: String? let dist: Int? diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Submission.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Submission.swift index e62ce11fe098fdf49e0dd52b1bc832e76c38056e..f6804fcddfc3e7fbf8905b2bd1efb6771dbaacf2 100644 --- a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Submission.swift +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Submission.swift @@ -21,6 +21,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. +// TODO: use coding keys to refactor these fields as more Swifty public struct Submission: RedditDataItem { public let author: String? public let author_flair_css_class: String? diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Subreddit.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Subreddit.swift new file mode 100644 index 0000000000000000000000000000000000000000..f67f08402a6ee16ffff21e2fa7ac0e9d8c3e5664 --- /dev/null +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Data Types/Subreddit.swift @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Jim Wallace +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +// TODO: Add conformance to RedditDataItem +public struct Subreddit: RedditDataItem { + + public var id: String? + public let submitText: String? + public let displayName: String? + public let headerImage: String? + public let descriptionHTML: String? + public let title: String? + public let collapseDeletedComments: Bool + public let over18: Bool + public let publicDescriptionHTML: String? + public let headerTitle: String? + public let description: String? + public let accountsActive: UInt? + public let publicTraffic: Bool + public let subscribers: UInt? + public let created: Int? + public let url: String? + public let createdUTC: Int + public let publicDescription: String? + + public var created_utc: Int32? { Int32(createdUTC) } + + + + enum CodingKeys: String, CodingKey { + case submitText = "submit_text" + case displayName = "display_name" + case headerImage = "header_image" + case descriptionHTML = "description_html" + case title = "title" + case collapseDeletedComments = "collapse_deleted_comments" + case over18 = "over18" + case publicDescriptionHTML = "public_description_html" + case headerTitle = "header_title" + case description = "description" + case accountsActive = "accounts_active" + case publicTraffic = "public_traffic" + case subscribers = "subscribers" + case created = "created" + case url = "url" + case createdUTC = "created_utc" + case publicDescription = "public_description" + } + +} diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + Subreddit Search.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + Subreddit Search.swift index 36c0772fb33c255840cc5b9cd68ab971454cdbb7..68b97a61af59ead67d2a295bd8d1df92efe7d4dc 100644 --- a/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + Subreddit Search.swift +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + Subreddit Search.swift @@ -25,6 +25,25 @@ import Foundation extension Session { + @inlinable + func aboutSubreddit(_ subreddit: String) async throws -> Subreddit { + let parameters: [String : String] = [String:String]() + + let (data, _) = try await _GET(endpoint: "r/\(subreddit)/about", parameters: parameters) + + do { + let data = try JSONDecoder().decode(ListingDataItem.self, from: data) + if data.kind == .subreddit { + return data.data as! Subreddit + } + throw SessionError(message: "Unable to decode server response.") + } catch { + throw SessionError(message: "Unable to decode server response.") + } + + } + + /// Returns a `RedditListing` for the provided search terms from the r/subreddit/search endpoint. @inlinable func searchSubreddit( diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + _GET.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + _GET.swift new file mode 100644 index 0000000000000000000000000000000000000000..57f3166fe58f6b1038648b8cbe2fa36860ca84ef --- /dev/null +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Network Endpoints/Session + _GET.swift @@ -0,0 +1,86 @@ +// Copyright (c) 2023 Jim Wallace +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +import Foundation + +extension Session { + + /// UTILITY Method + /// Perform a basic GET to the Reddit API given an endpoint and parameters + @inlinable + internal func _GET(endpoint: String, parameters: [String : String]) async throws -> (Data, HTTPURLResponse) { + guard isAuthenticated else { + throw SessionError(message: "Client not authenticated.") + } + + var url = URLComponents(string: authorizedAPIURL + endpoint) + + // Load up our query items + var queryItems = [URLQueryItem]() + for (key, value) in parameters { + queryItems.append(URLQueryItem(name: key, value: value)) + } + url?.queryItems = queryItems + + guard let url = url?.url else { + throw SessionError(message: "Could not form query URL") + } + + // Create a data request + var request = URLRequest(url: url) + request.httpMethod = "GET" + + // Set headers + request.allHTTPHeaderFields = httpHeaders + + //debugPrint(request) + + let (data, response) = try await session.data(for: request) + + //debugPrint(String(data: data, encoding: .utf8)!) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw SessionError(message: "Bad server response" + response.description) + } + + // Monitor rate limits + if httpResponse.allHeaderFields.keys.contains("x-ratelimit-used") { + if let tmp = httpResponse.allHeaderFields["x-ratelimit-used"] as? String { + rateLimitUsed = Int(tmp) ?? -1 + } + } + if httpResponse.allHeaderFields.keys.contains("x-ratelimit-remaining") { + if let tmp = httpResponse.allHeaderFields["x-ratelimit-remaining"] as? String { + rateLimitUsed = Int(tmp) ?? -1 + } + } + if httpResponse.allHeaderFields.keys.contains("x-ratelimit-reset") { + if let tmp = httpResponse.allHeaderFields["x-ratelimit-reset"] as? String { + rateLimitReset = Int(tmp) ?? -1 + } + } + + return (data,httpResponse) + } + +} diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit API/Session.swift b/Sources/SwiftNLP/1. Data Collection/Reddit API/Session.swift index abb14b55a8f3ade5ebb673f096769c0920a27623..0dee111a0f5330b6b78c22f82df2d65b36ed81cd 100644 --- a/Sources/SwiftNLP/1. Data Collection/Reddit API/Session.swift +++ b/Sources/SwiftNLP/1. Data Collection/Reddit API/Session.swift @@ -50,6 +50,7 @@ class Session { let baseAPIURL = "https://www.reddit.com/api/" let authorizedAPIURL = "https://oauth.reddit.com/" + // TODO: use this information when we have multi-pronged and multi-threaded calls var rateLimitUsed: Int = -1 var rateLimitRemaining: Int = -1 var rateLimitReset: Int = -1 @@ -62,65 +63,13 @@ class Session { } - - -extension Session { - - // UTILITY Method - // Perform a basic GET given an endpoint and parameters - // - @inlinable - internal func _GET(endpoint: String, parameters: [String : String]) async throws -> (Data, HTTPURLResponse) { - guard isAuthenticated else { - throw SessionError(message: "Client not authenticated.") - } - - var url = URLComponents(string: authorizedAPIURL + endpoint) - - // Load up our query items - var queryItems = [URLQueryItem]() - for (key, value) in parameters { - queryItems.append(URLQueryItem(name: key, value: value)) - } - url?.queryItems = queryItems - - guard let url = url?.url else { - throw SessionError(message: "Could not form query URL") - } - - // Create a data request - var request = URLRequest(url: url) - request.httpMethod = "GET" - - // Set headers - request.allHTTPHeaderFields = httpHeaders - - let (data, response) = try await session.data(for: request) - - print(String(data: data, encoding: .utf8)!) - - guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { - throw SessionError(message: "Bad server response" + response.description) - } - - // Monitor rate limits - if httpResponse.allHeaderFields.keys.contains("x-ratelimit-used") { - if let tmp = httpResponse.allHeaderFields["x-ratelimit-used"] as? String { - rateLimitUsed = Int(tmp) ?? -1 - } - } - if httpResponse.allHeaderFields.keys.contains("x-ratelimit-remaining") { - if let tmp = httpResponse.allHeaderFields["x-ratelimit-remaining"] as? String { - rateLimitUsed = Int(tmp) ?? -1 - } - } - if httpResponse.allHeaderFields.keys.contains("x-ratelimit-reset") { - if let tmp = httpResponse.allHeaderFields["x-ratelimit-reset"] as? String { - rateLimitReset = Int(tmp) ?? -1 - } - } - - return (data,httpResponse) - } - -} +//extension Session { +// +// func fetchThread(submissionID: String) -> RedditThread? { +// +// +// +// return nil +// } +// +//} diff --git a/Tests/SwiftNLPTests/Reddit API/Session Tests.swift b/Tests/SwiftNLPTests/Reddit API/Session Tests.swift index c3892f01989cc0bf9c4d72d7e810c721dc478e99..2c30427b0481005bdc405413ebcbe467ea9a38af 100644 --- a/Tests/SwiftNLPTests/Reddit API/Session Tests.swift +++ b/Tests/SwiftNLPTests/Reddit API/Session Tests.swift @@ -73,6 +73,24 @@ final class RedditSessionTest: XCTestCase { } + func testSubredditAbout() async throws { + let id = ProcessInfo.processInfo.environment["REDDIT_CLIENT_ID"] ?? nil + let secret = ProcessInfo.processInfo.environment["REDDIT_CLIENT_SECRET"] ?? nil + + guard let id = id, let secret = secret else { + fatalError("Unable to fetch REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET from ProcessInfo.") + } + + let client = Session(id: id, secret: secret) + guard let _ = try? await client.authenticate() else { + throw SessionError(message: "Error authenticating client.") + } + + let result: Subreddit = try await client.aboutSubreddit("uwaterloo") + + XCTAssert(result.displayName == "uwaterloo" && result.title == "University of Waterloo") + } + func testSubredditSearch() async throws { let id = ProcessInfo.processInfo.environment["REDDIT_CLIENT_ID"] ?? nil @@ -88,12 +106,7 @@ final class RedditSessionTest: XCTestCase { } let result: Listing = try await client.searchSubreddit("uwaterloo", query: "goose", limit: 10) - -// let r2: RedditListing = try await client.searchSubreddit("uwaterloo", query: "goose", limit: 10, type: [.comment] ) -// for r in r2.children { -// print(r) -// } - + XCTAssert(result.data.children.count > 0) }