19.5.0 • Published 6 years ago

telegram-bot-api-c v19.5.0

Weekly downloads
137
License
MIT
Repository
github
Last release
6 years ago

Codacy NodeSecurity

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

- 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:

  1. High stability;
  2. Low memory usage;
  3. Maximum performance;
  4. Flexibility.

Index

architecture

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();
    });
NameArgs
-
htmltext, disable_web_page_preview, disable_notification, reply_to_message_id, reply_markup
markdowntext, disable_web_page_preview, disable_notification, reply_to_message_id, reply_markup
-
texttext, parse_mode, disable_web_page_preview, disable_notification, reply_to_message_id, reply_markup
photophoto, caption, disable_notification, reply_to_message_id, reply_markup
audioaudio, performer, title, duration, caption, disable_notification, reply_to_message_id, reply_markup
documentdocument, caption, disable_notification, reply_to_message_id, reply_markup
stickersticker, disable_notification, reply_to_message_id, reply_markup
videovideo, width, height, duration, caption, disable_notification, reply_to_message_id, reply_markup
voicevoice, duration, caption, disable_notification, reply_to_message_id, reply_markup
videoNotevideoNote, duration, length, disable_notification, reply_to_message_id, reply_markup
locationlatitude, longitude, disable_notification, reply_to_message_id, reply_markup
venuelatitude, longitude, title, address, foursquare_id, disable_notification, reply_to_message_id, reply_markup
contactphone_number, first_name, last_name, disable_notification, reply_to_message_id, reply_markup
chatActionaction
gamegame_short_name, disable_notification, reply_to_message_id, reply_markup
invoicetitle ... ... reply_markup
-
inlineQueryresults, next_offset, is_personal, cache_time, switch_pm_text, switch_pm_parameter
callbackQuerytext, show_alert
shippingQueryok, shipping_options, error_message
preCheckoutQueryok, 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)
*/
NameNote
-
_OxO / X
_Pn+ / -
_UdUpwards / Downwards arrow
_LrLeftwards / Rightwards arrow
_GbLike / Dislike
-
abcdABCD
numpad0-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

KeyNote
-
-jinsert white space into the output JSON string for readability purposes
-
--tokenhigh priority
--methodhigh 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

npm test

Module

MethodArgumentsNote
-
keyboardbuttons, paramsreturn: object; buttons: string/array; params: "resize once selective"
parseCmdtext, strictreturn: {type, name, text, cmd}; strict: maxLen32 + alphanum + underscore
-
calltoken, method, data, proxy
calloptions{token, method, proxy, tgUrlUpload}, data
callJsontoken, method, data, proxy
callJsonoptions{token, method, proxy, tgUrlUpload}, data

Instance

AttributeTypeNote
-
apiobjectSee Telegram Bot API
-
keyboardfunction
parseCmdfunction
MethodArgumentsReturn
-
-
enablekeythis
disablekeythis
enabledkeytrue/false
disabledkeytrue/false
-
engineinstancethis
promiseinstancethis
tokentokenthis or token
proxyproxythis
-
callmethod, data
callJsonmethod, data
-
rendertemplate, datastring
downloadfid, dir, callback(error, info {id,size,file,stream})promise or undefined
-
httpoptionsobject
pollingoptionsobject
virtualcallback(bot, cmd)object

Methods: Response Builder

NameArgsNote
-
inlineQuery(results)
callbackQuery(message)
-
render(data)
keyboard(buttons, params)
inlineKeyboard(buttons, isVertically)
-
isReply(flag)
send(callback)
-
text
photoExt: jpg, jpeg, gif, tif, png, bmp
audioExt: mp3
document
stickerExt: webp , jpg, jpeg, gif, tif, png, bmp
videoExt: mp4
voiceExt: ogg
location
venue
contact
chatAction
game

Methods: Server

NameArgumentsReturn
-
POLLING
-
startthis
stopthis
HTTP
-
botbot, pathnew srvInstance
-
VIRTUAL
-
inputerror, data
middleware
-
ALL
-
catchcallback(error)this
usetype, params, callback(bot, data, next)this
ontype, params, callback(data, params, next)this
offtypethis

Fields: bot | srv.on('', bot => 0)

NameTypeNote
-
isGroupbooleanbot.isGroup = bot.message.chat.type === supergroup
isReplybooleanbot.isReply = !!bot.message.reply_to_message
-
cidnumberbot.cid = bot.message.chat.id
midnumberbot.mid = bot.message.message_id
qidstringbot.qid = bot.inlineQuery.id
cqidstringbot.cqid = bot.callbackQuery.id
sidstringbot.sid = bot.shipping_query.id
pqidstringbot.pqid = bot.pre_checkout_query.id
-
commandobjectIncoming command
-
updateTypestring
updateSubTypestring
eventTypestring
eventSubTypestring
gotoStatestring
-
fromobjectPersistent
-
messageobjectIncoming message
inlineQueryobjectIncoming inline query
chosenInlineResultobjectThe result of an inline query that was chosen
callbackQueryobjectIncoming callback query
-
answerfunction()Response Builder; message; Uses: cid, mid
answerfunction()Response Builder; inlineQuery; Uses: qid
answerfunction()Response Builder; callbackQuery; Uses: cqid

