Source: classes/Bot.js

BotAccount.prototype.__proto__ = require('events').EventEmitter.prototype;
const AuthManager = require('./Auth.js');
const TradeHandler = require('./Trade.js');
const SteamCommunity = require('steamcommunity');
const SteamUser = require('steam-user');
const SteamStore = require('steamstore');
const TradeOfferManager = require('steam-tradeoffer-manager');
const SteamID = TradeOfferManager.SteamID;

/**
 * Creates a new BotAccount instance for a bot.
 * @class
 */


//
function BotAccount(username, password, details, settings, logger) {
    // Ensure account values are valid
    var self = this;
    // Init all required variables
    if (typeof username != "string" || typeof password != "string")
        if (!details.hasOwnProperty("steamguard") || !(details.hasOwnProperty("oAuthToken")))
            throw Error("Invalid username/password or missing oAuthToken/Steamguard code");
    self.username = username;
    self.password = password;
    self.settings = settings != undefined ? settings : {
            api_key: "",
            tradeCancelTime: 60 * 60 * 24,
            tradePendingCancelTime: 60 * 60 * 24,
            language: "en",
            tradePollInterval: 5000,
            tradeCancelOfferCount: 30,
            tradeCancelOfferCountMinAge: 60 * 60,
            cancelTradeOnOverflow: true
        };
    self.logger = logger;
    self.community = new SteamCommunity();
    self.client = new SteamUser();

    self.TradeOfferManager = new TradeOfferManager({
        "steam": self.client,
        "community": self.community,
        "cancelTime": self.settings.tradeCancelTime, // Keep offers upto 1 day, and then just cancel them.
        "pendingCancelTime": self.settings.tradePendingCancelTime, // Keep offers upto 30 mins, and then cancel them if they still need confirmation
        "cancelOfferCount": self.settings.tradeCancelOfferCount,// Cancel offers once we hit 7 day threshold
        "cancelOfferCountMinAge": self.settings.tradeCancelOfferCountMinAge,// Keep offers until 7 days old
        "language": self.settings.language, // We want English item descriptions
        "pollInterval": 5000 // We want to poll every 5 seconds since we don't have Steam notifying us of offers
    });
    self.SteamID = TradeOfferManager.SteamID;
    self.request = self.community.request;
    self.store = new SteamStore();
    self.loggedIn = false;
    self.rateLimited = true;
    self.delayedTasks = [];
    self.details = details;
    self.AuthManager = new AuthManager(self, logger);
    self.AuthManager.on('updatedAccountDetails', function () {
        self.emit('updatedAccountDetails');
    });


    self.TradeManager = new TradeHandler(self.TradeOfferManager, self.AuthManager, self.settings, logger);

};

/**
 * Get the account's username, used to login to Steam
 * @returns {String} username
 */
BotAccount.prototype.getAccountName = function () {
    var self = this;
    return self.username;
};
/**
 * Get the account's username, used to login to Steam
 * @returns {String} username
 */
BotAccount.prototype.TradeManager = function () {
    var self = this;
    return self.TradeOfferManager;
};


/**
 * Get if the API/account is rate limited by SteamAPI
 * @returns {Boolean} rateLimited
 */
BotAccount.prototype.getRateLimited = function () {
    var self = this;
    return self.rateLimited;
};

/**
 * Get if the API/account is rate limited by SteamAPI
 * @returns {Boolean} rateLimited
 */
BotAccount.prototype.setRateLimited = function (rateLimited) {
    var self = this;
    return self.rateLimited = rateLimited;
};


/**
 * Fetch SteamID Object from the SteamID.
 * @returns {Error | String}
 */
BotAccount.prototype.fromIndividualAccountID = function (id) {
    var self = this;
    var SteamID = TradeOfferManager.SteamID;
    return new self.SteamID(id);
};


/**
 * Get account details
 * @returns {*|{username: *, password: *}}
 */
BotAccount.prototype.getAccount = function () {
    var self = this;
    return self.accountDetails;
};

/**
 * Send a chat message to a receipient with callback
 * @param {SteamID} recipient - Recipient of the message
 * @param {String} message - Message to send
 * @param {String} type - saytest or typing (message ignored for 'typing')
 * @param {messageCallback} callback - Callback upon sending the message (undefined, or Error)
 */
