first build of v2

This commit is contained in:
2026-03-15 01:29:16 +00:00
parent 177cb12b59
commit ab9c7223ca
13 changed files with 281 additions and 390 deletions

8
.gitignore vendored
View File

@@ -1,5 +1,5 @@
bin/
obj/
SkyBot.sln
.env
Program.cs.old
.gitignore
SkyBot/bin/
SkyBot/obj/
SkyBot/SkyBot.sln

View File

@@ -1,248 +0,0 @@
using Valour.Sdk.Client;
using Valour.Sdk.Models;
using DotNetEnv;
using SkyBot;
Env.Load();
var token = Environment.GetEnvironmentVariable("TOKEN");
var allowedUserIds = new List<long> { 15652354820931584 };
var ownerId = 15652354820931584;
var prefix = Environment.GetEnvironmentVariable("PREFIX");
var client = new ValourClient("https://api.valour.gg/");
client.SetupHttpClient();
if (string.IsNullOrWhiteSpace(token))
{
Console.WriteLine("TOKEN environment variable not set.");
return;
}
var loginResult = await client.InitializeUser(token);
if (!loginResult.Success)
{
Console.WriteLine($"Login Failed: {loginResult.Message}");
return;
}
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
await Utils.UpdateValourUserCountAsync();
Utils.StartValourUserUpdater();
//Dictionaries
var channelCache = new Dictionary<long, Channel>();
var InitializedPlanets = new HashSet<long>();
await Utils.InitializePlanetsAsync(client, channelCache, InitializedPlanets);
client.PlanetService.JoinedPlanetsUpdated += async () =>
{
await Utils.InitializePlanetsAsync(client, channelCache, InitializedPlanets);
};
client.MessageService.MessageReceived += async (message) =>
{
string content = message.Content ?? "";
long channelId = message.ChannelId;
var member = await message.FetchAuthorMemberAsync();
var pingMember = $"«@m-{member.Id}»";
if (content is null) return;
if (message.AuthorUserId == client.Me.Id) return;
if (allowedUserIds.Contains(message.AuthorUserId))
{
if (Utils.IsSingleEmoji(content))
{
await message.AddReactionAsync(content);
}
};
if (Utils.ContainsAny(content, $"{prefix}react"))
{
if (message.AuthorUserId != ownerId) return;
string[] args = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length < 2) return;
string emoji = args[1];
var interceptor = new Utils.ReactionInterceptor(Console.Out);
Console.SetOut(interceptor);
while (true)
{
interceptor.Reset();
await message.AddReactionAsync(emoji);
if (interceptor.DetectedAlreadyExists)
{
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
Console.WriteLine("Reaction already exists, stopping.");
break;
}
}
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
}
var echoprefixes = new[] { $"{prefix}echo"};
if (Utils.ContainsAny(content, echoprefixes))
{
var matchedPrefix = echoprefixes.First(p => content.StartsWith(p, StringComparison.OrdinalIgnoreCase));
var reply = content.Substring(matchedPrefix.Length).TrimStart();
if (string.IsNullOrWhiteSpace(reply)) await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Enter a message to echo.");
reply = $"{pingMember} {reply}";
if (reply.Length > 2048)
{
reply = reply.Substring(0, 2048);
}
await Utils.SendReplyAsync(channelCache, channelId, reply);
};
var echorawprefixes = new[] { $"{prefix}rawecho"};
if (Utils.ContainsAny(content, echorawprefixes))
{
if (message.AuthorUserId != ownerId)
{
await Utils.SendReplyAsync(channelCache, channelId, "You do not have permission to execute this command.");
return;
}
var matchedPrefix = echorawprefixes.First(p => content.StartsWith(p, StringComparison.OrdinalIgnoreCase));
var reply = content.Substring(matchedPrefix.Length).TrimStart();
if (string.IsNullOrWhiteSpace(reply))
{
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Enter a message to echo.");
return;
}
reply = $"{reply}";
if (reply.Length > 2048)
{
reply = reply.Substring(0, 2048);
}
await Utils.SendReplyAsync(channelCache, channelId, reply);
};
if (Utils.ContainsAny(content, $"{prefix}suggest"))
{
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can suggest a command to be added here: https://docs.google.com/spreadsheets/d/1CzcpLAuMiPL_RODrZ5x25cPj8yE-rR3mEnqrd_2Fbmk");
};
if (Utils.ContainsAny(content, $"{prefix}source"))
{
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can see my source code here: https://github.com/SkyJoshua/SkyBot");
};
if (Utils.ContainsAny(content, $"{prefix}joincode"))
{
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can use this to join a planet: https://github.com/SkyJoshua/JoinPlanet");
};
if (Utils.ContainsAny(content, $"{prefix}joinsite"))
{
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} You can use this website to easily add your bot to a planet: https://skyjoshua.xyz/planetjoiner");
};
if (Utils.ContainsAny(content, $"{prefix}api", $"{prefix}swagger"))
{
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Here is a link to the Swagger API: https://api.valour.gg/swagger");
};
if (Utils.ContainsAny(content, $"{prefix}cmds", $"{prefix}help"))
{
await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} Here is a list of my commands:
- `s/echo <text> - Echos text into the chat`
- `s/suggest - Shares the suggestions link`
- `s/source - Sends link for the source code`
- `s/joincode - Sends a link to a github that you can use to make your bot join your planet.`
- `s/joinsite - Sends a link to a website that you can use to make yout bot join your planet.`
- `s/api|swagger - Sends a link to the Swagger API`
- `s/cmds|help - Shows this list`
- `s/usercount - Shows the user count of Valour`
- `s/devcentral - Sends the invite link to the Dev Central Planet`
- `s/mc - Sends Unofficial ValourSMP IP`
");
};
if (Utils.ContainsAny(content, $"{prefix}usercount"))
{
await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember}
Current Valour user count is: {Utils.ValourUserCount:N0}
You can see a graph of the user count here: /meow");
};
if (Utils.ContainsAny(content, $"{prefix}devcentral"))
{
await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} you can join the Dev Central (ID: 42439954653511681) planet here: https://app.valour.gg/I/k2tz9c4i");
}
if (Utils.ContainsAny(content, $"{prefix}mc"))
{
await Utils.SendReplyAsync(channelCache, channelId, @$"{pingMember} you can join the Unofficial ValourSMP Minecraft Server by using this ip:
Java: `valour.sxsc.xyz`, Bedrock: `valourbr.sxsc.xyz` Both with the default ports.
Cool features can be found here: https://sxsc.xyz/servers/valour/");
}
if (Utils.ContainsAny(content, $"{prefix}invite"))
{
if(message.AuthorUserId != ownerId) return;
string[] args = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length < 2)
{
await Utils.SendReplyAsync(channelCache, channelId, "Usage: s/invite <planetId> [inviteCode]");
}
if (!long.TryParse(args[1], out long planetId))
{
await Utils.SendReplyAsync(channelCache, channelId, "Planet ID is not valid.");
return;
}
string inviteCode = args.Length > 2 ? args[2] : "";
var joinResult = await client.PlanetService.JoinPlanetAsync(planetId, inviteCode);
if (joinResult.Success && joinResult.Data != null)
{
await Task.Delay(200);
if (client.Cache.Planets.TryGet(planetId, out var planet))
{
if (planet is null) return;
await Utils.SendReplyAsync(channelCache, channelId, $"Joined planet: {planet.Name}");
}
else
{
await Utils.SendReplyAsync(channelCache, channelId, "Joined planet, but could not retrieve its name.");
}
}
else
{
await Utils.SendReplyAsync(channelCache, channelId, $"Failed to join planet: {joinResult.Message}");
}
};
};
Console.WriteLine("Listening for messages...");
await Task.Delay(Timeout.Infinite);

