Skip to content

Commit

Permalink
feat: add helix endpoints for subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
LosFarmosCTL committed Jan 30, 2024
1 parent 4b1a71a commit b79a068
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Sources/Twitch/API/Endpoints/HelixData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ internal struct HelixData<T>: 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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Subscription>?) = 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
}
Original file line number Diff line number Diff line change
@@ -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<Subscriber>?) = 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)
}
}
65 changes: 65 additions & 0 deletions Tests/TwitchTests/API/Endpoints/SubscriptionsTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 5 additions & 0 deletions Tests/TwitchTests/API/MockedData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit b79a068

Please sign in to comment.