Source: extensions/twitch/server/server.js

/**
 * Copyright (C) 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
// ============================================================================
//                           IMPORTS/VARIABLES
// ============================================================================
import fs from "fs";
import { dirname } from "path";
import { fileURLToPath } from "url";
import * as logger from "../../../backend/data_center/modules/logger.js";
import sr_api from "../../../backend/data_center/public/streamroller-message-api.cjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
//twurple imports
import { ApiClient } from '@twurple/api';
import { StaticAuthProvider } from '@twurple/auth';


// our helper files
import * as eventSubApi from "./eventsub.js";


// local config for volatile data
const localConfig =
{
    host: "",
    port: "",
    DataCenterSocket: null,
    heartBeatTimeout: 5000,
    heartBeatHandle: null,
    status: {
        connected: false,
        color: "red"
    },
    twitchReconnectTimer: 5000,
    SYSTEM_LOGGING_TAG: "[EXTENSION]",

    clientId: "",
    streamerData: "",
    authProvider: "",
    apiClient: null,
    gameCategories: [],
    // variables from the Settings widget
    twitchCategoriesDropdownId: "twitchGameCategoryDropdownSelector",
    twitchTitleDropdownId: "twitchTitleDropdownSelector",
    twitchTitlesTextElementId: "twitchTitleTextElement",
    // use these fields to send errors in searching back to the user
    twitchCategoryErrorsText: "",
    twitchCategoryErrorsShowCounter: 0,
    currentTwitchGameCategoryId: -1, // as reported by twitch
}
const default_serverConfig = {
    __version__: "0.5",
    extensionname: "twitch",
    channel: "TWITCH",
    twitchenabled: "off",
    twitchresetdefaults: "off",
    twitchstreamername: "",
    twitchCategoriesHistory: [],
    twitchGameCategory_clearHistory: "off",
    twitchTitlesHistory: [],
    lastSelectedTwitchTitleId: -1
}
let serverConfig = structuredClone(default_serverConfig);
const localCredentials =
{
    twitchOAuthState: "",
    twitchOAuthToken: ""
}
// To further expand this list check out HELIX APIS at
// https://twurple.js.org/reference/api/classes/ApiClient.html
// 1) select the api and functions you want on the left you want to implement/add
// 2) add the action_ trigger and function
// 3) check that a callback trigger_ is defined (should be in the triggers list and implemented in the eventsub.js script)
// 5) run a test to see if the scopes need updating in twitch.js (for authorizing a user, will need to re-authorize through the admin page to update the twitch tokens)
//
// triggers are implemented in eventsub.js
const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "Twitch handles messages to and from twitch",
    version: "0.1",
    channel: serverConfig.channel,
    triggers:
        [
            {
                name: "UserBanned",
                displaytitle: "UserBanned",
                description: "A user was banned",
                messagetype: "trigger_TwitchUserBanned",
                parameters: {
                    streamer: "",
                    endDate: "",
                    isPermanent: "",
                    moderator: "",
                    reason: "",
                    user: ""
                }
            },
            {
                name: "CharityCampaignProgress",
                displaytitle: "Charity Campaign Progress",
                description: "Progress of a charity Campaign",
                messagetype: "trigger_TwitchCharityCampaignProgress",
                parameters: {
                    streamer: "",
                    charityName: "",
                    charityDescription: "",
                    charityWebsite: "",
                    charityLogo: "",
                    currentAmount: "",
                    targetAmount: "",
                    id: "",
                }
            },
            {
                name: "CharityCampaignStart",
                displaytitle: "Charity Campaign Start",
                description: "Start of a charity campaign",
                messagetype: "trigger_TwitchCharityCampaignStart",
                parameters: {
                    streamer: "",
                    charityName: "",
                    charityDescription: "",
                    charityWebsite: "",
                    charityLogo: "",
                    currentAmount: "",
                    targetAmount: "",
                    id: "",
                    startDate: ""
                }
            },
            {
                name: "CharityCampaignStop",
                displaytitle: "Charity Campaign Stop",
                description: "Charity campaign stopped",
                messagetype: "trigger_TwitchCharityCampaignStop",
                parameters: {
                    streamer: "",
                    charityName: "",
                    charityDescription: "",
                    charityWebsite: "",
                    charityLogo: "",
                    currentAmount: "",
                    targetAmount: "",
                    id: "",
                    endDate: ""
                }
            },
            {
                name: "CharityDonation",
                displaytitle: "Charity Donation",
                description: "Charity Donation",
                messagetype: "trigger_TwitchCharityDonation",
                parameters: {
                    streamer: "",
                    amount: "",
                    campaignId: "",
                    charityName: "",
                    charityDescription: "",
                    charityWebsite: "",
                    charityLogo: "",
                    donor: ""
                }
            },
            {
                name: "Cheer",
                displaytitle: "Cheer",
                description: "Someone donated some bits",
                messagetype: "trigger_TwitchCheer",
                parameters: {
                    streamer: "",
                    bits: "",
                    isAnonymous: "",
                    message: "",
                    user: "",
                }
            },
            {
                name: "Follow",
                displaytitle: "Follow",
                description: "Someone Followed",
                messagetype: "trigger_TwitchFollow",
                parameters: {
                    streamer: "",
                    user: "",
                }
            },
            {
                name: "GoalBegin",
                displaytitle: "Goal Begin",
                description: "A stream goal began",
                messagetype: "trigger_TwitchGoalBegin",
                parameters: {
                    streamer: "",
                    currentAmount: "",
                    description: "",
                    startDate: "",
                    targetAmount: "",
                    type: "",
                }
            },
            {
                name: "GoalEnd",
                displaytitle: "Goal End",
                description: "A stream goal Ended",
                messagetype: "trigger_TwitchGoalEnd",
                parameters: {
                    streamer: "",
                    currentAmount: "",
                    description: "",
                    startDate: "",
                    endDate: "",
                    targetAmount: "",
                    type: "",
                    isAchieved: ""
                }
            },
            {
                name: "GoalProgress",
                displaytitle: "Goal Progress",
                description: "A stream goal Progress",
                messagetype: "trigger_TwitchGoalProgress",
                parameters: {
                    streamer: "",
                    currentAmount: "",
                    description: "",
                    startDate: "",
                    targetAmount: "",
                    type: "",
                }
            },
            {
                name: "HypeTrainBegin",
                displaytitle: "Hype Train Begin",
                description: "A Hype Train Started",
                messagetype: "trigger_TwitchHypeTrainBegin",
                parameters: {
                    streamer: "",
                    expiryDate: "",
                    goal: "",
                    id: "",
                    lastContribution: "",
                    level: "",
                    progress: "",
                    startDate: "",
                    topContributors: "",
                    total: "",
                }
            },
            {
                name: "HypeTrainEnd",
                displaytitle: "Hype Train End",
                description: "A Hype Train Ended",
                messagetype: "trigger_TwitchHypeTrainEnd",
                parameters: {
                    streamer: "",
                    cooldownEndDate: "",
                    endDate: "",
                    id: "",
                    level: "",
                    startDate: "",
                    topContributors: "",
                    total: "",
                }
            },
            {
                name: "HypeTrainProgress",
                displaytitle: "HypeTrainProgress",
                description: "A hypeTrain is in progress",
                messagetype: "trigger_TwitchHypeTrainProgress",
                parameters: {
                    streamer: "",
                    expiryDate: "",
                    goal: "",
                    id: "",
                    lastContribution: "",
                    level: "",
                    progress: "",
                    startDate: "",
                    topContributors: "",
                    total: "",
                }
            },
            {
                name: "ModAdded",
                displaytitle: "Mod Added",
                description: "A User was added to the Mod list",
                messagetype: "trigger_TwitchModAdded",
                parameters: {
                    user: ""
                }
            },
            {
                name: "ModRemoved",
                displaytitle: "Mod Removed",
                description: "A User was removed to the Mod list",
                messagetype: "trigger_TwitchModRemoved",
                parameters: {
                    user: ""
                }
            },
            {
                name: "PollBegin",
                displaytitle: "PollBegin",
                description: "A Poll Started",
                messagetype: "trigger_TwitchPollBegin",
                parameters: {
                    streamer: "",
                    bitsPerVote: "",
                    channelPointsPerVote: "",
                    choices: "",
                    endDate: "",
                    id: "",
                    isBitsVotingEnabled: "",
                    isChannelPointsVotingEnabled: "",
                    startDate: "",
                    title: "",
                }
            },
            {
                name: "PollEnd",
                displaytitle: "PollEnd",
                description: "A Poll Ended",
                messagetype: "trigger_TwitchPollEnd",
                parameters: {
                    streamer: "",
                    bitsPerVote: "",
                    channelPointsPerVote: "",
                    choices: "",
                    endDate: "",
                    id: "",
                    isBitsVotingEnabled: "",
                    isChannelPointsVotingEnabled: "",
                    startDate: "",
                    status: "",
                    title: "",
                }
            },
            {
                name: "PollProgress",
                displaytitle: "PollProgress",
                description: "A Poll Progressed",
                messagetype: "trigger_TwitchPollProgress",
                parameters: {
                    streamer: "",
                    bitsPerVote: "",
                    channelPointsPerVote: "",
                    choices: "",
                    endDate: "",
                    id: "",
                    isBitsVotingEnabled: "",
                    isChannelPointsVotingEnabled: "",
                    startDate: "",
                    title: "",
                }
            },
            {
                name: "PredictionBegin",
                displaytitle: "Prediction Begin",
                description: "A Prediction Began",
                messagetype: "trigger_TwitchPredictionBegin",
                parameters: {
                    streamer: "",
                    id: "",
                    lockDate: "",
                    outcomes: "",
                    predictions: "",
                    startDate: "",
                    title: "",
                }
            },
            {
                name: "Prediction",
                displaytitle: "Prediction",
                description: "A Prediction ",
                messagetype: "trigger_TwitchPrediction",
                parameters: {
                    streamer: "",
                    id: "",
                    duration: "",
                    title: "",
                    status: "",
                    outcomes: "",
                    winner: "",
                    winnerId: "",
                    lockDate: "",
                    endDate: "",
                }
            },
            {
                name: "PredictionEnd",
                displaytitle: "Prediction End",
                description: "A Prediction Ended",
                messagetype: "trigger_TwitchPredictionEnd",
                parameters: {
                    streamer: "",
                    endDate: "",
                    id: "",
                    outcomes: "",
                    predictions: "",
                    startDate: "",
                    title: "",
                    winningOutcome: "",
                    winningOutcomeId: ""
                }
            },
            {
                name: "PredictionLock",
                displaytitle: "Prediction Lock",
                description: "A Prediction Locked",
                messagetype: "trigger_TwitchPredictionLock",
                parameters: {
                    streamer: "",
                    id: "",
                    lockDate: "",
                    outcomes: "",
                    predictions: "",
                    startDate: "",
                    title: "",
                }
            },
            {
                name: "PredictionProgress",
                displaytitle: "Prediction Progress",
                description: "A Prediction Progressed",
                messagetype: "trigger_TwitchPredictionProgress",
                parameters: {
                    streamer: "",
                    id: "",
                    lockDate: "",
                    outcomes: "",
                    predictions: "",
                    startDate: "",
                    title: "",
                }
            },
            {
                name: "RaidFrom",
                displaytitle: "Raid From",
                description: "Another streamer raided the channel",
                messagetype: "trigger_TwitchRaidFrom",
                parameters: {
                    streamer: "",
                    raider: "",
                    viewers: "",
                }
            },
            {
                name: "RaidTo",
                displaytitle: "Raid To",
                description: "A raid was started",
                messagetype: "trigger_TwitchRaidTo",
                parameters: {
                    streamer: "",
                    raider: "",
                    viewers: "",
                }
            },

            {
                name: "RedemptionAdd",
                displaytitle: "Points Redemption Add",
                description: "A user used channel points for a redemption",
                messagetype: "trigger_TwitchRedemptionAdd",
                parameters: {
                    streamer: "",
                    id: "",
                    message: "",
                    rewardId: "",
                    cost: "",
                    prompt: "",
                    title: "",
                    status: "",
                    user: ""
                }
            },
            {
                name: "RedemptionUpdate",
                displaytitle: "Points Redemption Update",
                description: "A user used channel points for a redemption update??",
                messagetype: "trigger_TwitchRedemptionUpdate",
                parameters: {
                    streamer: "",
                    message: "",
                    cost: "",
                    id: "",
                    prompt: "",
                    title: "",
                    user: ""
                }
            },
            {
                name: "RewardAdd",
                displaytitle: "Reward Add",
                description: "Reward added by streamer to channel",
                messagetype: "trigger_TwitchRewardAdd",
                parameters: {
                    autoApproved: "",
                    backgroundColor: "",
                    broadcasterDisplayName: "",
                    broadcasterId: "",
                    broadcasterName: "",
                    cooldownExpiryDate: "",
                    cost: "",
                    globalCooldown: "",
                    id: "",
                    isEnabled: "",
                    isInStock: "",
                    isPaused: "",
                    maxRedemptionsPerStream: "",
                    maxRedemptionsPerUserPerStream: "",
                    prompt: "",
                    redemptionsThisStream: "",
                    title: "",
                    userInputRequired: ""
                }
            },
            {
                name: "RewardRemove",
                displaytitle: "Reward Remove",
                description: "Reward removed by streamer to channel",
                messagetype: "trigger_TwitchRewardRemove",
                parameters: {
                    autoApproved: "",
                    backgroundColor: "",
                    broadcasterDisplayName: "",
                    broadcasterId: "",
                    broadcasterName: "",
                    cooldownExpiryDate: "",
                    cost: "",
                    globalCooldown: "",
                    id: "",
                    isEnabled: "",
                    isInStock: "",
                    isPaused: "",
                    maxRedemptionsPerStream: "",
                    maxRedemptionsPerUserPerStream: "",
                    prompt: "",
                    redemptionsThisStream: "",
                    title: "",
                    userInputRequired: ""
                }
            },
            {
                name: "RewardUpdate",
                displaytitle: "Reward Update",
                description: "Reward updated by streamer to channel",
                messagetype: "trigger_TwitchRewardUpdate",
                parameters: {
                    streamer: "",
                    autoApproved: "",
                    bgColor: "",
                    expiryDate: "",
                    cost: "",
                    cooldown: "",
                    id: "",
                    enabled: "",
                    inStock: "",
                    paused: "",
                    maxPerStream: "",
                    maxPerUserPerStream: "",
                    prompt: "",
                    redemptionsThisStream: "",
                    title: "",
                    inputRequired: ""
                }
            },
            {
                name: "ShieldModeBegin",
                displaytitle: "Shield Mode Begin",
                description: "Shield mode was started",
                messagetype: "trigger_TwitchShieldModeBegin",
                parameters: {
                    streamer: "",
                    moderator: "",
                }
            },
            {
                name: "ShieldModeEnd",
                displaytitle: "Shield Mode End",
                description: "Shield mode was ended",
                messagetype: "trigger_TwitchShieldModeEnd",
                parameters: {
                    streamer: "",
                    moderator: "",
                }
            },
            {
                name: "ShoutoutSent",
                displaytitle: "Shoutout Sent",
                description: "A shoutout was performed by the streamer",
                messagetype: "trigger_TwitchShoutoutCreate",
                parameters: {
                    streamer: "",
                    cooldownDate: "",
                    moderator: "",
                    targetName: "",
                    targetCooldown: "",
                    viewerCount: "",
                }
            },
            {
                name: "ShoutoutReceive",
                displaytitle: "Shoutout Receive",
                description: "A shoutout was received for the streamer",
                messagetype: "trigger_TwitchShoutoutReceive",
                parameters: {
                    streamer: "",
                    shouterName: "",
                    viewerCount: "",
                }
            },
            {
                name: "Subscription",
                displaytitle: "Subscription",
                description: "Someone subscribed",
                messagetype: "trigger_TwitchSubscription",
                parameters: {
                    streamer: "",
                    isGift: "",
                    tier: "",
                    userDisplayName: "",
                }
            },
            {
                name: "SubscriptionEnd",
                displaytitle: "Subscription End",
                description: "Someone subscription ended",
                messagetype: "trigger_TwitchSubscriptionEnd",
                parameters: {
                    streamer: "",
                    isGift: "",
                    tier: "",
                    user: "",
                }
            },
            {
                name: "SubscriptionGift",
                displaytitle: "Subscription Gift",
                description: "Someone gifted a subscription",
                messagetype: "trigger_TwitchSubscriptionGift",
                parameters: {
                    streamer: "",
                    amount: "",
                    cumulativeAmount: "",
                    isGift: "",
                    gifter: "",
                    anon: "",
                    tier: "",

                }
            },
            {
                name: "SubscriptionMessage",
                displaytitle: "Subscription Message",
                description: "Announcement of a channel subscription by the subscriber",
                messagetype: "trigger_TwitchSubscriptionMessage",
                parameters: {
                    streamer: "",
                    cumulativeMonths: "",
                    durationMonths: "",
                    emoteOffsets: "",
                    message: "",
                    streakMonths: "",
                    tier: "",
                    user: ""

                }
            },
            {
                name: "UserUnBanned",
                displaytitle: "UserUnBanned",
                description: "A user was unbanned",
                messagetype: "trigger_TwitchUserUnBanned",
                parameters: {
                    streamer: "",
                    moderator: "",
                    user: ""
                }
            },
            {
                name: "TitleChanged",
                displaytitle: "TitleChanged",
                description: "The Stream title was changed",
                messagetype: "trigger_TwitchTitleChanged",
                parameters: {
                    title: ""
                }
            },
            {
                name: "GamedChanged",
                displaytitle: "Gamed Changed",
                description: "The Game was changed",
                messagetype: "trigger_TwitchGamedChanged",
                parameters: {
                    triggerId: "",
                    gameId: "",
                    name: "",
                    imageURL: ""
                }
            },
            {
                name: "StreamIdChanged",
                displaytitle: "Stream Id Changed",
                description: "The stream ID has changed",
                messagetype: "trigger_TwitchStreamIdChanged",
                parameters: {
                    id: ""
                }
            },
            {
                name: "StreamLanguageChanged",
                displaytitle: "Stream language Changed",
                description: "The stream language has changed",
                messagetype: "trigger_TwitchStreamLanguageChanged",
                parameters: {
                    language: ""
                }
            },
            {
                name: "StreamerNameChanged",
                displaytitle: "Streamer name Changed",
                description: "The streamer name has changed",
                messagetype: "trigger_TwitchStreamerNameChanged",
                parameters: {
                    user: ""
                }
            },
            {
                name: "StreamStarted",
                displaytitle: "Stream Started",
                description: "The Stream Started",
                messagetype: "trigger_TwitchStreamStarted",
                parameters: {}
            },
            {
                name: "StreamEnded",
                displaytitle: "Stream Ended",
                description: "The Stream Ended",
                messagetype: "trigger_TwitchStreamEnded",
                parameters: {}
            },
            {// triggered from here as eventsub doesn't have this event
                name: "CommercialStarted",
                displaytitle: "Commercial started",
                description: "A Commercial was started",
                messagetype: "trigger_TwitchCommercialStarted",
                parameters: {
                    duration: ""
                }
            },
            {// triggered from here as eventsub doesn't have this event
                name: "VIPAdded",
                displaytitle: "VIP Added",
                description: "A User was added to the VIP list",
                messagetype: "trigger_TwitchVIPAdded",
                parameters: {
                    user: ""
                }
            },
            {// triggered from here as eventsub doesn't have this event
                name: "VIPRemoved",
                displaytitle: "VIP Removed",
                description: "A User was removed to the VIP list",
                messagetype: "trigger_TwitchVIPRemoved",
                parameters: {
                    user: ""
                }
            },
            {
                name: "Editors",
                displaytitle: "Editors",
                description: "A list of editors for the channel",
                messagetype: "trigger_TwitchEditors",
                parameters: {
                    editors: ""
                }
            },
            {
                name: "VIPs",
                displaytitle: "VIPs",
                description: "A list of VIPs for the channel",
                messagetype: "trigger_TwitchVIPs",
                parameters: {
                    VIPs: ""
                }
            },
            {
                name: "FollowerCount",
                displaytitle: "Follower Count",
                description: "Follower count",
                messagetype: "trigger_TwitchFollowerCount",
                parameters: { count: "" }
            },
            {
                name: "FollowedChannels",
                displaytitle: "Followed Channels",
                description: "Followed channels",
                messagetype: "trigger_TwitchFollowedChannels",
                parameters: { channels: "" }
            },
            {
                name: "CheerEmotes",
                displaytitle: "Cheer Emotes",
                description: "Cheer emotes",
                messagetype: "trigger_TwitchCheerEmotes",
                parameters: { emotes: "" }
            },
            {
                name: "Leaderboard",
                displaytitle: "Leaderboard",
                description: "Bits leaderboard",
                messagetype: "trigger_TwitchLeaderboard",
                parameters: { leaderboard: "" }
            },
            {
                name: "Poll",
                displaytitle: "Poll",
                description: "A poll",
                messagetype: "trigger_TwitchPoll",
                parameters: {
                    id: "",
                    title: "",
                    status: "",
                    choices: "",
                    duration: "",
                    enabled: "",
                    pointsPerVote: "",
                    startDate: "",
                    endDate: "",
                }
            },
            {
                name: "UserBlocks",
                displaytitle: "User blocks",
                description: "Who this user has blocked",
                messagetype: "trigger_TwitchUserBlocks",
                parameters: {
                    username: "",
                    blocked: ""
                }
            },
            {
                name: "ClipCreated",
                displaytitle: "Twitch clip created",
                description: "A twitch clip",
                messagetype: "trigger_TwitchClipCreated",
                parameters: {
                    clipName: ""
                }
            },
            {
                name: "VodClip",
                displaytitle: "Twitch Vod clip",
                description: "A twitch vod clip",
                messagetype: "trigger_TwitchVodClip",
                parameters: {
                    streamer: "",
                    date: "",
                    creator: "",
                    duration: "",
                    embedUrl: "",
                    gameId: "",
                    id: "",
                    language: "",
                    thumbnail: "",
                    title: "",
                    url: "",
                    videoId: "",
                    views: "",
                    vodOffset: ""
                }
            },
            {
                name: "UserDetails",
                displaytitle: "Twitch User Details",
                description: "Twitch User Data",
                messagetype: "trigger_TwitchUserDetails",
                parameters: {
                    username: "",
                    userNameInvalid: false,
                    userId: "",
                    userDisplayName: "",
                    creationDate: "",
                    description: "",
                    offlinePlaceholderUrl: "",
                    profilePictureUrl: "",
                    type: ""
                }
            },
            {
                name: "GameCategories",
                displaytitle: "List of games",
                description: "Updated list of games",
                messagetype: "trigger_TwitchGameCategories",
                parameters: {
                    id: 0,
                    games: []
                }
            },
        ],
    actions:
        [
            {
                name: "ChangeTitle",
                displaytitle: "Change Title",
                description: "Change channel title",
                messagetype: "action_TwitchChangeTitle",
                parameters: { title: "" }
            },
            {
                name: "StartCommercial",
                displaytitle: "Start Commercial",
                description: "Start a Commercial for (30, 60, 90, 120, 150, 180) seconds",
                messagetype: "action_TwitchStartCommercial",
                parameters: { duration: "" }
            },
            {
                name: "GetEditors",
                displaytitle: "GetEditors",
                description: "Get a list of editors for the channel",
                messagetype: "action_TwitchGetEditors",
                parameters: {}
            },
            {
                name: "GetVIPs",
                displaytitle: "GetVIPs",
                description: "Get a list of VIPs for the channel",
                messagetype: "action_TwitchGetVIPs",
                parameters: {}
            },
            {
                name: "AddVIP",
                displaytitle: "Add VIP",
                description: "Promote user to VIP",
                messagetype: "action_TwitchAddVIP",
                parameters: { user: "" }
            },
            {
                name: "RemoveVIP",
                displaytitle: "Remove VIP",
                description: "Demote user from VIP",
                messagetype: "action_TwitchRemoveVIP",
                parameters: { user: "" }
            },
            {
                name: "AddMod",
                displaytitle: "Add Mod",
                description: "Promote user to Mod",
                messagetype: "action_TwitchAddMod",
                parameters: { user: "" }
            },
            {
                name: "RemoveMod",
                displaytitle: "Remove Mod",
                description: "Demote user from Mod",
                messagetype: "action_TwitchRemoveMod",
                parameters: { user: "" }
            },
            {
                name: "Ban",
                displaytitle: "Ban a user",
                description: "Bans a user from the stream",
                messagetype: "action_TwitchBan",
                parameters: { user: "", reason: "" }
            },
            {
                name: "Unban",
                displaytitle: "Unban a user",
                description: "Unbans a user from the stream",
                messagetype: "action_TwitchUnban",
                parameters: { user: "", reason: "" }
            }, {
                name: "FollowerCount",
                displaytitle: "Follower Count",
                description: "Get follower count",
                messagetype: "action_TwitchFollowerCount",
                parameters: {}
            },
            {
                name: "FollowedChannels",
                displaytitle: "Followed Channels",
                description: "Get followed channels",
                messagetype: "action_TwitchFollowedChannels",
                parameters: {}
            },
            {
                name: "CheerEmotes",
                displaytitle: "Cheer Emotes",
                description: "Get cheer emotes",
                messagetype: "action_TwitchCheerEmotes",
                parameters: {}
            },
            {
                name: "Leaderboard",
                displaytitle: "Leaderboard",
                description: "Get bits leaderboard",
                messagetype: "action_TwitchLeaderboard",
                parameters: {}
            },
            {
                name: "GetPolls",
                displaytitle: "Get Polls",
                description: "Gets a list of polls",
                messagetype: "action_TwitchGetPolls",
                parameters: {}
            },
            {
                name: "GetPoll",
                displaytitle: "Get Poll",
                description: "Get a poll",
                messagetype: "action_TwitchGetPoll",
                parameters: { id: "" }
            },
            {
                name: "CreatePoll",
                displaytitle: "Create Poll",
                description: "Create a poll",
                messagetype: "action_TwitchCreatePoll",
                parameters: {
                    title: "",
                    duration: "",
                    choices: "",//comma separated
                    points: ""
                }
            },
            {
                name: "EndPoll",
                displaytitle: "End Poll",
                description: "End a poll",
                messagetype: "action_TwitchEndPoll",
                parameters: {
                    id: "",
                    display: ""
                }
            },
            {
                name: "StartPrediction",
                displaytitle: "StartPrediction",
                description: "Start a prediction",
                messagetype: "action_TwitchStartPrediction",
                parameters: {
                    title: "",
                    choices: "",
                    duration: ""
                }
            },
            {
                name: "CancelPrediction",
                displaytitle: "CancelPrediction",
                description: "Cancel a prediction",
                messagetype: "action_TwitchCancelPrediction",
                parameters: {
                    id: "",
                }
            },
            {
                name: "GetPredictions",
                displaytitle: "Get Predictions",
                description: "Gets a list of predictions",
                messagetype: "action_TwitchGetPredictions",
                parameters: {
                    state: ""
                }
            },
            {
                name: "GetPrediction",
                displaytitle: "Get Prediction",
                description: "Get a prediction",
                messagetype: "action_TwitchGetPrediction",
                parameters: { id: "" }
            },
            {
                name: "LockPrediction",
                displaytitle: "Lock Prediction",
                description: "Lock a prediction",
                messagetype: "action_TwitchLockPrediction",
                parameters: { id: "" }
            },
            {
                name: "RemovePrediction",
                displaytitle: "Remove Prediction",
                description: "Remove a prediction",
                messagetype: "action_TwitchLRemovePrediction",
                parameters: { id: "" }
            },
            {
                name: "ResolvePrediction",
                displaytitle: "Resolve Prediction",
                description: "Resolve a prediction",
                messagetype: "action_TwitchLResolvePrediction",
                parameters: {
                    id: "",
                    outcomeId: ""
                }
            },
            //users
            {
                name: "CreateUserBlock",
                displaytitle: "Create User Block",
                description: "Block a user",
                messagetype: "action_TwitchCreateBlock",
                parameters: {
                    username: "",
                    reason: "",
                    context: ""
                }
            },
            {
                name: "DeleteUserBlock",
                displaytitle: "Delete User Block",
                description: "Unblock a user",
                messagetype: "action_TwitchDeleteBlock",
                parameters: {
                    username: "",
                }
            },
            {
                name: "GetUser",
                displaytitle: "Get Users Details",
                description: "Gets a Users Details",
                messagetype: "action_TwitchGetUser",
                parameters: {
                    username: "",
                    data: null,
                }
            },
            {
                name: "GetBlocks",
                displaytitle: "Get blocked users",
                description: "Get a list of blocked users",
                messagetype: "action_TwitchGetBlocks",
                parameters: {}
            },
            {
                name: "CreateClip",
                displaytitle: "Create clip",
                description: "Create a twitch clip",
                messagetype: "action_TwitchCreateClip",
                parameters: {}
            },
            {
                name: "GetClipById",
                displaytitle: "Get Clip By Id",
                description: "Get clip by id",
                messagetype: "action_TwitchGetClipById",
                parameters: { clipName: "" }
            },
            {
                name: "GetClipsForBroadcaster",
                displaytitle: "Get Clips For Broadcaster",
                description: "Get clips for a broadcaster",
                messagetype: "action_TwitchGetClipsForBroadcaster",
                parameters: {
                    name: "",
                    count: ""
                }
            },
            {
                name: "GetClipsForGame",
                displaytitle: "Get Clips For Game",
                description: "Get clips for a game",
                messagetype: "action_TwitchGetClipsForGame",
                parameters: {
                    game: "",
                    count: ""
                }
            },
            {
                name: "TwitchGameCategories",
                displaytitle: "Get a list of games",
                description: "Get the list of games",
                messagetype: "action_GetTwitchGameCategories",
                parameters: {
                    id: 0,
                }
            },
            {
                name: "TwitchGetStats",
                displaytitle: "Get the Current Twitch Stats",
                description: "Return will be a set of triggers for current game etc",
                messagetype: "action_GetTwitchStats",
                parameters: {
                    actionID: "",
                }
            },

        ],
}
// ============================================================================
//                           FUNCTION: start
// ============================================================================
/**
 * Starts the extension using the given data.
 * @param {string} host 
 * @param {number} port 
 * @param {number} nonce
 * @param {number} clientId
 * @param {number} heartbeat 
 */