BotAccount.prototype.sendMessage = function (recipient, message, type, callback) {
    var self = this;
    return self.community.chatMessage(recipient, message, type, callback);
};

/**
 * Send a chat message to a receipient with callback
 * @param {SteamID} recipient - Recipient of the message
 * @param {String} message - Message to send
 * @param {messageCallback} callback - Callback upon sending the message (undefined, or Error)
 */
BotAccount.prototype.sendMessage = function (recipient, message, callback) {
    var self = this;
    return self.community.chatMessage(recipient, message, callback);
};
/**
 * Send a chat message to a receipient without callback
 * @param {SteamID} recipient - Recipient of the message
 * @param {String} message - Message to send
 */
BotAccount.prototype.sendMessage = function (recipient, message) {
    var self = this;
    return self.community.chatMessage(recipient, message);
};


/**
 * Set the user we are chatting with
 * @param {*|{username: *, sid: *}} chattingUserInfo
 */
BotAccount.prototype.setChatting = function (chattingUserInfo) {
    var self = this;
    self.currentChatting = chattingUserInfo;
};

/**
 * Get the display name of the account
 * @returns {String|undefined} displayName - Display name of the account
 */
BotAccount.prototype.getDisplayName = function () {
    var self = this;


    return (self.displayName ? self.displayName : undefined);
};

/**
 * Function wrapper used to delay function calls by name and paramters
 * @param fn - function reference
 * @param context - Context to use for call
 * @param params - Parameters in arraylist to send with function
 * @returns {Function}
 */
BotAccount.prototype.wrapFunction = function (fn, context, params) {
    return function () {
        fn.apply(context, params);
    };
};
/**
 * Add a function to the queue which runs when we login usually.
 * @param functionV
 * @param functionData
 */
BotAccount.prototype.addToQueue = function (queueName, functionV, functionData) {
    var self = this;
    var functionVal = self.wrapFunction(functionV, self, functionData);
    if (!self.delayedTasks.hasOwnProperty(queueName))
        self.delayedTasks[queueName] = [];
    self.delayedTasks[queueName].push(functionVal);
};
/**
 * Process the queue to run tasks that were delayed.
 * @param queueName
 * @param callback
 */
BotAccount.prototype.processQueue = function (queueName, callback) {
    var self = this;
    if (self.delayedTasks.hasOwnProperty(queueName)) {
        while (self.delayedTasks[queueName].length > 0) {
            (self.delayedTasks[queueName].shift())();
        }
    }
    callback(undefined);
};

/**
 * Upvote an attachement file on SteamCommunity
 * @param sharedFileId
 * @param callback
 */
BotAccount.prototype.upvoteSharedFile = function (sharedFileId, callback) {
    var self = this;

    var options = {
        form: {
            'sessionid': self.accountDetails.sessionID,
            'id': sharedFileId
        }
    };

    self.community.httpRequestPost('https://steamcommunity.com/sharedfiles/voteup', options, function (error, response, body) {
        if (!error && response.statusCode == 200)
            callback(undefined);
        else
            callback(error);
    });
};
/**
 * Downvote an attachement file on SteamCommunity.
 * @param sharedFileId
 * @param callback
 */
BotAccount.prototype.downvoteSharedFile = function (sharedFileId, callback) {
    var self = this;

    var options = {
        form: {
            'sessionid': self.accountDetails.sessionID,
            'id': sharedFileId
        }
    };

    self.community.httpRequestPost('https://steamcommunity.com/sharedfiles/votedown', options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            callback(undefined);
        }
        else
            callback(error);
    });
};

/**
 * Change the display name of the account (with prefix)
 * @param {String} newName - The new display name
 * @param {String} namePrefix - The prefix if there is one (Nullable)
 * @param {errorCallback} errorCallback - A callback returned with possible errors
 */
BotAccount.prototype.changeName = function (newName, namePrefix, errorCallback) {
    var self = this;
    if (!self.loggedIn) {
        self.addToQueue('login', self.changeName, [newName, namePrefix, errorCallback]);
    }
    else {
        if (namePrefix == undefined) namePrefix = '';

        self.community.editProfile({name: namePrefix + newName}, function (err) {
            if (err)
                return errorCallback(err.Error);
            self.username = newName;
            self.emit('updatedAccountDetails');
            errorCallback(undefined);
        });
    }
};

