diff --git a/Sources/RunTest/main.swift b/Sources/RunTest/main.swift new file mode 100644 index 0000000..eda35d9 --- /dev/null +++ b/Sources/RunTest/main.swift @@ -0,0 +1,66 @@ +import Foundation +import Mocker +import TwitchIRC + +@testable import Twitch + +ISO8601DateFormatter().date(from: "2019-02-15T21:19:50.380833Z") + +struct Test: Codable { let test: DateInterval } + +let test = Test(test: DateInterval(start: Date(), end: Date())) + +print(String(data: try JSONEncoder().encode(test), encoding: .utf8)!) + +print(Date().formatted(.iso8601)) + +// let configuration = URLSessionConfiguration.default +// configuration.protocolClasses = [MockingURLProtocol.self] +// let mockingURLSession = URLSession(configuration: configuration) + +let helix = try Helix( + authentication: .init( + oAuth: "g9p64vxsopd4cy3qer8niqkaf6xtsv", clientID: "gp762nuuoqcoxypju8c569th9wz7q5", + userId: "101676978")) + +let streams = try await helix.getFollowedStreams() + +// let url = URL(string: "https://api.twitch.tv/helix/invalid")! +// Mock(url: url, contentType: .json, statusCode: 500, data: [.get: "".data(using: .utf8)!]) +// .register() + +// let _: [Broadcaster] = try await helix.request(.get("invalid")) + +// _ = try await helix.getAdSchedule(broadcasterId: "101676978") + +let cc = ChatClient(.anonymous) + +print("Connecting...") +let clock = ContinuousClock() + +var stream: AsyncThrowingStream? +let elapsed = try await clock.measure { stream = try await cc.connect() } + +print("Took \(elapsed)") +print("Joining channel...") +do { try await cc.join(to: "forsen") } catch { print("you are fucked") } + +var count = 10 + +print("Listening...") +guard let stream else { throw HelixError.missingClientID } +for try await message in stream { + print("Received message: \(message)") + if case .privateMessage(let privmsg) = message { + print(privmsg.message) + count -= 1 + } + + if count == 0 { + try await cc.part(from: "forsen") + count = 10 + + try await Task.sleep(nanoseconds: UInt64(5 * Double(NSEC_PER_SEC))) + try await cc.join(to: "forsen") + } +} diff --git a/Sources/Twitch/API/Endpoints/HelixData.swift b/Sources/Twitch/API/Endpoints/HelixData.swift index 5f79d03..a7bc38c 100644 --- a/Sources/Twitch/API/Endpoints/HelixData.swift +++ b/Sources/Twitch/API/Endpoints/HelixData.swift @@ -2,12 +2,14 @@ internal struct HelixData: Decodable where T: Decodable { let data: [T] let pagination: Pagination? let total: Int? + let points: Int? let template: String? enum CodingKeys: String, CodingKey { case data case pagination case total + case points case template } diff --git a/Sources/Twitch/API/Endpoints/Subscriptions/Helix+checkUserSubscription.swift b/Sources/Twitch/API/Endpoints/Subscriptions/Helix+checkUserSubscription.swift index e69de29..1aa55ba 100644 --- a/Sources/Twitch/API/Endpoints/Subscriptions/Helix+checkUserSubscription.swift +++ b/Sources/Twitch/API/Endpoints/Subscriptions/Helix+checkUserSubscription.swift @@ -0,0 +1,73 @@ +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +extension Helix { + public func checkUserSubscription(to channelId: String) async throws -> Subscription? { + let queryItems = self.makeQueryItems( + ("broadcaster_id", channelId), ("user_id", self.authenticatedUserId)) + + let (rawResponse, result): (_, HelixData?) = try await self.request( + .get("subscriptions/user"), with: queryItems) + + guard let result else { throw HelixError.invalidResponse(rawResponse: rawResponse) } + + return result.data.first + } +} + +public struct Subscription: Decodable { + let broadcasterId: String + let broadcasterLogin: String + let broadcasterName: String + + let gifter: SubGifter? + + let tier: SubTier + + enum CodingKeys: String, CodingKey { + case broadcasterId = "broadcaster_id" + case broadcasterLogin = "broadcaster_login" + case broadcasterName = "broadcaster_name" + + case isGift = "is_gift" + case gifterId = "gifter_id" + case gifterLogin = "gifter_login" + case gifterName = "gifter_name" + + case tier + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.broadcasterId = try container.decode(String.self, forKey: .broadcasterId) + self.broadcasterLogin = try container.decode(String.self, forKey: .broadcasterLogin) + self.broadcasterName = try container.decode(String.self, forKey: .broadcasterName) + + if try container.decode(Bool.self, forKey: .isGift) { + self.gifter = .init( + id: try container.decode(String.self, forKey: .gifterId), + login: try container.decode(String.self, forKey: .gifterLogin), + name: try container.decode(String.self, forKey: .gifterName)) + } else { + self.gifter = nil + } + + self.tier = try container.decode(SubTier.self, forKey: .tier) + } +} + +public enum SubTier: String, Decodable { + case tier1 = "1000" + case tier2 = "2000" + case tier3 = "3000" +} + +public struct SubGifter: Decodable { + let id: String + let login: String + let name: String +} diff --git a/Sources/Twitch/API/Endpoints/Subscriptions/Helix+getBroadcasterSubscriptions.swift b/Sources/Twitch/API/Endpoints/Subscriptions/Helix+getBroadcasterSubscriptions.swift index e69de29..7e48c3c 100644 --- a/Sources/Twitch/API/Endpoints/Subscriptions/Helix+getBroadcasterSubscriptions.swift +++ b/Sources/Twitch/API/Endpoints/Subscriptions/Helix+getBroadcasterSubscriptions.swift @@ -0,0 +1,88 @@ +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +extension Helix { + public func getBroadcasterSubscribers( + userIDs: [String]? = nil, limit: Int? = nil, after startCursor: String? = nil, + before endCursor: String? = nil + ) async throws -> ( + (total: Int, points: Int), subscribers: [Subscriber], cursor: String? + ) { + var queryItems = self.makeQueryItems( + ("broadcaster_id", self.authenticatedUserId), ("first", limit.map(String.init)), + ("after", startCursor), ("before", endCursor)) + + queryItems.append( + contentsOf: userIDs?.map { URLQueryItem(name: "user_id", value: $0) } ?? []) + + let (rawResponse, result): (_, HelixData?) = try await self.request( + .get("subscriptions"), with: queryItems) + + guard let result else { throw HelixError.invalidResponse(rawResponse: rawResponse) } + guard let total = result.total, let points = result.points else { + throw HelixError.invalidResponse(rawResponse: rawResponse) + } + + return ((total, points), result.data, result.pagination?.cursor) + } +} + +public struct Subscriber: Decodable { + let userId: String + let userLogin: String + let userName: String + + let broadcasterId: String + let broadcasterLogin: String + let broadcasterName: String + + let gifter: SubGifter? + + let planName: String + let tier: SubTier + + enum CodingKeys: String, CodingKey { + case userId = "user_id" + case userLogin = "user_login" + case userName = "user_name" + + case broadcasterId = "broadcaster_id" + case broadcasterLogin = "broadcaster_login" + case broadcasterName = "broadcaster_name" + + case isGift = "is_gift" + case gifterId = "gifter_id" + case gifterLogin = "gifter_login" + case gifterName = "gifter_name" + + case planName = "plan_name" + case tier + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.userId = try container.decode(String.self, forKey: .userId) + self.userLogin = try container.decode(String.self, forKey: .userLogin) + self.userName = try container.decode(String.self, forKey: .userName) + + self.broadcasterId = try container.decode(String.self, forKey: .broadcasterId) + self.broadcasterLogin = try container.decode(String.self, forKey: .broadcasterLogin) + self.broadcasterName = try container.decode(String.self, forKey: .broadcasterName) + + if try container.decode(Bool.self, forKey: .isGift) { + self.gifter = .init( + id: try container.decode(String.self, forKey: .gifterId), + login: try container.decode(String.self, forKey: .gifterLogin), + name: try container.decode(String.self, forKey: .gifterName)) + } else { + self.gifter = nil + } + + self.planName = try container.decode(String.self, forKey: .planName) + self.tier = try container.decode(SubTier.self, forKey: .tier) + } +} diff --git a/Tests/TwitchTests/API/Endpoints/SubscriptionsTests.swift b/Tests/TwitchTests/API/Endpoints/SubscriptionsTests.swift new file mode 100644 index 0000000..73eeb4b --- /dev/null +++ b/Tests/TwitchTests/API/Endpoints/SubscriptionsTests.swift @@ -0,0 +1,65 @@ +import Foundation +import Mocker +import XCTest + +@testable import Twitch + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +final class SubscriptionsTests: XCTestCase { + private var helix: Helix! + + override func setUpWithError() throws { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockingURLProtocol.self] + let urlSession = URLSession(configuration: configuration) + + helix = try Helix( + authentication: .init( + oAuth: "1234567989", clientID: "abcdefghijkl", userId: "1234"), + urlSession: urlSession) + } + + func testGetBroadcasterSubscribers() async throws { + let url = URL( + string: "https://api.twitch.tv/helix/subscriptions?broadcaster_id=1234&first=2")! + + Mock( + url: url, contentType: .json, statusCode: 200, + data: [.get: MockedData.getBroadcasterSubscriptionsJSON] + ).register() + + let ((total, points), subscribers, cursor) = + try await helix.getBroadcasterSubscribers(limit: 2) + + XCTAssertEqual(total, 13) + XCTAssertEqual(points, 13) + + XCTAssertEqual(cursor, "jnksdfyg7is8do7fv7yuwbisudg") + + XCTAssertEqual(subscribers.count, 2) + XCTAssertNotNil(subscribers.first?.gifter) + XCTAssertEqual(subscribers.first?.gifter?.id, "12826") + XCTAssertEqual(subscribers.first?.gifter?.login, "twitch") + XCTAssertEqual(subscribers.first?.gifter?.name, "Twitch") + XCTAssertNil(subscribers.last?.gifter) + } + + func testCheckUserSubscription() async throws { + let url = URL( + string: + "https://api.twitch.tv/helix/subscriptions/user?broadcaster_id=1234&user_id=1234")! + + Mock( + url: url, contentType: .json, statusCode: 200, + data: [.get: MockedData.checkUserSubscriptionJSON] + ).register() + + let subscription = try await helix.checkUserSubscription(to: "1234") + + XCTAssertEqual(subscription?.broadcasterId, "141981764") + XCTAssertEqual(subscription?.gifter?.id, "12826") + } +} diff --git a/Tests/TwitchTests/API/MockResources/Endpoints/Subscriptions/checkUserSubscription.json b/Tests/TwitchTests/API/MockResources/Endpoints/Subscriptions/checkUserSubscription.json new file mode 100644 index 0000000..87156ff --- /dev/null +++ b/Tests/TwitchTests/API/MockResources/Endpoints/Subscriptions/checkUserSubscription.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "broadcaster_id": "141981764", + "broadcaster_login": "twitchdev", + "broadcaster_name": "TwitchDev", + "gifter_id": "12826", + "gifter_login": "twitch", + "gifter_name": "Twitch", + "is_gift": true, + "tier": "1000" + } + ] +} \ No newline at end of file diff --git a/Tests/TwitchTests/API/MockResources/Endpoints/Subscriptions/getBroadcasterSubscriptions.json b/Tests/TwitchTests/API/MockResources/Endpoints/Subscriptions/getBroadcasterSubscriptions.json new file mode 100644 index 0000000..bd2887f --- /dev/null +++ b/Tests/TwitchTests/API/MockResources/Endpoints/Subscriptions/getBroadcasterSubscriptions.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "broadcaster_id": "141981764", + "broadcaster_login": "twitchdev", + "broadcaster_name": "TwitchDev", + "gifter_id": "12826", + "gifter_login": "twitch", + "gifter_name": "Twitch", + "is_gift": true, + "tier": "1000", + "plan_name": "Channel Subscription (twitchdev)", + "user_id": "527115020", + "user_name": "twitchgaming", + "user_login": "twitchgaming" + }, + { + "broadcaster_id": "141981764", + "broadcaster_login": "twitchdev", + "broadcaster_name": "TwitchDev", + "is_gift": false, + "tier": "1000", + "plan_name": "Channel Subscription (twitchdev)", + "user_id": "527115020", + "user_name": "twitchgaming", + "user_login": "twitchgaming" + } + ], + "pagination": { + "cursor": "jnksdfyg7is8do7fv7yuwbisudg" + }, + "total": 13, + "points": 13 +} \ No newline at end of file diff --git a/Tests/TwitchTests/API/MockedData.swift b/Tests/TwitchTests/API/MockedData.swift index 375ac85..99e5650 100644 --- a/Tests/TwitchTests/API/MockedData.swift +++ b/Tests/TwitchTests/API/MockedData.swift @@ -64,6 +64,11 @@ public final class MockedData { forResource: "getStreams", withExtension: "json")!.data public static let getFollowedStreamsJSON: Data = Bundle.module.url( forResource: "getFollowedStreams", withExtension: "json")!.data + + public static let getBroadcasterSubscriptionsJSON: Data = Bundle.module.url( + forResource: "getBroadcasterSubscriptions", withExtension: "json")!.data + public static let checkUserSubscriptionJSON: Data = Bundle.module.url( + forResource: "checkUserSubscription", withExtension: "json")!.data } extension URL {