From a716d0dd0a0aa3d85879f6a19c34652fe1b0b67b Mon Sep 17 00:00:00 2001 From: Jim Wallace <james.wallace@uwaterloo.ca> Date: Tue, 19 Dec 2023 10:54:52 -0500 Subject: [PATCH] Added /user/username endpoint --- .../Reddit/RedditClient + User Search.swift | 323 ++++++++++++++++++ .../Reddit API/RedditClient.swift | 86 +++-- 2 files changed, 386 insertions(+), 23 deletions(-) create mode 100644 Sources/SwiftNLP/1. Data Collection/Reddit/RedditClient + User Search.swift diff --git a/Sources/SwiftNLP/1. Data Collection/Reddit/RedditClient + User Search.swift b/Sources/SwiftNLP/1. Data Collection/Reddit/RedditClient + User Search.swift new file mode 100644 index 00000000..bf0b7af5 --- /dev/null +++ b/Sources/SwiftNLP/1. Data Collection/Reddit/RedditClient + User Search.swift @@ -0,0 +1,323 @@ +// 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 + +// TODO: Refactor this to serve *all* `/user/username/` endpoints: +// - overview +// - submitted +// - comments +// - upvoted +// - downvoted +// - hidden +// - saved +// - gilded +extension RedditClient { + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserOverview( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("overview", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserSubmitted( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("submitted", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/ endpoint + func searchUserComments( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("comments", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserUpvoted( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("upvoted", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserDownvoted( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("downvoted", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserHidden( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("hidden", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserSaved( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("saved", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment listing corresponding to /user/username/comments/overview endpoint + func searchUserGilded( + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + return try await _searchUserEndpoint("gilded", + userName: userName, + context: context, + sort: sort, + time: time, + type: type, + after: after, + before: before, + count: count, + limit: limit, + expandSubreddits: expandSubreddits + ) + } + + /// Returns a comment tree corresponding to a search of a given /user/username/ endpoint + @inlinable + internal func _searchUserEndpoint( + _ endPoint: String, + userName: String, + context: UInt = 10, + // SHOW? + sort: ListingSortOrder = .new, + time: ListingTime = .all, + type: RedditContentType = .link, + after: String? = nil, + before: String? = nil, + count: UInt = 0, + limit: Int? = nil, + expandSubreddits: Bool = false + ) async throws -> RedditListing { + + var parameters: [String : String] = [String:String]() + + guard context >= 2, context <= 10 else { + throw RedditClientError(message: "Context must be value between 2 and 10") + } + + parameters["sort"] = sort.rawValue + parameters["t"] = time.rawValue + + + guard type == .link || type == .comment else { + throw RedditClientError(message: "Type must be either .link or .comment") + } + parameters["type"] = type.rawValue + + if let after = after { + parameters["after"] = after + } + + if let before = before { + parameters["before"] = before + } + + parameters["count"] = String(count) + + if let limit = limit { + parameters["limit"] = String(limit) + } + + parameters["sr_detail"] = String(expandSubreddits).lowercased() + + let (data, _ ) = try await _GET( + endpoint: "user/\(userName)/\(endPoint)", + parameters: parameters + ) + + do { + let redditListing = try JSONDecoder().decode(RedditListing.self, from: data) + return redditListing + + } catch { + throw RedditClientError(message: "Unable to decode server response.") + } + } + +} diff --git a/Tests/SwiftNLPTests/Reddit API/RedditClient.swift b/Tests/SwiftNLPTests/Reddit API/RedditClient.swift index ef2ec1e3..1a7952a0 100644 --- a/Tests/SwiftNLPTests/Reddit API/RedditClient.swift +++ b/Tests/SwiftNLPTests/Reddit API/RedditClient.swift @@ -97,27 +97,67 @@ final class RedditClientTest: XCTestCase { XCTAssert(result.data.children.count > 0) } -// func testCommentSearch() 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 = RedditClient(id: id, secret: secret) -// guard let _ = try? await client.authenticate() else { -// throw RedditClientError(message: "Error authenticating client.") -// } -// -// // https://www.reddit.com/r/uwaterloo/comments/18lbokl/conestoga_college_finally_being_called_out_by_the/ -// let submission = RedditSubmission(id: "18lbokl", subreddit: "uwaterloo") -// -// let result = try await client.searchComment(submission: submission) -// -// -// XCTAssert(result.count > 0) -// //XCTAssert(result[1].children.count > 0) -// } + func testCommentSearch() 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 = RedditClient(id: id, secret: secret) + guard let _ = try? await client.authenticate() else { + throw RedditClientError(message: "Error authenticating client.") + } + + // https://www.reddit.com/r/uwaterloo/comments/18lbokl/conestoga_college_finally_being_called_out_by_the/ + let submission = RedditSubmission(id: "18lbokl", subreddit: "uwaterloo") + + let result = try await client.searchComment(submission: submission) + + + XCTAssert(result.count > 0) + //XCTAssert(result[1].children.count > 0) + } + + func testUserSearch() 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 = RedditClient(id: id, secret: secret) + guard let _ = try? await client.authenticate() else { + throw RedditClientError(message: "Error authenticating client.") + } + + let overviewResult = try await client.searchUserOverview(userName: "jimntonik") + XCTAssert(overviewResult.children.count > 0) + + let submittedResult = try await client.searchUserSubmitted(userName: "jimntonik") + XCTAssert(submittedResult.children.count > 0) + + let commentResult = try await client.searchUserComments(userName: "jimntonik") + XCTAssert(commentResult.children.count > 0) + + //let upvotedResult = try await client.searchUserUpvoted(userName: "jimntonik") // TODO: 403 - Forbidden - requires login? + //XCTAssert(upvotedResult.children.count > 0) + + //let downvotedResult = try await client.searchUserDownvoted(userName: "jimntonik") // TODO: 403 - Forbidden + //XCTAssert(downvotedResult.children.count > 0) + + //let hiddenResult = try await client.searchUserHidden(userName: "jimntonik") // TODO: 403 - Forbidden + //XCTAssert(hiddenResult.children.count > 0) + + //let savedResult = try await client.searchUserSaved(userName: "jimntonik") // TODO: 403 - Forbidden + //XCTAssert(savedResult.children.count > 0) + + //let gildedResult = try await client.searchUserGilded(userName: "jimntonik") // TODO: 403 - Forbidden + //XCTAssert(gildedResult.children.count > 0) + } + } -- GitLab