View File

@@ -0,0 +1,30 @@
using System.Collections.Concurrent;
using SkyBot.Helpers;
using Valour.Sdk.Models;
namespace SkyBot.Commands
{
public static class HelpCommand
{
public static async Task Execute(ConcurrentDictionary<long, Channel> channelCache, long channelId, String prefix, PlanetMember member)
{
string helpMessage = $@"**Skybot Commands**:
- `s/echo <text>` - Echos text into the chat
- `s/suggest` - Shares the suggestions link
- `s/source` - Sends link for the source code
- `s/joincode` - Sends a link to a github that you can use to make your bot join your planet.
- `s/joinsite` - Sends a link to a website that you can use to make yout bot join your planet.
- `s/api|swagger` - Sends a link to the Swagger API
- `s/cmds|help` - Shows this list
- `s/usercount` - Shows the user count of Valour
- `s/devcentral` - Sends the invite link to the Dev Central Planet
- `s/mc` - Sends Unofficial ValourSMP IPs
";
if (channelCache.TryGetValue(channelId, out var channel))
{
await channel.SendMessageAsync($"{MentionHelper.Mention(member)}\n{helpMessage}");
}
}
}
}

9
SkyBot/Config.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace Skybot
{
public static class Config {
public static readonly long OwnerId = 15652354820931584;
public static readonly string Prefix = "sd/";
}
}

