new commands

This commit is contained in:
2026-03-29 02:07:51 +01:00
parent f84120883a
commit 5a44d56a24
8 changed files with 846 additions and 92 deletions

View File

@@ -1,7 +1,10 @@
# Logging in to the Bot
Before we start we need to create the .NET application area.
Create a new .NET console app and add the Valour SDK + DotEnv compatability:
Before we start we need to create the .NET application and install the required packages.
### Setting up the project
Create a new .NET console app and add the Valour SDK and DotNetEnv:
```bash
dotnet new console -n BasicValourBot
@@ -10,19 +13,19 @@ dotnet add package Valour.SDK
dotnet add package DotNetEnv
```
### Creating the .env file
In the root of your Bot folder create a file called `.env`. This file will contain your bot token so **DO NOT** show it to anyone!
Inside of this file add the following line, replacing the placeholder with your actual token:
### Firstly we need to create another file!
In the root of your Bot folder create another file called ```.env``` this file will contain your bot token so **DO NOT** show it to anyone!
<br>
Inside of this file we will input the following:
<br>
```
TOKEN=bot-your-bot-token-here
```
### Next we can start coding the bot!
### Logging in
The below code will make a bot and login as that bot account you created.
The code below creates a `ValourClient`, loads the token from your `.env` file, and logs in as your bot.
```c#
// Importing the Client SDK and DotEnv.
@@ -41,7 +44,7 @@ Env.Load();
string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
// Check if the token is valid.
if (String.IsNullOrWhiteSpace(token))
if (String.IsNullOrWhiteSpace(token))
{
// The token in your .env file is missing or invalid, make sure you set a valid token!
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
@@ -62,13 +65,15 @@ Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
await Task.Delay(Timeout.Infinite);
```
Now to run the bot we need to make sure we are inside the root directory of the app and then run:
### Running the bot
Make sure you are inside the root directory of the app and run:
```bash
dotnet run
```
The bot should then start up and you should see `Logged in as botname (ID: botid)`
You should then see `Logged in as botname (ID: botid)` in the console.
##
## [The Next step is to Add the bot to a Planet.](2.JoinPlanet.md)
---
## [The Next step is to Add the bot to a Planet.](2.JoinPlanet.md)

View File