function start (host, port, nonce, clientId, heartbeat)
{
    localConfig.host = host;
    localConfig.port = port;
    localConfig.clientId = clientId;
    if (typeof (heartbeat) != "undefined")
        localConfig.heartBeatTimeout = heartbeat;
    else
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".start", "DataCenterSocket no heartbeat passed:", heartbeat);
    try
    {
        ConnectToDataCenter(localConfig.host, localConfig.port);
    }
    catch (err)
    {
        logger.err(serverConfig.extensionname + " server.start", "initialise failed:", err.message);
    }
}
// ============================================================================
//                           FUNCTION: ConnectToDataCenter
// ============================================================================
/**
 * Connects to the StreamRoller server
 * @param {string} host 
 * @param {number} port 
 */
function ConnectToDataCenter (host, port)
{
    try
    {
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect, onDataCenterDisconnect,
            host, port);
    } catch (err)
    {
        logger.err(serverConfig.extensionname + " server.initialise", "DataCenterSocket connection failed:", err);
    }
}
// ============================================================================
//                           FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
 * called on StreamRoller websocket disconnect
 * @param {string} reason 
 */
function onDataCenterDisconnect (reason)
{
    logger.err(serverConfig.extensionname + " server.initialise", "DataCenterSocket connection failed:", reason);
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
/**
 * called on StreamRoller websocket connect
 * @param {object} socket 
 */
function onDataCenterConnect (socket)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestCredentials", serverConfig.extensionname));

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel));

    clearTimeout(localConfig.heartBeatHandle);
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * Called when we receive a StreamRoller message
 * @param {object} server_packet 
 */