View File

@@ -0,0 +1,10 @@
using Valour.Sdk.Models;
namespace SkyBot.Helpers
{
public static class MentionHelper
{
public static string Mention(this PlanetMember member) => $"«@m-{member.Id}»";
public static string Mention(this User user) => $"«@u-{user.Id}»";
}
}

View File

@@ -0,0 +1,19 @@
using Valour.Sdk.Models;
using Valour.Shared.Authorization;
namespace SkyBot.Helpers
{
public static class PermissionHelper
{
public static async Task<bool> HasPermAsync(PlanetMember member, PlanetPermission[] permissions, bool requireAll = false)
{
if (member == null) return false;
if (member.HasPermission(PlanetPermissions.FullControl)) return true;
if (member.Roles.Any(r => r.IsAdmin)) return true;
return requireAll
? permissions.All(permission => member.HasPermission(permission))
: permissions.Any(permission => member.HasPermission(permission));
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Concurrent;
using DotNetEnv;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
namespace SkyBot.Services
{
public static class BotService
{
public static async Task InitializeBotAsync(
ValourClient client,
ConcurrentDictionary<long, Channel> channelCache,
ConcurrentDictionary<long, bool> initalizedPlanets)
{
Env.Load();
var token = Environment.GetEnvironmentVariable("TOKEN");
if (string.IsNullOrWhiteSpace(token)) {Console.WriteLine("TOKEN not set."); return;}
var loginResult = await client.InitializeUser(token);
if (!loginResult.Success) {Console.WriteLine($"Login Failed: {loginResult.Message}"); return;}
Console.WriteLine($"Logged in as {client.Me.Name} (ID: {client.Me.Id})");
await PlanetService.InitializePlanetsAsync(client, channelCache, initalizedPlanets);
client.PlanetService.JoinedPlanetsUpdated += async () =>
{
await PlanetService.InitializePlanetsAsync(client, channelCache, initalizedPlanets);
};
client.MessageService.MessageReceived += async (message) =>
{
await Messages.Create.MessageAsync(client, channelCache, message);
};
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Concurrent;
using Valour.Sdk.Models;
using Valour.Shared.Models;
namespace SkyBot.Services
{
public static class ChannelService
{
private static readonly SemaphoreSlim _channelSemaphore = new SemaphoreSlim(3, 3);
public static async Task InitializeChannelsAsync(
ConcurrentDictionary<long, Channel> channelCache,
Planet planet)
{
var tasks = planet.Channels.Select(async channel =>
{
channelCache[channel.Id] = channel;
if (channel.ChannelType == ChannelTypeEnum.PlanetChat)
{
await _channelSemaphore.WaitAsync();
try
{
await channel.OpenWithResult("SkyBot");
Console.WriteLine($"Realtime opened for: {planet.Name} (ID: {planet.Id}) -> {channel.Name} (ID: {channel.Id})");
await Task.Delay(250);
}
finally
{
_channelSemaphore.Release();
}
}
});
await Task.WhenAll(tasks);
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Concurrent;
using Skybot;
using SkyBot.Commands;
using SkyBot.Helpers;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
namespace SkyBot.Services.Messages
{
public static class Create
{
public static async Task MessageAsync(
ValourClient client,
ConcurrentDictionary<long, Channel> channelCache,
Message message
)
{
string prefix = Config.Prefix;
if (message.AuthorUserId == client.Me.Id) return;
string content = message.Content ?? "";
if (string.IsNullOrWhiteSpace(content)) return;
if (!content.StartsWith(prefix)) return;
long channelId = message.ChannelId;
PlanetMember member = await message.FetchAuthorMemberAsync();
var parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return;
string command = parts[0].ToLower();
string[] args = parts[1..];
switch (command)
{
case "help":
await HelpCommand.Execute(channelCache, channelId, prefix, member);
break;
default:
if (channelCache.TryGetValue(channelId, out var channel))
{
await channel.SendMessageAsync($"{MentionHelper.Mention(member)} Unknown command.");
}
break;
}
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Concurrent;
using SkyBot.Services;
using Valour.Sdk.Client;
using Valour.Sdk.Models;
using Valour.Sdk.Models.Messages.Embeds;
namespace SkyBot.Services
{
public static class PlanetService
{
public static async Task InitializePlanetsAsync(
ValourClient client,
ConcurrentDictionary<long, Channel> channelCache,
ConcurrentDictionary<long, bool> initializedPlanets)
{
var tasks = client.PlanetService.JoinedPlanets
.Where(planet => !initializedPlanets.ContainsKey(planet.Id))
.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) => {
await ChannelService.InitializeChannelsAsync(channelCache, planet);
};
});
await Task.WhenAll(tasks);
}
}
}

51
SkyBot/SkyBot.cs Normal file
View File

@@ -0,0 +1,51 @@
using Valour.Sdk.Client;
using Valour.Sdk.Models;
using SkyBot.Services;
using System.Collections.Concurrent;
namespace SkyBot
{
public class SkyBot
{
private readonly ValourClient _client;
private readonly ConcurrentDictionary<long, Channel> _channelCache = new();
private readonly ConcurrentDictionary<long, bool> _initializedPlanets = new();
public SkyBot()
{
_client = new ValourClient("https://api.valour.gg/");
_client.SetupHttpClient();
}
public async Task StartAsync()
{
await BotService.InitializeBotAsync(_client, _channelCache, _initializedPlanets);
}
}
public class Program
{
public static async Task Main(string[] args)
{
while (true)
{
try
{
await new SkyBot().StartAsync();
Console.WriteLine("Ready and listening...");
await Task.Delay(Timeout.Infinite);
} catch (InvalidOperationException ex) when (ex.Message.Contains("concurrent update"))
{
Console.WriteLine("Concurrent update detected, restarting...");
await Task.Delay(1000);
} catch (Exception ex)
{
Console.WriteLine($"Fatal error: {ex.Message}");
break;
}
}
}
}
}

138
utils.cs
View File

@@ -1,138 +0,0 @@
using System.Globalization;
using System.Text.Json;
using Valour.Sdk.Models;
using Valour.Sdk.Client;
using System.Text;
namespace SkyBot
{
public static class Utils
{
private static readonly HttpClient _http = new HttpClient();
private static long _valourUserCount;
public static long ValourUserCount => _valourUserCount;
public static bool IsSingleEmoji(string input)
{
if (string.IsNullOrWhiteSpace(input))
return false;
input = input.Trim();
var enumerator = StringInfo.GetTextElementEnumerator(input);
int count = 0;
while (enumerator.MoveNext())
count++;
return count == 1;
}
public static bool ContainsAny(string input, params string[] values)
{
var lower = input.ToLower();
foreach (var value in values)
{
if (lower.Contains(value.ToLower()))
return true;
};
return false;
}
public static async Task SendReplyAsync(Dictionary<long, Channel> channelCache, long channel, string reply)
{
if (channelCache.TryGetValue(channel, out var chan))
{
await chan.SendMessageAsync(reply);
}
else
{
Console.WriteLine($"Channel {channel} was not found in the cache.");
};
}
public static async Task UpdateValourUserCountAsync()
{
try
{
var response = await _http.GetStringAsync("https://api.valour.gg/api/users/count");
_valourUserCount = JsonSerializer.Deserialize<long>(response);
Console.WriteLine($"Valour user count updated: {_valourUserCount}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to update Valour user count: {ex.Message}");
}
}
public static void StartValourUserUpdater()
{
var timer = new System.Timers.Timer(300_000);
timer.Elapsed += async (_, _) => await UpdateValourUserCountAsync();
timer.AutoReset = true;
timer.Start();
}
public static async Task InitializePlanetsAsync(ValourClient client, Dictionary<long, Channel> channelCache, HashSet<long> initializedPlanets)
{
foreach (var planet in client.PlanetService.JoinedPlanets)
{
if (initializedPlanets.Contains(planet.Id))
continue;
Console.WriteLine($"Initializing Planet: {planet.Name}");
await planet.EnsureReadyAsync();
await planet.FetchInitialDataAsync();
foreach (var channel in planet.Channels)
{
channelCache[channel.Id] = channel;
if (channel.ChannelType == Valour.Shared.Models.ChannelTypeEnum.PlanetChat)
{
await channel.OpenWithResult("SkyBot");
Console.WriteLine($"Realtime opened for: {planet.Name} -> {channel.Name}");
}
}
initializedPlanets.Add(planet.Id);
}
}
public class ReactionInterceptor : TextWriter
{
private readonly TextWriter _original;
public bool DetectedAlreadyExists { get; private set; }
public override Encoding Encoding => _original.Encoding;
public ReactionInterceptor(TextWriter original)
{
_original = original;
}
public void Reset() => DetectedAlreadyExists = false;
public override void WriteLine(string value)
{
if (value?.Contains("Reaction already exists") == true)
DetectedAlreadyExists = true;
_original.WriteLine(value);
}
public override void Write(string value)
{
if (value?.Contains("Reaction already exists") == true)
DetectedAlreadyExists = true;
_original.Write(value);
}
}
};
};