1.7.4 • Published 2 months ago

amethystjs v1.7.4

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

Installation

npm : npm install amethystjs
yarn : yarn add amethystjs
pnpm : pnpm add amethystjs

Features

With this powerful framework you can :

See an example right here

Create Amythyst Client

Import the client

import { AmethystClient } from 'amethystjs';

const client = new AmethystClient({
    // Discord.js client options
}, {
    botName: "your bot's name", // Optionnal
    botNameWorksAsPrefix: true, // Wether if we can use the bot's name as prefix - optionnal
    commandsFolder: './yourCommandsFolder', // Optionnal
    eventsFolder: './yourEventsFolder', // Optionnal
    prefix: "bot's prefix", // Optionnal
    strictPrefix: true, // Wether if the prefix must be exactly the same - optionnal
    mentionWorksAsPrefix: true, // Wether if we can use the bot by mentionning it
    token: "Your bot's token",
    debug: false, // Enable debug mode (get a lot of messages in the console) - optionnal
    defaultCooldownTime: 5, // Default cooldown time
    preconditionsFolder: "./yourPreconditionsFolder", // Specify the preconditions folder - optionnal
    autocompleteListenersFolder: "./autocompleteListenersFolder", // Specify the autocomplete folder - optionnal
    buttonsFolder: './buttonsFolder', // Specify the button folder for button handlers - optionnal
    customPrefixAndDefaultAvailable?: true, // Specify if the default prefix is usable when a custom prefix is set - optionnal
    modalHandlersFolder: "./yourModalHandlersFolder", // Specify the modal handlers folder
    debbugColors: 'none', // 'none' | 'icon' | 'line', defines if the debugger uses colors - optional
    commandsArchitecture: 'simple', // 'simple' | 'double', if simple, the commands inside the commandsFolder will be read, if double, the commands inside the directories of the commandsFolder will be read
    eventsArchitecture: 'simple' // 'simple' | 'double', if simple, the events inside the eventsFolder will be read, if double, the events inside the directories of the eventsFolder will be read
});
client.start({
    // All are optionnal
    loadCommands: true, // Load commands
    loadEvents: true, // Load events
    loadPreconditions: true, // Load preconditions
    loadAutocompleteListeners: true, // Load autocomplete listeners
    loadModals: true // Load modals handlers
});
const { AmethystClient } = require('amethystjs');

const client = new AmethystClient({
    // Discord.js client options
}, {
    botName: "your bot's name", // Optionnal
    botNameWorksAsPrefix: true, // Wether if we can use the bot's name as prefix - optionnal
    commandsFolder: './yourCommandsFolder', // Optionnal
    eventsFolder: './yourEventsFolder', // Optionnal
    prefix: "bot's prefix", // Optionnal
    strictPrefix: true, // Wether if the prefix must be exactly the same - optionnal
    mentionWorksAsPrefix: true, // Wether if we can use the bot by mentionning it
    token: "Your bot's token",
    debug: false, // Enable debug mode (get a lot of messages in the console) - optionnal
    defaultCooldownTime: 5, // Default cooldown time
    preconditionsFolder: "./yourPreconditionsFolder", // Specify the preconditions folder - optionnal
    autocompleteListenersFolder: "./autocompleteListenersFolder", // Specify the autocomplete folder - optionnal
    buttonsFolder: './buttonsFolder', // Specify the button folder for button handlers - optionnal
    customPrefixAndDefaultAvailable?: true, // Specify if the default prefix is usable when a custom prefix is set - optionnal
    modalHandlersFolder: "./yourModalHandlersFolder", // Specify the modal handlers folder
    debbugColors: 'none', // 'none' | 'icon' | 'line', defines if the debugger uses colors - optional
    commandsArchitecture: 'simple' // 'simple' | 'double', if simple, the commands inside the commandsFolder will be read, if double, the commands inside the directories of the commandsFolder will be read
    eventsArchitecture: 'simple', // 'simple' | 'double', if simple, the events inside the eventsFolder will be read, if double, the events inside the directories of the eventsFolder will be read
    commandLocalizationsUsedAsNames: true // Allows the localized names to be used as message command names
});
client.start({
    // All are optionnal
    loadCommands: true, // Load commands
    loadEvents: true, // Load events
    loadPreconditions: true, // Load preconditions
    loadAutocompleteListeners: true // Load autocomplete listeners
});