function onDataCenterMessage (server_packet)
{
    try
    {
        // -----------------------------------------------------------------------------------
        //                  RECEIVED CONFIG
        // -----------------------------------------------------------------------------------
        if (server_packet.type === "ConfigFile")
        {
            // check it is our config
            if (server_packet.data
                && server_packet.data.extensionname
                && server_packet.data.extensionname === serverConfig.extensionname)
            {
                let configSubVersions = 0;
                let defaultSubVersions = default_serverConfig.__version__.split('.');
                if (server_packet.data == "")
                {
                    // server data is empty, possibly the first run of the code so just default it
                    serverConfig = structuredClone(default_serverConfig);
                    SaveConfigToServer();
                }
                else
                    configSubVersions = server_packet.data.__version__.split('.')

                if (configSubVersions[0] != defaultSubVersions[0])
                {
                    // Major version number change. Replace config with defaults
                    // perform a deep clone overwriting our server config.
                    serverConfig = structuredClone(default_serverConfig);
                    // notify the user their config has been updated.
                    console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated", "The config file has been Updated to the latest version v" + default_serverConfig.__version__ + ". Your settings may have changed" + "\x1b[0m");
                    SaveConfigToServer();
                }
                else if (configSubVersions[1] != defaultSubVersions[1])
                {
                    // Minor version number change. Overwrite config with defaults
                    // perform a merge replacing any values we currently have and keeping the new variables
                    serverConfig = { ...default_serverConfig, ...server_packet.data };
                    // update the version number to the current default number
                    serverConfig.__version__ = default_serverConfig.__version__;
                    console.log(serverConfig.extensionname + " ConfigFile Updated", "The config file has been Updated to the latest version v" + default_serverConfig.__version__);
                    SaveConfigToServer();
                }
                else
                    // no version number changed so we can just saved file
                    serverConfig = structuredClone(server_packet.data);
            }
        }
        // -----------------------------------------------------------------------------------
        //                  RECEIVED CREDENTIALS
        // -----------------------------------------------------------------------------------
        else if (server_packet.type === "CredentialsFile")
        {
            if (server_packet.to === serverConfig.extensionname && server_packet.data && server_packet.data != ""
                && server_packet.data.twitchOAuthState != "" && server_packet.data.twitchOAuthToken != "")
            {
                localCredentials.twitchOAuthState = server_packet.data.twitchOAuthState;
                localCredentials.twitchOAuthToken = server_packet.data.twitchOAuthToken;
                localConfig.authProvider = new StaticAuthProvider(localConfig.clientId, localCredentials.twitchOAuthToken);
                if (serverConfig.twitchenabled == "on")
                {
                    setTimeout(() =>
                    {
                        disconnectTwitch();
                        connectTwitch();
                    }, 1000);
                }
            }
        }
        // -----------------------------------------------------------------------------------
        //                      ### EXTENSION MESSAGE ###
        // -----------------------------------------------------------------------------------
        else if (server_packet.type === "ExtensionMessage")
        {

            let extension_packet = server_packet.data;
            // -----------------------------------------------------------------------------------
            //                   REQUEST FOR SETTINGS DIALOG
            // -----------------------------------------------------------------------------------
            if (extension_packet.type === "RequestSettingsWidgetSmallCode")
            {
                SendSettingsWidgetSmall(extension_packet.from);
            }
            // -----------------------------------------------------------------------------------
            //                   REQUEST FOR CREDENTIALS DIALOG
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "RequestCredentialsModalsCode")
                SendCredentialsModal(extension_packet.from);
            // -----------------------------------------------------------------------------------
            //                   SETTINGS DIALOG DATA
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "SettingsWidgetSmallData")
            {
                if (extension_packet.to === serverConfig.extensionname)
                {
                    // check if we have asked to reset to defaults
                    if (extension_packet.data.twitchresetdefaults == "on")
                    {
                        serverConfig = structuredClone(default_serverConfig);
                        console.log("\x1b[31m" + serverConfig.extensionname + " Defaults restored", "The config files have been reset. Your settings may have changed" + "\x1b[0m");
                        SaveConfigToServer();
                    }

                    else
                    {
                        let restart = handleSettingsWidgetSmallData(extension_packet.data);
                        SaveConfigToServer();

                        if (restart)
                        {
                            if (localConfig.status.connected)
                                disconnectTwitch();
                            connectTwitch()
                        }
                    }
                    SendSettingsWidgetSmall();
                }
            }
            // -----------------------------------------------------------------------------------
            //                   REQUEST FOR USER TRIGGERS
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "SendTriggerAndActions")
            {
                sr_api.sendMessage(localConfig.DataCenterSocket,
                    sr_api.ServerPacket("ExtensionMessage",
                        serverConfig.extensionname,
                        sr_api.ExtensionPacket(
                            "TriggerAndActions",
                            serverConfig.extensionname,
                            triggersandactions,
                            "",
                            server_packet.from
                        ),
                        "",
                        server_packet.from
                    )
                )
            }
            // -----------------------------------------------------------------------------------
            //                   Change title
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchChangeTitle")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.title != "")
                        setStreamTitle(extension_packet.data.title)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to change title with no title provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchStartCommercial
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchStartCommercial")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.duration != "")
                        startCommercial(extension_packet.data.duration)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to start a commercial with no duration provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetEditors
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetEditors")
            {
                if (serverConfig.twitchenabled == "on")
                    getChannelEditors()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetVIPs
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetVIPs")
            {
                if (serverConfig.twitchenabled == "on")
                    getChannelVIPs()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchAddVIP
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchAddVIP")
            {

                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.user != "")
                        addVIP(extension_packet.data.user)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to VIP a user with no username provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchRemoveVIP
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchRemoveVIP")
            {
                if (serverConfig.twitchenabled == "on") 
                {
                    if (extension_packet.data.user != "")
                        removeVIP(extension_packet.data.user)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to remove VIP from a user with no username provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchAddMod
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchAddMod")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.user != "")
                        addMod(extension_packet.data.user)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to Mod a user with no username provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchRemoveMod
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchRemoveMod")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.user != "")
                        removeMod(extension_packet.data.user)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to remove Mod from a user with no username provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchBan
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchBan")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.user != "")
                        banUser(extension_packet.data.user, extension_packet.data.reason)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to ban a user with no username provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchUnban
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchUnban")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.user != "")
                        unbanUser(extension_packet.data.user, extension_packet.data.reason)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to unban user with no username provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchFollowerCount
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchFollowerCount")
            {
                if (serverConfig.twitchenabled == "on")
                    followerCount()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchFollowedChannels
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchFollowedChannels")
            {
                if (serverConfig.twitchenabled == "on")
                    followedChannels()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchCheerEmotes
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchCheerEmotes")
            {
                if (serverConfig.twitchenabled == "on")
                    cheerEmotes()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchLeaderboard
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchLeaderboard")
            {
                if (serverConfig.twitchenabled == "on")
                    leaderboard()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetPolls
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetPolls")
            {
                if (serverConfig.twitchenabled == "on")
                    getPolls()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetPoll
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetPoll")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data.id != "")
                        getPoll(extension_packet.data.id)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to get a poll with no poll id");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchCreatePoll
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchCreatePoll")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        createPoll(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to create a poll with no poll data");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchEndPoll
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchEndPoll")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        endPoll(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to end a poll with no poll data");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchStartPrediction
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchStartPrediction")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        startPrediction(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to start a prediction with no prediction data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchCancelPrediction
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchCancelPrediction")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        cancelPrediction(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to cancel a prediction with no prediction data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetPrediction
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetPrediction")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        getPrediction()
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to get a prediction with no prediction data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetPredictions
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetPredictions")
            {
                if (extension_packet.data != "")
                    if (serverConfig.twitchenabled == "on")
                        getPredictions(extension_packet.data)
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchLockPrediction
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchLockPrediction")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        lockPrediction(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to lock a prediction with no prediction data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchLRemovePrediction
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchLRemovePrediction")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        removePrediction(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to remove a prediction with no prediction data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchLResolvePrediction
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchLResolvePrediction")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        resolvePrediction(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to resolve a prediction with no prediction data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchLCreateBlock
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchCreateBlock")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        createBlock(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to block a user with no data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchLDeleteBlock
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchDeleteBlock")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        deleteBlock(extension_packet.data)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to unblock a user with no data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetUser
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetUser")
            {
                if (serverConfig.twitchenabled == "on")
                {
                    if (extension_packet.data != "")
                        getUser(extension_packet.data.username)
                    else
                        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Attempt to unblock a user with no data provided");
                }
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetBlocks
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetBlocks")
            {
                if (serverConfig.twitchenabled == "on")
                    getBlockedUsers()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchCreateClip
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchCreateClip")
            {
                if (serverConfig.twitchenabled == "on")
                    createClip()
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetClipById
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetClipById")
            {
                if (serverConfig.twitchenabled == "on")
                    getClipById(extension_packet.data)
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetClipsForBroadcaster
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetClipsForBroadcaster")
            {
                if (serverConfig.twitchenabled == "on")
                    getClipsByBroadcaster(extension_packet.data)
            }
            // -----------------------------------------------------------------------------------
            //                   action_TwitchGetClipsForGame
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_TwitchGetClipsForGame")
            {
                if (serverConfig.twitchenabled == "on")
                    getClipsByGame(extension_packet.data)
            }
            // -----------------------------------------------------------------------------------
            //                   action_GetTwitchGameCategories
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_GetTwitchGameCategories")
            {
                sendGameCategoriesTrigger(extension_packet.data.parameters.actionID)
            }
            // -----------------------------------------------------------------------------------
            //                   action_GetTwitchStats
            // -----------------------------------------------------------------------------------
            else if (extension_packet.type === "action_GetTwitchStats")
            {
                sendCurrentGameData(extension_packet.data.actionID)
            }
        }
        // -----------------------------------------------------------------------------------
        //                           UNKNOWN CHANNEL MESSAGE RECEIVED
        // -----------------------------------------------------------------------------------
        else if (server_packet.type === "UnknownChannel")
        {
            // channel might not exist yet, extension might still be starting up so lets 
            // reschedule the join attempt need to add some sort of flood control here 
            // so we are only attempting to join one at a time
            if (server_packet.data != "" && server_packet.channel != undefined)
            {
                setTimeout(() =>
                {
                    sr_api.sendMessage(localConfig.DataCenterSocket,
                        sr_api.ServerPacket(
                            "JoinChannel",
                            serverConfig.extensionname,
                            server_packet.data
                        ));
                }, 5000);
            }
        }
        else if (server_packet.type === "ChannelJoined"
            || server_packet.type === "ChannelCreated"
            || server_packet.type === "ChannelLeft"
            || server_packet.type === "HeartBeat"
            || server_packet.type === "UnknownExtension"
            || server_packet.type === "ChannelJoined"
            || server_packet.type === "LoggingLevel"
        )
        {
            // just a blank handler for items we are not using to avoid message from the catch all
        }
        // ------------------------ unknown message type received ----------------------------------
        else
            logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Unhandled message type:", server_packet);
    }
    catch (error)
    {
        logger.err(serverConfig.extensionname + ".onDataCenterMessage", "Unhandled exception:", error);
    }
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * Save our config to the server
 */
function SaveConfigToServer ()
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            "SaveConfig",
            serverConfig.extensionname,
            serverConfig,
        ));
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 * Send our small settings widget html code to the given extension
 * @param {string} toChannel 
 */
function SendSettingsWidgetSmall (toChannel = "")
{
    fs.readFile(__dirname + "/twitchsettingswidgetsmall.html", function (err, filedata)
    {
        if (err)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
        }
        else
        {
            let modalString = filedata.toString();
            let statusHtml = ""
            if (serverConfig.twitchenabled == "off")
                statusHtml = `<BR><div style = "color:rgb(255 255 0 / 80%)">Extension turned off</div>`
            else if (!localConfig.status.connected)
                statusHtml = `<BR><div style = "color:rgb(255 255 0 / 80%)">Waiting for extension to get data from twitch</div>`

            for (const [key, value] of Object.entries(serverConfig))
            {
                // checkboxes
                if (value === "on")
                    modalString = modalString.replace(key + "checked", "checked");
                else if (typeof (value) === "string" || typeof (value) === "number")
                    modalString = modalString.replaceAll(key + "text", value);
            }

            // This code could be hardcoded but we only want to show it when the twitch extensions is up and running
            if (serverConfig.twitchenabled == "off" || !localConfig.status.connected)
            {
                modalString = modalString.replace("twitchStreamTitleSelector", statusHtml);
                modalString = modalString.replace("twitchGameCategorySelector", statusHtml);
                modalString = modalString.replace("twitchTwitchSearchForGame", statusHtml);
            }
            else
            {
                modalString = modalString.replace("twitchStreamTitleSelector", getTextboxWithHistoryHTML(
                    localConfig.twitchTitleDropdownId,
                    localConfig.twitchTitlesTextElementId,
                    serverConfig.twitchTitlesHistory,
                    serverConfig.lastSelectedTwitchTitleId
                ));
                // add our searchable dropdown category selector
                modalString = modalString.replace("twitchGameCategorySelector",
                    createDropdownWithSearchableHistory(
                        localConfig.twitchCategoriesDropdownId,
                        localConfig.gameCategories,
                        serverConfig.twitchCategoriesHistory,
                        localConfig.currentTwitchGameCategoryId));
                modalString = modalString.replace("twitchTwitchSearchForGame", `<input type="text" class="form-control" id="twitchSearchForTwitchGameElementId" name="twitchSearchForTwitchGameElementId" placeholder="Enter Game name to search for (added to history when found)">`);
            }
            if (localConfig.twitchCategoryErrorsShowCounter > 0)
            {
                localConfig.twitchCategoryErrorsShowCounter--;
                modalString = modalString.replace("twitchGameCategorySearchErrors",
                    "<div>" + localConfig.twitchCategoryErrorsText + "</div>"
                )
            }
            else
                modalString = modalString.replace("twitchGameCategorySearchErrors", "")
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode",
                        serverConfig.extensionname,
                        modalString,
                        "",
                        toChannel,
                        serverConfig.channel
                    ),
                    "",
                    toChannel
                ))
        }
    });
}
// ===========================================================================
//                           FUNCTION: SendCredentialsModal
// ===========================================================================
/**
 * Send our CredentialsModal to whoever requested it
 * @param {string} extensionname 
 */
function SendCredentialsModal (extensionname)
{
    fs.readFile(__dirname + "/twitchcredentialsmodal.html", function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendCredentialsModal", "failed to load modal", err);
        //throw err;
        else
        {
            let modalString = filedata.toString();
            for (const [key, value] of Object.entries(serverConfig))
            {
                if (value === "on")
                    modalString = modalString.replaceAll(key + "checked", "checked");
                else if (typeof (value) == "string" || typeof (value) == "number")
                    modalString = modalString.replaceAll(key + "text", value);
            }
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket("ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "CredentialsModalCode",
                        serverConfig.extensionname,
                        modalString,
                        "",
                        extensionname,
                        serverConfig.channel
                    ),
                    "",
                    extensionname)
            )
        }
    });
}
// ===========================================================================
//                           FUNCTION: handleSettingsWidgetSmallData
// ===========================================================================
/**
 * Handles data from a user submit on our small settings widget
 * @param {object} modalCode 
 * @returns boolean restart connection needed due to data change
 */
function handleSettingsWidgetSmallData (modalCode)
{
    try
    {
        let restartConnection = false;
        /* check enabled change */
        if (serverConfig.twitchenabled != modalCode.twitchenabled)
        {
            restartConnection = true;
            if (modalCode.twitchenabled)
                serverConfig.twitchenabled = "on"
            else
                serverConfig.twitchenabled = "off"
        }
        /* check streamername change */
        if (modalCode.twitchstreamername != serverConfig.twitchstreamername)
        {
            restartConnection = true;
            serverConfig.twitchstreamername = modalCode["twitchstreamername"]
        }

        /* check clear history/cagegories flag */
        let clearTwitchTitles = modalCode[localConfig.twitchTitleDropdownId + "_clearHistory"]
        let clearTwitchCategories = modalCode[localConfig.twitchCategoriesDropdownId + "_clearHistory"]

        if (clearTwitchTitles || clearTwitchCategories)
        {
            if (clearTwitchCategories)
                serverConfig.twitchCategoriesHistory = [];
            if (clearTwitchTitles)
            {
                serverConfig.twitchTitlesHistory = [];
                localConfig.currentTwitchGameCategoryId = -1
            }
            // return now to clear options
            return restartConnection;
        }
        // search for game on twitch
        if (modalCode["twitchSearchForTwitchGameElementId"] && modalCode["twitchSearchForTwitchGameElementId"] != "")
        {
            let gameName = modalCode["twitchSearchForTwitchGameElementId"];
            // Add game name to history list (true)
            addGameToHistoryFromGameName(gameName)
            // return now as we only want to process the search on this submit
            return restartConnection;
        }
        /* Process Twitch Category */
        const userSelectedCategoryId = modalCode[localConfig.twitchCategoriesDropdownId]
        if (userSelectedCategoryId)
        {
            getGameFromId(userSelectedCategoryId)
                .then((game) =>
                {
                    if (game)
                    {
                        // found game check if we have it in our history already
                        const inHistory = serverConfig.twitchCategoriesHistory.findIndex(e => e.id === game.id);
                        // add to history if not already there
                        if (inHistory == -1)
                            serverConfig.twitchCategoriesHistory.push(game)
                        //  set game if we are connected and the game is different
                        if (localConfig.status.connected && localConfig.currentTwitchGameCategoryId != game.id)
                            setStreamGame(game.id)
                    }
                    else
                        logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                            ".handleSettingsWidgetSmallData", "Couldn't find game id on twitch. Id:", userSelectedCategoryId);

                    /* Process Twitch Title */
                    if (modalCode[localConfig.twitchTitlesTextElementId] && modalCode[localConfig.twitchTitlesTextElementId] != "")
                    {
                        let titleIndex = serverConfig.twitchTitlesHistory.findIndex(x => x === modalCode[localConfig.twitchTitlesTextElementId]);
                        if (titleIndex > -1)
                            serverConfig.lastSelectedTwitchTitleId = titleIndex;
                        else
                            serverConfig.lastSelectedTwitchTitleId = serverConfig.twitchTitlesHistory.push(modalCode[localConfig.twitchTitlesTextElementId]) - 1

                        setStreamTitle(serverConfig.twitchTitlesHistory[serverConfig.lastSelectedTwitchTitleId])
                    }
                    return restartConnection
                })
                .catch((err) =>
                {
                    logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".handleSettingsWidgetSmallData.getGameFromId", "Error", err, err.message);
                })
        }
        return restartConnection
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".handleSettingsWidgetSmallData", "Error", err, err.message);
    }
}
// ============================================================================
//                           FUNCTION: heartBeat
// ============================================================================
/**
 * Sends out heartbeat messages so other extensions can see our status
 */