/**
 * @callback inventoryCallback
 * @param {Error} error - An error message if the process failed, undefined if successful
 * @param {Array} inventory - An array of Items returned via fetch (if undefined, then game is not owned by user)
 * @param {Array} currencies - An array of currencies (Only a few games use this) - (if undefined, then game is not owned by user)
 */

/**
 * Retrieve account inventory based on filters
 * @param {Integer} appid - appid by-which to fetch inventory based on.
 * @param {Integer} contextid - contextid of lookup (1 - Gifts, 2 - In-game Items, 3 - Coupons, 6 - Game Cards, Profile Backgrounds & Emoticons)
 * @param {Boolean} tradableOnly - Items retrieved must be tradable
 * @param {inventoryCallback} inventoryCallback - Inventory details (refer to inventoryCallback for more info.)
 */
BotAccount.prototype.getInventory = function (appid, contextid, tradableOnly, inventoryCallback) {
    var self = this;
    if (!self.loggedIn) {
        self.addToQueue('login', self.getInventory, [appid, contextid, tradableOnly, inventoryCallback]);
    }
    else
        self.TradeOfferManager.loadInventory(appid, contextid, tradableOnly, inventoryCallback);
};

/**
 * Retrieve account inventory based on filters and provided steamID
 * @param {SteamID} steamID - SteamID to use for lookup of inventory
 * @param {Integer} appid - appid by-which to fetch inventory based on.
 * @param {Integer} contextid - contextid of lookup (1 - Gifts, 2 - In-game Items, 3 - Coupons, 6 - Game Cards, Profile Backgrounds & Emoticons)
 * @param {Boolean} tradableOnly - Items retrieved must be tradableOnly
 * @param {inventoryCallback} inventoryCallback - Inventory details (refer to inventoryCallback for more info.)
 */
BotAccount.prototype.getUserInventory = function (steamID, appid, contextid, tradableOnly, inventoryCallback) {
    var self = this;
    if (!self.loggedIn) {
        self.addToQueue('login', self.getUserInventory, [steamID, appid, contextid, tradableOnly, inventoryCallback]);
    }
    else
        self.TradeOfferManager.loadUserInventory(steamID, appid, contextid, tradableOnly, inventoryCallback);
};
/**
 * Add a phone-number to the account (For example before setting up 2-factor authentication)
 * @param phoneNumber - Certain format must be followed
 * @param {errorCallback} errorCallback - A callback returned with possible errors
 */
BotAccount.prototype.addPhoneNumber = function (phoneNumber, errorCallback) {
    var self = this;
    self.store.addPhoneNumber(phoneNumber, true, function (err) {
        errorCallback(err);
    });
};


/**
 * @callback acceptedTradesCallback
 * @param {Error} error - An error message if the process failed, undefined if successful
 * @param {Array} acceptedTrades - An array of trades that were confirmed in the process.
 */

/**
 * Confirm (not accept) all sent trades associated with a certain SteamID via the two-factor authenticator.
 * @param {SteamID} steamID - SteamID to use for lookup of inventory
 * @param {acceptedTradesCallback} acceptedTradesCallback - Inventory details (refer to inventoryCallback for more info.)
 */
BotAccount.prototype.confirmTradesFromUser = function (SteamID, callback) {
    var self = this;

    self.TradeOfferManager.getOffers(1, undefined, function (err, sent, received) {
        var acceptedTrades = [];
        for (var sentOfferIndex in sent) {
            if (sent.hasOwnProperty(sentOfferIndex)) {
                var sentOfferInfo = sent[sentOfferIndex];
                if (sentOfferInfo.partner.getSteamID64() == SteamID.getSteamID64) {
                    sentOfferInfo.accept();
                    acceptedTrades.push(sentOfferInfo);
                }
            }
        }

        for (var receivedOfferIndex in received) {
            if (received.hasOwnProperty(receivedOfferIndex)) {
                var receievedOfferInfo = received[receivedOfferIndex];
                if (receievedOfferInfo.partner.getSteamID64() == SteamID.getSteamID64) {
                    receievedOfferInfo.accept();
                    acceptedTrades.push(receievedOfferInfo);
                }
            }
        }
        self.confirmOutstandingTrades(function (err, confirmedTrades) {
            callback(err, acceptedTrades);
        });
    });
};