Create a command

Go in your commands folder, and create a new file.

Architecture

The emplacement of the file depends of your architecture ( commandsArchitecture, defined when creating the client )

If it is simple, it looks like this :

|_commands
  |_info.ts/js
  |_ban.ts/js

If you configured it to double, it looks like this :

|_commands
  |_moderation
    |_ban.ts/js
    |_info.ts/js

Command creation

Import AmethystCommand and exports it

import { AmethystCommand } from 'amethystjs';

export default new AmethystCommands({
    name: 'command name', // Command name
    description: "Description of the command", // Description - required
    cooldown: 5, // Cooldown time
    permissions: [ 'Administrator' ], // Permissions for the user - optionnal
    clientPermissions: [ 'ManageChannels' ], // Permissions for the bot - optionnal
    preconditions: [  ], // Preconditions for the command - optionnal
    messageInputChannelTypes: [], // Channel types allowed for message input running - optionnal
    aliases: ['alias 1', 'alias 2', '...'], // Command aliases - optionnal
    messageInputDescription: "Description of the message command (optionnal)", // Message description - optionnal
    userContextName: "name of the user context command", // Name of the user context command - optionnal
    messageContextName: "Name of the message context command" // Name of the message context command - optionnal
})
.setMessageRun((options) => {
    // Write code for message commands (optionnal)
})
.setChatInputRun((options) => {
    // Write code for slash commands (optionnal)
}).setUserContextMenuRun((options) => {
    // Write code for user context menu command (optionnal)
})
.setMessageContextMenuRun((options) => {
    // Write code for message context menu command (optionnal)
})
const { Amethystcommand } = require('amethystjs');

module.exports = new AmethystCommands({
    cooldown: 5, // Cooldown time
    name: 'command name', // Command name
    permissions: [ 'Administrator' ], // Permissions for the user - optionnal
    clientPermissions: [ 'ManageChannels' ], // Permissions for the bot - optionnal
    preconditions: [  ], // Preconditions for the command - optionnal
    messageInputChannelTypes: [], // Channel types allowed for message input running - optionnal
    aliases: ['alias 1', 'alias 2', '...'], // Command aliases - optionnal
    description: "Description of the command", // Description - required
    messageInputDescription: "Description of the message command (optionnal)", // Message description - optionnal
    userContextName: "name of the user context command", // Name of the user context command - optionnal
    messageContextName: "Name of the message context command" // Name of the message context command - optionnal
})
.setMessageRun((options) => {
    // Write code for message commands (optionnal)
})
.setChatInputRun((options) => {
    // Write code for slash commands (optionnal)
}).setUserContextMenuRun((options) => {
    // Write code for user context menu command (optionnal)
})
.setMessageContextMenuRun((options) => {
    // Write code for message context menu command (optionnal)
})

Handle errors

Use the commandDenied event to handle command denietions

Use the commandError event to handle command errors

Record your own preconditions

Amethyst JS allows you to create your own preconditions (because it's fun 🙂)

First, import the Precondition from Amethyst

import { Precondition } from 'amethystjs';

export default new Precondition("Your precondition's name")
.setChatInputRun((options) => {
    // Run your precondition here for slash commands
    // You have to return something like this :
    return {
        ok: true,
        message: 'Message in case of fail',
        metadata: {/* some more datas */},
        interaction: options.interaction,
        type: 'chatInput'
    }
})
.setMessageRun((options) => {
    // Run your precondition here for message commands
    // Return something like this
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* some extra datas */ },
        type: 'message',
        channelMessage: options.message
    }
}).setModalRun((options) => {
    // Run your modal precondition here
    // Return something like so
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* some data here */ },
        type: 'modal',
        modal: options.modal
    }
}).setUserContextMenuRun((options) => {
    // Run your precondition for user context command here
    // Return something like this
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* Some more datas */ },
        type: 'userContextMenu',
        contextMenu: options.interaction
    }
}).setMessageContextMenuRun((options) => {
    // Run your precondition for message context command here
    // Return something like this
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* Some more datas */ },
        type: 'messageContextMenu',
        contextMenu: options.interaction
    }
})
const { Precondition } = require('amethystjs');

