πŸ‘©β€πŸ’» Development#

We will try to explain the functioning and usage of our npm package @olvid/bot-node. Ideally, it is better to follow step by step in order to understand the progression, but if you feel comfortable, you can try to access directly the section that interests you.

About @olvid/bot-web

@olvid/bot-web package is very similar but have specificity (see 🌐 Web Browser)

  • It does not use gRPC directly, so it must use a proxy cf: Daemon et proxy

  • He does not have access to the environment, so it is necessary to pass the server url and client key when creating an OlvidClient (or passing it by other means).

  • file sending does not work, therefore methods like sendMessageWithAttachments are not implemented.

You can easily adapt the code present on this page, but it will not work as is.

The basics#

OlvidClient#

To interact with the daemon, you will need to systematically create an instance of the OlvidClient class. This can be the original class or a child class.

In this node, this class will automatically retrieve a client key using the environment or a .env file. (cf. Configuration)

Thanks to this client, we will be able to execute commands and react to notifications.

Here is what a basic main.ts file looks like containing the creation of an Olvid client and the execution of a command (displaying the current identity). This structure should be used in each of the code examples on this page, just replace the contents of the main function with your code.

import { OlvidClient, datatypes } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();
    const identity: datatypes.Identity = await client.identityGet();
    console.log(identity)
}

main().then();

Command#

All the gRPC command methods exposed by the daemon are easily accessible through methods of the OlvidClient class. For example, to send a message (the MessageSend method in gRPC), you would use the messageSend method of our OlvidClient instance.

In cases where we know the identifier of the discussion in which to post, it would look like this:

import { OlvidClient, datatypes } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();
    await client.messageSend({discussionId: 1, body: "Use Olvid !"});
    console.log(identity)
}

main().then();

Notification#

The OlvidClient class makes it easy to listen for notifications emitted by the daemon. These methods all start with the prefix on and allow you to add a callback function that will be executed for each new notification.

For example, to display in the terminal when a message is received and when a reaction is added, you can do:

import { OlvidClient, datatypes } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();
    client.onMessageReceived({
        callback: (message: datatypes.Message) => {
            console.log("Message received:", message.body);
        }
    })
    client.onMessageReactionAdded({
        callback: (message: datatypes.Message, reaction: datatypes.MessageReaction) => {
            console.log("Reaction added:", reaction.reaction);
        }
    })
    
    await client.runForever();
}

main().then();

Advanced Usage#

Filtering of elements#

To access specific elements in the database, you can apply a filter during commands of the List type.

Here are some examples of possible filters, the different filters are defined in the protobuf description of the daemon’s API (here).

import {datatypes, OlvidClient} from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();

    /*
     ** list one to one discussions
     */
    let otoDiscussionFilter = new datatypes.DiscussionFilter({
        type: datatypes.DiscussionFilter_Type.OTO
    });
    for await (const discussion of client.discussionList({filter: otoDiscussionFilter})) {
        console.log(`OneToOne discussion: ${discussion.id} - ${discussion.title}`)
    }
    
    /*
    ** list messages in a specific discussion with reactions 
     */
    const messageFilter = new datatypes.MessageFilter({
        discussionId: 1n,
        hasReaction: datatypes.MessageFilter_Reaction.HAS
    })
    const filteredMessages = await Array.fromAsync(client.messageList({filter: messageFilter}));
    console.log("Message with reactions in discussion", filteredMessages)
}

main().then();

Notification Filtering#

For a more granular implementation of listening to notifications, a filtering system can be used. The different forms of filtering depend on the type of notification and are defined in messages of the MessageReceivedNotificationSubscription type (cf protobuf description of the daemon’s API).

count#

All notifications can be scheduled to only be received a set number of times using the argument count.

filter#

Most notifications have one or more filter attributes. If a filter is specified, a notification must meet all of the specified criteria in order to be sent.

Example#

MessageFilter

import { OlvidClient, datatypes, tools } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();

    client.onMessageReceived({
        callback: async (message: datatypes.Message) => {
            await client.messageSend({
                discussionId: message.discussionId,
                body: "Hello World !",
            })
        },
        filter: new datatypes.MessageFilter({bodySearch: "hello"}),
        count: 1n
    })
    await client.waitForCallbacksEnd();
}

main().then();

Tip

In filters, string fields ending in search are interpreted by a regexp engine.

Shortcuts#