BotAccount.prototype.verifyPhoneNumber = function (code, callback) {
    var self = this;
    self.store.verifyPhoneNumber(code, function (err) {
        if (err) {
            callback(err);
        }
        else {
            callback(undefined);
        }
    });
};


BotAccount.prototype.getRequest = function (url, callback) {
    var self = this;
    self.request({
        url: url,
        method: "GET",
        json: true
    }, function (err, response, body) {
        callback(err, body);
    });
}


/**
 * @callback callbackRequestAPI
 * @param {Error} error - An error message if the process failed, undefined if successful
 * @param {Object} body - An object of the parsed response (undefined if failed)
 */

/**
 * Send GET Request to SteamAPI with details
 * @param apiInterface (String) - Interface name
 * @param version (String) - Interface version (v1 or v2 depending on interface)
 * @param method (String) - method to access
 * @param options - Data to attach to request
 * @param callbackRequestAPI -
 */
BotAccount.prototype.getRequestAPI = function (apiInterface, version, method, options, callbackRequestAPI) {
    var self = this;

    var string = '?';
    var x = 0
    for (var option in options)
        string += option + "=" + options[option] + (x++ < Object.keys(options).length - 1 ? "&" : '');
    self.logger.log('debug', "Sending GET request to " + string);
    self.community.request({
        url: 'http://api.steampowered.com/' + apiInterface + '/' + method + '/' + version + '/' + string,
        method: "GET",
        json: true,
    }, function (err, response, body) {
        callbackRequestAPI(err, body);
    });
}

BotAccount.prototype.getFriendsSummariesNonAPI = function (friends, atCount, friendsCompiled, callback) {
    var self = this;
    var steamids = ""
    var maxCount = atCount + 100;
    if (atCount + 100 > friends.length)
        maxCount = friends.length;
    for (var x = 0 + atCount; x < (maxCount); x++)
        steamids += friends[x].steamid + (x < maxCount - 1 ? ',' : "");


    self.getRequestAPI('ISteamUser', 'v2', 'GetPlayerSummaries', {
        key: self.settings.api_key,
        steamids: steamids
    }, function (err, body) {
        if (err)
            return callback(err, friendsCompiled);
        var compiledFriends = body.response.players;

        if (maxCount < friends.length) {
            self.getFriendsSummaries(friends, atCount + 100, friendsCompiled.concat(compiledFriends), function (err, friendsSummaries) {
                return callback(undefined, compiled);
            });
        }
        else
            return callback(undefined, friendsCompiled.concat(compiledFriends));

    })
};
BotAccount.prototype.getFriendsSummaries = function (friends, atCount, friendsCompiled, callback) {
    var self = this;
    var steamids = ""
    var maxCount = atCount + 100;
    if (atCount + 100 > friends.length)
        maxCount = friends.length;
    for (var x = 0 + atCount; x < (maxCount); x++)
        steamids += friends[x].steamid + (x < maxCount - 1 ? ',' : "");


    self.getRequestAPI('ISteamUser', 'v2', 'GetPlayerSummaries', {
        key: self.settings.api_key,
        steamids: steamids
    }, function (err, body) {
        if (err)
            return callback(err, friendsCompiled);
        var compiledFriends = body.response.players;

        if (maxCount < friends.length) {
            self.getFriendsSummaries(friends, atCount + 100, friendsCompiled.concat(compiledFriends), function (err, friendsSummaries) {
                return callback(undefined, compiled);
            });
        }
        else
            return callback(undefined, friendsCompiled.concat(compiledFriends));

    })
};