module.exports = new Precondition("Your precondition's name")
.setChatInputRun((options) => {
    // Run your precondition here for slash commands
    // You have to return something like this :
    return {
        ok: true,
        message: 'Message in case of fail',
        metadata: {/* some more datas */},
        interaction: options.interaction,
        type: 'chatInput'
    }
})
.setMessageRun((options) => {
    // Run your precondition here for message commands
    // Return something like this
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* some extra datas */ },
        type: 'message',
        channelMessage: options.message
    }
})
.setButtonRun((options) => {
    // Run your precondition here for button
    // You have to return something like this :
    return {
        ok: true,
        message: 'Message in case of fail',
        metadata: {/* Some extra options */},
        button: options.button,
        type: 'button'
    }
}).setModalRun((options) => {
    // Run your modal precondition here
    // Return something like so
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* some data here */ },
        type: 'modal',
        modal: options.modal
    }
}).setUserContextMenuRun((options) => {
    // Run your precondition for user context command here
    // Return something like this
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* Some more datas */ },
        type: 'userContextMenu',
        contextMenu: options.interaction
    }
}).setMessageContextMenuRun((options) => {
    // Run your precondition for message context command here
    // Return something like this
    return {
        ok: true,
        message: "Message in case of fail",
        metadata: { /* Some more datas */ },
        type: 'messageContextMenu',
        contextMenu: options.interaction
    }
})

To use a custom precondition in a command, use it like so :

import yourPrecondition from 'your precondition file';
import { AmethystCommand } from 'amethystjs';

export default new AmethystCommand({
    name: 'name',
    preconditions: [ yourPrecondition ]
})
const precondition = require('your precondition file');
const { AmethystCommand } = require('amethystjs');

module.exports = new AmethystCommand({
    name: 'name',
    preconditions: [ precondition ]
});

Registering events

Go in your events folder and add a new file

Architecture of events

The emplacement of the file depends of your architecture ( eventsArchitecture, defined when creating the client )

If it is simple, it looks like this :

|_events
  |_ready.ts/js
  |_commandDenied.ts/js

If you configured it to double, it looks like this :

|_events
  |_bot
    |_ready.ts/js
    |_guildCreate.ts/js
  |_users
    |_commandDenied.ts/js
import { AmethystEvent } from 'amethystjs';

export default new AmethystEvent('eventName', (/* event options */) => {
    // Run your event
})
const { AmethytsEvent } = require('amethystjs');

module.exports = new AmethystEvent('eventName',/* event options */ () => {
    // Run your event
})

Events list

Amethyst JS adds events to the Discord Client. Here is the list of the events you can resiter via Events handler :

EventWhen its activatedarguments listTypes
amethystDebugWhen the client debugs somethingmessagestring
commandDeniedWhen a precondition stops a commandcommand, reasoncommandDeniedPayload, deniedReason
commandErrorWhen an error occurscommand, reasoncommandDeniedPayload, errorReason
buttonInteractionWhen a button is pressedinteraction, messageButtonInteraction, Message
modalSubmitWhen a modal interaction is createdinteractionModalSubmitInteraction
buttonDeniedWhen a button is stopped because of a preconditionbuttonbuttonDenied
selectMenuInteractionWhen any select menu is interactedselectorAnySelectMenuInteraction
stringSelectInteractionWhen a string select menu is interactedselectorStringSelectMenuInteraction
roleSelectMenuInteractionWhen a role select menu is interactedselectorRoleSelectMenuInteraction
userSelectInteractionWhen an user select menu is interactedselectorUserSelectMenuInteraction
channelSelectInteractionWhen a channel select menu is interactedselectorChannelSelectMenuInteraction
mentionableSelectInteractionWhen a mentionable select menu is interactedselectorMentionableSelectMenuInteraction
modalRejectedWhen a modal is stopped because of a preconditionreasonModalDenied

Autocomplete listeners

Autocomplete listeners are things that replies to an autocomplete interaction (interaction options with a lot of choices)

Go in your autocomplete listeners and create a new file

import { AutocompleteListener } from 'amethystjs';

export default new AutocompleteListener({
    commandName: [ { commandName: 'command name here' }, { commandName: 'another command name here', optionName: 'optionnal option name in the command here' } ],
    run: (options) => {
        // Make your choice here are return :
        return [ {name: 'Name', value: 'value', nameLocalizations: {} } ]
    }
});
const { AutocompleteListener } = require('amethystjs');