function heartBeatCallback ()
{
    localConfig.status.color = "red"
    if (serverConfig.twitchenabled == "on" && localConfig.status.connected)
        localConfig.status.color = "green"
    else if (serverConfig.twitchenabled == "on")
        localConfig.status.color = "orange"

    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ChannelData",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "HeartBeat",
                serverConfig.extensionname,
                localConfig.status,
                serverConfig.channel),
            serverConfig.channel
        ),
    );
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ===========================================================================
//                           TWITCH PUBSUB
// ===========================================================================

// ===========================================================================
//                           FUNCTION: connectTwitch
// ===========================================================================
/**
 *  Connects to the twitch api
 */
async function connectTwitch ()
{
    try
    {
        if (localConfig.authProvider == "")
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".connectTwitch", "Missing authorization, go to http://localhost:3000/twitch/auth to authorise for twitch");
            return;
        }
        if (serverConfig.twitchstreamername == "")
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".connectTwitch", "Missing stream name, please set a stream name to work with in the settings");
            return;
        }
        // setup client
        let auth = localConfig.authProvider
        localConfig.apiClient = new ApiClient({ authProvider: auth });

        // get some data about the streamer (id etc)
        localConfig.streamerData = await localConfig.apiClient.users.getUserByName(serverConfig.twitchstreamername)

        if (localConfig.streamerData == null)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".connectTwitch", "Streamer Not found");
            return
        }

        // get the current channel info (needed so we can trigger on changes)
        //let channelData = await localConfig.apiClient.channels.getChannelInfoById(localConfig.streamerData.id)
        localConfig.apiClient.channels.getChannelInfoById(localConfig.streamerData.id)
            .then((channelData) =>
            {
                /*console.log("---------GameDataForChannel-------------")
                console.log("contentClassificationLabels", channelData.contentClassificationLabels);
                console.log("delay", channelData.delay);
                console.log("displayName", channelData.displayName);
                console.log("gameId", channelData.gameId);
                console.log("gameName", channelData.gameName);
                console.log("id", channelData.id);
                console.log("isBrandedContent", channelData.isBrandedContent);
                console.log("language", channelData.language);
                console.log("name", channelData.name);
                console.log("tags", channelData.tags);
                console.log("title", channelData.title);*/


                // set our current game id and add it to the history
                localConfig.currentTwitchGameCategoryId = channelData.gameId;
                // need to do this here as we don't have the game image in the current data.
                addGameToHistoryFromGameName(channelData.gameName)
                // Connect to the pub sub event listener
                eventSubApi.init(localConfig, serverConfig, triggersandactions, pubSubTriggerCallback)
                eventSubApi.startEventSub(localConfig.streamerData.id, localConfig.apiClient, channelData)

                // set us to connected at this time
                localConfig.status.connected = true;

                // get the game categories for twitch
                getAllGameCategories();

                // send out the game changed trigger (as we have only just connected) but give chance for extensions
                //to start up (if we have just started the server)
                setTimeout(() =>
                {
                    sendCurrentGameData("twitch")
                }, 5000);

            })
            .catch((err) =>
            {
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".connectTwitch:", "ERROR", err, err.message);
            });
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".connectTwitch", "ERROR", err.message);
        localConfig.status.connected = false;
        setTimeout(() =>
        {
            connectTwitch()
        }, localConfig.twitchReconnectTimer);
    }
}
// ===========================================================================
//                           FUNCTION: getAllGameCategories
// ===========================================================================
/**
 * Collates and sends out the twitch 'Top' category list 
 * @param {string} id trigger identifier or "twitch"
 */