BotAccount.prototype.getFriends = function (callback) {
    var self = this;
    var onlineFriendsList = [];
    self.logger.log('debug', "Getting friends list");
    if (self.cachedFriendsList && (typeof self.cachedFriendsList == 'object') && ((new Date().getTime() / 1000) - (self.cachedFriendsList.cacheTime)) < (60 * 10)) {
        onlineFriendsList = self.cachedFriendsList.friendsList.slice();
        self.logger.log('debug', "Used cached friendslist");
        return callback(undefined, onlineFriendsList);
    } else {
        // Due to the fact that we must submit an API call everytime we need friends list, we will cach the data for 5 minutes. Clear cach on force.
        if (!self.loggedIn) {
            self.logger.log('debug', "Queued getFriends method until login.");
            self.addToQueue('login', self.getFriends, [callback]);
        }
        else {
            self.logger.log('debug', "Getting a fresh list of friends");
            self.getRequestAPI('ISteamUser', 'v1', 'GetFriendList', {
                key: self.settings.api_key,
                relationship: 'friend',
                steamid: self.community.steamID
            }, function (err, body) {
                if (err)
                    return callback(err, undefined);

                if (body.hasOwnProperty("friendslist")) {
                    var friends = body.friendslist.friends;
                    self.getFriendsSummaries(friends, 0, [], function (err, friendsSummaries) {
                        // We need to convert SteamID to names... To do that, we need SteamCommunity package.
                        for (var id in friendsSummaries) {
                            onlineFriendsList.push({
                                username: friendsSummaries[id].personaname,
                                accountSid: friendsSummaries[id].steamid
                            });
                        }
                        self.cachedFriendsList = {
                            friendsList: onlineFriendsList,
                            cacheTime: new Date().getTime() / 1000
                        };

                        return callback(undefined, onlineFriendsList.slice());
                    });
                } else {
                    self.logger.log('debug', body);
                    self.logger.log('debug', "Failed to fetch friends - API call failed");
                    return callback(undefined, onlineFriendsList.slice());
                }
            })
        }
    }
};


/**
 * This is a private method - but incase you would like to edit it for your own usage...
 * @param cookies - Cookies sent by Steam when logged in
 * @param sessionID - Session ID as sent by Steam
 * @param {loginCallback} loginCallback - Login details (refer to loginCallback for more info.)
 */
BotAccount.prototype.loggedInAccount = function (cookies, sessionID, loginCallback) {
    var self = this;
    self.chatLogon(500, 'web');
    self.logger.log('debug', 'Logged into %j', self.getAccountName());
    if (self.sessionID != sessionID || self.cookies != cookies) {
        self.sessionID = sessionID;
        self.cookies = cookies;
        delete self.twoFactorCode;
    }
    self.emit('loggedIn', self);
    if (self.cookies) {
        self.community.setCookies(cookies);
        self.store.setCookies(cookies);
        self.TradeOfferManager.setCookies(cookies, function (err) {
            if (err) {
                self.logger.log("debug", "Failed to get API Key - TradeOverflowChecking disabled for %j & getOffers call disabled.", self.getAccountName(), err.Error);
                if (err.Error == "Access Denied")
                    self.api_access = false;
            }
            else
                self.api_access = true;

            self.TradeManager.setAPIAccess(self.api_access);
        });
    }
    self.loggedIn = true;
    self.processQueue('login', function (err) {
        if (err) {
            self.logger.log('error', err);
            return loginCallback(err);
        }
        self.community.on('chatTyping', function (senderID) {
            self.emit('chatTyping', senderID);
        });
        self.community.on('chatLoggedOff', function () {
            self.emit('chatLoggedOff');
        });
        self.community.on('chatMessage', function (senderID, message) {
            if (self.currentChatting != undefined && senderID == self.currentChatting.sid) {
                console.log(("\n" + self.currentChatting.username + ": " + message).green);
            }
            /**
             * Emitted when a friend message or chat room message is received.
             *
             * @event BotAccount#friendOrChatMessage
             * @type {object}
             * @property {SteamID} senderID - The message sender, as a SteamID object
             * @property {String} message - The message text
             * @property {SteamID} room - The room to which the message was sent. This is the user's SteamID if it was a friend message
             */
            self.emit('chatMessage', senderID, message);
        });
        self.community.on('sessionExpired', function (err) {
            self.logger.log('debug', "Login session expired due to " + err);
            self.emit('sessionExpired', err);
        });
        self.TradeOfferManager.on('sentOfferChanged', function (offer, oldState) {
            /**
             * Emitted when a trade offer changes state (Ex. accepted, pending, escrow, etc...)
             *
             * @event BotAccount#offerChanged
             * @type {object}
             * @property {TradeOffer} offer - The new offer's details
             * @property {TradeOffer} oldState - The old offer's details
             */
            self.emit('offerChanged', offer, oldState);
        });

        self.TradeOfferManager.on('receivedOfferChanged', function (offer, oldState) {
            self.emit('receivedOfferChanged', offer, oldState);
        });
        self.TradeOfferManager.on('offerList', function (filter, sent, received) {
            self.emit('offerList', filter, sent, received);
        });
        self.TradeOfferManager.on('newOffer', function (offer) {
            /**
             * Emitted when we receive a new trade offer
             *
             * @event BotAccount#newOffer
             * @type {object}
             * @property {TradeOffer} offer - The offer's details
             */
            self.emit('newOffer', offer);
        });

        self.TradeOfferManager.on('sentOfferChanged', function (offer) {
            /**
             * Emitted when we receive a new trade offer notification (only provides amount of offers and no other details)
             *
             * @event BotAccount#tradeOffers
             * @type {object}
             * @property {Integer} count - The amount of active trade offers (can be 0).
             */
            self.emit('sentOfferChanged', offer);
        });
        self.TradeOfferManager.on('realTimeTradeConfirmationRequired', function (offer) {
            /**
             * Emitted when a trade offer is cancelled
             *
             * @event BotAccount#tradeOffers
             * @type {object}
             * @property {Integer} count - The amount of active trade offers (can be 0).
             */
            self.emit('realTimeTradeConfirmationRequired', offer);
        });
        self.TradeOfferManager.on('realTimeTradeCompleted', function (offer) {
            /**
             * Emitted when a trade offer is cancelled
             *
             * @event BotAccount#tradeOffers
             * @type {object}
             * @property {Integer} count - The amount of active trade offers (can be 0).
             */
            self.emit('realTimeTradeCompleted', offer);
        });
        self.TradeOfferManager.on('sentOfferCanceled', function (offer) {
            /**
             * Emitted when a trade offer is cancelled
             *
             * @event BotAccount#tradeOffers
             * @type {object}
             * @property {Integer} count - The amount of active trade offers (can be 0).
             */
            self.emit('sentOfferCanceled', offer);
        });

        /**
         * Emitted when we fully sign into Steam and all functions are usable.
         *
         * @event BotAccount#loggedIn
         */
        return loginCallback(undefined);
    });
}


