ChatBot (wymagany)

Aby framework działał poprawnie, musisz dostarczyć trzy kluczowe providery - możesz przekazać je jako klasę bądź instancję klasy, która implementuje wymagany interfejs.

export type Provider<T> = {
    token: string;
    useClass?: new (...args: any[]) => T;
    useValue?: T;
}

type ClassOrValue<T> = Omit<Provider<T>, 'token'>;
ChatBotModule.forRoot({
    listenChannels: ClassOrValue<IListenChannelsProvider>,
    channelOptions: ClassOrValue<IChannelOptionsProvider<any>>,
    tokenRepository: ClassOrValue<TokenRepositoryProvider>
}),

1. ListenChannelsProvider – Definiowanie kanałów do nasłuchiwania

Musisz określić, które kanały Twitcha bot ma monitorować. W tym celu tworzysz własny provider implementujący IListenChannelsProvider

Wymagane metody:

  • getChannelIds() – zwraca listę identyfikatorów kanałów (string[] lub Promise<string[]>)

  • getRefreshInterval() – określa, jak często lista kanałów ma być odświeżana (w milisekundach)

/* Wymagany interfejs */
interface IListenChannelsProvider {
    getChannelIds(): Promise<string[]> | string[];
    getRefreshInterval(): number;
}

💡 Przykład: Jeśli chcesz, aby bot nasłuchiwał dwóch kanałów i odświeżał listę co 60 sekund, Twój provider może wyglądać tak:

class MyListenChannelsProvider implements IListenChannelsProvider {
    getChannelIds(): string[] {
        return ['123456', '7891011'];
    }
    getRefreshInterval(): number {
        return 60000; // 60 sekund
    }
}

Metoda getChannelIds() może korzystać również z zewnętrznych źródeł takich jak: lokalny plik, endpoint, baza danych - jedynym wymogiem jest zwrócenie identyfikatorów użytkowników na platformie twitch.


2. ChannelOptionsProvider – Konfiguracja kanałów

Provider ma domyślnie ustawione pole prefix dla komend bota, ale możesz dodać dodatkowe opcje, np. zapisywanie czasu ostatniej wiadomości. Tworzysz własny provider implementujący IChannelOptionsProvider<Extend>, gdzie Extend zawiera dodatkowe ustawienia dla kanału.

Wymagane metody:

  • getOptions(channelId: string) – pobiera konfigurację dla danego kanału.

  • setOptions(channelId: string, options: TChannelOptions<Extend>) – pozwala zmienić konfigurację kanału.

/* Typy danych */
type ChannelBaseOptions = {
    prefix: string;
}

type TChannelOptions<Extend extends Record<string, any>> = ChannelBaseOptions & Extend;

type TorPromiseT<T> = T | Promise<T>;
/* Wymagany interfejs */
interface IChannelOptionsProvider<Extend extends Record<string, any>> {
    getOptions(channelId: string): TorPromiseT<TChannelOptions<Extend>>;
    setOptions(channelId: string, options: TChannelOptions<Extend>): TorPromiseT<void>;
}

💡 Przykład: Jeśli chcesz dodać opcję przechowywania czasu ostatniej wiadomości (lastMessageTimestamp):

interface MyChannelOptions {
    lastMessageTimestamp: number;
}

class MyChannelOptionsProvider implements IChannelOptionsProvider<MyChannelOptions> {
    private readonly baseOptions: ChannelOptionsExtend = {
        prefix: '!',
        lastMessageTimestamp: 0,
    };
    
    private readonly changedOptions = new Map<string, ChannelOptionsExtend>();

    async getOptions(channelId: string): Promise<ChannelOptionsExtend> {
        if (this.changedOptions.has(channelId)) {
            return this.changedOptions.get(channelId) as ChannelOptionsExtend;
        }
        return this.baseOptions;
    }
    
    async setOptions(channelId: string, options: ChannelOptionsExtend): Promise<void> {
        this.changedOptions.set(channelId, options);
    }
}