module.exports = new AutocompleteListener({
    commandName: [ { commandName: 'command name here' }, { commandName: 'another command name here', optionName: 'optionnal option name in the command here' } ],
    run: (options) => {
        // Make your choice here are return :
        return [ {name: 'Name', value: 'value', nameLocalizations: {} } ]
        
    }
});

As you've maybe noticed, commandName is an array containing a commandName and a potential optionName.

It means that the autocomplete will be applied to every command with the name included in the array, and if optionName is specified, it will also check if the option name correspond to the one specified.

Button handler

Amethyst JS can handle button interactions for you.

Go to your buttons folder and create a new file

import { ButtonHandler } from 'amethystjs';

export default new ButtonHandler({
    customId: 'buttonCustomId',
    permissions: ['Permissions required for the user'],
    clientPermissions: ["Permissions required for the client"],
    identifiers: [ 'optionnal array of more button custom identifiers' ]
})
.setRun((options) => {
    // Execute your code here
})
const { ButtonHandler } = require('amethystjs');

module.exports = new ButtonHandler({
    customId: 'buttonCustomId',
    permissions: ['Permissions required for the user'],
    clientPermissions: ["Permissions required for the client"],
    identifiers: [ 'optionnal array of more button custom identifiers' ]
})
.setRun((options) => {
    // Execute your code here
})

If you specify permissions, you have to handle it in case of error.

The customId and identifiers propreties are button custom ID

To handle it, create a new event and record for the buttonDenied event.

Wait for messages

You can wait for messages using amethyst JS.

You'll use waitForMessage() function.

import { waitForMessage } from 'amethystjs';

// Important : this works only in an async function
// For exemple, I'll do a simple client.on('messageCreate') to show you how to use it
client.on('messageCreate', async(message) => {
    if (message.content === '!ping') {
        await message.channel.send(`Would you like me to reply ?\nReply by \`yes\` or \`no\``);
        const reply = await waitForMessage({
            user: message.author,
            whoCanReact: 'useronly',
            channel: message.channel,
            time: 120000
        });

        if (!reply) message.channel.send(`You haven't replied :/`);
        if (reply.content === 'yes') message.reply("Pong !");
    }
})
const { waitForMessage } = require('amethystjs');

// Important : this works only in an async function
// For exemple, I'll do a simple client.on('messageCreate') to show you how to use it
client.on('messageCreate', async(message) => {
    if (message.content === '!ping') {
        await message.channel.send(`Would you like me to reply ?\nReply by \`yes\` or \`no\``);
        const reply = await waitForMessage({
            user: message.author,
            whoCanReact: 'useronly',
            channel: message.channel,
            time: 120000
        });

        if (!reply) message.channel.send(`You haven't replied :/`);
        if (reply.content === 'yes') message.reply("Pong !");
    }
})

Wait for interactions

Amethyst JS allows you to wait for interaction responses, like a select menu or a button click

import { waitForInteraction } from 'amethystjs';
import { ButtonBuilder, ActionRowBuilder, componentType, Message } from 'discord.js';

// This function works only in an async function.
// Here i'm gonna show you in a very simple async function.
// In this exemple, interaction is already defined

(async() => {
    const msg = await interaction.reply({
        message: "Yes or no",
        components: [ new ActionRowBuilder({
            components: [
                new ButtonBuilder({ label: 'Yes', style: ButtonStyle.Success, customId: 'yes' }),
                new ButtonBuilder({ label: 'No', style: ButtonStyle.Danger, customId: 'no' })
            ]
        }) as ActionRowBuilder<ButtonBuilder>]
    }) as Message<true>;

    const reply = await waitForInteraction({
        message: msg,
        time: 120000,
        whoCanReact = 'useronly',
        user: interaction.user,
        componentType: componentType.Button
    });

    if (!reply || reply.customId === 'no') return interaction.editReply("Ok, no");
    interaction.editReply("Yes !");
})()
const { waitForInteraction } = require('amethystjs');
const { ActionRowBuilder, ButtonBuilder, componentType } = require('discord.js');

// This function works only in an async function.
// Here i'm gonna show you in a very simple async function.
// In this exemple, interaction is already defined

