diff --git a/nodecg-io-twitch-pubsub/extension/index.ts b/nodecg-io-twitch-pubsub/extension/index.ts new file mode 100644 index 000000000..91886cf49 --- /dev/null +++ b/nodecg-io-twitch-pubsub/extension/index.ts @@ -0,0 +1,31 @@ +import { NodeCG } from "nodecg/types/server"; +import { emptySuccess, Result, success } from "nodecg-io-core/extension/utils/result"; +import { ServiceBundle } from "nodecg-io-core/extension/serviceBundle"; +import { PubSubServiceClient } from "./pubSubClient"; + +export { PubSubServiceClient } from "./pubSubClient"; + +export interface PubSubServiceConfig { + oauthKey: string; +} + +module.exports = (nodecg: NodeCG) => { + new TwitchPubSubService(nodecg, "twitch-pubsub", __dirname, "../pubsub-schema.json").register(); +}; + +class TwitchPubSubService extends ServiceBundle { + async validateConfig(config: PubSubServiceConfig): Promise> { + await PubSubServiceClient.getTokenInfo(config); // This will throw a error if the token is invalid + return emptySuccess(); + } + + async createClient(config: PubSubServiceConfig): Promise> { + const client = await PubSubServiceClient.createClient(config); + + return success(client); + } + + stopClient(_: PubSubServiceClient): void { + // Not possible + } +} diff --git a/nodecg-io-twitch-pubsub/extension/pubSubClient.ts b/nodecg-io-twitch-pubsub/extension/pubSubClient.ts new file mode 100644 index 000000000..9446f5dc8 --- /dev/null +++ b/nodecg-io-twitch-pubsub/extension/pubSubClient.ts @@ -0,0 +1,89 @@ +import { ServiceClient } from "nodecg-io-core/extension/types"; +import { getTokenInfo, StaticAuthProvider, TokenInfo } from "twitch-auth"; +import { PubSubServiceConfig } from "./index"; +import { + PubSubBitsBadgeUnlockMessage, + PubSubBitsMessage, + PubSubChatModActionMessage, + PubSubClient, + PubSubListener, + PubSubRedemptionMessage, + PubSubSubscriptionMessage, + PubSubWhisperMessage, +} from "twitch-pubsub-client"; +import { ApiClient } from "twitch"; + +export class PubSubServiceClient implements ServiceClient { + constructor(private client: PubSubClient, private userid: string) {} + + /** + * Creates a instance of TwitchServiceClient using the credentials from the passed config. + */ + static async createClient(cfg: PubSubServiceConfig): Promise { + // Create a twitch authentication provider + const tokenInfo = await PubSubServiceClient.getTokenInfo(cfg); + const authProvider = new StaticAuthProvider(tokenInfo.clientId, this.normalizeToken(cfg), tokenInfo.scopes); + + // Create the actual chat client and connect + const apiClient = new ApiClient({ authProvider }); + const pubSubClient = new PubSubClient(); + const userID = await pubSubClient.registerUserListener(apiClient); + + return new PubSubServiceClient(pubSubClient, userID); + } + + /** + * Gets the token info for the passed config. + */ + static async getTokenInfo(cfg: PubSubServiceConfig): Promise { + return await getTokenInfo(this.normalizeToken(cfg)); + } + + /** + * Strips any "oauth:" before the token away, because the client needs the token without it. + */ + static normalizeToken(cfg: PubSubServiceConfig): string { + return cfg.oauthKey.replace("oauth:", ""); + } + + getNativeClient(): PubSubClient { + return this.client; + } + + getUserID(): string { + return this.userid; + } + + async onRedemption( + callback: (message: PubSubRedemptionMessage) => void, + ): Promise> { + return await this.client.onRedemption(this.userid, callback); + } + + async onSubscription( + callback: (message: PubSubSubscriptionMessage) => void, + ): Promise> { + return await this.client.onSubscription(this.userid, callback); + } + + async onBits(callback: (message: PubSubBitsMessage) => void): Promise> { + return await this.client.onBits(this.userid, callback); + } + + async onBitsBadgeUnlock( + callback: (message: PubSubBitsBadgeUnlockMessage) => void, + ): Promise> { + return await this.client.onBitsBadgeUnlock(this.userid, callback); + } + + async onModAction( + channelID: string, + callback: (message: PubSubChatModActionMessage) => void, + ): Promise> { + return await this.client.onModAction(this.userid, channelID, callback); + } + + async onWhisper(callback: (message: PubSubWhisperMessage) => void): Promise> { + return await this.client.onWhisper(this.userid, callback); + } +} diff --git a/nodecg-io-twitch-pubsub/package.json b/nodecg-io-twitch-pubsub/package.json new file mode 100644 index 000000000..28348c184 --- /dev/null +++ b/nodecg-io-twitch-pubsub/package.json @@ -0,0 +1,43 @@ +{ + "name": "nodecg-io-twitch-pubsub", + "version": "0.1.0", + "description": "Allows access to the Twitch PubSub API.", + "homepage": "https://nodecg.io/samples/twitch-pubsub", + "author": { + "name": "derNiklaas", + "url": "https://github.com/derNiklaas" + }, + "repository": { + "type": "git", + "url": "https://github.com/codeoverflow-org/nodecg-io.git", + "directory": "nodecg-io-twitch-pubsub" + }, + "main": "extension", + "scripts": { + "build": "tsc -b", + "watch": "tsc -b -w", + "clean": "tsc -b --clean" + }, + "keywords": [ + "nodecg-io", + "nodecg-bundle" + ], + "nodecg": { + "compatibleRange": "^1.1.1", + "bundleDependencies": { + "nodecg-io-core": "^0.1.0" + } + }, + "license": "MIT", + "devDependencies": { + "@types/node": "^14.6.4", + "nodecg": "^1.6.1", + "typescript": "^4.0.2" + }, + "dependencies": { + "nodecg-io-core": "^0.1.0", + "twitch": "^4.3.6", + "twitch-pubsub-client": "^4.3.6", + "ws": "^7.4.1" + } +} diff --git a/nodecg-io-twitch-pubsub/pubsub-schema.json b/nodecg-io-twitch-pubsub/pubsub-schema.json new file mode 100644 index 000000000..66d9d9c34 --- /dev/null +++ b/nodecg-io-twitch-pubsub/pubsub-schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "oauthKey": { + "type": "string", + "description": "A OAuth2 key for the twitch api." + } + }, + "required": ["oauthKey"] +} diff --git a/nodecg-io-twitch-pubsub/tsconfig.json b/nodecg-io-twitch-pubsub/tsconfig.json new file mode 100644 index 000000000..1c8405620 --- /dev/null +++ b/nodecg-io-twitch-pubsub/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.common.json" +} diff --git a/samples/pubsubsample/extension/index.ts b/samples/pubsubsample/extension/index.ts new file mode 100644 index 000000000..5ce1a72cb --- /dev/null +++ b/samples/pubsubsample/extension/index.ts @@ -0,0 +1,27 @@ +import { NodeCG } from "nodecg/types/server"; +import { PubSubServiceClient } from "nodecg-io-twitch-pubsub/extension"; +import { requireService } from "nodecg-io-core/extension/serviceClientWrapper"; + +module.exports = function (nodecg: NodeCG) { + nodecg.log.info("Sample bundle for twitch-pubsub started"); + + const pubsub = requireService(nodecg, "twitch-pubsub"); + + pubsub?.onAvailable((client) => { + nodecg.log.info("PubSub client has been updated, adding handlers for messages."); + client.onSubscription((message) => { + console.log(`${message.userDisplayName} just subscribed (${message.cumulativeMonths} months)`); + }); + client.onBits((message) => { + console.log(`${message.userName} cheered ${message.bits} Bits`); + }); + client.onBitsBadgeUnlock((message) => { + console.log(`${message.userName} just unlocked the ${message.badgeTier} Badge`); + }); + client.onRedemption((message) => { + console.log(`${message.userDisplayName} redeemed ${message.rewardName} (${message.message})`); + }); + }); + + pubsub?.onUnavailable(() => nodecg.log.info("PubSub client has been unset.")); +}; diff --git a/samples/pubsubsample/package.json b/samples/pubsubsample/package.json new file mode 100644 index 000000000..4b6f18b29 --- /dev/null +++ b/samples/pubsubsample/package.json @@ -0,0 +1,25 @@ +{ + "name": "pubsubsample", + "version": "0.1.0", + "nodecg": { + "compatibleRange": "^1.1.1", + "bundleDependencies": { + "nodecg-io-twitch-pubsub": "0.1.0" + } + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w" + }, + "license": "MIT", + "devDependencies": { + "@types/ws": "^7.2.6", + "@types/node": "^14.6.4" + }, + "dependencies": { + "nodecg-io-twitch-pubsub": "0.1.0", + "nodecg-io-core": "0.1.0", + "nodecg": "^1.6.1", + "typescript": "^4.0.2" + } +} diff --git a/samples/pubsubsample/tsconfig.json b/samples/pubsubsample/tsconfig.json new file mode 100644 index 000000000..c8bb01bee --- /dev/null +++ b/samples/pubsubsample/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.common.json" +}