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