<commit_before>//
//  NewsBlurAPICaller.swift
//  Account
//
//  Created by <NAME> on 3/9/20.
//  Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
//

import Foundation
import RSWeb
import Secrets

final class NewsBlurAPICaller: NSObject {
	static let SessionIdCookie = "newsblur_sessionid"

	let baseURL = URL(string: "https://www.newsblur.com/")!
	var transport: Transport!
	var suspended = false

	var credentials: Credentials?
	weak var accountMetadata: AccountMetadata?

	init(transport: Transport!) {
		super.init()
		self.transport = transport
	}

	/// Cancels all pending requests rejects any that come in later
	func suspend() {
		transport.cancelAll()
		suspended = true
	}

	func resume() {
		suspended = false
	}

	func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
		requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in
			switch result {
			case .success((let response, let payload)):
				guard let url = response.url, let headerFields = response.allHeaderFields as? [String: String], payload?.code != -1 else {
					let error = payload?.errors?.username ?? payload?.errors?.others
					if let message = error?.first {
						completion(.failure(NewsBlurError.general(message: message)))
					} else {
						completion(.failure(NewsBlurError.unknown))
					}
					return
				}

				guard let username = self.credentials?.username else {
					completion(.failure(NewsBlurError.unknown))
					return
				}

				let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
				for cookie in cookies where cookie.name == Self.SessionIdCookie {
					let credentials = Credentials(type: .newsBlurSessionId, username: username, secret: cookie.value)
					completion(.success(credentials))
					return
				}

				completion(.failure(NewsBlurError.general(message: "Failed to retrieve session")))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func logout(completion: @escaping (Result<Void, Error>) -> Void) {
		requestData(endpoint: "api/logout", completion: completion)
	}

	func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent("reader/feeds")
				.appendingQueryItems([
					URLQueryItem(name: "flat", value: "true"),
					URLQueryItem(name: "update_counts", value: "false"),
				])

		requestData(callURL: url, resultType: NewsBlurFeedsResponse.self) { result in
			switch result {
			case .success((_, let payload)):
				completion(.success((payload?.feeds, payload?.folders)))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func retrieveStoryHashes(endpoint: String, completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent(endpoint)
				.appendingQueryItems([
					URLQueryItem(name: "include_timestamps", value: "true"),
				])

		requestData(
				callURL: url,
				resultType: NewsBlurStoryHashesResponse.self,
				dateDecoding: .secondsSince1970
		) { result in
			switch result {
			case .success((_, let payload)):
				let hashes = payload?.unread ?? payload?.starred
				completion(.success(hashes))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
		retrieveStoryHashes(
				endpoint: "reader/unread_story_hashes", 
				completion: completion
		)
	}

	func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
		retrieveStoryHashes(
				endpoint: "reader/starred_story_hashes", 
				completion: completion
		)
	}

	func retrieveStories(feedID: String, page: Int, completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent("reader/feed/\(feedID)")
				.appendingQueryItems([
					URLQueryItem(name: "page", value: String(page)),
					URLQueryItem(name: "order", value: "newest"),
					URLQueryItem(name: "read_filter", value: "all"),
					URLQueryItem(name: "include_hidden", value: "true"),
					URLQueryItem(name: "include_story_content", value: "true"),
				])

		requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
			switch result {
			case .success(let (response, payload)):
				completion(.success((payload?.stories, HTTPDateInfo(urlResponse: response)?.date)))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent("reader/river_stories")
				.appendingQueryItem(.init(name: "include_hidden", value: "true"))?
				.appendingQueryItems(hashes.map {
					URLQueryItem(name: "h", value: $0.hash)
				})

		requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
			switch result {
			case .success(let (response, payload)):
				completion(.success((payload?.stories, HTTPDateInfo(urlResponse: response)?.date)))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
		sendUpdates(
				endpoint: "reader/mark_story_hash_as_unread", 
				payload: NewsBlurStoryStatusChange(hashes: hashes),
				completion: completion
		)
	}

	func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
		sendUpdates(
				endpoint: "reader/mark_story_hashes_as_read", 
				payload: NewsBlurStoryStatusChange(hashes: hashes),
				completion: completion
		)
	}
<commit_msg>Fixes #3056 on Mac<commit_after>//
//  NewsBlurAPICaller.swift
//  Account
//
//  Created by <NAME> on 3/9/20.
//  Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
//

import Foundation
import RSWeb
import Secrets

final class NewsBlurAPICaller: NSObject {
	static let SessionIdCookie = "newsblur_sessionid"

	let baseURL = URL(string: "https://www.newsblur.com/")!
	var transport: Transport!
	var suspended = false

	var credentials: Credentials?
	weak var accountMetadata: AccountMetadata?

	init(transport: Transport!) {
		super.init()
		self.transport = transport
	}

	/// Cancels all pending requests rejects any that come in later
	func suspend() {
		transport.cancelAll()
		suspended = true
	}

	func resume() {
		suspended = false
	}

	func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
		requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in
			switch result {
			case .success((let response, let payload)):
				guard let url = response.url, let headerFields = response.allHeaderFields as? [String: String], payload?.code != -1 else {
					let error = payload?.errors?.username ?? payload?.errors?.others
					if let message = error?.first {
						completion(.failure(NewsBlurError.general(message: message)))
					} else {
						completion(.failure(NewsBlurError.unknown))
					}
					return
				}

				guard let username = self.credentials?.username else {
					completion(.failure(NewsBlurError.unknown))
					return
				}

				let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
				for cookie in cookies where cookie.name == Self.SessionIdCookie {
					let credentials = Credentials(type: .newsBlurSessionId, username: username, secret: cookie.value)
					completion(.success(credentials))
					return
				}

				completion(.failure(NewsBlurError.general(message: "Failed to retrieve session")))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func logout(completion: @escaping (Result<Void, Error>) -> Void) {
		requestData(endpoint: "api/logout", completion: completion)
	}

	func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent("reader/feeds")
				.appendingQueryItems([
					URLQueryItem(name: "flat", value: "true"),
					URLQueryItem(name: "update_counts", value: "false"),
				])

		requestData(callURL: url, resultType: NewsBlurFeedsResponse.self) { result in
			switch result {
			case .success((_, let payload)):
				completion(.success((payload?.feeds, payload?.folders)))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func retrieveStoryHashes(endpoint: String, completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent(endpoint)
				.appendingQueryItems([
					URLQueryItem(name: "include_timestamps", value: "true"),
				])

		requestData(
				callURL: url,
				resultType: NewsBlurStoryHashesResponse.self,
				dateDecoding: .secondsSince1970
		) { result in
			switch result {
			case .success((_, let payload)):
				let hashes = payload?.unread ?? payload?.starred
				completion(.success(hashes))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
		retrieveStoryHashes(
				endpoint: "reader/unread_story_hashes", 
				completion: completion
		)
	}

	func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
		retrieveStoryHashes(
				endpoint: "reader/starred_story_hashes", 
				completion: completion
		)
	}

	func retrieveStories(feedID: String, page: Int, completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent("reader/feed/\(feedID)")
				.appendingQueryItems([
					URLQueryItem(name: "page", value: String(page)),
					URLQueryItem(name: "order", value: "newest"),
					URLQueryItem(name: "read_filter", value: "all"),
					URLQueryItem(name: "include_hidden", value: "false"),
					URLQueryItem(name: "include_story_content", value: "true"),
				])

		requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
			switch result {
			case .success(let (response, payload)):
				completion(.success((payload?.stories, HTTPDateInfo(urlResponse: response)?.date)))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
		let url = baseURL
				.appendingPathComponent("reader/river_stories")
				.appendingQueryItem(.init(name: "include_hidden", value: "false"))?
				.appendingQueryItems(hashes.map {
					URLQueryItem(name: "h", value: $0.hash)
				})

		requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
			switch result {
			case .success(let (response, payload)):
				completion(.success((payload?.stories, HTTPDateInfo(urlResponse: response)?.date)))
			case .failure(let error):
				completion(.failure(error))
			}
		}
	}

	func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
		sendUpdates(
				endpoint: "reader/mark_story_hash_as_unread", 
				payload: NewsBlurStoryStatusChange(hashes: hashes),
				completion: completion
		)
	}

	func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
		sendUpdates(
				endpoint: "reader/mark_story_hashes_as_read", 
				payload: NewsBlurStoryStatusChange(hashes: hashes),
				completion: completion
		)
	}

{'fraction_non_alphanumeric': 0.13181540608915643, 'fraction_numerical': 0.002791590334118468, 'mean_word_length': 4.253895508707608, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 45, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2648915', 'n_tokens_mistral': 4303, 'n_tokens_neox': 3528, 'n_words': 793}