BotAccount.prototype.hasPhone = function (callback) {
    var self = this;
    self.store.hasPhone(function (err, hasPhone, lastDigits) {
        callback(err, hasPhone, lastDigits);
    });
};

BotAccount.prototype.getStateName = function (state) {
    return TradeOfferManager.getStateName(state);
};

BotAccount.prototype.setSetting = function (settingName, tempSettingValusettingName) {
    var self = this;
    self.settings[tempSetting] = tempSettingValue;
};
BotAccount.prototype.getSetting = function (tempSetting) {
    var self = this;
    if (self.settings.hasOwnProperty(tempSetting))
        return self.settings[tempSetting];
    return undefined;
};
BotAccount.prototype.deleteSetting = function (tempSetting) {
    var self = this;
    if (self.settings.hasOwnProperty(tempSetting))
        delete self.settings[tempSetting];
};

BotAccount.prototype.chatLogon = function (interval, uiMode) {
    var self = this;
    if (interval == undefined)
        interval = 500;
    if (uiMode == undefined)
        uiMode = 'web';
    self.logger.log('debug', 'Logged on to chat on %j', self.getAccountName());
    self.community.chatLogon(interval, uiMode);
};
BotAccount.prototype.logoutAccount = function () {
    var self = this;
    self.community = new SteamCommunity();
    self.client = new SteamUser();
    self.TradeOfferManager = new TradeOfferManager({
        "steam": self.client,
        "community": self.community,
        "cancelTime": self.settings.tradeCancelTime, // Keep offers upto 1 day, and then just cancel them.
        "pendingCancelTime": self.settings.tradePendingCancelTime, // Keep offers upto 30 mins, and then cancel them if they still need confirmation
        "cancelOfferCount": self.settings.tradeCancelOfferCount,// Cancel offers once we hit 7 day threshold
        "cancelOfferCountMinAge": self.settings.tradeCancelOfferCountMinAge,// Keep offers until 7 days old
        "language": self.settings.language, // We want English item descriptions
        "pollInterval": 5000 // We want to poll every 5 seconds since we don't have Steam notifying us of offers
    });
    self.request = self.community.request;
    self.store = new SteamStore();
    self.loggedIn = false;
};


module.exports = BotAccount;