async function getAllGameCategories (id = "twitch")
{
    try
    {
        // Fetch paginated results for top games
        const gamePaginator = localConfig.apiClient.games.getTopGamesPaginated();

        // Asynchronously iterate through all pages
        for await (const game of gamePaginator)
            localConfig.gameCategories.push({ id: game.id, name: game.name, imageURL: game.boxArtUrl })
        sendGameCategoriesTrigger(id)
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getAllGameCategories", "Error fetching categories:", err, err.message);
    }
}
// ===========================================================================
//                           FUNCTION: addGameToHistoryFromGameName
// ===========================================================================
/**
 * Adds a game by name to the history list
 * @param {string} gameName 
 */
async function addGameToHistoryFromGameName (gameName)
{
    try
    {
        // get the game index if we already have the data
        const categoryIndex = localConfig.gameCategories.findIndex(e => e.name === gameName);
        // not got this game in our local cache so get it from twitch
        if (categoryIndex == -1)
        {
            if (!localConfig.apiClient.games)
            {
                console.log("localConfig.apiClient.games not available yet.")
                return null;
            }
            // Fetch the game date if we don't have it already
            localConfig.apiClient.games.getGameByName(gameName)
                .then((game) =>
                {
                    if (game)
                    {
                        if (!serverConfig.twitchCategoriesHistory
                            || serverConfig.twitchCategoriesHistory.findIndex(e => e.name === game.name) == -1)
                        {
                            const gameObject = { id: game.id, name: game.name, imageURL: game.boxArtUrl }
                            serverConfig.twitchCategoriesHistory.push(gameObject);
                            localConfig.twitchCategoryErrorsShowCounter = 0
                            SendSettingsWidgetSmall()
                        }
                    }
                    else
                    {
                        localConfig.twitchCategoryErrorsText = "Couldn't Find Game '" + gameName + "' on twitch"
                        // how many reloads to keep displaying the error 
                        // due to the chance of another update going out too quickly
                        localConfig.twitchCategoryErrorsShowCounter = 3;
                        SendSettingsWidgetSmall()
                    }

                })
                .catch((err) =>
                {
                    logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getAllGameCategories getGameByName failed to fetch Id", "Error fetching categories:", err, err.message);
                })

        }
        else
        {
            // get the game from our list
            let game = localConfig.gameCategories[categoryIndex]
            // if not in the history already then add it.
            if (!serverConfig.twitchCategoriesHistory || serverConfig.twitchCategoriesHistory.findIndex(e => e.name === game.name) == -1)
            {
                serverConfig.twitchCategoriesHistory.push(game);
                localConfig.twitchCategoryErrorsShowCounter = 0;
            }

        }
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getAllGameCategories", "Error fetching categories:", err, err.message);
        return null
    }
}
// ===========================================================================
//                           FUNCTION: getGameFromId
// ===========================================================================
/**
 * Get a game object from a game Id
 * @param {number} gameId 
 * @returns gameobject
 */
async function getGameFromId (gameId)
{

    try
    {
        // check if we have this game in our list from twitch
        const categoryIndex = localConfig.gameCategories.findIndex(e => e.id === gameId);
        if (!localConfig.apiClient || !localConfig.apiClient.games)
        {
            console.log("localConfig.apiClient.games not available yet.")
            return null;
        }
        if (categoryIndex == -1)
        {
            // wd don't have this game so lets go get it.
            localConfig.apiClient.games.getGameById(gameId)
                .then((game) => 
                {
                    if (game)
                        return { id: game.id, name: game.name, imageURL: game.boxArtUrl }
                    else
                        return null
                })
        }
        else
            return (localConfig.gameCategories[categoryIndex])

    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getGameFromId", "Error fetching/finding game:" + gameId, err, err.message);
        return null
    }
}
// ===========================================================================
//                           FUNCTION: disconnectTwitch
// ===========================================================================
/**
 * Disconnect the twitch API
 */
function disconnectTwitch ()
{
    eventSubApi.removeSubs()
}
// ===========================================================================
//                           FUNCTION: setStreamTitle
// ===========================================================================
/**
 * Sets the current stream title
 * @param {string} title 
 */
async function setStreamTitle (title)
{
    localConfig.apiClient.channels.updateChannelInfo(localConfig.streamerData.id, { title: title })
}
// ===========================================================================
//                           FUNCTION: setStreamGame
// ===========================================================================
/**
 * Sets the current game/category in twitch
 * @param {number} gameId 
 */
async function setStreamGame (gameId)
{
    if (gameId)
        localConfig.apiClient.channels.updateChannelInfo(localConfig.streamerData.id, { gameId: gameId })
    else
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".setStreamGame", "No game category id passed");

}
// ===========================================================================
//                           FUNCTION: startCommercial
// ===========================================================================
/**
 * Start a add/commercial running on twitch
 * @param {number} length ["30", "60", "90", "120", "150", "180"]
 */