(async() => {
    const msg = await interaction.reply({
        message: "Yes or no",
        components: [ new ActionRowBuilder({
            components: [
                new ButtonBuilder({ label: 'Yes', style: ButtonStyle.Success, customId: 'yes' }),
                new ButtonBuilder({ label: 'No', style: ButtonStyle.Danger, customId: 'no' })
            ]
        }) ]
    })

    const reply = await waitForInteraction({
        message: msg,
        time: 120000,
        whoCanReact = 'useronly',
        user: interaction.user,
        componentType: componentType.Button
    });

    if (!reply || reply.customId === 'no') return interaction.editReply("Ok, no");
    interaction.editReply("Yes !");
})()

Register custom prefixes

The Amethyst client has a prefixesManager proprety that allows you to set different prefixes for different servers

:warning: Important The client does not save the prefixes, you have to use a database to save it for your bot. The manager register prefixes only to use it with Amethyst client

Summary

Here is the summary of the prefixes manager

PropretyType
setPrefixMethod
getPrefixMethod
samePrefixMethod
listProprety
jsonProprety

setPrefix

Set a prefix for a server.

:warning: Use it when you load your bot, in a ready event, for example.

const prefixes = [
    { guildId: '1324', prefix: '!' },
    { guildId: '4321', prefix: '!!' },
    { guildId: '1324', prefix: '??' }
];

for (const data of prefixes) {
    client.prefixesManager.setPrefix({
        guildId: data.guildId,
        prefix: data.prefix
    });
}

getPrefix

Get the custom prefix of a server.

Returns default prefix if the server has no custom prefix

client.prefixesManager.getPrefix('guild ID')

Same prefix

Return all the servers with the same prefix

The method returns an array of objects with 2 propreties :

{
    guildId: string;
    prefix: string
}
client.prefixesManager.samePrefix('!')

prefixes list

You can get the prefixes data to manage it if you want.

This method will return a map. If you want to get it as an array, use json method instead

The map has 2 propreties :

Map<
    string, // Corresponding to the guild ID
    string // Corresponding to the prefix
>
client.prefixesManager.list;

Prefixes json

You can get the prefixes data to manage it if you want.

This method will return an array. Use map method to get it as a map if you need.

The array has 2 propreties :

{
    guildId: string;
    prefix: string;
}[];
client.prefixesManager.json;

Wait

You can wait for a certain time before executing something else with Amethyst JS. Here is how to use the wait method

// Import wait
import { wait } from 'amethystjs'

// Use it in an async function, it wont work otherwise
(async() => {
    await wait(1000); // Wait 1s
    await wait(1, 's'); // Wait 1s
    await wait(1, 'm'); // Wait 1 minute
})();
// Import wait
const { wait } = require('amethystjs');

// Use it in an async function, it wont work otherwise
(async() => {
    await wait(1000); // Wait 1s
    await wait(1, 's'); // Wait 1s
    await wait(1, 'm'); // Wait 1m
})()

Modal Handlers

You can use modal handlers with Amethyst JS, it can handle modals trought an object inside the modals handler folder

import { ModalHandler } from 'amethystjs';

export default new ModalHandler({
    modalId: ['identifiers of modals to be handled'],
    name: "Name of the handler"
}).setRun((opts) => {
    opts.modal;
    opts.user;
});
const { ModalHandler } = require('amethystjs');

module.exports = new ModalHandler({
    modalId: ['identifiers of modals to be handled'],
    name: "Name of the handler"
}).setRun((opts) => {
    opts.modal;
    opts.user;
});

Make a control panel

You can make a control panel for your bot

image

You need to have the client created

Creation

Here's how to create a control panel

import { AmethystClient, ControlPanel } from 'amethystjs';
import rebootHandler from './buttons/reboot';
import const { panelEmbed }  from './contents/embeds';

const client = new AmethystClient({
    intents: ['Guilds']
}, {
    token: 'token',
    debug: true,
    buttonsFolder: './dist/buttons'
})

client.start({}) // You can start it after the panel is created if you want

const panel = new ControlPanel({
    client: client,
    channelID: 'Id of the channel where the panel will be',
    deleteMessages: true, // Delete the other messages in the channel - optional
    pin: true, // Pin the panel - optional
    content: { 
        content: "Control panel",
        embeds: [panelEmbed]
    } // Content of the message - optional
});
const { AmethystClient, ControlPanel } = require('amethystjs');
const { panelEmbed } = require('./contents/embeds');

const client = new AmethystClient({
    intents: ['Guilds']
}, {
    token: 'token',
    debug: true,
    buttonsFolder: './dist/buttons'
})