The base classes of the datatypes module (such as Message, Discussion, Attachment, …) implement shortcut methods to have more concise and pleasant code to write.

Here are some examples of methods, the exhaustive list is defined in the source code of the classes, or can be accessed here.

import { OlvidClient, datatypes, tools } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();

    client.onMessageReceived({
        callback: async (message: datatypes.Message) => {
            // easily reply to a message, and access to it's sender details
            const senderContact: datatypes.Contact = (await message.getSenderContact(client))
            await message.reply(
                client,
                `Hello ${senderContact.displayName} ! πŸ‘‹`
            );
            
            // save attachments on disk if there are some
            if (message.attachmentsCount) {
                (await message.getAttachments(client)).forEach((attachment) => {
                    attachment.save(client, "./attachments")
                })
            }
        }
    })

    // post a message in a discussion and wait for message to be uploaded on server
    const discussion = (await Array.fromAsync(client.discussionList()))[0]
    const sentMessage = await discussion.postMessage(client, "Hello there");
    await sentMessage.waitFor.messageToBeUploaded(client);
    
    await client.runForever();
}

main().then();

Decorators#

We have implemented decorators that allow you to easily listen for notifications and set up commands for ChatBot. They allows you to write cleaner code for infinite-looping bot programs.

To use them, you need to subclass the OlvidClient class, as shown in the following example.

import { OlvidClient, datatypes, onDiscussionNew, command } from "@olvid/bot-node";

class MyBot extends OlvidClient {
    @onDiscussionNew()
    async discussionNew(discussion: datatypes.Discussion) {
        await discussion.postMessage(this, `Hello ${discussion.title}} !`)
        // post help message when a new discussion is created
        await discussion.postMessage(this, this.getHelpMessage())
    }
    
    @command("!help")
    async help(message: datatypes.Message) {
        await message.reply(this, this.getHelpMessage())
    }

    @command("!ping")
    async ping(message: datatypes.Message) {
        await message.reply(this, "pong")
    }

    getHelpMessage() {
        return "### Available commands\n- *!help*\n- *!ping*"
    }
}

async function main() {
    const bot = new MyBot();
    await bot.runForever();
}

main().then();

Tips and Tricks#

Invitations#

To make it easier to connect with a bot, you can enable automatic acceptance of invitations. Simply modify the daemon’s configuration using the enableAutoInvitation method of an Olvid client.

Note

Automatic acceptance of invitations can only accept presentations, group invitations, and invitations to a personal discussion. It cannot complete direct invitations that involve the exchange of SAS code.

Here is a program that launches a bot after configuring the automatic acceptance of invitations.

import { OlvidClient, datatypes, tools } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();
    client.onMessageReceived({
        callback: (message: datatypes.Message) => {
            console.log("Message received:", message.body);
        }
    })

    await client.enableAutoInvitation({acceptAll: true});

    await client.runForever();
}

main().then();

Cleaning messages#

For performance and privacy reasons, we recommend that you enable automatic message cleanup.

It is possible to limit the number of messages stored globally or per conversation (global_count and discussion_count) and/or define the maximum age of a message (existence_duration).

We also recommend that you enable the deletion of messages in closed discussions.

import { OlvidClient, datatypes, tools } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();
    // keep messages up to 7 days,
    // with a maximum of 100 messages and 20 messages per discussions,
    // and deletes messages when a discussion is locked
    await client.setMessageRetentionPolicy({
        globalCount: 100n, discussionCount: 20n,
        existenceDuration: 60n*60n*24n*7n, cleanLockedDiscussions: true
    });
}

main().then();

Various#

Send an ephemeral message#

The API endpoints messageSend and messageSendWithAttachments allow you to specify the ephemerality of the message to be sent. To do this, use the datatypes.MessageEphemerality object.

Here is an example in typescript. It is possible to specify the parameters read_once, visibility_duration and existence_duration independently. The existence duration and visibility duration are in seconds.

import { OlvidClient, datatypes, tools } from "@olvid/bot-node";

async function main() {
    const client = new OlvidClient();
    
    for await (const discussion of client.discussionList()) {
        await client.messageSend({
            discussionId: discussion.id,
            body: "Self destruct-message",
            ephemerality: new datatypes.MessageEphemerality({
                visibilityDuration: 10n,
                existenceDuration: 60n,
                readOnce: true
            })
        })
    }
}

main().then();