async function startCommercial (length)
{
    try
    {
        let lengths = ["30", "60", "90", "120", "150", "180"]
        if (!lengths.includes(length))
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startCommercial", "Commercial length invalid, must be one of 30, 60, 90, 120, 150, 180")
        else
        {
            try
            {
                localConfig.apiClient.channels.startChannelCommercial(localConfig.streamerData.id, length)
                    .then(ret =>
                    {
                        //TBD need to move trigger to pubsub/eventsub callback, if we ever have time to work out how to do that
                        let trigger = findTriggerByMessageType("trigger_TwitchCommercialStarted");
                        trigger.parameters.duration = length;
                        sendTrigger(trigger)
                    })
                    .catch((err) =>
                    {
                        if (err && err._body, JSON.parse(err._body).message)
                            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startCommercial:Error", JSON.parse(err._body).message)
                        else
                            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startCommercial:Error", JSON.stringify(err, null, 2))

                    })
            }
            catch (err)
            {
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startCommercial:Error", JSON.stringify(err, null, 2));
            }
        }
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startCommercial", "ERROR", "Failed to start commercial, is streamer live?");
            console.log(err, err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startCommercial", "ERROR", "Failed to start commercial (try reauthorising by going to  go to http://localhost:3000/twitch/auth)");
            console.log(err, err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: getChannelEditors
// ===========================================================================
/**
 * sends trigger trigger_TwitchEditors containing the chat editors
 */
async function getChannelEditors ()
{
    try
    {
        let editors = await localConfig.apiClient.channels.getChannelEditors(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchEditors");
        // need to clear out the last run otherwise we will get repeated names in here
        trigger.parameters.editors = ""
        editors.forEach(function (value, key)
        {
            trigger.parameters.editors += value.userDisplayName + " "
        })
        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getChannelEditors", "ERROR", "Failed to get editors (try reauthorising by going to  go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: getChannelVIPs
// ===========================================================================
/**
 * send trigger_TwitchVIPs containing chat VIP's
 */
async function getChannelVIPs ()
{
    try
    {
        let VIPs = await localConfig.apiClient.channels.getVips(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchVIPs");
        // need to clear out the last run otherwise we will get repeated names in here
        trigger.parameters.VIPs = ""
        VIPs.data.forEach(function (value, key)
        {
            trigger.parameters.VIPs += value.displayName + " "
        })
        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getChannelVIPs", "ERROR", "Failed to get VIPs (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: addVIP
// ===========================================================================
/**
 * Make the username a VIP
 * @param {string} username 
 */
async function addVIP (username)
{
    try
    {
        let user = await localConfig.apiClient.users.getUserByName(username)
        await localConfig.apiClient.channels.addVip(localConfig.streamerData.id, user.id)
        //TBD need to move trigger to pubsub/eventsub if we ever work out how to do that
        let trigger = findTriggerByMessageType("trigger_TwitchVIPAdded");
        trigger.parameters.user = username;
        sendTrigger(trigger)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".addVIP", "ERROR", "Failed to add VIP?");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".addVIP", "ERROR", "Failed to add VIP)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION: removeVIP
// ===========================================================================
/**
 * Remove VIP status from user
 * @param {string} username 
 */
async function removeVIP (username)
{
    try
    {
        let user = await localConfig.apiClient.users.getUserByName(username)
        await localConfig.apiClient.channels.removeVip(localConfig.streamerData.id, user.id)
        //TBD need to move trigger to pubsub/eventsub if we ever work out how to do that
        let trigger = findTriggerByMessageType("trigger_TwitchVIPRemoved");
        trigger.parameters.user = username;
        sendTrigger(trigger)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removeVIP", "ERROR", "Failed to remove VIP?");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removeVIP", "ERROR", "Failed to remove VIP)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION: addMod
// ===========================================================================
/**
 * make username a mod
 * @param {string} username 
 */
async function addMod (username)
{
    try
    {
        let user = await localConfig.apiClient.users.getUserByName(username)
        await localConfig.apiClient.moderation.addModerator(localConfig.streamerData.id, user.id)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".addMod", "ERROR", "Failed to add Mod?");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".addMod", "ERROR", "Failed to add Moderator)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION: removeMod
// ===========================================================================
/**
 * Remove mod status from username
 * @param {string} username 
 */
async function removeMod (username)
{
    try
    {
        let user = await localConfig.apiClient.users.getUserByName(username)
        await localConfig.apiClient.moderation.removeModerator(localConfig.streamerData.id, user.id)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removeMod", "ERROR", "Failed to remove Mod?");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removeMod", "ERROR", "Failed to remove Moderator)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION: banUser
// ===========================================================================
/**
 * Ban username for given reason
 * @param {string} username 
 * @param {string} reason 
 */
async function banUser (username, reason)
{
    try
    {
        localConfig.apiClient.users.getUserByName(username)
            .then((user) =>
            {
                if (user)
                {
                    let banData = {
                        duration: 0,
                        reason: "streamrollerBan:" + reason,
                        user: user.id
                    }
                    localConfig.apiClient.moderation.banUser(localConfig.streamerData.id, banData)
                        .then((response) =>
                        {
                            //console.log("twitch.banUser User returned", response)
                        })
                }
                else
                {
                    console.log("BanUser Error: User '" + username + "' was not found on twitch")
                }
            })
            .catch((err) =>
            {
                console.log("BanUser.getUserByName error", err)
            })


    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".banUser", "ERROR", "Failed to ban User?");
            console.log(err, err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".banUser", "ERROR", "Failed to ban User)");
            console.log(err, err._body);
        }
    }
}

// ===========================================================================
//                           FUNCTION: unbanUser
// ===========================================================================
/**
 * Unban username for give reason
 * @param {string} username 
 * @param {string} reason 
 */
async function unbanUser (username, reason)
{
    try
    {
        await localConfig.apiClient.users.getUserByName(username)
            .then((user) =>
            {
                if (user && user.id && user.name)
                {
                    let banData = {
                        duration: 0,
                        reason: "streamrollerBan:" + reason,
                        user: user.id
                    }
                    localConfig.apiClient.moderation.unbanUser(localConfig.streamerData.id, user.id)
                        .then((response) =>
                        {
                            //console.log("User '"+username+"'was unbanned");
                        })
                        .catch((err) =>
                        {
                            if (err._body && err._body)
                                console.log("Error banning user", JSON.parse(err._body).message.replaceAll("in the user_id query parameter is not banned", username) + ". User is not banned.");
                            else
                                console.log("Error banning user", err)
                        })
                }
                else
                {
                    console.log("Can't unban User '" + username + "' as they were not found on twitch")
                }
            })
            .catch((err) =>
            {
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".unbanUser.getUserByName", "ERROR", err);
                console.log("JSON error", JSON.stringify(err, null, 2));
            })
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".unbanUser", "ERROR", "Failed to unBan User)");
        console.log("JSON error", JSON.stringify(err, null, 2));

    }
}
// ===========================================================================
//                           FUNCTION: followerCount
// ===========================================================================
/**
 * Send trigger_TwitchFollowerCount
 */
async function followerCount ()
{
    try
    {
        let count = await localConfig.apiClient.channels.getChannelFollowerCount(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchFollowerCount");
        trigger.parameters.count = count;
        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".followerCount", "ERROR", "Failed to get follower count)");
        console.log(err);

    }
}
// ===========================================================================
//                           FUNCTION: followedChannels
// ===========================================================================
/**
 * sends trigger_TwitchFollowedChannels
 */
async function followedChannels ()
{
    // TBD need to use teh paginator to get all the channels.
    try
    {
        let channels = await localConfig.apiClient.channels.getFollowedChannels(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchFollowedChannels");
        // need to clear out the last run otherwise we will get repeated names in here
        trigger.parameters.channels = ""
        channels.data.forEach(function (value, key)
        {
            trigger.parameters.channels += value.broadcasterDisplayName + " "
        })
        trigger.parameters.channels = "(" + channels.total + ") " + trigger.parameters.channels + "..."
        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".followedChannels", "ERROR", "Failed followed channels (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: cheerEmotes
// ===========================================================================
/**
 * send trigger_TwitchCheerEmotes
 */
async function cheerEmotes ()
{
    // TBD need to use teh paginator to get all the channels.
    try
    {
        let emotes = await localConfig.apiClient.bits.getCheermotes(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchCheerEmotes");
        trigger.parameters.emotes = emotes.getPossibleNames().join(" ")
        sendTrigger(trigger)
    }
    catch (err)
    {
        console.log(err)
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".cheerEmotes", "ERROR", "Failed to get cheer emotes  (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: leaderboard
// ===========================================================================
/**
 * send trigger_TwitchLeaderboard
 */
async function leaderboard ()
{
    // TBD need to use teh paginator to get all the channels.
    try
    {
        let leaderboardlist = await localConfig.apiClient.bits.getLeaderboard(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchLeaderboard");
        // need to clear out the last run otherwise we will get repeated names in here
        trigger.parameters.leaderboard = ""
        for (let i = 0; i < leaderboardlist.totalCount; i++)
        {
            if (i > 0)
                trigger.parameters.leaderboard += " "
            trigger.parameters.leaderboard += leaderboardlist.entries[i].rank + ") " + leaderboardlist.entries[i].userDisplayName + " " + leaderboardlist.entries[i].amount
        }
        sendTrigger(trigger)
    }
    catch (err)
    {
        console.log(err)
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".leaderboard", "ERROR", "Failed to get leaderboard data  (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: getPolls
// ===========================================================================
/**
 * send a trigger_TwitchPoll message for each poll running
 */
async function getPolls ()
{
    try
    {
        let polls = await localConfig.apiClient.polls.getPolls(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchPoll");
        polls.data.forEach(function (value, key)
        {
            trigger.parameters.id = value.id
            trigger.parameters.title = value.title
            trigger.parameters.status = value.status
            trigger.parameters.choices = ""
            value.choices.forEach(function (choice, index)
            {
                if (index > 0)
                    trigger.parameters.choices += " ,  "
                trigger.parameters.choices += choice.title + " " + choice.totalVotes
            })
            trigger.parameters.duration = value.durationInSeconds
            trigger.parameters.enabled = value.isChannelPointsVotingEnabled
            trigger.parameters.pointsPerVote = value.channelPointsPerVote
            trigger.parameters.startDate = value.startDate
            trigger.parameters.endDate = value.endDate
            sendTrigger(trigger)
        })

    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getPolls", "ERROR", "Failed to get polls (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: getPoll
// ===========================================================================
/**
 * send trigger_TwitchPoll
 * @param {number} id 
 */
async function getPoll (id)
{
    try
    {
        let poll = await localConfig.apiClient.polls.getPollById(localConfig.streamerData.id, id)
        let trigger = findTriggerByMessageType("trigger_TwitchPoll");
        trigger.parameters.id = poll.id
        trigger.parameters.title = poll.title
        trigger.parameters.status = poll.status
        trigger.parameters.choices = ""
        poll.choices.forEach(function (choice, index)
        {
            if (index > 0)
                trigger.parameters.choices += " ,  "
            trigger.parameters.choices += choice.title + " " + choice.totalVotes
        })
        trigger.parameters.duration = poll.durationInSeconds
        trigger.parameters.enabled = poll.isChannelPointsVotingEnabled
        trigger.parameters.pointsPerVote = poll.channelPointsPerVote
        trigger.parameters.startDate = poll.startDate
        trigger.parameters.endDate = poll.endDate
        sendTrigger(trigger)

    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getPoll", "ERROR", "Failed to get poll by id (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: createPoll
// ===========================================================================
/**
 * Creates a twitch poll
 * @param {object} data 
 */
async function createPoll (data)
{
    try
    {
        let newPoll =
        {
            title: data.title,
            choices: data.choices.split(","),
            duration: data.duration,
            channelPointsPerVote: data.points,
        }
        let poll = await localConfig.apiClient.polls.createPoll(localConfig.streamerData.id, newPoll)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".createPoll", "ERROR", "Failed to create a poll");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".createPoll", "ERROR", "Failed to create a poll (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: endPoll
// ===========================================================================
/**
 * Ends the given poll
 * @param {object} data 
 */
async function endPoll (data)
{
    try
    {
        let poll = await localConfig.apiClient.polls.endPoll(localConfig.streamerData.id, data.id, data.display)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".endPoll", "ERROR", "Failed to end a poll");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".endPoll", "ERROR", "Failed to end a poll (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: startPrediction
// ===========================================================================
/**
 * Starts a prediction
 * @param {object} data 
 */
async function startPrediction (data)
{
    try
    {
        let newPoll =
        {
            title: data.title,
            outcomes: data.choices.split(","),
            autoLockAfter: data.duration,
        }
        let poll = await localConfig.apiClient.predictions.createPrediction(localConfig.streamerData.id, newPoll)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startPrediction", "ERROR", "Failed to create a prediction");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".startPrediction", "ERROR", "Failed to create a prediction (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: cancelPrediction
// ===========================================================================
/**
 * Cancels a prediction
 * @param {object} data 
 */
async function cancelPrediction (data)
{
    try
    {
        let prediction = await localConfig.apiClient.predictions.cancelPrediction(localConfig.streamerData.id, data.id)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".cancelPrediction", "ERROR", "Failed to cancel a prediction");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".cancelPrediction", "ERROR", "Failed to cancel a prediction (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: getPredictions
// ===========================================================================
/**
 * sends trigger_TwitchPrediction for each current predictions
 * @param {object} data 
 */
async function getPredictions (data)
{
    try
    {
        let predictions = await localConfig.apiClient.predictions.getPredictions(localConfig.streamerData.id)
        let trigger = findTriggerByMessageType("trigger_TwitchPrediction");
        predictions.data.forEach(function (value, key)
        {
            if (data.state == "" || data.state == value.status)
                getPrediction(value.id)
        })
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getPredictions", "ERROR", "Failed to get predictions list (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: getPrediction
// ===========================================================================
/**
 * Sends trigger_TwitchPrediction for the given prediction
 * @param {object} data 
 */
async function getPrediction (data)
{
    try
    {
        let prediction = await localConfig.apiClient.predictions.getPredictionById(localConfig.streamerData.id, data.id)
        let trigger = findTriggerByMessageType("trigger_TwitchPrediction");
        trigger.parameters.streamer = prediction.broadcasterDisplayName
        trigger.parameters.id = prediction.id
        trigger.parameters.duration = prediction.autoLockAfter
        trigger.parameters.title = prediction.title
        trigger.parameters.status = prediction.status
        trigger.parameters.winner = prediction.winningOutcome
        trigger.parameters.winnerId = prediction.winningOutcomeId
        trigger.parameters.endDate = prediction.endDate
        trigger.parameters.lockDate = prediction.lockDate

        //  ----------- outcomes --------------
        trigger.parameters.outcomes = ""
        prediction.outcomes.forEach(function (outcome, index)
        {
            if (index > 0)
                trigger.parameters.outcomes += " ,  "
            trigger.parameters.outcomes += "id:" + outcome.id + " "
            trigger.parameters.outcomes += "title:" + outcome.title + " "
            trigger.parameters.outcomes += "color:" + outcome.color + " "
            trigger.parameters.outcomes += "points:" + outcome.totalChannelPoints + " "
            trigger.parameters.outcomes += "users:" + outcome.users + " "
            trigger.parameters.outcomes += "["
            // outcome predictors
            outcome.topPredictors.forEach(function (topP, topPIndex)
            {
                if (topPIndex > 0)
                    trigger.parameters.outcomes += " ,  "
                trigger.parameters.outcomes += "user:" + topP.userDisplayName + " "
                trigger.parameters.outcomes += "pointsUsed:" + topP.channelPointsUsed + " "
                trigger.parameters.outcomes += "pointsWon:" + topP.channelPointsWon + " "
            });
            trigger.parameters.outcomes += "]"
        })
        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getPrediction", "ERROR", "Failed to get prediction by id (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
        console.log(err._body);
    }
}
// ===========================================================================
//                           FUNCTION: lockPrediction
// ===========================================================================
/**
 * Locks a prediction in it's current state
 * @param {object} data 
 */
async function lockPrediction (data)
{
    try
    {
        let prediction = await localConfig.apiClient.predictions.lockPrediction(localConfig.streamerData.id, data.id)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".lockPrediction", "ERROR", "Failed to lock a prediction");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".lockPrediction", "ERROR", "Failed to lock a prediction (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: removePrediction
// ===========================================================================
/**
 * Delete prediction
 * @param {object} data 
 */
async function removePrediction (data)
{
    try
    {
        let prediction = await localConfig.apiClient.predictions.cancelPrediction(localConfig.streamerData.id, data.id)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removePrediction", "ERROR", "Failed to remove a prediction");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removePrediction", "ERROR", "Failed to remove a prediction (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: resolvePrediction
// ===========================================================================
/**
 * Resolves the current prediction with the given data
 * @param {object} data 
 */
async function resolvePrediction (data)
{
    try
    {
        let prediction = await localConfig.apiClient.predictions.resolvePrediction(localConfig.streamerData.id, data.id, data.outcomeId)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".resolvePrediction", "ERROR", "Failed to resolve a prediction");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".resolvePrediction", "ERROR", "Failed to resolve a prediction (try reauthorising by going to go to http://localhost:3000/twitch/auth)");
            console.log(err._body);
        }
    }
}
// ===========================================================================
//                           FUNCTION: createBlock
// ===========================================================================
/**
 * Block the user given
 * @param {object} data 
 */
async function createBlock (data)
{
    try
    {
        let user = await localConfig.apiClient.users.getUserByName(data.username)
        let extraInfo = {}
        if (data.reason != "")
            extraInfo["reason"] = data.reason
        if (data.reason != "")
            extraInfo["sourceContext"] = data.context

        let ret = await localConfig.apiClient.users.createBlock(localConfig.streamerData.id, user.id, extraInfo)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".createBlock", "ERROR", "Failed to block user");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".createBlock", "ERROR", "Failed to block user)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION: deleteBlock
// ===========================================================================
/**
 * Remove a Block from the given user
 * @param {object} data 
 */
async function deleteBlock (data)
{
    try
    {
        let user = await localConfig.apiClient.users.getUserByName(data.username)
        await localConfig.apiClient.users.deleteBlock(localConfig.streamerData.id, user.id)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".deleteBlock", "ERROR", "Failed to unblock user");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".deleteBlock", "ERROR", "Failed to unblock user)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION: containsBadChars
// ===========================================================================
/**
 * Tests for unicode charts in a string
 * @param {string} s 
 * @returns boolean
 */
function containsBadChars (s)
{
    //return /[\u3040-\uffff]/.test(s);
    return /[\u3100-\uffff]/.test(s);
}
// ===========================================================================
//                           FUNCTION: getUser
// ===========================================================================
/**
 * send trigger_TwitchUserDetails message for given user
 * @param {string} username 
 */
async function getUser (username)
{

    let trigger = findTriggerByMessageType("trigger_TwitchUserDetails")
    try
    {
        if (containsBadChars(username))
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".server.getUser", "username contains invalid chars, ignoring", username);
            trigger.parameters.username = username;
            trigger.parameters.userNameInvalid = true;
        }
        else
        {
            if (localConfig.apiClient && localConfig.apiClient.users)
            {
                let user = await localConfig.apiClient.users.getUserByName(username)
                if (user)
                {
                    trigger.parameters.username = user.name
                    trigger.parameters.userId = user.id
                    trigger.parameters.userDisplayName = user.displayName
                    trigger.parameters.creationDate = user.creationDate
                    trigger.parameters.description = user.description
                    trigger.parameters.offlinePlaceholderUrl = user.offlinePlaceholderUrl
                    trigger.parameters.profilePictureUrl = user.profilePictureUrl
                    trigger.parameters.type = user.type
                }
                else
                {
                    logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".server.getUser", "username not found", username);
                    console.log(user);
                    trigger.parameters.username = username;
                    trigger.parameters.userNameInvalid = true;
                }
            }
        }
        sendTrigger(trigger)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".server.getUser", "ERROR", "400:Failed to get user", username);
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".server.getUser", "ERROR", "Failed to get user)", username);
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION:  getBlockedUsers
// ===========================================================================
/**
 * send a trigger_TwitchUserBlocks message containing current blocked users
 */
async function getBlockedUsers ()
{
    try
    {
        let user = await localConfig.apiClient.users.getBlocks(localConfig.streamerData.id)

        let trigger = findTriggerByMessageType("trigger_TwitchUserBlocks")
        trigger.parameters.blocked = ""
        trigger.parameters.userDisplayName = user.userDisplayName
        user.data.forEach(function (value, key)
        {
            if (key > 0)
                trigger.parameters.blocked += " "
            trigger.parameters.blocked += value.userDisplayName
        })
        sendTrigger(trigger)
    }
    catch (err)
    {
        if (err._statusCode == 400)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ". getAuthenticatedUser", "ERROR", "Failed to get authenticated user");
            console.log(err._body);
        }
        else
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ". getAuthenticatedUser", "ERROR", "Failed to get authenticated user)");
            console.log(err);
        }
    }
}
// ===========================================================================
//                           FUNCTION:  createClip
// ===========================================================================
/**
 * Create a twitch clip and send out trigger_TwitchClipCreated when done
 */
async function createClip ()
{
    try
    {
        let clip = await localConfig.apiClient.clips.createClip({ channel: localConfig.streamerData.id })
        let trigger = findTriggerByMessageType("trigger_TwitchClipCreated")
        trigger.parameters.clipName = clip
        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".createClip", "ERROR", "Failed to create clip)");
        console.log(err);
    }
}
// ===========================================================================
//                           FUNCTION:  getClipById
// ===========================================================================
/**
 * Gets a clip using data.clipName,if clip isn't available yet (i.e. still being processed)
 * setup a reschedule timer to wait for clip. Sends trigger_TwitchVodClip when found
 * @param {object} data 
 * @param {number} [rollbackCount=5] in seconds, reduced by 1 second each rollback
 */
async function getClipById (data, rollbackCount = 5)
{
    try
    {
        if (data.clipName == "")
            return
        let clip = await localConfig.apiClient.clips.getClipById(data.clipName)
        if (clip == null)
        {
            //if we have triggered off a clip being created it might take a bit for
            // twitch to make it available so give it a few seconds.
            if (rollbackCount > 0)
            {
                setTimeout(() =>
                {
                    console.log("clip not found, rescheduling in case twitch is still processing it")
                    getClipById(data, rollbackCount--)
                }, 1000);
            }
            return;
        }
        let trigger = findTriggerByMessageType("trigger_TwitchVodClip")
        trigger.parameters.streamer = clip.broadcasterDisplayName
        trigger.parameters.date = clip.creationDate
        trigger.parameters.creator = clip.creatorDisplayName
        trigger.parameters.duration = clip.duration
        trigger.parameters.embedUrl = clip.embedUrl
        trigger.parameters.gameId = clip.gameId
        trigger.parameters.id = clip.id
        trigger.parameters.language = clip.language
        trigger.parameters.thumbnail = clip.thumbnailUrl
        trigger.parameters.title = clip.title
        trigger.parameters.url = clip.url
        trigger.parameters.videoId = clip.videoId
        trigger.parameters.views = clip.views
        trigger.parameters.vodOffset = clip.vodOffset

        sendTrigger(trigger)
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getClipById", "ERROR", "Failed to get clip by name)");
        console.log(err);
    }
}
// ===========================================================================
//                           FUNCTION:  getClipsByBroadcaster
// ===========================================================================
/**
 * Sends out a trigger_TwitchVodClip message for each clip the broadcaster has
 * data is formatted from an action. 
 * @param {object} data 
 * @param {number} [rollbackCount=5] number of times to check (1 second polls, in case clip is still being processed)
 */
async function getClipsByBroadcaster (data, rollbackCount = 5)
{
    try
    {
        if (data.name == "")
            return

        let paginatedFilter = {
            // after ?: string, A cursor to get the following page of.
            // before ?: string, A cursor to get the previous page of.
            // endDate ?: string,The latest date to find clips for.
            // limit ?: number, The number of results per page.
            // startDate ?: string,The earliest date to find clips for.
        }
        if (data.count != "")
            paginatedFilter.limit = Number(data.count)

        let user = await localConfig.apiClient.users.getUserByName(data.name)
        let clips = await localConfig.apiClient.clips.getClipsForBroadcaster(user.id, paginatedFilter)
        if (clips == null)
        {
            //if we have triggered off a clip being created it might take a bit for
            // twitch to make it available so give it a few seconds.
            if (rollbackCount > 0)
            {
                setTimeout(() =>
                {
                    console.log("clip not ready found, rescheduling in case twitch is still processing it")
                    getClipById(data, rollbackCount--)
                }, 1000);
            }
            return;
        }
        clips.data.forEach(function (clip, index)
        {
            let trigger = findTriggerByMessageType("trigger_TwitchVodClip")
            trigger.parameters.streamer = clip.broadcasterDisplayName
            trigger.parameters.date = clip.creationDate
            trigger.parameters.creator = clip.creatorDisplayName
            trigger.parameters.duration = clip.duration
            trigger.parameters.embedUrl = clip.embedUrl
            trigger.parameters.gameId = clip.gameId
            trigger.parameters.id = clip.id
            trigger.parameters.language = clip.language
            trigger.parameters.thumbnail = clip.thumbnailUrl
            trigger.parameters.title = clip.title
            trigger.parameters.url = clip.url
            trigger.parameters.videoId = clip.videoId
            trigger.parameters.views = clip.views
            trigger.parameters.vodOffset = clip.vodOffset
            sendTrigger(trigger)
        });
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getClipById", "ERROR", "Failed to get clip by name)");
        console.log(err);
    }
}
// ===========================================================================
//                           FUNCTION:  getClipsByBroadcaster
// ===========================================================================
/**
 * sends a trigger_TwitchVodClip for each clip from a game category based on data object
 * @param {object} data 
 */
async function getClipsByGame (data)
{
    try
    {
        if (data.game == "")
            return

        let paginatedFilter = {
            // after ?: string, A cursor to get the following page of.
            // before ?: string, A cursor to get the previous page of.
            // endDate ?: string,The latest date to find clips for.
            // limit ?: number, The number of results per page.
            // startDate ?: string,The earliest date to find clips for.
        }
        if (data.count != "")
            paginatedFilter.limit = Number(data.count)

        let game = await localConfig.apiClient.games.getGameByName(data.game)
        let clips = await localConfig.apiClient.clips.getClipsForGame(game.id, paginatedFilter)
        if (clips == null)
        {
            console.log("No clips found for game name")
            return;
        }
        clips.data.forEach(function (clip, index)
        {
            let trigger = findTriggerByMessageType("trigger_TwitchVodClip")
            trigger.parameters.streamer = clip.broadcasterDisplayName
            trigger.parameters.date = clip.creationDate
            trigger.parameters.creator = clip.creatorDisplayName
            trigger.parameters.duration = clip.duration
            trigger.parameters.embedUrl = clip.embedUrl
            trigger.parameters.gameId = clip.gameId
            trigger.parameters.id = clip.id
            trigger.parameters.language = clip.language
            trigger.parameters.thumbnail = clip.thumbnailUrl
            trigger.parameters.title = clip.title
            trigger.parameters.url = clip.url
            trigger.parameters.videoId = clip.videoId
            trigger.parameters.views = clip.views
            trigger.parameters.vodOffset = clip.vodOffset
            sendTrigger(trigger)
        });
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getClipById", "ERROR", "Failed to get clip by name)");
        console.log(err);
    }
}
// ===========================================================================
//                           FUNCTION: sendGameCategoriesTrigger
// ===========================================================================
/**
 * sends trigger_TwitchGameCategories containing all games in our category
 * @param {number} [id="twitch"] ref id from action
 */
function sendGameCategoriesTrigger (id = "twitch")
{
    let trigger = findTriggerByMessageType("trigger_TwitchGameCategories");
    trigger.parameters.id = id;
    trigger.parameters.games = localConfig.gameCategories;
    // we also send our settings out in case the categories have change
    SendSettingsWidgetSmall()
    sendTrigger(trigger)
}
// ===========================================================================
//                           FUNCTION: sendCurrentGameData
// ===========================================================================
/**
 * sends trigger_TwitchGamedChanged
 * @param {number} [triggerId="twitch"] ref id from action
 */
function sendCurrentGameData (triggerId = "twitch")
{
    let trigger = findTriggerByMessageType("trigger_TwitchGamedChanged");
    const game = serverConfig.twitchCategoriesHistory.find(e => e.id === localConfig.currentTwitchGameCategoryId);
    if (game)
    {
        trigger.parameters = { triggerId: triggerId, id: game.id, name: game.name, imageURL: game.imageURL }
        sendTrigger(trigger)
    }
}
// ===========================================================================
//                           FUNCTION: pubSubTriggerCallback
// ===========================================================================
/**
 * Callback for all triggers used in the twitch pubsub.
 * gets called whenever the pubsub module needs to create a trigger
 * @param {object} trigger 
 */
function pubSubTriggerCallback (trigger)
{
    //This gets called whenever the pubsub modules gets a callback from twitch
    // we've had a twitch game change callback
    if (trigger.messagetype == "trigger_TwitchGamedChanged")
    {
        // change our setup so it matches the data from twitch
        localConfig.currentTwitchGameCategoryId = trigger.parameters.gameId;
        addGameToHistoryFromGameName(trigger.parameters.name)
        //update any of our modals
        SendSettingsWidgetSmall()
        //save the serverConfig so we remember the changes
        SaveConfigToServer()
    }
    else
        logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".getClipById", "pubSubTriggerCallback() no handler for ", trigger.messagetype, " twitch callback message");
}
// ===========================================================================
//                           FUNCTION: sendTrigger
// ===========================================================================
/**
 * Sends the given trigger out on our channel
 * @param {object} trigger 
 */
function sendTrigger (trigger)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket(
            'ChannelData',
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                trigger.messagetype,
                serverConfig.extensionname,
                trigger,
                serverConfig.channel,
                ''),
            serverConfig.channel,
            ''
        )
    );
}
// ============================================================================
//                           FUNCTION: createDropdownWithSearchableHistory
// ============================================================================
/**
 * Creates an html dropdown list that is searchable using the given information
 * @param {string} id 
 * @param {string} categories 
 * @param {string} history 
 * @param {string} currentSelectedId 
 * @returns html string containing dropdown code
 */
function createDropdownWithSearchableHistory (id, categories = [], history = [], currentSelectedId = -1)
{
    let dropdownHtml = ""
    dropdownHtml += '<div class="d-flex-align w-100">';
    dropdownHtml += `<select class='selectpicker btn-secondary' data-style='btn-danger' style="max-width: 85%;" title='Current Game Category' id="${id}" value='${currentSelectedId}' name="${id}" required="">`
    // add history section if we have one
    if (history.length)
    {
        // add history separator
        dropdownHtml += '<option value="separator" disabled style="color:rgb(255 255 0 / 80%);font-weight: bold">--HISTORY--</option>'
        // Append history options first
        history.forEach(item =>
        {
            if (item.id == currentSelectedId)
                dropdownHtml += "<option value=\"" + item.id + "\" selected>" + item.name + "</option>";
            else
                dropdownHtml += "<option value=\"" + item.id + "\">" + item.name + "</option>";
        });

        // add history separator
        dropdownHtml += '<option value="separator" disabled style="color:rgb(255 255 0 / 80%);font-weight: bold">--END HISTORY--</option>';
    }
    else
        dropdownHtml += '<option value="separator" disabled style="color:rgb(255 255 0 / 80%);font-weight: bold">--Select an option--</option>'
    // check if we have loaded the categories yet
    if (serverConfig.twitchenabled == "off")
        dropdownHtml += '<option value="separator" disabled style="color:red;font-weight: bold">Extension turned off ...</option>';
    else if (!categories.length)
        dropdownHtml += '<option value="separator" disabled style="color:red;font-weight: bold">LOADING...</option>';
    // append categories
    categories.forEach(option =>
    {
        // only include if it isn't already in the history list
        if (!history.some(e => e.id === option.id))
        {
            if (option.id == currentSelectedId)
                dropdownHtml += '<option value="' + option.id + '" selected>' + option.name + '</option>';
            else
                dropdownHtml += '<option value="' + option.id + '">' + option.name + '</option>';
        }
    });
    dropdownHtml += '</select>';
    // add clear history checkbox
    dropdownHtml += `&nbsp<div class="form-check form-check-inline">
        <input class="form-check-input" name="${id}_clearHistory" type="checkbox" id="${id}_clearHistory" ${id}_clearHistorychecked>
        <label class="form-check-label" for="${id}_clearHistory">Clear History</label>
      </div>`
    dropdownHtml += '</div>';
    return dropdownHtml;
}
// ============================================================================
//                           FUNCTION: getTextboxWithHistoryHTML
// ============================================================================
/**
 * Creates an html dropdown list on a text box is searchable using the given information
 * @param {string} SelectEleId 
 * @param {string} TextEleId 
 * @param {string} history 
 * @param {string} currentSelectedId 
 * @returns html string containing textbox code
 */
function getTextboxWithHistoryHTML (SelectEleId, TextEleId, history, currentSelectedId)
{
    let dropdownHtml = "";
    dropdownHtml += '<div class="d-flex-align w-100">';
    if (history[currentSelectedId])
        dropdownHtml += `<input type="text" class="form-control" id="${TextEleId}" name="${TextEleId}" value = "${history[currentSelectedId]}" placeholder="${history[currentSelectedId]}">`
    else
        dropdownHtml += `<input type="text" class="form-control" id="${TextEleId}" name="${TextEleId}" placeholder="Please Enter a new Title or select one from the history">`


    dropdownHtml += `<select class='selectpicker btn-secondary' data-style='btn-danger' style="max-width: 85%;" title='Current Title' id="${SelectEleId}" value='${currentSelectedId}' name="${SelectEleId}" onchange="document.getElementById('${TextEleId}').value = this.options[this.selectedIndex].text">`

    if (history.length)
    {
        history.forEach((item, i) => 
        {
            if (i == currentSelectedId)
                dropdownHtml += `<option value="` + i + `" selected >` + item + `</option>`;

            else
                dropdownHtml += `<option value="` + i + `">` + item + `</option>`;
        });

    }
    else
        dropdownHtml += '<option value="separator" disabled style="color:rgb(255 255 0 / 80%);font-weight: bold">--No History Available--</option>'
    dropdownHtml += '</select>';
    dropdownHtml += `&nbsp<div class="form-check form-check-inline">
        <input class="form-check-input" name="${SelectEleId}_clearHistory" type="checkbox" id="${SelectEleId}_clearHistory" ${SelectEleId}_clearHistorychecked>
        <label class="form-check-label" for="${SelectEleId}_clearHistory">Clear History</label>
      </div>`

    dropdownHtml += "</div>"

    return dropdownHtml
}
// ============================================================================
//                           FUNCTION: findTriggerByMessageType
// ============================================================================
/**
 * Finds the trigger using the passed messagetype
 * @param {string} messagetype 
 * @returns trigger
 */
function findTriggerByMessageType (messagetype)
{
    for (let i = 0; i < triggersandactions.triggers.length; i++)
    {
        if (triggersandactions.triggers[i].messagetype.toLowerCase() == messagetype.toLowerCase())
            return triggersandactions.triggers[i];
    }
    logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
        ".findTriggerByMessageType", "failed to find trigger", messagetype);
}

export { start, triggersandactions };