client.start({}) // You can start it after the panel is created if you want

const panel = new ControlPanel({
    client: client,
    channelID: 'Id of the channel where the panel will be',
    deleteMessages: true, // Delete the other messages in the channel - optional
    pin: true, // Pin the panel - optional
    content: { 
        content: "Control panel",
        embeds: [panelEmbed]
    } // Content of the message - optional
});

Register a button

Now you surely want to register your buttons

import rebootHandler from './buttons/reboot';

panel.registerButton({
    label: 'Disconnect voice',
    handler: 'panel.disconnect', // Use the handler that handles 'panel.disconnect'
    style: 'Primary'
})
.registerButton({
    label: 'Reboot',
    style: 'Danger',
    handler: rebootHandler // Use the imported handler
})
const rebootHandler = require('./buttons/reboot');

panel.registerButton({
    label: 'Disconnect voice',
    handler: 'panel.disconnect', // Use the handler that handles 'panel.disconnect'
    style: 'Primary'
})
.registerButton({
    label: 'Reboot',
    style: 'Danger',
    handler: rebootHandler // Use the imported handler
})

Once it's done, don't forget to start the panel with panel.start() method

Log4JS

Amethyst JS allows you to log things in a log file with log4js

This object has two methods

Trace

The trace() method allows you to write something in the log file

const { log4js } = require('amethystjs');

log4js.trace("Something just happened");

Config

The config() method configs log4js.

For now, you can config :

  • Trigger the time displaying
  • Time displaying format
  • log file
  • Add a onLog event
  • Object indentation
const { log4js } = require('amethystjs');

log4js.config('displayTime', true);
log4js.config('onLog', (message) => {
    console.log(`Log4JS traced something`)
})
log4js.config('file', 'logs.txt')
log4js.config('objectIndentation', 1)
log4js.config('displayTimeFormat', (time) => `[${time.getDay()}/${time.getMonth()}/${time.getFullYear()}:${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}]`)

Paginator

The Greensky's paginator under the name of AmethystPaginator. The method is the exact same than the package.

Examples

Here are some repositories that use Amethyst JS :

Contributing

Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the documentation.

1.7.4

2 months ago

1.7.0-beta.0

8 months ago

1.7.0-beta.3

8 months ago

1.7.0-beta.4

8 months ago

1.7.0-beta.1

8 months ago

1.7.0-beta.2

8 months ago

1.6.3

8 months ago

1.7.0-beta.5

8 months ago

1.7.0-beta.6

8 months ago

1.7.3

7 months ago

1.7.2

7 months ago

1.7.1

8 months ago

1.7.0

8 months ago

1.6.2

10 months ago

1.6.1

10 months ago

1.6.0

10 months ago

1.5.3

10 months ago

1.5.2

12 months ago

1.5.1

12 months ago

1.5.0

12 months ago

1.5.21

12 months ago

1.4.1

1 year ago

1.4.0

1 year ago

1.2.7

1 year ago

1.2.6

1 year ago

1.2.5

1 year ago

1.3.2

1 year ago

1.3.1

1 year ago

1.3.0

1 year ago

1.2.61

1 year ago

1.2.71

1 year ago

1.2.0

1 year ago

1.1.1

1 year ago

1.0.2

1 year ago

1.1.0

1 year ago

1.0.1

1 year ago

1.1.6

1 year ago

1.2.4

1 year ago

1.1.5

1 year ago

1.2.3

1 year ago

1.1.4

1 year ago

1.0.5

1 year ago

1.2.2

1 year ago

1.1.3

1 year ago

1.0.4

1 year ago

1.2.1

1 year ago

1.1.2

1 year ago

1.0.3

1 year ago

1.2.23

1 year ago

1.2.24

1 year ago

1.2.21

1 year ago

1.2.22

1 year ago

7.0.3

2 years ago

1.2.25

1 year ago

1.2.26

1 year ago

1.0.51

1 year ago

1.0.0

2 years ago

5.0.0

2 years ago

6.0.0

2 years ago

3.0.0

2 years ago

4.0.0

2 years ago

7.0.0

2 years ago

7.0.2

2 years ago

2.0.1

2 years ago

7.0.1

2 years ago

2.0.0

2 years ago

0.0.2

7 years ago

0.0.1

7 years ago