Identycznie jak w poprzednim przypadku - getOptions oraz setOptions mogą wykorzystywać zewnętrzne źródło danych (np.: odczyt/zapis do/z bazy danych). W powyższym przykładzie konfiguracje przechowuje się w zmiennej changedOptions. Pamiętaj jednak, że getOptions powinno zawsze zwrócić wszystkie wymagane opcje (tutaj: gdy opcje nie zostały nadpisane, zwraca bazowe ustawienie).

Pole prefix będzie później wykorzystywane do wywoływania komend z czatu (każdy czat może mieć inny prefix. Prefix można zmienić w czasie działania programu - patrz: !prefix)


3. TokenRepositoryProvider – Obsługa tokenów autoryzacyjnych

Bot wymaga tokenów dostępu do API Twitcha. Musisz dostarczyć własny provider implementujący ITokenRepositoryProvider, który zarządza przechowywaniem i aktualizacją tokenów.

Wymagane metody:

  • Pobieranie i zapisywanie tokenów aplikacji:

    • getAppToken(): Pozyskanie tokenu dostępu dla aplikacji

    • saveAppToken(token): Zapisanie tokenu dostępu dla aplikacji

    UWAGA!: Gdy getAppToken() zwróci null framework sam wyśle żądanie o ten token oraz zapisze go przy użyciu saveAppToken(token). Nie musisz wysyłać żadnych żądań manualnie o token.

  • Pobieranie, zapisywanie i usuwanie tokenów użytkownika:

    • getUserAccessToken(userId): Pozyskiwanie tokenu dostępu dla konkretnego użytkownika

    • saveUserAccessToken(userId, token): Zapisywanie tokenu dostępu dla konkretnego użytkownika

    • removeUserAccessToken(userId): Usuwanie tokenu dostępu dla konkretnego użytkownika

    UWAGA!: Podobnie jak w powyższym przypadku dla tokenów dostępu dla aplikacji - powyższe metody służą do obsługi "pamięci" - jeżeli getUserAccessToken(userId) zwróci null - wykorzystana zostanie poniższa metoda getUserRefreshToken(userId) w celu pozyskania tokena dostępu dla użytkownika, a następnie saveUserAccessToken(userId, token) w celu zapisania tokena (token dostępu ważny jest przez około 15 minut od wygenerowania, po tym czasie zostanie automatycznie usunięty - metoda removeUserAccessToken(userId)). Jednak jeżeli getUserRefreshToken(userId) zwróci null - pozyskanie tokenu dostępu będzie niemożliwe - żądana akcja (np.: żądanie API, inne działanie wymagające tokenu dostępu tego użytkownika) zakończy się niepowodzeniem.

  • Pobieranie i usuwanie refresh tokenów:

    • getUserRefreshToken(userId): Pozyskiwanie tokenu odświeżającego token dostępowy dla konkretnego użytkownika.

    • removeUserRefreshToken(userId): Usuwanie tokenu odświeżającego token dostępowy dla konkretnego użytkownika.

    UWAGA!: Metoda removeUserRefreshToken(userId) wywoływana jest w momencie, gdy żądanie o token dostępu dla tego użytkownika zakończy się niepowodzeniem (możliwe powody: token jest nieprawidłowy, użytkownik cofnął autoryzacje aplikacji, inne). UWAGA 2!: Metoda getUserRefreshToken(userId) dla userId, które odpowiada userId zdefiniowanemu w dekoratorze @TwitchBot powinien zawsze zwrócić token odświeżający. W innym przypadku większość funkcji bota będzie bezużyteczna.

/* Typy danych */
type TokenSaveTimestamp = {
    savedAt: number;
}

export type AppToken = {
    access_token: string;
    expires_in: number;
} & TokenSaveTimestamp;

export type UserToken = {
    access_token: string;
    refresh_token: string;
    expires_in: number;
    scope: string[];
} & TokenSaveTimestamp;
/* Wymagany interfejs */
export interface ITokenRepositoryProvider {
    // App / Client
    getAppToken(): Promise<AppToken | null>;
    saveAppToken(token: AppToken): Promise<void>;
    // User - Access
    getUserAccessToken(userId: string): Promise<UserToken | null>;
    saveUserAccessToken(userId: string, token: UserToken): Promise<void>;
    removeUserAccessToken(userId: string): Promise<void>;
    // User - Refresh
    getUserRefreshToken(userId: string): Promise<string | null>;
    removeUserRefreshToken(userId: string): Promise<void>;
};