Events: use / on

NameArgsNote
-
messagebot, message, next
editedMessagebot, message, next
-
channelPostbot, post, next
editedChannelPostbot, post, next
-
inlineQuerybot, data, next
chosenInlineResultbot, data, next
callbackQuerybot, data, next
-
pinnedMessagebot, message, next
-
invoicebot, data, next
successfulPaymentbot, data, next
-
enterChatbot, data, next
leftChatbot, data, next
-
chatTitlebot, data, next
chatNewPhotobot, data, next
chatDeletePhotobot, data, next
-
chatCreatedbot, data, next
superChatCreatedbot, data, next
channelChatCreatedbot, data, next
-
migrateToChatIdbot, data, next
migrateFromChatIdbot, data, next
-
textbot, data, next
photobot, data, next
audiobot, data, next
documentbot, data, next
stickerbot, data, next
videobot, data, next
voicebot, data, next
videoNotebot, data, next
locationbot, data, next
venuebot, data, next
contactbot, data, next
gamebot, data, next
-
*bot, data, next
/namebot, params, nextCMD
-
(regexp)bot, params, next

License

MIT


@ Daeren @ Telegram

19.5.0

6 years ago

19.4.0

7 years ago

19.2.1

7 years ago

19.1.0

7 years ago

19.0.0

7 years ago

18.0.0

7 years ago

17.1.2

8 years ago

17.1.1

8 years ago

17.1.0

8 years ago

17.0.0

8 years ago

16.1.0

8 years ago

16.0.0

8 years ago

15.1.0

8 years ago

15.0.1

8 years ago

15.0.0

8 years ago

14.0.1

8 years ago

14.0.0

8 years ago

13.0.0

8 years ago

12.0.3

8 years ago

12.0.2

8 years ago

12.0.1

8 years ago

12.0.0

8 years ago

11.1.0

8 years ago

11.0.3

8 years ago

11.0.2

8 years ago

11.0.1

8 years ago

11.0.0

8 years ago

10.2.1

8 years ago

10.2.0

8 years ago

10.1.0

8 years ago

10.0.1

8 years ago

10.0.0

8 years ago

9.2.2

8 years ago

9.2.1

8 years ago

9.2.0

8 years ago

9.1.3

8 years ago

9.1.2

8 years ago

9.1.1

8 years ago

9.1.0

8 years ago

9.0.1

8 years ago

9.0.0

8 years ago

8.1.2

8 years ago

8.1.1

8 years ago

8.1.0

8 years ago

8.0.1

8 years ago

8.0.0

8 years ago

7.0.0

8 years ago

6.6.6

8 years ago

6.6.5

8 years ago

6.6.4

8 years ago

6.6.3

8 years ago

6.6.2

8 years ago

6.6.1

8 years ago

6.6.0

8 years ago

6.5.2

8 years ago

6.5.1

8 years ago

6.5.0

8 years ago

6.4.0

8 years ago

6.3.3

8 years ago

6.3.2

8 years ago

6.3.1

8 years ago

6.3.0

8 years ago

6.2.2

8 years ago

6.2.1

8 years ago

6.2.0

8 years ago

6.1.0

8 years ago

6.0.0

9 years ago

5.1.2

9 years ago

5.1.1

9 years ago

5.1.0

9 years ago

5.0.6

9 years ago

5.0.5

9 years ago

5.0.4

9 years ago

5.0.3

9 years ago

5.0.2

9 years ago

5.0.1

9 years ago

5.0.0

9 years ago

4.0.2

9 years ago

4.0.1

9 years ago

4.0.0

9 years ago

3.5.0

9 years ago

3.4.6

9 years ago

3.4.5

9 years ago

3.4.4

9 years ago

3.4.3

9 years ago

3.4.2

9 years ago

3.4.1

9 years ago

3.4.0

9 years ago

3.3.3

9 years ago

3.3.2

9 years ago

3.3.1

9 years ago

3.3.0

9 years ago

3.2.0

9 years ago

3.1.1

9 years ago

3.1.0

9 years ago

3.0.2

9 years ago

3.0.1

9 years ago

3.0.0

9 years ago

2.1.11

9 years ago

2.1.10

9 years ago

2.1.9

9 years ago

2.1.8

9 years ago

2.1.7

9 years ago

2.1.6

9 years ago

2.1.5

9 years ago

2.1.4

9 years ago

2.1.3

9 years ago

2.1.2

9 years ago

2.1.1

9 years ago

2.1.0

9 years ago

2.0.2

9 years ago

2.0.1

9 years ago

2.0.0

9 years ago

1.2.1

9 years ago

1.2.0

9 years ago

1.1.1

9 years ago

1.1.0

9 years ago

1.0.6

9 years ago

1.0.5

9 years ago

1.0.4

9 years ago

1.0.3

9 years ago

1.0.2

9 years ago

1.0.1

9 years ago

1.0.0

9 years ago

0.2.3

9 years ago

0.2.2

9 years ago

0.2.1

9 years ago

0.2.0

9 years ago

0.1.0

9 years ago

0.0.6

9 years ago

0.0.5

9 years ago

0.0.4

9 years ago

0.0.3

9 years ago

0.0.2

9 years ago

0.0.1

9 years ago