telegram-bot-api-c v19.5.0
npm -g install telegram-bot-api-c
git clone https://github.com/Daeren/telegram-bot-api-c.git
require("telegram-bot-api-c").call("TK", "sendMessage", [0, "+"])
require("telegram-bot-api-c")("TK").api.sendMessage({chat_id: 0, text: "+"})
require("telegram-bot-api-c")("TK").polling(bot => bot.answer().html("+").send())
> tg-bot --token TK --method sendMessage --chat_id 0 --text "+"
Telegram Bot API, Bot API 2.x, Bot API 3.5
- Proxy: +
- Array and Map as a data source (.call, .callJson, .apimethod): +
- Analytics: tgb-pl-botanio
- Added: tgBot.apisendMethod => error.retryAfter
- Added: sendMediaGroup (doesn't support "attach://")
- All methods in the Bot API are case-insensitive (method: .call, .callJson)
- message: buffer, stream, string
- location|venue|contact: buffer, string
- photo|audio|voice|video|document|sticker|video_note: buffer, stream, file_id, path, url
- certificate: buffer, stream, path, url
Goals:
- High stability;
- Low memory usage;
- Maximum performance;
- Flexibility.
Index
- Start
- Proxy
- Polling
- HTTP
- Virtual
- mServer
- Nginx + Node.js
- Response Builder
- Tg Upload
- Plugin
- Goto
- JS Generators
- Render
- Keyboard
- Download
- InlineQuery
- Send file as Buffer
- CLI
- Test
const rTgBot = require("telegram-bot-api-c");
const gBot = rTgBot(process.env.TELEGRAM_BOT_TOKEN),
gApi = gBot.api;
//----------------------------]>
gBot.promise(require("bluebird"));
//----------------------------]>
gApi
.sendMessage(["0", "Hi"])
.then(console.info, console.error);
gApi.sendMessage(["0", "Hi"], (e, data) => console.log(e || data));
// e - Error: request/JSON.parse/response.ok
// data - JSON: response.result or null
//-------]>
gBot.callJson("sendMessage", ["0", "Hi"], (e, data, res) => console.log(e || data));
// e - Error: request/JSON.parse
// data - JSON: response or null
// res - Class: http.IncomingMessage or null
//-------]>
gBot.call("sendMessage", ["0", "Hi"], (e, data, res) => console.log(e || data));
// e - Error: request
// data - Buffer: response or null
// res - Class: http.IncomingMessage or null
//------------]>
/*
e.code - gApi.sendMessage( ...
data.error_code - callJson("sendMessage" ...
rTgBot or gBot
gBot.ERR_INTERNAL_SERVER
gBot.ERR_NOT_FOUND
gBot.ERR_FORBIDDEN
gBot.ERR_MESSAGE_LIMITS
gBot.ERR_USED_WEBHOOK
gBot.ERR_INVALID_TOKEN
gBot.ERR_BAD_REQUEST
gBot.ERR_BAD_PROXY
gBot.ERR_FAILED_PARSE_DATA
*/
//----------------------------]>
gBot
.polling(onDefault)
.catch(onError)
.use(bot => "syncGotoMyMenu")
.use((bot, data, next) => next(new Error("never get")))
.use("/start", bot => { })
.on("/start", onCmdStart_1)
.on("/start", onCmdStart_2)
.on("/start", onCmdStart_3)
.on("enterChat", onEnterChat)
.on("text:syncGotoMyMenu", onText)
.on("photo document", onPhotoOrDoc)
.on("pinnedMessage", onPinnedMessage)
.on(/^id\s+(\d+)/i, onTextRegEx)
.on(/^(id)\s+(\d+)/i, "type id", onTextRegEx)
.on(/^(login)\s+(\w+)/i, ["type", "login"], onTextRegEx);
function onDefault(bot) { }
function onError(error) { }
function onCmdStart_1(bot, params, next) { next(); } // <-- Async
function onCmdStart_2(bot, params) { } // <-- Sync
function onCmdStart_3(bot, params) { } // <-- Sync | end
function onEnterChat(bot, member) { }
function onText(bot, text) { }
function onPhotoOrDoc(bot, data) { }
function onPinnedMessage(bot, message) { }
function onTextRegEx(bot, data) { }
//-----------]>
/*
bot | gBot -> Sugar -> CtxPerRequest
bot instanceof gBot.constructor | true
bot.command.type | common or private
/start [text] -> common
/start@bot [text] -> private
@bot /start [text] -> private
*/
Proxy
const gBot = rBot(process.env.TELEGRAM_BOT_TOKEN);
const gProxyStr = "127.0.0.1:1337", // <-- Only HTTPS
gProxyArr = ["127.0.0.1", "1337"],
gProxyObj = {
"host": "127.0.0.1",
"port": 1337
};
//------------------]>
function getMe(callback) { gBot.api.getMe(callback); }
//------------------]>
gBot.proxy(gProxyObj);
getMe(t => {
objBot.proxy(gProxyStr);
getMe(t => {
objBot.proxy(); // <-- Remove
getMe();
});
});
rBot.callJson({
"token": process.env.TELEGRAM_BOT_TOKEN,
"method": "getMe",
"proxy": gProxyArr
}, (e, d) => {});
rBot.callJson(process.env.TELEGRAM_BOT_TOKEN, "getMe", (e, d) => {}, gProxyObj);
Polling
const gBot = rBot(process.env.TELEGRAM_BOT_TOKEN);
const gOptions = {
"limit": 100,
"timeout": 0,
"interval": 2 // <-- Default / Sec.
};
//------------------]>
const gSrv = gBot
.polling(gOptions, onMsg)
.on("/stop", onCmdStop);
//------------------]>
function onMsg(bot) {
const msg = bot.isGroup && bot.isReply ? ">_>" : "Stop me: /stop";
bot.answer().isReply().text(msg).send();
}
function onCmdStop(bot, params) {
gSrv.stop();
bot.answer().text(JSON.stringify(params)).send();
}
HTTP
const rBot = require("telegram-bot-api-c");
//-----------------------------------------------------
const gSrvOptions = {
// For Self-signed certificate, you need to upload your public key certificate
// "selfSigned": "fullPath/stream/buffer", // <-- If you use Auto-Webhook
"certDir": "/www/site",
"key": "/3_site.xx.key",
"cert": "/2_site.xx.crt",
"ca": [
"/AddTrustExternalCARoot.crt",
"/COMODORSAAddTrustCA.crt",
"/COMODORSADomainValidationSecureServerCA.crt"
],
"host": "site.xx"
};
//------------------]>
const gBotFather = rBot();
const gMyBot = rBot(process.env.TG_BOT_TOKEN_MY),
gOtherBot = rBot(process.env.TG_BOT_TOKEN_OTHER);
const gSrv = gBotFather.http(gSrvOptions);
gSrv
.bot(gMyBot) // <-- Auto-Webhook: "/tg_bot_<sha256(token)>"
.on("/start", onCmdStart)
.on("/stop", onCmdStop);
gSrv
.bot(gOtherBot, "/urlOtherBot", onMsgOtherBot); // <-- Auto-Webhook
//------------------]>
function onMsgOtherBot(bot) { }
function onCmdStart(bot, params) { }
function onCmdStop(bot, params) { }
Virtual
const gBot = rBot(process.env.TELEGRAM_BOT_TOKEN);
const gSrv = gBot
.virtual(function(bot) {
bot.answer().text("Not found!").send();
})
.on("photo", console.log);
//----[Proxy: express]----}>
gBot
.api
.setWebhook({"url": "https://site.xx/dev-bot"})
.then(function(isOk) {
const rExpress = require("express"),
rBodyParser = require("body-parser");
rExpress()
.use(rBodyParser.json())
.post("/dev-bot", gSrv.middleware)
.listen(3000, "localhost");
});
//----[Stress Tests]----}>
gSrv.input(null, {
"update_id": 0,
"message": {
"message_id": 0,
"from": {
"id": 0,
"first_name": "D",
"username": ""
},
"chat": {
"id": 0,
"first_name": "D",
"username": "",
"type": "private"
},
"date": 0,
"text": "Hello"
}
});
mServer
const gBot = rBot(process.env.TELEGRAM_BOT_TOKEN);
gBot
.api
.setWebhook({"url": "https://site.xx/myBot"})
.then(function(isOk) {
if(!isOk) {
throw new Error("Oops...problem with the webhook...");
}
gBot.http(gSrvOptions, cbMsg);
});
NGINX + Node.js
const gBot = rBot();
const gSrvOptions = {
"ssl": false,
"autoWebhook": "site.xx:88", // <-- Default: (host + port); `false` - disable
"host": "localhost",
"port": 1490
};
gBot.http(gSrvOptions, onMsg);
//----[DEFAULT]----}>
gBot.http();
gBot.http(onMsg);
// host: localhost
// port: 1488
// autoWebhook: false
// ssl: false
Response Builder
objSrv
.use(function(bot) {
bot
.answer() // <-- Builder + Queue
.chatAction("typing") // <-- Element
.text("https://google.com", "markdown") // <-- Element
//.parseMode("markdown")
.disableWebPagePreview() // <-- Modifier (for the last element)
.keyboard([["X"], ["Y"]]) // <-- Modifier
.markdown("*text*") // <-- Element
.html("<a>text</a>")
.chatAction("upload_photo")
.photo("https://www.google.ru/images/logos/ps_logo2.png", "myCaption")
.caption("#2EASY") // <-- Modifier
.keyboard("old")
.keyboard("new", "selective") // <-- Uses: bot.mid (selective)
.location(69, 96)
.latitude(13)
.keyboard() // <-- Hide
.send() // <-- Uses: bot.cid
.then(console.log); // <-- Return: array | results
//------[ONE ELEMENT]------}>
const customKb = {
"keyboard": [["1"], ["2"], ["3"]],
"resize_keyboard": true
};
bot
.answer()
.text("Hi")
.keyboard(customKb)
.send((e, r) => console.log(e || r)); // <-- Return: hashTable | result
//------[RENDER]------}>
const template = "Hi, {name}!";
const buttons = [["{btnMenu}", "{btnOptions}"]];
const input = {
"name": "MiElPotato",
"btnMenu": "Menu +",
"btnOptions": "Options"
};
bot
.answer()
.text(template)
.keyboard(buttons, "resize")
.render(input) // <-- text + keyboard
.send();
bot
.answer()
.text("Msg: {0} + {1}")
.render(["H", "i"]) // <-- text
.keyboard([["X: {0}", "Y: {1}"]])
.send();
});
Name | Args |
---|---|
- | |
html | text, disable_web_page_preview, disable_notification, reply_to_message_id, reply_markup |
markdown | text, disable_web_page_preview, disable_notification, reply_to_message_id, reply_markup |
- | |
text | text, parse_mode, disable_web_page_preview, disable_notification, reply_to_message_id, reply_markup |
photo | photo, caption, disable_notification, reply_to_message_id, reply_markup |
audio | audio, performer, title, duration, caption, disable_notification, reply_to_message_id, reply_markup |
document | document, caption, disable_notification, reply_to_message_id, reply_markup |
sticker | sticker, disable_notification, reply_to_message_id, reply_markup |
video | video, width, height, duration, caption, disable_notification, reply_to_message_id, reply_markup |
voice | voice, duration, caption, disable_notification, reply_to_message_id, reply_markup |
videoNote | videoNote, duration, length, disable_notification, reply_to_message_id, reply_markup |
location | latitude, longitude, disable_notification, reply_to_message_id, reply_markup |
venue | latitude, longitude, title, address, foursquare_id, disable_notification, reply_to_message_id, reply_markup |
contact | phone_number, first_name, last_name, disable_notification, reply_to_message_id, reply_markup |
chatAction | action |
game | game_short_name, disable_notification, reply_to_message_id, reply_markup |
invoice | title ... ... reply_markup |
- | |
inlineQuery | results, next_offset, is_personal, cache_time, switch_pm_text, switch_pm_parameter |
callbackQuery | text, show_alert |
shippingQuery | ok, shipping_options, error_message |
preCheckoutQuery | ok, error_message |
Tg Upload
gBot.enable("tgUrlUpload");
gBot
.polling()
.on("text", function(bot, url) {
bot.answer().photo(url).send();
});
/*
Added the option to specify an HTTP URL for a file in all methods where InputFile or file_id can be used (except voice messages).
Telegram will get the file from the specified URL and send it to the user.
Files must be smaller than 5 MB for photos and smaller than 20 MB for all other types of content.
*/
Plugin
gSrv
.use(function(bot, data, next) {
console.log("Async | Type: any");
if(data === "next") {
next();
}
})
.use("text", function(bot) {
console.log("F:Sync | Type: text");
bot.user = {};
})
.use(function(bot) {
bot.user.id = 1;
});
gSrv
.on("text", function(bot, data) {
bot.user.id;
});
Goto
gSrv
.use(function(bot, data, next) {
next(data === "room" ? "room.menu" : "");
})
.use(function(bot) {
console.log("If not the room");
// return "room.menu";
})
.on("text", function(bot, data) { })
.on("text:room.menu", function(bot, data) { });
JS Generators
gBot
.polling(function* (bot) {
const result = yield send(bot);
console.info(result);
yield error();
})
.catch(function* (error) {
console.error(error);
})
.use(function* (bot) {
yield auth("D", "13");
})
.use("text", function* (bot, data) {
yield save();
if(data === "key") {
return "eventYield";
}
})
.on("text:eventYield", function* (bot, data) {
console.log("eventYield:", data);
});
//----------------]>
function auth(login, password) {
return new Promise(x => setTimeout(x, 1000));
}
function send(bot) {
return bot.answer().text("Ok, let's go...").send();
}
Render
//-----[EJS]-----}>
gBot.engine(require("ejs"))
data = {"x": "H", "y": "i"};
bot.render("EJS | Text: <%= x %> + <%= y %>", data);
//-----[DEFAULT]-----}>
data = ["H", "i"];
bot.render("Array | Text: {0} + {1}", data);
data = {"x": "H", "y": "i"};
bot.render("Hashtable | Text: {x} + {y}", data);
Keyboard
const rBot = require("telegram-bot-api-c");
function onMsg(bot) {
const data = {};
data.chat_id = bot.cid;
data.text = "Hell Word!";
data.reply_markup = bot.keyboard(); // Or: bot.keyboard.hide()
data.reply_markup = bot.keyboard([["1", "2"], ["3"]]);
data.reply_markup = bot.keyboard.hOx();
data.reply_markup = bot.keyboard.inline.hOx();
bot.api.sendMessage(data);
}
rBot.keyboard.numpad(true); // <-- Once
rBot.keyboard.numpad(false, true); // <-- Selective
rBot.keyboard.inline.numpad();
//------------------------------
rBot.keyboard(buttons[, params])
rBot.keyboard.inline(inlButtons, isVertically)
/*
buttons: `string`, `array of array` or `false`
inlButtons: `string`, `array of array` or `object`
params: "resize once selective"
v - vertically; h - horizontally;
vOx, hOx, vPn, hPn, vLr, hLr, vGb, hGb
abcd, numpad, hide
Normal keyboard:
vOx(once, selective)
numpad(once, selective)
*/
Name | Note |
---|---|
- | |
_Ox | O / X |
_Pn | + / - |
_Ud | Upwards / Downwards arrow |
_Lr | Leftwards / Rightwards arrow |
_Gb | Like / Dislike |
- | |
abcd | ABCD |
numpad | 0-9 |
- | |
hide |
Download
gBot.download("file_id", "dir"/*, callback*/);
gBot.download("file_id", "dir", "name.mp3"/*, callback*/);
gBot
.download("file_id")
.then(function(info) {
info.stream.pipe(require("fs").createWriteStream("./" + info.name));
});
gBot
.download("file_id", function(error, info) {
info.stream.pipe(require("fs").createWriteStream("./myFile"));
});
InlineQuery
https://core.telegram.org/bots/inline
gBot
.polling()
.on("inlineQuery", function(bot, data) {
const idx = Date.now().toString(32) + Math.random().toString(24);
const results = [
{
"type": "article",
"title": "Title #1",
"message_text": "Text...",
"thumb_url": "https://pp.vk.me/c627530/v627530230/2fce2/PF9loxF4ick.jpg"
},
{
"type": "article",
"title": "Title #2: " + data.query,
"message_text": "Text...yeah"
},
{
"type": "photo",
"photo_width": 128,
"photo_height": 128,
"photo_url": "https://pp.vk.me/c627530/v627530230/2fce2/PF9loxF4ick.jpg",
"thumb_url": "https://pp.vk.me/c627530/v627530230/2fce2/PF9loxF4ick.jpg"
}
]
.map((t, i) => { t.id = idx + i; return t; });
// results = {results};
bot
.answer()
.inlineQuery(results)
.send()
.then(console.info, console.error);
});
//------------]>
bot
.api
.answerInlineQuery({
"inline_query_id": 0,
"results": results
})
.then(console.info, console.error);
Send file as Buffer
const imgBuffer = require("fs").readFileSync(__dirname + "/MiElPotato.jpg");
//------------]>
objSrv
.use(function(bot, next) {
bot
.answer()
.photo(imgBuffer)
.filename("MiElPotato.jpg") // <-- It is important
.filename("/path/MiElPotato.jpg") // <-- Same as above
.send();
});
//------------]>
api.sendPhoto({
"chat_id": 0,
"photo": imgBuffer,
"filename": "MiElPotato.jpg" // <-- It is important
});
api.sendDocument({
"chat_id": 0,
"document": imgBuffer
});
CLI
Key | Note |
---|---|
- | |
-j | insert white space into the output JSON string for readability purposes |
- | |
--token | high priority |
--method | high priority |
--proxy | "ip:port" |
// Environment variables: low priority
> set TELEGRAM_BOT_TOKEN=X
> set TELEGRAM_BOT_METHOD=X
> set TELEGRAM_BOT_PROXY=X
...
> tg-bot --token X --method sendMessage --key val -bool
> node telegram-bot-api-c --token X --method sendMessage --key val -bool
...
> tg-bot --token X --method sendMessage --chat_id 0 --text "Hi" -disable_web_page_preview
> tg-bot --token X --method sendMessage < "./examples/msg.json"
> tg-bot --token X --method sendPhoto --chat_id 0 --photo "/path/MiElPotato.jpg"
> tg-bot --token X --method sendPhoto --chat_id 0 --photo "https://www.google.ru/images/logos/ps_logo2.png"
...
> tg-bot
> {"token": "", "method": "sendMessage", "chat_id": 0, "text": "1"}
> <enter>
(result)
> {"chat_id": 0, "text": "2", "j": true, "proxy": "ip:port"}
> <enter>
(result)
Test
npm -g install mocha
npm install chai
set TELEGRAM_BOT_TOKEN=X
set TELEGRAM_CHAT_ID=X
set TELEGRAM_MSG_ID=X
cd <module>
npm test
Module
Method | Arguments | Note |
---|---|---|
- | ||
keyboard | buttons, params | return: object; buttons: string/array; params: "resize once selective" |
parseCmd | text, strict | return: {type, name, text, cmd}; strict: maxLen32 + alphanum + underscore |
- | ||
call | token, method, data, proxy | |
call | options{token, method, proxy, tgUrlUpload}, data | |
callJson | token, method, data, proxy | |
callJson | options{token, method, proxy, tgUrlUpload}, data |
Instance
Attribute | Type | Note |
---|---|---|
- | ||
api | object | See Telegram Bot API |
- | ||
keyboard | function | |
parseCmd | function |
Method | Arguments | Return |
---|---|---|
- | ||
- | ||
enable | key | this |
disable | key | this |
enabled | key | true/false |
disabled | key | true/false |
- | ||
engine | instance | this |
promise | instance | this |
token | token | this or token |
proxy | proxy | this |
- | ||
call | method, data | |
callJson | method, data | |
- | ||
render | template, data | string |
download | fid, dir, callback(error, info {id,size,file,stream}) | promise or undefined |
- | ||
http | options | object |
polling | options | object |
virtual | callback(bot, cmd) | object |
Methods: Response Builder
Name | Args | Note |
---|---|---|
- | ||
inlineQuery | (results) | |
callbackQuery | (message) | |
- | ||
render | (data) | |
keyboard | (buttons, params) | |
inlineKeyboard | (buttons, isVertically) | |
- | ||
isReply | (flag) | |
send | (callback) | |
- | ||
text | ||
photo | Ext: jpg, jpeg, gif, tif, png, bmp | |
audio | Ext: mp3 | |
document | ||
sticker | Ext: webp , jpg, jpeg, gif, tif, png, bmp | |
video | Ext: mp4 | |
voice | Ext: ogg | |
location | ||
venue | ||
contact | ||
chatAction | ||
game |
Methods: Server
Name | Arguments | Return |
---|---|---|
- | ||
POLLING | ||
- | ||
start | this | |
stop | this | |
HTTP | ||
- | ||
bot | bot, path | new srvInstance |
- | ||
VIRTUAL | ||
- | ||
input | error, data | |
middleware | ||
- | ||
ALL | ||
- | ||
catch | callback(error) | this |
use | type, params, callback(bot, data, next) | this |
on | type, params, callback(data, params, next) | this |
off | type | this |
Fields: bot | srv.on('', bot => 0)
Name | Type | Note |
---|---|---|
- | ||
isGroup | boolean | bot.isGroup = bot.message.chat.type === supergroup |
isReply | boolean | bot.isReply = !!bot.message.reply_to_message |
- | ||
cid | number | bot.cid = bot.message.chat.id |
mid | number | bot.mid = bot.message.message_id |
qid | string | bot.qid = bot.inlineQuery.id |
cqid | string | bot.cqid = bot.callbackQuery.id |
sid | string | bot.sid = bot.shipping_query.id |
pqid | string | bot.pqid = bot.pre_checkout_query.id |
- | ||
command | object | Incoming command |
- | ||
updateType | string | |
updateSubType | string | |
eventType | string | |
eventSubType | string | |
gotoState | string | |
- | ||
from | object | Persistent |
- | ||
message | object | Incoming message |
inlineQuery | object | Incoming inline query |
chosenInlineResult | object | The result of an inline query that was chosen |
callbackQuery | object | Incoming callback query |
- | ||
answer | function() | Response Builder; message; Uses: cid, mid |
answer | function() | Response Builder; inlineQuery; Uses: qid |
answer | function() | Response Builder; callbackQuery; Uses: cqid |
Events: use / on
Name | Args | Note |
---|---|---|
- | ||
message | bot, message, next | |
editedMessage | bot, message, next | |
- | ||
channelPost | bot, post, next | |
editedChannelPost | bot, post, next | |
- | ||
inlineQuery | bot, data, next | |
chosenInlineResult | bot, data, next | |
callbackQuery | bot, data, next | |
- | ||
pinnedMessage | bot, message, next | |
- | ||
invoice | bot, data, next | |
successfulPayment | bot, data, next | |
- | ||
enterChat | bot, data, next | |
leftChat | bot, data, next | |
- | ||
chatTitle | bot, data, next | |
chatNewPhoto | bot, data, next | |
chatDeletePhoto | bot, data, next | |
- | ||
chatCreated | bot, data, next | |
superChatCreated | bot, data, next | |
channelChatCreated | bot, data, next | |
- | ||
migrateToChatId | bot, data, next | |
migrateFromChatId | bot, data, next | |
- | ||
text | bot, data, next | |
photo | bot, data, next | |
audio | bot, data, next | |
document | bot, data, next | |
sticker | bot, data, next | |
video | bot, data, next | |
voice | bot, data, next | |
videoNote | bot, data, next | |
location | bot, data, next | |
venue | bot, data, next | |
contact | bot, data, next | |
game | bot, data, next | |
- | ||
* | bot, data, next | |
/name | bot, params, next | CMD |
- | ||
(regexp) | bot, params, next |
License
MIT
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago