Merge pull request #3 from SkyJoshua/dev

Welcome Messages!
This commit is contained in:
SkyJoshua
2026-03-18 03:02:08 +00:00
committed by GitHub
11 changed files with 363 additions and 83 deletions

View File

@@ -2,61 +2,53 @@
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Privacy Policy</h1>
<p><span class="bold">Effective Date:</span> February 26, 2026</p>
<p>This Privacy Policy describes how the bot (the Bot) collects, uses, and stores information when used within a server.</p>
<p><strong>Effective Date:</strong> March 16, 2026</p>
<p>This Privacy Policy describes how SkyBot ("the Bot") collects, uses, and stores information when used within a Valour planet.</p>
<hr>
<h2>1. Information Collected</h2>
<p>The Bot collects and stores only the minimum data necessary to provide its intended functionality.</p>
<h3>Information Stored:</h3>
<p>The Bot collects only the minimum data necessary to provide its intended functionality. All data is stored in-memory and is lost when the Bot restarts. The Bot does not persist any data to disk.</p>
<h3>Information Temporarily Held in Memory:</h3>
<ol>
<li>Message IDs</li>
<li>Channel IDs</li>
<li>Server (“Planet”) IDs</li>
<li>Planet Configuration data associated with those channels</li>
<li>Channel IDs (for routing messages and commands)</li>
<li>Planet IDs (for planet-specific operations)</li>
<li>Member IDs (for moderation commands)</li>
</ol>
<h3>Information Not Stored:</h3>
<h3>Information Never Stored:</h3>
<ol>
<li>Message content</li>
<li>User-generated message content</li>
<li>Direct Messages (“DMs”)</li>
<li>Direct Messages ("DMs")</li>
<li>Personal account information (including usernames, email addresses, or other personally identifiable information)</li>
<li>Any data that persists beyond the Bot's current session</li>
</ol>
<hr>
<h2>2. Purpose of Data Collection</h2>
<p>Stored information is used exclusively to:</p>
<p>Temporarily held information is used exclusively to:</p>
<ol>
<li>Maintain server-specific configuration settings</li>
<li>Associate Planets with designated channels</li>
<li>Enable and maintain core bot functionality</li>
<li>Route commands to the correct channels and planets</li>
<li>Enable moderation commands such as ban, unban, and kick</li>
<li>Enable core bot functionality during the current session</li>
</ol>
<p>The Bot does not use stored information for profiling, marketing, analytics, or tracking purposes.</p>
<p>The Bot does not use any information for profiling, marketing, analytics, or tracking purposes.</p>
<hr>
<h2>3. Data Storage and Security</h2>
<p>All stored data is maintained securely on the Bots hosting server. Reasonable technical measures are implemented to protect stored information against unauthorized access, alteration, or disclosure.</p>
<p>The Bot does not sell, rent, trade, or otherwise share stored data with third parties.</p>
<p>Since all data is stored in-memory only, no data is written to disk, databases, or any external storage. All temporarily held data is automatically cleared when the Bot restarts.</p>
<p>The Bot does not sell, rent, trade, or otherwise share any data with third parties.</p>
<hr>
<h2>4. Data Retention</h2>
<p>Configuration data is retained only while the Bot remains active within a server.</p>
<p>If the Bot is removed from a server, associated configuration data may be deleted within a reasonable timeframe.</p>
<p>All data is held only for the duration of the Bot's current session. No data is retained beyond a restart. There is no mechanism for long-term data storage in this Bot.</p>
<hr>
<h2>5. Future Changes to Logging or Data Practices</h2>
<h2>5. Self-Hosting</h2>
<p>SkyBot is designed for self-hosting. If you choose to host your own instance of SkyBot, you are responsible for the privacy and security of any data processed by your instance. This policy applies to the official instance of SkyBot only.</p>
<hr>
<h2>6. Future Changes to Logging or Data Practices</h2>
<p>If additional operational logging or data collection practices are introduced in the future, this Privacy Policy will be updated to reflect those changes prior to implementation.</p>
<p>Continued use of the Bot after updates to this policy constitutes acceptance of the revised policy.</p>
<hr>
<h2>6. Contact Information</h2>
<h2>7. Contact Information</h2>
<p>For privacy-related inquiries, requests, or concerns, please contact:</p>
<p><span class="bold">Email:</span> contact@skyjoshua.xyz</p>
<p><strong>Email:</strong> contact@skyjoshua.xyz</p>
</body>
</html>
</html>

View File

@@ -1,106 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<body>
<h1>SkyBot</h1>
<p>
SkyBot is a Valour.gg bot.
SkyBot is a Valour.gg bot built with .NET 10.
</p>
<h2>Features</h2>
<ul>
<li>Designed for self-hosting</li>
<li>Open-source under AGPL-3.0</li>
<li>Built with .NET</li>
<li>Built with .NET 10</li>
<li>Command system with automatic registration</li>
<li>Moderation commands (ban, unban, kick)</li>
<li>Fun commands (8ball, coinflip, dice, rock paper scissors, and more)</li>
<li>Info commands (user info, planet info, ping, uptime)</li>
</ul>
<h2>Data &amp; Privacy</h2>
<p>SkyBot stores only the minimum data required for operation:</p>
<ul>
<li>Message IDs</li>
<li>Server (Planet) IDs</li>
<li>Channel IDs</li>
</ul>
<p>SkyBot stores only the minimum data required for operation. All data is stored in-memory and is lost on restart. SkyBot does <strong>not</strong> persist any data to disk.</p>
<p>SkyBot does <strong>not</strong> store:</p>
<ul>
<li>Message content</li>
<li>Direct messages</li>
<li>Personal user data</li>
</ul>
<p>
Full privacy policy:<br>
<a href="https://github.com/SkyJoshua/SkyBot/blob/main/PRIVACY.md">
https://github.com/SkyJoshua/SkyBot/blob/main/PRIVACY.md
</a>
</p>
<h2>License</h2>
<p>
This project is licensed under the
<strong>GNU Affero General Public License v3.0 (AGPL-3.0)</strong>.
</p>
<p>
See the LICENSE file for details:<br>
<a href="https://github.com/SkyJoshua/SkyBot/blob/main/LICENSE">
https://github.com/SkyJoshua/SkyBot/blob/main/LICENSE
</a>
</p>
<p>
Because this project is licensed under AGPL-3.0, if you modify and deploy it
publicly (including as a hosted service), you must make your source code
available under the same license.
</p>
<h2>Requirements</h2>
<ul>
<li>.NET 10</li>
<li>A Valour bot token</li>
</ul>
<h2>Installation</h2>
Fork this Repository
<p>Fork this repository, then:</p>
<pre><code>git clone https://github.com/YOUR_USERNAME/SkyBot.git
cd SkyBot
cd SkyBot/SkyBot
dotnet restore
</code></pre>
<p>
All required NuGet packages will be installed automatically using the
provided <code>SkyBot.csproj</code> file.
</p>
<h2>Configuration</h2>
<p>Before running the bot, create a <code>.env</code> file in the root directory of the project with the following content:</p>
<p>Create a <code>.env</code> file in the root directory of the project with your bot token:</p>
<pre><code>TOKEN=your-bot-token-here
PREFIX=your-prefix-here
</code></pre>
<p>Then open <code>Config.cs</code> and update the following values:</p>
<pre><code>public static readonly long OwnerId = your-owner-id-here;
public static readonly string Prefix = "your-prefix-here";
public static readonly string SourceLink = "your-source-link-here";
</code></pre>
<ul>
<li>Replace <code>your-bot-token-here</code> with your actual bot token.</li>
<li>Ensure the bot has proper permissions in the target server.</li>
<li>Replace <code>your-owner-id-here</code> with your Valour user ID.</li>
<li>Replace <code>your-prefix-here</code> with your desired command prefix (e.g. <code>s/</code>).</li>
<li>Replace <code>your-source-link-here</code> with a link to your fork of the repository.</li>
</ul>
<p>
Sensitive data such as bot tokens should never be committed to the repository.
Use environment variables or secure configuration methods.
</p>
<p>Never commit your <code>.env</code> file to the repository. Ensure it is listed in your <code>.gitignore</code>.</p>
<h2>Running the Bot</h2>
<pre><code>dotnet run
</code></pre>
<pre><code>dotnet run</code></pre>
<h2>Contributing</h2>
<p>
Contributions are welcome. By submitting a contribution, you agree that your
contributions will be licensed under AGPL-3.0.
</p>
<ol>
<li>Fork the repository</li>
<li>Create a feature branch</li>
<li>Submit a pull request</li>
</ol>
</body>
</html>
</html>

View File

@@ -7,7 +7,7 @@ using Valour.Shared;
namespace SkyBot.Commands
{
public class Test : ICommand
public class Edit : ICommand
{
public string Name => "edit";
public string[] Aliases => [];
@@ -29,6 +29,7 @@ namespace SkyBot.Commands
if(!PermissionHelper.IsOwner(member))
{
await MessageHelper.ReplyAsync(ctx, channel, "This is a Dev only command.");
return;
}
if (message.ReplyToId == null)

View File

@@ -0,0 +1,103 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using SkyBot.Models;
using SkyBot.Services;
using Valour.Sdk.Models;
using Valour.Shared.Authorization;
using Valour.Shared.Models;
namespace SkyBot.Commands
{
public class SetWelcome : ICommand
{
public string Name => "setwelcome";
public string[] Aliases => [];
public string Description => "Sets the welcome channel, message or active.";
public string Section => "Mod";
public string Usage => "set <channel|message|active [value]";
public async Task Execute(CommandContext ctx)
{
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
long channelId = ctx.ChannelId;
Message message = ctx.Message;
PlanetMember member = ctx.Member;
Planet planet = ctx.Planet;
string[] args = ctx.Args;
if (!channelCache.TryGetValue(channelId, out var channel)) return;
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Manage]) && !PermissionHelper.IsOwner(member))
{
await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command.");
return;
}
if (args.Length == 0)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please specify `channel` or `message`.");
return;
}
switch (args[0].ToLower())
{
case "channel":
case "c":
long targetChannelId;
if (message.Mentions != null && message.Mentions.Any(m => m.Type == MentionType.Channel)) {targetChannelId = message.Mentions.First(m => m.Type == MentionType.Channel).TargetId;}
else if (args.Length > 1 && long.TryParse(args[1], out long parsedChannelId)) {targetChannelId = parsedChannelId;}
else {targetChannelId = channelId;}
if (!channelCache.ContainsKey(targetChannelId)) {await MessageHelper.ReplyAsync(ctx, channel, "Could not find that channel."); return;}
await WelcomeService.SetWelcomeChannel(planet.Id, targetChannelId);
await MessageHelper.ReplyAsync(ctx, channel, $"Welcome channel set to «@c-{targetChannelId}».");
break;
case "message":
case "m":
if (args.Length < 2)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a message. Valid variables: {username} {nickname} {fulluser} {mention} {id}");
return;
}
string msg = string.Join(" ", args[1..]);
await WelcomeService.SetWelcomeMessage( planet.Id, msg);
await MessageHelper.ReplyAsync(ctx, channel, $"Welcome message set to: `{msg}`");
break;
case "active":
case "a":
if (args.Length < 2)
{
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a value. Use `true`, `false`, or `toggle`.");
return;
}
string value = args[1].ToLower();
if (value != "toggle" && value != "true" && value != "false")
{
await MessageHelper.ReplyAsync(ctx, channel, "Invalid value. Use `true`, `false`, `toggle`");
return;
}
if (value == "toggle")
{
var toggle = await WelcomeService.SetActive(planet.Id);
await MessageHelper.ReplyAsync(ctx, channel, toggle.Value ? "Welcome messages enabled." : "Welcome messages disabled.");
return;
}
bool.TryParse(value, out var active);
await WelcomeService.SetActive(planet.Id, active);
await MessageHelper.ReplyAsync(ctx, channel, active ? "Welcome messages enabled." : "Welcome messages disabled.");
break;
default:
await MessageHelper.ReplyAsync(ctx, channel, "Invalid option. Use `channel`, `message` or `active`.");
break;
}
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.Data.Sqlite;
namespace SkyBot.Helpers
{
public static class DatabaseHelper
{
private const string ConnectionString = "Data Source=database.db";
public static SqliteConnection GetConnection()
{
SqliteConnection connection = new SqliteConnection(ConnectionString);
connection.Open();
return connection;
}
public static async Task InitializeAsync()
{
using SqliteConnection connection = GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS WelcomeConfigs (
PlanetId INTEGER PRIMARY KEY,
ChannelId INTEGER NOT NULL DEFAULT 0,
Message TEXT NOT NULL DEFAULT 'Welcome to the planet, {username}!',
Active INTEGER NOT NULL DEFAULT 0
);
";
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("Database initialized.");
}
}
}

View File

@@ -0,0 +1,7 @@
public class WelcomeConfig
{
public long PlanetId { get; set; }
public long ChannelId { get; set; }
public string Message { get; set; } = "Welcome to the planet, {username}!";
public bool Active { get; set; } = false;
}

View File

@@ -5,10 +5,14 @@ using SkyBot.Models;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
namespace SkyBot.Services.Messages
{
public static class Create
{
private static readonly ConcurrentDictionary<long, DateTime> _cooldowns = new();
private static readonly TimeSpan _cooldown = TimeSpan.FromSeconds(2);
public static async Task MessageAsync(
ValourClient client,
ConcurrentDictionary<long, Channel> channelCache,
@@ -40,6 +44,11 @@ namespace SkyBot.Services.Messages
Client = client
};
if (_cooldowns.TryGetValue(message.AuthorUserId, out var lastUsed) && DateTime.UtcNow - lastUsed < _cooldown)
return;
_cooldowns[message.AuthorUserId] = DateTime.UtcNow;
if (CommandRegistry.Commands.TryGetValue(command, out var handler))
{
await handler.Execute(ctx);

View File

@@ -1,14 +1,13 @@
using System.Collections.Concurrent;
using SkyBot.Services;
using Valour.Sdk.Client;
using Valour.Sdk.ModelLogic;
using Valour.Sdk.Models;
using Valour.Sdk.Models.Messages.Embeds;
namespace SkyBot.Services
{
public static class PlanetService
{
private static readonly DateTime _startTime = DateTime.UtcNow;
public static async Task InitializePlanetsAsync(
ValourClient client,
ConcurrentDictionary<long, Channel> channelCache,
@@ -19,15 +18,27 @@ namespace SkyBot.Services
.Select(async planet =>
{
Console.WriteLine($"Initializing Planet: {planet.Name}");
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
await ChannelService.InitializeChannelsAsync(channelCache, planet);
planet.Channels.Changed += async (channelEvent) => {
planet.Channels.Changed += async _ =>
{
await ChannelService.InitializeChannelsAsync(channelCache, planet);
};
});
planet.Members.Changed += async memberEvent =>
{
if ((DateTime.UtcNow - _startTime).TotalSeconds < 10) return;
if (memberEvent is ModelAddedEvent<PlanetMember> addedEvent)
{
await WelcomeService.OnMemberJoin(addedEvent.Model, channelCache);
}
};
initializedPlanets.TryAdd(planet.Id, true);
});
await Task.WhenAll(tasks);
}
}

View File

@@ -0,0 +1,137 @@
using System.Collections.Concurrent;
using Microsoft.Data.Sqlite;
using SkyBot.Helpers;
using Valour.Sdk.Models;
namespace SkyBot.Services
{
public static class WelcomeService
{
private static readonly ConcurrentDictionary<long, WelcomeConfig> _cache = new();
public static async Task InitializeAsync()
{
using SqliteConnection connection = DatabaseHelper.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM WelcomeConfigs";
using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var config = new WelcomeConfig
{
PlanetId = (long)reader["PlanetId"],
ChannelId = (long)reader["ChannelId"],
Message = (string)reader["Message"],
Active = (long)reader["Active"] == 1
};
_cache[config.PlanetId] = config;
}
Console.WriteLine("WelcomeService initialized.");
Console.WriteLine($"Loaded {_cache.Count} welcome configs from database.");
}
public static async Task OnMemberJoin(PlanetMember member, ConcurrentDictionary<long, Channel> channelCache)
{
if (!_cache.TryGetValue(member.PlanetId, out var config)) { Console.WriteLine("No config found"); return; }
if (!config.Active) { Console.WriteLine("Not active"); return; }
Channel? channel = null;
if (config.ChannelId != 0 && channelCache.TryGetValue(config.ChannelId, out var configChannel))
{
channel = configChannel;
}
else
{
channel = channelCache.Values.FirstOrDefault(c => c.PlanetId == member.PlanetId && c.IsDefault);
}
if (channel == null) { Console.WriteLine("No channel found"); return; }
string message = config.Message
.Replace("{username}", member.Name)
.Replace("{fulluser}", member.User.NameAndTag)
.Replace("{nickname}", string.IsNullOrWhiteSpace(member.Nickname) ? member.Name : member.Nickname)
.Replace("{mention}", MessageHelper.Mention(member))
.Replace("{id}", $"{member.Id}");
await channel.SendMessageAsync(message);
}
public static async Task SetWelcomeChannel(long planetId, long channelId)
{
using SqliteConnection connection = DatabaseHelper.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO WelcomeConfigs (PlanetId, ChannelId) VALUES ($planetId, $channelId)
ON CONFLICT(PlanetId) DO UPDATE SET ChannelId = $channelId;
";
cmd.Parameters.AddWithValue("$planetId", planetId);
cmd.Parameters.AddWithValue("$channelId", channelId);
await cmd.ExecuteNonQueryAsync();
if (_cache.TryGetValue(planetId, out var config))
{
config.ChannelId = channelId;
}
else
{
_cache[planetId] = new WelcomeConfig{PlanetId = planetId, ChannelId = channelId};
}
}
public static async Task SetWelcomeMessage(long planetId, string message)
{
using SqliteConnection connection = DatabaseHelper.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO WelcomeConfigs (PlanetId, Message) VALUES ($planetId, $message)
ON CONFLICT(PlanetId) DO UPDATE SET Message = $message;
";
cmd.Parameters.AddWithValue("$planetId", planetId);
cmd.Parameters.AddWithValue("$message", message);
await cmd.ExecuteNonQueryAsync();
if (_cache.TryGetValue(planetId, out var config))
{
config.Message = message;
}
else
{
_cache[planetId] = new WelcomeConfig{PlanetId = planetId, Message = message};
}
}
public static async Task SetActive(long planetId, bool active)
{
using SqliteConnection connection = DatabaseHelper.GetConnection();
using SqliteCommand cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO WelcomeConfigs (PlanetId, Active) VALUES ($planetId, $active)
ON CONFLICT(PlanetId) DO UPDATE SET Active = $active;
";
cmd.Parameters.AddWithValue("$planetId", planetId);
cmd.Parameters.AddWithValue("$active", active ? 1 : 0);
await cmd.ExecuteNonQueryAsync();
if (_cache.TryGetValue(planetId, out var config))
{
config.Active = active;
}
else
{
_cache[planetId] = new WelcomeConfig{PlanetId = planetId, Active = active};
}
}
public static async Task<bool?> SetActive(long planetId)
{
if (!_cache.TryGetValue(planetId, out var config)) return null;
bool newActive = !config.Active;
await SetActive(planetId, newActive);
return newActive;
}
}
}

View File

@@ -2,6 +2,7 @@ using Valour.Sdk.Client;
using Valour.Sdk.Models;
using SkyBot.Services;
using System.Collections.Concurrent;
using SkyBot.Helpers;
namespace SkyBot
{
@@ -21,6 +22,8 @@ namespace SkyBot
public async Task StartAsync()
{
StartTime = DateTime.UtcNow;
await DatabaseHelper.InitializeAsync();
await WelcomeService.InitializeAsync();
await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets);
}
}
@@ -34,7 +37,6 @@ namespace SkyBot
try
{
await new SkyBot().StartAsync();
Console.WriteLine("Ready and listening...");
await Task.Delay(Timeout.Infinite);

View File

@@ -5,11 +5,12 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.2.0.0</Version>
<Version>0.2.1.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="Valour.Sdk" Version="0.5.19" />
</ItemGroup>