@@ -1,24 +1,24 @@
# Add the bot to a Planet
Bots join planets the same way users do - through invite links or the Discovery page.
<br>
There are a few way of doing this. I will be going through **two** of them in this guide.
There are two ways to do this. Both are covered below.
## 1. Using my website
The easiest way to add a bot to your Planet is to use my website [here](https://skyjoshua.xyz/planetjoiner)
<br>
You should see something like this below:
<br>
The easiest way to add a bot to your Planet is to use my website [here](https://skyjoshua.xyz/planetjoiner).
You should see something like this:
![Planet Joiner Site](https://sxsc.xyz/files/images/.PlanetJoiner.png)
<br>
Fill in the boxes with the required information and hit **Join Planet** and the bot should join the desired planet.
<br>
*Note: Sometimes the Invite Code is not required if the Planet you are trying to join is Discoverable*
*Note: Sometimes the Invite Code is not required if the Planet you are trying to join is Discoverable.*
## 2. Using code
To make a Bot join your Planet using code we can add 1 line to the previous code.
To join a planet from code, call `JoinPlanetAsync` with the planet's ID. If the planet is not open to discovery, you also need to pass an invite code.
```c#
// If the planet is open to discovery only the planet ID is required.
@@ -26,11 +26,13 @@ await client.PlanetService.JoinPlanetAsync(planetId);
```
or
```c#
// If the planet is not open to discovery an the last part of an invite (the invite code) is also required.
// If the planet requires an invite, pass the invite code as the second argument.
await client.PlanetService.JoinPlanetAsync(planetId, "INVITE_CODE");
```
Below is the full code required to add the bot to a planet. (Comments are new parts added.)
You can find a planet's ID and invite code from its settings page inside Valour.
<br><br>
### Full code below
```c#
using Valour.Sdk.Client;
@@ -44,7 +46,7 @@ Env.Load();
string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
if (string.IsNullOrWhiteSpace(token))
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
return;
@@ -58,13 +60,11 @@ if (!loginResult.Success)
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
// Join a planet
// Join a planet using its ID and invite code
await client.PlanetService.JoinPlanetAsync(00000000000000000, "ABCDEF");
await Task.Delay(Timeout.Infinite);
```
##
## [The Next step is to Connecting and Sending a message to a Planet](3.ConnectingAndSending.md)
---
## [The Next step is Connecting and Sending a message to a Planet](3.ConnectingAndSending.md)

View File

@@ -1,44 +1,46 @@
# Connecting and Sending a message to a Planet
After logging in and Joining a planet, you will need to open a real time connection before you can interact with it. This sets up the SignalR connection that delivers live events.
After logging in and joining a planet, you need to open a real-time connection before you can interact with it. This sets up the SignalR connection that delivers live events.
**Unfortunately due to a bug in the Valour SDK at this moment the caching of channels isnt working and so we have to do a workaround**
> **Note:** Due to a bug in the Valour SDK the built-in channel cache is not working right now, so we use our own dictionary as a workaround.
### Creating a channel cache
We store each channel by its ID so we can look them up quickly later.
Firstly we have to create a Channel Cache (as said above the current one is broken) we can do this like so:
```c#
// Make your own Channel Cache
// Make your own channel cache using the channel's ID as the key
Dictionary<long, Channel> channelCache = new();
```
Next we have to get the first planet the bot has joined.
### Loading planet data
Before we can read a planet's channels we need to fetch its data. We also loop through every channel and add it to the cache.
```c#
// Get the first planet the bot joined.
// Get the first planet the bot joined
Planet planet = client.PlanetService.JoinedPlanets.First();
```
Now that we have the first planet that the bot has joined we can use that to get all the channels that the planet has. We can do this with this code:
```c#
// Load the planet's data.
// Load the planet's data
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
// Fetch each channel.
// Add every channel to the cache
foreach (Channel chan in planet.Channels)
{
// Add the channel to the cache using its id to call back to it.
channelCache[chan.Id] = chan;
}
```
Now that we have all the channels and they have been added to our channel cache we can use that cache to find the default channel for that planet. We can do that with the code below:
### Finding the default channel
Now that the cache is filled we can grab the planet's primary chat channel to send messages to.
```c#
// Fetch the default chat channel
if (!channelCache.TryGetValue(planet.PrimaryChatChannel.Id, out var channel)) return;
// Fall back to the first channel
// Fall back to the first chat channel if the default wasn't found
if (channel is null)
{
channel = planet.Channels.FirstOrDefault(
@@ -46,20 +48,23 @@ if (channel is null)
);
}
// One final check if the channel is still null. (very rare!)
if (channel is null) {
// One final check if the channel is still null (very rare!)
if (channel is null)
{
Console.WriteLine("Unable to find the Default or First channel");
}
```
Now that we have the first channel we can use that channel to send a message to it.
### Sending a message
With a channel in hand, sending a message is just one line.
```c#
// Send message to the channel!
// Send a message to the channel
await channel.SendMessageAsync("Hello from my bot!");
```
Great now your bot has sent its first message in your Planet!
Great, your bot has sent its first message in your Planet!
<br><br>
### Full code below
@@ -77,10 +82,10 @@ Env.Load();
string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
// Make your own Channel Cache
// Make your own channel cache using the channel's ID as the key
Dictionary<long, Channel> channelCache = new();
if (string.IsNullOrWhiteSpace(token))
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
return;
@@ -94,24 +99,23 @@ if (!loginResult.Success)
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
// Get the first planet the bot joined.
// Get the first planet the bot joined
Planet planet = client.PlanetService.JoinedPlanets.First();
// Load the planet's data.
// Load the planet's data
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
// Fetch each channel.
// Add every channel to the cache
foreach (Channel chan in planet.Channels)
{
// Add the channel to the cache using its id to call back to it.
channelCache[chan.Id] = chan;
}
// Fetch the default chat channel
if (!channelCache.TryGetValue(planet.PrimaryChatChannel.Id, out var channel)) return;
// Fall back to the first channel
// Fall back to the first chat channel if the default wasn't found
if (channel is null)
{
channel = planet.Channels.FirstOrDefault(
@@ -119,17 +123,17 @@ if (channel is null)
);
}
// One final check if the channel is still null. (very rare!)
if (channel is null) {
Console.WriteLine("Unable to find the Default or First channel")
// One final check if the channel is still null (very rare!)
if (channel is null)
{
Console.WriteLine("Unable to find the Default or First channel");
}
// Send message to the channel!
// Send a message to the channel
await channel.SendMessageAsync("Hello from my bot!");
await Task.Delay(Timeout.Infinite);
```
##
## [The Next step is receiving Messages and Commands](4.MessagesAndCommands.md)
---
## [The Next step is Receiving Messages and Commands](4.MessagesAndCommands.md)

View File

@@ -1,14 +1,15 @@
# Receiving messages and Making commands
# Receiving Messages and Making Commands
Now that we can send messages to a channel using the bot its time to receive messages from users so that we can make commands!
Now that we can send messages, it's time to receive them from users so we can make commands!
**Unfortunately due to a bug in the Valour SDK at this moment the caching of channels isnt working and so we have to do a workaround**
> **Note:** Due to a bug in the Valour SDK the built-in channel cache is not working right now, so we keep using the workaround from the previous step.
### Opening every channel
First lets start with making it so the bot can see every channel for every planet its in. We can do this by editing the code just a little bit and looping through each planet and then opening every channel:
In the previous step we only opened one planet's channels. Now we loop through every planet the bot has joined and open all of their chat channels, so the bot can see messages everywhere.
```c#
// Fetch all the planets instead of just the first
// Loop through every planet the bot has joined
foreach (Planet planet in client.PlanetService.JoinedPlanets)
{
await planet.EnsureReadyAsync();
@@ -16,31 +17,34 @@ foreach (Planet planet in client.PlanetService.JoinedPlanets)
foreach (Channel chan in planet.Channels)
{
// Add the channel to the cache
channelCache[chan.Id] = chan;
// If the channel is a chat channel then open that channel to the bot to see.
// Open each chat channel so the bot can receive messages from it
if (chan.ChannelType == ChannelTypeEnum.PlanetChat)
{
// Open the channel with your bots name!
await chan.OpenWithResult("YourBotName");
}
}
}
```
After every channel is open we have to make the bot listen for incoming messages.
### Listening for messages
To receive messages we subscribe to the `MessageReceived` event on the message service.
```c#
// Get all messages that are send into text channels
// Subscribe to incoming messages
client.MessageService.MessageReceived += onMessageReceived;
```
In order to use this listener we have to make a Function called onMessageReceived (or whatever you named it above). We can do this directly on the listener but some things are restricted by doing this (idk dont ask :P)
We define the handler as a separate function rather than inline so we can use `await` inside it without restrictions.
Inside this listener is where we execute what we want to do with the incoming message. In this example we have a simple Ping command that replys with Pong!
### Creating a command
Inside `onMessageReceived` we check the message content against a prefix and respond to commands. Here's a simple `ping` command that replies with `Pong!`.
```c#
// Function for what to do with the received messages
async Task onMessageReceived(Message message)
{
// Prefix.. duh..
@@ -83,7 +87,7 @@ string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
Dictionary<long, Channel> channelCache = new();
if (string.IsNullOrWhiteSpace(token))
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
return;
@@ -97,7 +101,7 @@ if (!loginResult.Success)
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
// Fetch all the planets instead of just the first
// Loop through every planet the bot has joined
foreach (Planet planet in client.PlanetService.JoinedPlanets)
{
await planet.EnsureReadyAsync();
@@ -105,21 +109,20 @@ foreach (Planet planet in client.PlanetService.JoinedPlanets)
foreach (Channel chan in planet.Channels)
{
// Add the channel to the cache
channelCache[chan.Id] = chan;
// If the channel is a chat channel then open that channel to the bot to see.
// Open each chat channel so the bot can receive messages from it
if (chan.ChannelType == ChannelTypeEnum.PlanetChat)
{
// Open the channel with your bots name!
await chan.OpenWithResult("YourBotName");
}
}
}
// Get all messages that are send into text channels
// Subscribe to incoming messages
client.MessageService.MessageReceived += onMessageReceived;
// Function for what to do with the received messages
async Task onMessageReceived(Message message)
{
// Prefix.. duh..
@@ -144,5 +147,5 @@ async Task onMessageReceived(Message message)
await Task.Delay(Timeout.Infinite);
```
##
## End of Bot Guide (for now)
---
## [The Next step is Command Arguments](5.CommandArguments.md)

View File

@@ -0,0 +1,279 @@
# Command Arguments
Now that we have basic commands working, lets make them actually useful by adding arguments. Arguments are the extra words a user types after a command, for example:
`say Hello World` → the bot should reply with `Hello World`
`repeat 3 Hey` → the bot should reply with `Hey Hey Hey`
### How arguments work
After we strip the prefix from the message content, we can split the remaining text into an array of words. The first word is the command name, and everything after is the arguments.
```c#
// Strip the prefix from the content
string withoutPrefix = content.Substring(prefix.Length);
// Split into parts: parts[0] = command name, parts[1..] = arguments
string[] parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries);
// The command name is the first part (lowercase so Ping and ping both work)
string command = parts[0].ToLower();
// The arguments are everything after the command name
string[] args = parts.Skip(1).ToArray();
```
### Adding a `say` command
The `say` command takes all the arguments and echoes them back as a message. We join the args array back into a single string using `string.Join`.
```c#
if (command == "say")
{
// Make sure the user actually gave us something to say
if (args.Length == 0)
{
await channel.SendMessageAsync("Usage: bg/say <message>");
return;
}
// Join all the arguments back into one string and send it
string messageToSay = string.Join(' ', args);
await channel.SendMessageAsync(messageToSay);
}
```
### Adding a `repeat` command
The `repeat` command takes a number and a word, then repeats the word that many times. This shows how to parse an argument as a specific type (int).
```c#
else if (command == "repeat")
{
// Make sure the user gave us a number AND a word
if (args.Length < 2)
{
await channel.SendMessageAsync("Usage: bg/repeat <number> <word>");
return;
}
// Try to parse the first argument as a number
if (!int.TryParse(args[0], out int times))
{
await channel.SendMessageAsync("The first argument must be a number!");
return;
}
// Clamp the number so people can't repeat 10000 times
times = Math.Clamp(times, 1, 10);
// Repeat the word and send the result
string word = args[1];
string result = string.Join(' ', Enumerable.Repeat(word, times));
await channel.SendMessageAsync(result);
}
```
### Updating `onMessageReceived`
Now we replace the old if check with the new argument-aware approach inside `onMessageReceived`:
```c#
// Function for what to do with the received messages
async Task onMessageReceived(Message message)
{
// Prefix.. duh..
string prefix = "bg/";
// What the message says
string content = message.Content;
// Get the channel that the message was sent in
if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return;
// If the message doesnt start with the prefix then just ignore it
if (!content.StartsWith(prefix)) return;
// Strip the prefix and split the rest into parts
// parts[0] = command name, parts[1..] = arguments
string withoutPrefix = content.Substring(prefix.Length);
string[] parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries);
// If there's nothing after the prefix, ignore it
if (parts.Length == 0) return;
// Grab the command name (lowercase so Ping and ping both work)
string command = parts[0].ToLower();
// Grab any arguments that come after the command name
string[] args = parts.Skip(1).ToArray();
// Check if the command is ping
if (command == "ping")
{
// Sends the message `Pong!` to the channel
await channel.SendMessageAsync("Pong!");
}
// Check if the command is say
else if (command == "say")
{
// Make sure the user actually gave us something to say
if (args.Length == 0)
{
await channel.SendMessageAsync("Usage: bg/say <message>");
return;
}
// Join all the arguments back into one string and send it
string messageToSay = string.Join(' ', args);
await channel.SendMessageAsync(messageToSay);
}
// Check if the command is repeat
else if (command == "repeat")
{
// Make sure the user gave us a number AND a word
if (args.Length < 2)
{
await channel.SendMessageAsync("Usage: bg/repeat <number> <word>");
return;
}
// Try to parse the first argument as a number
if (!int.TryParse(args[0], out int times))
{
await channel.SendMessageAsync("The first argument must be a number!");
return;
}
// Clamp the number so people can't repeat 10000 times
times = Math.Clamp(times, 1, 10);
// Repeat the word and send the result
string word = args[1];
string result = string.Join(' ', Enumerable.Repeat(word, times));
await channel.SendMessageAsync(result);
}
}
```
<br><br>
### Full code below
```c#
using Valour.Sdk.Client;
using Valour.Shared;
using DotNetEnv;
using Valour.Sdk.Models;
using Valour.Shared.Models;
ValourClient client = new ValourClient("https://api.valour.gg/");
client.SetupHttpClient();
Env.Load();
string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
Dictionary<long, Channel> channelCache = new();
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
return;
}
TaskResult loginResult = await client.InitializeUser(token);
if (!loginResult.Success)
{
Console.WriteLine($"Login failed: {loginResult.Message}");
}
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
foreach (Planet planet in client.PlanetService.JoinedPlanets)
{
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
foreach (Channel chan in planet.Channels)
{
channelCache[chan.Id] = chan;
if (chan.ChannelType == ChannelTypeEnum.PlanetChat)
{
await chan.OpenWithResult("YourBotName");
}
}
}
client.MessageService.MessageReceived += onMessageReceived;
async Task onMessageReceived(Message message)
{
string prefix = "bg/";
string content = message.Content;
if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return;
if (!content.StartsWith(prefix)) return;
// Strip the prefix and split the rest into parts
// parts[0] = command name, parts[1..] = arguments
string withoutPrefix = content.Substring(prefix.Length);
string[] parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries);
// If there's nothing after the prefix, ignore it
if (parts.Length == 0) return;
// Grab the command name (lowercase so Ping and ping both work)
string command = parts[0].ToLower();
// Grab any arguments that come after the command name
string[] args = parts.Skip(1).ToArray();
if (command == "ping")
{
await channel.SendMessageAsync("Pong!");
}
// Check if the command is say
else if (command == "say")
{
// Make sure the user actually gave us something to say
if (args.Length == 0)
{
await channel.SendMessageAsync($"Usage: {prefix}say <message>");
return;
}
// Join all the arguments back into one string and send it
string messageToSay = string.Join(' ', args);
await channel.SendMessageAsync(messageToSay);
}
// Check if the command is repeat
else if (command == "repeat")
{
// Make sure the user gave us a number AND a word
if (args.Length < 2)
{
await channel.SendMessageAsync($"Usage: {prefix}repeat <number> <word>");
return;
}
// Try to parse the first argument as a number
if (!int.TryParse(args[0], out int times))
{
await channel.SendMessageAsync("The first argument must be a number!");
return;
}
// Clamp the number so people can't repeat 10000 times
times = Math.Clamp(times, 1, 10);
// Repeat the word and send the result
string word = args[1];
string result = string.Join(' ', Enumerable.Repeat(word, times));
await channel.SendMessageAsync(result);
}
}
await Task.Delay(Timeout.Infinite);
```
---
## [The Next step is getting Member Info](6.MemberInfo.md)

219
Guides/6.MemberInfo.md Normal file
View File

@@ -0,0 +1,219 @@
# Member Info
Now that we have commands with arguments, lets make them more personal by fetching info about the user who sent the message.
A `bg/whoami` command will reply with something like:
```
**Username:** SkyJoshua
**Display Name:** SkyJoshua
**User ID:** 15652354820931584
**Member ID:** 42170205766156288
**Roles:** Admin, Moderator, Member
**Joined Valour:** 01/01/2022 00:00:00
**Bot:** False
```
### Fetching the member
Every `Message` has a `FetchAuthorMemberAsync()` method that returns a `PlanetMember` — the sender's membership on that planet. We always check if it's null before using it.
```c#
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
```
### Getting the linked User
From the `PlanetMember` you can access the global `User` account directly via `member.User`. This gives you their username, ID, join date, and whether they are a bot.
```c#
// Get the linked user account
User user = member.User;
```
### Reading member properties
Here are the useful properties available on `member` and `user`:
| Property | Description |
|---|---|
| `user.Name` | Their global Valour username |
| `member.Nickname` | Their planet-specific nickname (empty if not set) |
| `user.Id` | Their global user ID |
| `member.Id` | Their member ID on this planet |
| `member.Roles` | The list of roles they have on this planet |
| `user.TimeJoined` | When they created their Valour account |
| `user.Bot` | Whether they are a bot account |
### Handling nicknames
A member may or may not have a nickname set on the planet. We fall back to their username when there isn't one.
```c#
// Use their nickname if they have one, otherwise fall back to their username
string displayName = string.IsNullOrWhiteSpace(member.Nickname) ? user.Name : member.Nickname;
```
### Getting role names
`member.Roles` gives us the list of roles the member has. We can use LINQ to pull out each role's name and join them into a single string.
```c#
// Build a comma-separated list of the member's role names
string roleNames = string.Join(", ", member.Roles.Select(r => r.Name));
```
### Putting it together
Here is the full `whoami` command using everything above:
```c#
else if (command == "whoami")
{
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
// Get the linked user account
User user = member.User;
// Use their nickname if they have one, otherwise fall back to their username
string displayName = string.IsNullOrWhiteSpace(member.Nickname) ? user.Name : member.Nickname;
// Build a comma-separated list of the member's role names
string roleNames = string.Join(", ", member.Roles.Select(r => r.Name));
// Build and send the info reply
await channel.SendMessageAsync($@"
**Username:** {user.Name}
**Display Name:** {displayName}
**User ID:** {user.Id}
**Member ID:** {member.Id}
**Roles:** {roleNames}
**Joined Valour:** {user.TimeJoined}
**Bot:** {user.Bot}
");
}
```
<br><br>
### Full code below
```c#
using Valour.Sdk.Client;
using Valour.Shared;
using DotNetEnv;
using Valour.Sdk.Models;
using Valour.Shared.Models;
ValourClient client = new ValourClient("https://api.valour.gg/");
client.SetupHttpClient();
Env.Load();
string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
Dictionary<long, Channel> channelCache = new();
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
return;
}
TaskResult loginResult = await client.InitializeUser(token);
if (!loginResult.Success)
{
Console.WriteLine($"Login failed: {loginResult.Message}");
}
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
foreach (Planet planet in client.PlanetService.JoinedPlanets)
{
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
foreach (Channel chan in planet.Channels)
{
channelCache[chan.Id] = chan;
if (chan.ChannelType == ChannelTypeEnum.PlanetChat)
{
await chan.OpenWithResult("YourBotName");
}
}
}
client.MessageService.MessageReceived += onMessageReceived;
async Task onMessageReceived(Message message)
{
string prefix = "bg/";
string content = message.Content;
if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return;
if (!content.StartsWith(prefix)) return;
string withoutPrefix = content.Substring(prefix.Length);
string[] parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return;
string command = parts[0].ToLower();
string[] args = parts.Skip(1).ToArray();
if (command == "ping")
{
await channel.SendMessageAsync("Pong!");
}
else if (command == "whoami")
{
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
// Get the linked user account
User user = member.User;
// Use their nickname if they have one, otherwise fall back to their username
string displayName = string.IsNullOrWhiteSpace(member.Nickname) ? user.Name : member.Nickname;
// Build a comma-separated list of the member's role names
string roleNames = string.Join(", ", member.Roles.Select(r => r.Name));
// Build and send the info reply
await channel.SendMessageAsync($@"
**Username:** {user.Name}
**Display Name:** {displayName}
**User ID:** {user.Id}
**Member ID:** {member.Id}
**Roles:** {roleNames}
**Joined Valour:** {user.TimeJoined}
**Bot:** {user.Bot}
");
}
}
await Task.Delay(Timeout.Infinite);
```
---
## [The Next step is Permission Checking](7.PermissionChecking.md)

View File

@@ -0,0 +1,230 @@
# Permission Checking
Now that we can fetch member info, we can use permissions to restrict commands to only members who have the right access. Valour has two types of permission checks — planet-level and channel-level.
### How permissions work
Permissions in Valour are stored as flags on roles. When you call `member.HasPermission()` it calculates the combined permissions from all of the member's roles and checks if the requested permission is included.
> **Note:** You do not need to separately check if a member is the planet owner or has an admin role. `HasPermission()` handles both of these automatically — it always returns `true` for the planet owner and for any member with an admin role.
### Planet permissions
Planet permissions are server-wide — they apply regardless of which channel the command is used in. To check one, call `member.HasPermission()` with a `PlanetPermissions` value.
```c#
// Check if the member has the Kick planet permission
// This also returns true for planet owners and members with an admin role
if (!member.HasPermission(PlanetPermissions.Kick))
{
await channel.SendMessageAsync("You do not have permission to use this command!");
return;
}
```
Here are all the available planet permissions:
| Permission | Description |
|---|---|
| `PlanetPermissions.Kick` | Can kick members |
| `PlanetPermissions.Ban` | Can ban members |
| `PlanetPermissions.Manage` | Can edit planet settings |
| `PlanetPermissions.Invite` | Can send invites |
| `PlanetPermissions.CreateChannels` | Can create channels |
| `PlanetPermissions.ManageRoles` | Can manage roles |
| `PlanetPermissions.MentionAll` | Can @mention all roles |
| `PlanetPermissions.BypassAutomod` | Bypasses automod |
| `PlanetPermissions.FullControl` | Full owner-level control |
### Channel permissions
Channel permissions are specific to a single channel — a member may have different permissions in different channels depending on their role overrides. To check one, call `await member.HasPermission(channel, ...)` with a `ChatChannelPermissions` value. Note that this one is `async` so needs `await`.
```c#
// Check if the member has the ManageMessages permission in this specific channel
if (!await member.HasPermission(channel, ChatChannelPermissions.ManageMessages))
{
await channel.SendMessageAsync("You do not have permission to manage messages in this channel!");
return;
}
```
Here are all the available channel permissions:
| Permission | Description |
|---|---|
| `ChatChannelPermissions.View` | Can see the channel in the list |
| `ChatChannelPermissions.ViewMessages` | Can read messages |
| `ChatChannelPermissions.PostMessages` | Can send messages |
| `ChatChannelPermissions.ManageMessages` | Can delete/manage messages |
| `ChatChannelPermissions.ManageChannel` | Can edit channel settings |
| `ChatChannelPermissions.ManagePermissions` | Can manage channel permission overrides |
| `ChatChannelPermissions.Embed` | Can post embedded content |
| `ChatChannelPermissions.AttachContent` | Can upload files |
| `ChatChannelPermissions.UseReactions` | Can use reactions |
### Putting it together
Here are both commands using everything above:
```c#
else if (command == "planetperms")
{
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
// Check if the member has the Kick planet permission
// This also returns true for planet owners and members with an admin role
if (!member.HasPermission(PlanetPermissions.Kick))
{
await channel.SendMessageAsync("You do not have permission to use this command!");
return;
}
await channel.SendMessageAsync("Hello, you have permission to kick members!");
}
else if (command == "channelperms")
{
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
// Check if the member has the ManageMessages permission in this specific channel
if (!await member.HasPermission(channel, ChatChannelPermissions.ManageMessages))
{
await channel.SendMessageAsync("You do not have permission to manage messages in this channel!");
return;
}
await channel.SendMessageAsync("Hello, you can manage messages in this channel!");
}
```
<br><br>
### Full code below
```c#
using Valour.Sdk.Client;
using Valour.Shared;
using DotNetEnv;
using Valour.Sdk.Models;
using Valour.Shared.Models;
using Valour.Shared.Authorization;
ValourClient client = new ValourClient("https://api.valour.gg/");
client.SetupHttpClient();
Env.Load();
string token = Environment.GetEnvironmentVariable("TOKEN") ?? string.Empty;
Dictionary<long, Channel> channelCache = new();
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine($"Token invalid. Please make sure you set a valid token in your .env");
return;
}
TaskResult loginResult = await client.InitializeUser(token);
if (!loginResult.Success)
{
Console.WriteLine($"Login failed: {loginResult.Message}");
}
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
foreach (Planet planet in client.PlanetService.JoinedPlanets)
{
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
foreach (Channel chan in planet.Channels)
{
channelCache[chan.Id] = chan;
if (chan.ChannelType == ChannelTypeEnum.PlanetChat)
{
await chan.OpenWithResult("YourBotName");
}
}
}
client.MessageService.MessageReceived += onMessageReceived;
async Task onMessageReceived(Message message)
{
string prefix = "bg/";
string content = message.Content;
if (!channelCache.TryGetValue(message.ChannelId, out var channel)) return;
if (!content.StartsWith(prefix)) return;
string withoutPrefix = content.Substring(prefix.Length);
string[] parts = withoutPrefix.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return;
string command = parts[0].ToLower();
string[] args = parts.Skip(1).ToArray();
if (command == "ping")
{
await channel.SendMessageAsync("Pong!");
}
else if (command == "planetperms")
{
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
// Check if the member has the Kick planet permission
// This also returns true for planet owners and members with a role with the Administrator Permission
if (!member.HasPermission(PlanetPermissions.Kick))
{
await channel.SendMessageAsync("You do not have permission to use this command!");
return;
}
await channel.SendMessageAsync("Hello, you have permission to kick members!");
}
else if (command == "channelperms")
{
// Fetch the member object for the message author
PlanetMember member = await message.FetchAuthorMemberAsync();
if (member is null)
{
await channel.SendMessageAsync("Could not find your member info!");
return;
}
// Check if the member has the ManageMessages permission in this specific channel
if (!await member.HasPermission(channel, ChatChannelPermissions.ManageMessages))
{
await channel.SendMessageAsync("You do not have permission to manage messages in this channel!");
return;
}
await channel.SendMessageAsync("Hello, you can manage messages in this channel!");
}
}
await Task.Delay(Timeout.Infinite);
```

View File

@@ -1,7 +1,15 @@
# Valour Bot Guide
By SkyJoshua
This guide walks you through creating a bot on Valour.gg, connecting it with the SDK and getting it to do things on a planet.
> **This guide is still a work in progress!**
This guide walks you through creating a bot on Valour.gg, connecting it with the SDK and getting it to do things on a planet. Basic knowledge in .NET is not required but is highly recommended!
## Prerequisites
- A [Valour](https://app.valour.gg) account
- [.NET SDK](https://dotnet.microsoft.com/download) installed
- Basic knowledge of C#
- The [Valour SDK](https://www.nuget.org/packages/Valour.SDK) (installed as part of the guide)
## 1. Create a bot account
1. Log into [Valour](https://app.valour.gg) with your regular account.
@@ -9,10 +17,16 @@ This guide walks you through creating a bot on Valour.gg, connecting it with the
3. Click **Create Bot** and give it a name.
4. **Copy the token immediately** - it is only shown once. If you lose it, you can regenerate it from the bot's edit page (this invalidates the old one).
## 2. Making the bot.
## 2. Making the bot
### Step 1. [Logging in as the bot](Guides/1.Login.md)
### Step 2. [Joining a planet](Guides/2.JoinPlanet.md)
### Step 3. [Connecting and Sending a Message to a Planet](Guides/3.ConnectingAndSending.md)
### Step 4. [Reveiving Messages and Commands](Guides/4.MessagesAndCommands.md)
### Step 1. [Logging in as the bot](Guides/1.Login.md) — Setting up the project and connecting to Valour
### Step 2. [Joining a planet](Guides/2.JoinPlanet.md) — Adding your bot to a planet
### Step 3. [Connecting and Sending a Message to a Planet](Guides/3.ConnectingAndSending.md) — Opening a real-time connection and sending your first message
### Step 4. [Receiving Messages and Commands](Guides/4.MessagesAndCommands.md) — Listening for messages and creating a basic command
### Step 5. [Command Arguments](Guides/5.CommandArguments.md) — Parsing arguments from commands
### Step 6. [Member Info](Guides/6.MemberInfo.md) — Fetching info about the message author
### Step 7. [Permission Checking](Guides/7.PermissionChecking.md) — Restricting commands to members with the right permissions
---
For more information check out the [Valour GitHub](https://github.com/Valour-Software/Valour) and the [Valour SDK on NuGet](https://www.nuget.org/packages/Valour.SDK).