💡 Przykład: W projekcie została utworzona predefiniowana klasa InMemoryTokenRepository, która implementuje wymagany interfejs i może być użyta, w przypadku gdy będziemy korzystać z interakcji, do których wymagany będzie token dostępowy aplikacji lub token dostępowy użytkownika (zdefiniowanego jako bot - userId).

import { AppToken, ITokenRepositoryProvider, UserToken } from "@tfxjs/tfxjs";

export default class InMemoryTokenRepository implements ITokenRepositoryProvider {
    private logger: Logger;
    private appAccessToken: AppToken | null = null;
    private userAccessTokens: Map<string, UserToken> = new Map();
    private userRefreshTokens: Map<string, string> = new Map();

    constructor(userId: string, userRefreshToken: string) {
        if(!userId || !userRefreshToken) {
            const message = 'User ID and refresh token must be provided';
            this.logger.error(message);
            throw new Error(message);
        }
        this.userRefreshTokens.set(userId, userRefreshToken);
    }

    getAppToken(): Promise<AppToken | null> {
        return Promise.resolve(this.appAccessToken);
    }

    saveAppToken(token: AppToken): Promise<void> {
        this.appAccessToken = token;
        return Promise.resolve();
    }

    getUserAccessToken(userId: string): Promise<UserToken | null> {
        return Promise.resolve(this.userAccessTokens.get(userId) || null);
    }

    saveUserAccessToken(userId: string, token: UserToken): Promise<void> {
        this.userAccessTokens.set(userId, token);
        return Promise.resolve();
    }

    removeUserAccessToken(userId: string): Promise<void> {
        this.userAccessTokens.delete(userId);
        return Promise.resolve();
    }

    getUserRefreshToken(userId: string): Promise<string | null> {
        return Promise.resolve(this.userRefreshTokens.get(userId) || null);
    }

    removeUserRefreshToken(userId: string): Promise<void> {
        this.userRefreshTokens.delete(userId);
        return Promise.resolve();
    }
}

Aby użyć powyższej klasy w Twoim projekcie musisz utworzyć instancje tej klasy wraz z wymaganymi w konstruktorze argumentami (userRefreshToken powinien być wygenerowany przy użyciu userId). Następnie musisz przekazać instancję w opcjach modułu z użyciem pola useValue

ChatBotModule.forRoot({
    /* 2 pozostałe providery */
    tokenRepository: { useValue: new InMemoryTokenRepository(userId, userRefreshToken) },
})

Dla bardziej zaawansowanych przypadków (np.: korzystanie z tokenów odświeżających i dostępowych większej liczby użytkowników) należy zdefiniować tą klasę samemu. Krytycznie ważne metody dla takiego zastosowania to:

  • getUserRefreshToken(userId)

  • removeUserRefreshToken(userId)

Powinny one korzystać z zewnętrznego źródła, gdzie przechowywane są tokeny odświeżające (zapisane po pomyślnej autoryzacji użytkownika dla tej aplikacji).

class AdvancedTokenRepositoryProvider implements ITokenRepositoryProvider {
    private appToken: AppToken | null = null;
    private userTokens: Map<string, UserToken> = new Map();

    async getAppToken(): Promise<AppToken | null> {
        return this.appToken;
    }

    async saveAppToken(token: AppToken): Promise<void> {
        this.appToken = token;
    }

    async getUserAccessToken(userId: string): Promise<UserToken | null> {
        return this.userTokens.get(userId) || null;
    }

    async saveUserAccessToken(userId: string, token: UserToken): Promise<void> {
        this.userTokens.set(userId, token);
    }

    async removeUserAccessToken(userId: string): Promise<void> {
        this.userTokens.delete(userId);
    }

    async getUserRefreshToken(userId: string): Promise<string | null> {
        // Pobieranie z: Zewnętrzne źródło tokenów odświeżających
    }

    async removeUserRefreshToken(userId: string): Promise<void> {
        // Usuwanie z: Zewnętrzne źródło tokenów odświeżających
    }
}

Identycznie jak w poprzednich przypadkach - wszystkie metody mogą wykorzystywać zewnętrzne źródła danych.

Last updated