Compare commits
1 Commits
06c088c4f8
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e125911d4 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,7 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
SkyBot.sln
|
||||||
.env
|
.env
|
||||||
.gitignore
|
Program.cs.old
|
||||||
SkyBot/bin/
|
|
||||||
SkyBot/obj/
|
|
||||||
SkyBot/SkyBot.sln
|
|
||||||
SkyBot/database.db
|
|
||||||
SkyBot/Config.cs
|
|
||||||
|
|||||||
134
PRIVACY.md
134
PRIVACY.md
@@ -1,92 +1,62 @@
|
|||||||
# Privacy Policy
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<body>
|
||||||
|
|
||||||
**Effective Date:** March 20, 2026
|
<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>
|
||||||
|
<hr>
|
||||||
|
|
||||||
This Privacy Policy describes how SkyBot ("the Bot") collects, uses, and stores information when used within a Valour planet.
|
<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>
|
||||||
|
<ol>
|
||||||
|
<li>Message IDs</li>
|
||||||
|
<li>Channel IDs</li>
|
||||||
|
<li>Server (“Planet”) IDs</li>
|
||||||
|
<li>Planet Configuration data associated with those channels</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
## 1. Information Collected
|
<h3>Information Not Stored:</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Message content</li>
|
||||||
|
<li>User-generated message content</li>
|
||||||
|
<li>Direct Messages (“DMs”)</li>
|
||||||
|
<li>Personal account information (including usernames, email addresses, or other personally identifiable information)</li>
|
||||||
|
</ol>
|
||||||
|
<hr>
|
||||||
|
|
||||||
The Bot collects only the minimum data necessary to provide its intended functionality. Most data is stored in-memory and is lost when the Bot restarts. A small amount of server configuration data is persisted to a local SQLite database for features that require it.
|
<h2>2. Purpose of Data Collection</h2>
|
||||||
|
<p>Stored 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>
|
||||||
|
</ol>
|
||||||
|
<p>The Bot does not use stored information for profiling, marketing, analytics, or tracking purposes.</p>
|
||||||
|
<hr>
|
||||||
|
|
||||||
### Information Temporarily Held in Memory
|
<h2>3. Data Storage and Security</h2>
|
||||||
|
<p>All stored data is maintained securely on the Bot’s 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>
|
||||||
|
<hr>
|
||||||
|
|
||||||
1. Channel IDs (for routing messages and commands)
|
<h2>4. Data Retention</h2>
|
||||||
2. Planet IDs (for planet-specific operations)
|
<p>Configuration data is retained only while the Bot remains active within a server.</p>
|
||||||
3. Member IDs (for moderation commands and game session tracking)
|
<p>If the Bot is removed from a server, associated configuration data may be deleted within a reasonable timeframe.</p>
|
||||||
4. Member display names (for game contributor lists in Hangman, Wordle, and Trivia)
|
<hr>
|
||||||
|
|
||||||
### Information Persisted to Disk
|
<h2>5. 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>
|
||||||
|
|
||||||
The following server configuration data is saved to a local SQLite database so that it survives restarts:
|
<h2>6. Contact Information</h2>
|
||||||
|
<p>For privacy-related inquiries, requests, or concerns, please contact:</p>
|
||||||
|
<p><span class="bold">Email:</span> contact@skyjoshua.xyz</p>
|
||||||
|
|
||||||
1. Planet IDs (to associate configuration with a planet)
|
</body>
|
||||||
2. Channel IDs (to remember the configured welcome channel)
|
</html>
|
||||||
3. Welcome message template (the text set by a moderator via `setwelcome message`)
|
|
||||||
4. Welcome system active state (enabled/disabled)
|
|
||||||
|
|
||||||
This data contains no personal user information. It is server configuration set by planet moderators and is stored locally on the host running the Bot.
|
|
||||||
|
|
||||||
### Information Never Stored
|
|
||||||
|
|
||||||
1. Message content
|
|
||||||
2. Direct Messages ("DMs")
|
|
||||||
3. Personal account information (including usernames, email addresses, or other personally identifiable information)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Purpose of Data Collection
|
|
||||||
|
|
||||||
Temporarily held information is used exclusively to:
|
|
||||||
|
|
||||||
1. Route commands to the correct channels and planets
|
|
||||||
2. Enable moderation commands such as ban, unban, and kick
|
|
||||||
3. Track active game sessions (Hangman, Wordle, Trivia) and display contributor lists
|
|
||||||
4. Enable core bot functionality during the current session
|
|
||||||
|
|
||||||
The Bot does not use any information for profiling, marketing, analytics, or tracking purposes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Data Storage and Security
|
|
||||||
|
|
||||||
Most data is stored in-memory only and is automatically cleared when the Bot restarts. The exception is server configuration for the welcome system (see Section 1), which is written to a local SQLite database on the host machine. No data is sent to any external storage or cloud service.
|
|
||||||
|
|
||||||
The Bot does not sell, rent, trade, or otherwise share any data with third parties.
|
|
||||||
|
|
||||||
Some features make outbound requests to third-party APIs to fetch content. These requests do not include any user data:
|
|
||||||
|
|
||||||
- **Datamuse** (datamuse.com) — word lists for Hangman and Wordle
|
|
||||||
- **Open Trivia Database** (opentdb.com) — trivia questions for Trivia
|
|
||||||
- **The Cat API** (thecatapi.com) — random cat images for the cat command
|
|
||||||
- **nekos.best** (nekos.best) — hug GIFs for the hug command
|
|
||||||
- **Pixabay** (pixabay.com) — images for the image command
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Data Retention
|
|
||||||
|
|
||||||
In-memory data (game sessions, moderation context, etc.) is held only for the duration of the Bot's current session and is cleared on restart. Server configuration data for the welcome system is retained in a local SQLite database until explicitly changed or deleted by a planet moderator.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Self-Hosting
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Future Changes to Logging or Data Practices
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Continued use of the Bot after updates to this policy constitutes acceptance of the revised policy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Contact Information
|
|
||||||
|
|
||||||
For privacy-related inquiries, requests, or concerns, please contact:
|
|
||||||
|
|
||||||
**Email:** contact@skyjoshua.xyz
|
|
||||||
|
|||||||
313
Program.cs
Normal file
313
Program.cs
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
using Valour.Sdk.Client;
|
||||||
|
using Valour.Sdk.Models;
|
||||||
|
using DotNetEnv;
|
||||||
|
using SkyBot;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
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 messageCache = new Dictionary<long, string>();
|
||||||
|
var InitializedPlanets = new HashSet<long>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
await Utils.InitializePlanetsAsync(client, channelCache, InitializedPlanets);
|
||||||
|
|
||||||
|
client.PlanetService.JoinedPlanetsUpdated += async () =>
|
||||||
|
{
|
||||||
|
await Utils.InitializePlanetsAsync(client, channelCache, InitializedPlanets);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.MessageService.MessageReceived += OnMessageReceived;
|
||||||
|
client.MessageService.MessageEdited += OnMessageEdit;
|
||||||
|
client.MessageService.MessageDeleted += OnMessageDelete;
|
||||||
|
|
||||||
|
async Task OnMessageDelete(Message message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var planet = await client.PlanetService.FetchPlanetAsync(message.PlanetId.Value);
|
||||||
|
var member = await planet.FetchMemberAsync(message.AuthorMemberId.Value);
|
||||||
|
Console.WriteLine($"{member.Name} deleted the message: {message.Content}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {ex.Message}");
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task OnMessageEdit(Message message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var planet = await client.PlanetService.FetchPlanetAsync(message.PlanetId.Value);
|
||||||
|
var member = await planet.FetchMemberAsync(message.AuthorMemberId.Value);
|
||||||
|
|
||||||
|
var before = messageCache.TryGetValue(message.Id, out var old) ? old : "(unknown)";
|
||||||
|
|
||||||
|
var after = message.Content;
|
||||||
|
|
||||||
|
Console.WriteLine($"{member.Name} edited a message.");
|
||||||
|
Console.WriteLine($"Before: {before}");
|
||||||
|
Console.WriteLine($"After: {after}");
|
||||||
|
|
||||||
|
messageCache[message.Id] = after;
|
||||||
|
|
||||||
|
} catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {ex.Message}");
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async Task OnMessageReceived(Message 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;
|
||||||
|
|
||||||
|
messageCache[message.Id] = message.Content;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
bool useRandom = args.Length < 2;
|
||||||
|
string emoji = !useRandom ? args[1] : null;
|
||||||
|
|
||||||
|
List<string> allEmojis = null;
|
||||||
|
if (useRandom)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var http = new HttpClient();
|
||||||
|
var json = await http.GetStringAsync("https://unpkg.com/unicode-emoji-json@0.0.4/data-ordered-emoji.json");
|
||||||
|
allEmojis = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to fetch emoji list: {ex.Message}");
|
||||||
|
await Utils.SendReplyAsync(channelCache, channelId, $"{pingMember} Failed to fetch emoji list.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var random = new Random();
|
||||||
|
var interceptor = new Utils.ReactionInterceptor(Console.Out);
|
||||||
|
Console.SetOut(interceptor);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
interceptor.Reset();
|
||||||
|
string currentEmoji = useRandom ? allEmojis[random.Next(allEmojis.Count)] : emoji;
|
||||||
|
await message.AddReactionAsync(currentEmoji);
|
||||||
|
if (interceptor.DetectedAlreadyExists)
|
||||||
|
{
|
||||||
|
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||||
|
Console.WriteLine("Stopping react spam.");
|
||||||
|
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);
|
||||||
195
README.md
195
README.md
@@ -1,129 +1,106 @@
|
|||||||
# SkyBot
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
|
||||||
SkyBot is a Valour.gg bot built with .NET 10.
|
<h1>SkyBot</h1>
|
||||||
|
|
||||||
---
|
<p>
|
||||||
|
SkyBot is a Valour.gg bot.
|
||||||
|
</p>
|
||||||
|
|
||||||
## Features
|
<h2>Features</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Designed for self-hosting</li>
|
||||||
|
<li>Open-source under AGPL-3.0</li>
|
||||||
|
<li>Built with .NET</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
- Designed for self-hosting
|
<h2>Data & Privacy</h2>
|
||||||
- Open-source under AGPL-3.0
|
<p>SkyBot stores only the minimum data required for operation:</p>
|
||||||
- Built with .NET 10
|
<ul>
|
||||||
- Command system with automatic registration
|
<li>Message IDs</li>
|
||||||
|
<li>Server (Planet) IDs</li>
|
||||||
|
<li>Channel IDs</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
### Fun
|
<p>SkyBot does <strong>not</strong> store:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Message content</li>
|
||||||
|
<li>Direct messages</li>
|
||||||
|
<li>Personal user data</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
- `8ball` — ask the magic 8 ball a question
|
<p>
|
||||||
- `coinflip` — flip a coin
|
Full privacy policy:<br>
|
||||||
- `dice` — roll a die
|
<a href="https://github.com/SkyJoshua/SkyBot/blob/main/PRIVACY.md">
|
||||||
- `rockpaperscissors` — play rock paper scissors against the bot
|
https://github.com/SkyJoshua/SkyBot/blob/main/PRIVACY.md
|
||||||
- `choose` — pick one of the given options
|
</a>
|
||||||
- `echo` — repeat text through the bot
|
</p>
|
||||||
- `reverse` — reverse yours or a replied message
|
|
||||||
- `mock` — mOcK tExT
|
|
||||||
- `t9encode` / `t9decode` — encode or decode old phone keypad multi-tap digits
|
|
||||||
- `hangman` — channel-wide game of hangman with optional category (`hg <letter or word>` to guess)
|
|
||||||
- `wordle` — channel-wide Wordle; guess the 5-letter word in 6 tries (`wg <word>` to guess)
|
|
||||||
- `trivia` — channel-wide trivia question with 30 seconds to answer (`tg <A/B/C/D>` to guess)
|
|
||||||
- `image` — fetch a random image matching your search (aliases: `img`)
|
|
||||||
|
|
||||||
### Chill
|
<h2>License</h2>
|
||||||
|
<p>
|
||||||
|
This project is licensed under the
|
||||||
|
<strong>GNU Affero General Public License v3.0 (AGPL-3.0)</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
- `cat` — post a random cat picture
|
<p>
|
||||||
- `hug` — send a hug with a random gif
|
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>
|
||||||
|
|
||||||
### Info
|
<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>
|
||||||
|
|
||||||
- `ping` — check bot latency
|
<h2>Installation</h2>
|
||||||
- `uptime` — show how long the bot has been running
|
Fork this Repository
|
||||||
- `info` — user and planet info
|
<pre><code>git clone https://github.com/YOUR_USERNAME/SkyBot.git
|
||||||
- `version` — show the current bot and Valour SDK version
|
cd SkyBot
|
||||||
- `usercount` — show the total Valour user count
|
|
||||||
- `source` — link to the bot's source code
|
|
||||||
- `joinsite` — link to a site to help bots join a planet
|
|
||||||
- `devcentral` — invite link to the Dev Central planet
|
|
||||||
- `swagger` — link to the Valour API docs
|
|
||||||
- `minecraft` — Unofficial ValourSMP server IPs
|
|
||||||
- `suggest` — submit a suggestion for the bot
|
|
||||||
|
|
||||||
### Moderation
|
|
||||||
|
|
||||||
- `ban` / `unban` / `kick` — member moderation
|
|
||||||
- `bans` — list all bans in the planet
|
|
||||||
- `setwelcome` — configure a welcome channel and message
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data & Privacy
|
|
||||||
|
|
||||||
SkyBot stores only the minimum data required for operation. Most data is stored in-memory and is lost on restart. A small amount of server configuration data is persisted to a local SQLite database for the welcome system.
|
|
||||||
|
|
||||||
SkyBot does **not** store:
|
|
||||||
|
|
||||||
- Message content
|
|
||||||
- Direct messages
|
|
||||||
- Personal user data
|
|
||||||
|
|
||||||
Full privacy policy:
|
|
||||||
https://git.skyjoshua.xyz/SkyJoshua/SkyBot/blob/main/PRIVACY.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**.
|
|
||||||
|
|
||||||
See the LICENSE file for details:
|
|
||||||
https://git.skyjoshua.xyz/SkyJoshua/SkyBot/blob/main/LICENSE
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- .NET 10
|
|
||||||
- A Valour bot token
|
|
||||||
- A [Pixabay API key](https://pixabay.com/api/docs/) (free) — required for the `image` command
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.skyjoshua.xyz/SkyJoshua/SkyBot.git
|
|
||||||
cd SkyBot/SkyBot
|
|
||||||
dotnet restore
|
dotnet restore
|
||||||
```
|
</code></pre>
|
||||||
|
|
||||||
All required NuGet packages will be installed automatically using the provided `SkyBot.csproj` file.
|
<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>
|
||||||
|
|
||||||
## Configuration
|
<pre><code>TOKEN=your-bot-token-here
|
||||||
|
PREFIX=your-prefix-here
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
Create a `.env` file in the root directory of the project:
|
<ul>
|
||||||
```
|
<li>Replace <code>your-bot-token-here</code> with your actual bot token.</li>
|
||||||
TOKEN=your-bot-token-here
|
<li>Ensure the bot has proper permissions in the target server.</li>
|
||||||
PIXABAY_API_KEY=your-pixabay-api-key-here
|
</ul>
|
||||||
```
|
|
||||||
|
|
||||||
Then open `Config.cs` and update the following values:
|
<p>
|
||||||
```cs
|
Sensitive data such as bot tokens should never be committed to the repository.
|
||||||
public static readonly long OwnerId = your-owner-id-here;
|
Use environment variables or secure configuration methods.
|
||||||
public static readonly string Prefix = "your-prefix-here";
|
</p>
|
||||||
public static readonly string SourceLink = "your-source-link-here";
|
|
||||||
```
|
|
||||||
|
|
||||||
- Replace `your-owner-id-here` with your Valour user ID.
|
<h2>Running the Bot</h2>
|
||||||
- Replace `your-prefix-here` with your desired command prefix (e.g. `s/`).
|
|
||||||
- Replace `your-source-link-here` with a link to your fork of the repository.
|
|
||||||
|
|
||||||
Never commit your `.env` file to the repository. Ensure it is listed in your `.gitignore`.
|
<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>
|
||||||
|
|
||||||
## Running the Bot
|
<ol>
|
||||||
```bash
|
<li>Fork the repository</li>
|
||||||
dotnet run
|
<li>Create a feature branch</li>
|
||||||
```
|
<li>Submit a pull request</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -5,12 +5,10 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.2.2.1</Version>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
|
|
||||||
<PackageReference Include="Valour.Sdk" Version="0.5.19" />
|
<PackageReference Include="Valour.Sdk" Version="0.5.19" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Cat : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "cat";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Posts a random cat picture.";
|
|
||||||
public string Section => "Chill";
|
|
||||||
public string Usage => "cat";
|
|
||||||
|
|
||||||
private static readonly HttpClient _http = new();
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
|
|
||||||
// Fetch a random cat from TheCatAPI
|
|
||||||
string json;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
json = await _http.GetStringAsync("https://api.thecatapi.com/v1/images/search");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "😿 Could not fetch a cat image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var root = doc.RootElement[0];
|
|
||||||
|
|
||||||
string catUrl = root.GetProperty("url").GetString()!;
|
|
||||||
int width = root.TryGetProperty("width", out var w) ? w.GetInt32() : 0;
|
|
||||||
int height = root.TryGetProperty("height", out var h) ? h.GetInt32() : 0;
|
|
||||||
|
|
||||||
string ext = Path.GetExtension(catUrl.Split('?')[0]).ToLowerInvariant();
|
|
||||||
string mime = ext == ".png" ? "image/png"
|
|
||||||
: ext == ".gif" ? "image/gif"
|
|
||||||
: "image/jpeg";
|
|
||||||
string fileName = $"cat{ext}";
|
|
||||||
|
|
||||||
// Download the image bytes
|
|
||||||
byte[] imageBytes;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
imageBytes = await _http.GetByteArrayAsync(catUrl);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "😿 Could not download the cat image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload to Valour CDN so the server can scan/serve it
|
|
||||||
string cdnUrl;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var form = new MultipartFormDataContent();
|
|
||||||
using var fileContent = new ByteArrayContent(imageBytes);
|
|
||||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mime);
|
|
||||||
form.Add(fileContent, "file", fileName);
|
|
||||||
|
|
||||||
var uploadResult = await ctx.Planet.Node.PostMultipartDataWithResponse<string>("upload/image", form);
|
|
||||||
if (!uploadResult.Success)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "😿 Could not upload the cat image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cdnUrl = uploadResult.Data!;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "😿 Could not upload the cat image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attachment = new MessageAttachment(MessageAttachmentType.Image)
|
|
||||||
{
|
|
||||||
Location = cdnUrl,
|
|
||||||
MimeType = mime,
|
|
||||||
FileName = fileName,
|
|
||||||
Width = width,
|
|
||||||
Height = height
|
|
||||||
};
|
|
||||||
|
|
||||||
await channel.SendMessageAsync("",attachments: [attachment]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Hug : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "hug";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Send a hug with a random gif.";
|
|
||||||
public string Section => "Chill";
|
|
||||||
public string Usage => "hug [@user]";
|
|
||||||
|
|
||||||
private static readonly HttpClient _http = new()
|
|
||||||
{
|
|
||||||
DefaultRequestHeaders = { { "User-Agent", "SkyBot/1.0" } }
|
|
||||||
};
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
|
|
||||||
// Fetch a random hug gif from nekos.best
|
|
||||||
string json;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
json = await _http.GetStringAsync("https://nekos.best/api/v2/hug");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not fetch a hug gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var results = doc.RootElement.GetProperty("results");
|
|
||||||
string gifUrl = results[0].GetProperty("url").GetString()!;
|
|
||||||
|
|
||||||
// Download the gif bytes
|
|
||||||
byte[] gifBytes;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
gifBytes = await _http.GetByteArrayAsync(gifUrl);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not download the hug gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read GIF dimensions from header (bytes 6-9, little-endian)
|
|
||||||
int width = 0, height = 0;
|
|
||||||
if (gifBytes.Length >= 10)
|
|
||||||
{
|
|
||||||
width = gifBytes[6] | (gifBytes[7] << 8);
|
|
||||||
height = gifBytes[8] | (gifBytes[9] << 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload to Valour CDN
|
|
||||||
string cdnUrl;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var form = new MultipartFormDataContent();
|
|
||||||
using var fileContent = new ByteArrayContent(gifBytes);
|
|
||||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/gif");
|
|
||||||
form.Add(fileContent, "file", "hug.gif");
|
|
||||||
|
|
||||||
var uploadResult = await ctx.Planet.Node.PostMultipartDataWithResponse<string>("upload/image", form);
|
|
||||||
if (!uploadResult.Success)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not upload the hug gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cdnUrl = uploadResult.Data!;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not upload the hug gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the hug target: replied-to message author > args > nobody
|
|
||||||
string? target = null;
|
|
||||||
if (ctx.Message.ReplyToId is not null)
|
|
||||||
{
|
|
||||||
var replied = await ctx.Message.FetchReplyMessageAsync();
|
|
||||||
if (replied is not null)
|
|
||||||
{
|
|
||||||
var author = await replied.FetchAuthorAsync();
|
|
||||||
if (author is not null)
|
|
||||||
target = author.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target is null && ctx.Args.Length > 0)
|
|
||||||
target = string.Join(" ", ctx.Args);
|
|
||||||
|
|
||||||
string sender = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown";
|
|
||||||
string text = target is not null
|
|
||||||
? $"{sender} hugs {target}! 🤗"
|
|
||||||
: $"{sender} wants a hug! 🤗";
|
|
||||||
|
|
||||||
var attachment = new MessageAttachment(MessageAttachmentType.Image)
|
|
||||||
{
|
|
||||||
Location = cdnUrl,
|
|
||||||
MimeType = "image/gif",
|
|
||||||
FileName = "hug.gif",
|
|
||||||
Width = width,
|
|
||||||
Height = height
|
|
||||||
};
|
|
||||||
|
|
||||||
await channel.SendMessageAsync(text, attachments: [attachment]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Pat : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "pat";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Give someone headpats with a random gif.";
|
|
||||||
public string Section => "Chill";
|
|
||||||
public string Usage => "pat [@user]";
|
|
||||||
|
|
||||||
private static readonly HttpClient _http = new()
|
|
||||||
{
|
|
||||||
DefaultRequestHeaders = { { "User-Agent", "SkyBot/1.0" } }
|
|
||||||
};
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
|
|
||||||
// Fetch a random pat gif from nekos.best
|
|
||||||
string json;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
json = await _http.GetStringAsync("https://nekos.best/api/v2/pat");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not fetch a pat gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var results = doc.RootElement.GetProperty("results");
|
|
||||||
string gifUrl = results[0].GetProperty("url").GetString()!;
|
|
||||||
|
|
||||||
// Download the gif bytes
|
|
||||||
byte[] gifBytes;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
gifBytes = await _http.GetByteArrayAsync(gifUrl);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not download the pat gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read GIF dimensions from header (bytes 6-9, little-endian)
|
|
||||||
int width = 0, height = 0;
|
|
||||||
if (gifBytes.Length >= 10)
|
|
||||||
{
|
|
||||||
width = gifBytes[6] | (gifBytes[7] << 8);
|
|
||||||
height = gifBytes[8] | (gifBytes[9] << 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload to Valour CDN
|
|
||||||
string cdnUrl;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var form = new MultipartFormDataContent();
|
|
||||||
using var fileContent = new ByteArrayContent(gifBytes);
|
|
||||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/gif");
|
|
||||||
form.Add(fileContent, "file", "pat.gif");
|
|
||||||
|
|
||||||
var uploadResult = await ctx.Planet.Node.PostMultipartDataWithResponse<string>("upload/image", form);
|
|
||||||
if (!uploadResult.Success)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not upload the pat gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cdnUrl = uploadResult.Data!;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not upload the pat gif. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the pat target: replied-to message author > args > nobody
|
|
||||||
string? target = null;
|
|
||||||
if (ctx.Message.ReplyToId is not null)
|
|
||||||
{
|
|
||||||
var replied = await ctx.Message.FetchReplyMessageAsync();
|
|
||||||
if (replied is not null)
|
|
||||||
{
|
|
||||||
var author = await replied.FetchAuthorAsync();
|
|
||||||
if (author is not null)
|
|
||||||
target = author.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target is null && ctx.Args.Length > 0)
|
|
||||||
target = string.Join(" ", ctx.Args);
|
|
||||||
|
|
||||||
string sender = ctx.Member.Nickname ?? ctx.Member.User?.Name ?? "Unknown";
|
|
||||||
string text = target is not null
|
|
||||||
? $"{sender} gives {target} headpats! 🥰"
|
|
||||||
: $"{sender} wants headpats! 🥰";
|
|
||||||
|
|
||||||
var attachment = new MessageAttachment(MessageAttachmentType.Image)
|
|
||||||
{
|
|
||||||
Location = cdnUrl,
|
|
||||||
MimeType = "image/gif",
|
|
||||||
FileName = "pat.gif",
|
|
||||||
Width = width,
|
|
||||||
Height = height
|
|
||||||
};
|
|
||||||
|
|
||||||
await channel.SendMessageAsync(text, attachments: [attachment]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using SkyBot.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public static class CommandRegistry
|
|
||||||
{
|
|
||||||
public static readonly Dictionary<string, ICommand> Commands = new();
|
|
||||||
public static readonly Dictionary<string, List<ICommand>> Sections = new();
|
|
||||||
|
|
||||||
static CommandRegistry()
|
|
||||||
{
|
|
||||||
var allCommands = AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.SelectMany(a => a.GetTypes())
|
|
||||||
.Where(t => typeof(ICommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
|
|
||||||
.Select(t => (ICommand?)Activator.CreateInstance(t))
|
|
||||||
.Select(c => c!);
|
|
||||||
|
|
||||||
foreach (var cmd in allCommands)
|
|
||||||
{
|
|
||||||
Commands[cmd.Name.ToLower()] = cmd;
|
|
||||||
foreach (var alias in cmd.Aliases)
|
|
||||||
{
|
|
||||||
Commands[alias.ToLower()] = cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sections = Commands.Values
|
|
||||||
.Distinct()
|
|
||||||
.GroupBy(c => c.Section.ToLower())
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class CommandTemplate : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "template";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "";
|
|
||||||
public string Section => "template";
|
|
||||||
public string Usage => "";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ValourClient Client = ctx.Client;
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
PlanetMember Member = ctx.Member;
|
|
||||||
Message Message = ctx.Message;
|
|
||||||
Planet Planet = ctx.Planet;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] Args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Delete : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "delete";
|
|
||||||
public string[] Aliases => ["del"];
|
|
||||||
public string Description => "Delete a bot message";
|
|
||||||
public string Section => "Dev";
|
|
||||||
public string Usage => "reply -> delete";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
ValourClient client = ctx.Client;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if (!PermissionHelper.IsOwner(member))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "This is a Dev only command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.ReplyToId == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please reply to a message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.Cache.Messages.TryGet(message.ReplyToId.Value, out var msg))
|
|
||||||
{
|
|
||||||
await msg.DeleteAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Edit : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "edit";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Edit the bots message";
|
|
||||||
public string Section => "Dev";
|
|
||||||
public string Usage => "reply -> edit <message>";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
ValourClient client = ctx.Client;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if(!PermissionHelper.IsOwner(member))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "This is a Dev only command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.ReplyToId == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please reply to a message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.Cache.Messages.TryGet(message.ReplyToId.Value, out var msg))
|
|
||||||
{
|
|
||||||
await MessageHelper.EditAsync(channel, msg, string.Join(" ", args));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class React : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "react";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Send a message with a reaction at a set count.";
|
|
||||||
public string Section => "Dev";
|
|
||||||
public string Usage => "react <emoji> <amount> <message>";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (!PermissionHelper.IsOwner(ctx.Member))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "This is a Dev only command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (args.Length < 3)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Usage: `{Config.Prefix}react <emoji> <amount> <message>`");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string emoji = args[0];
|
|
||||||
|
|
||||||
if (!int.TryParse(args[1], out int amount) || amount < 1)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Amount must be a number between 1 and 100.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string content = string.Join(" ", args.Skip(2));
|
|
||||||
|
|
||||||
var reactions = Enumerable.Range(0, amount)
|
|
||||||
.Select(_ => new MessageReaction
|
|
||||||
{
|
|
||||||
Emoji = emoji,
|
|
||||||
AuthorUserId = ctx.Client.Me.Id,
|
|
||||||
AuthorMemberId = ctx.Planet.MyMember?.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var msg = new Message(ctx.Client)
|
|
||||||
{
|
|
||||||
Content = content,
|
|
||||||
ChannelId = channelId,
|
|
||||||
PlanetId = ctx.Planet.Id,
|
|
||||||
AuthorUserId = ctx.Client.Me.Id,
|
|
||||||
AuthorMemberId = ctx.Planet.MyMember?.Id,
|
|
||||||
Reactions = reactions,
|
|
||||||
Fingerprint = Guid.NewGuid().ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
await ctx.Client.MessageService.SendMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class EightBall : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "8ball";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Ask the magic 8ball a question.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "8ball <question>";
|
|
||||||
|
|
||||||
private static readonly string[] Responses =
|
|
||||||
[
|
|
||||||
"It is certain.",
|
|
||||||
"It is decidedly so.",
|
|
||||||
"Without a doubt.",
|
|
||||||
"Yes, definitely.",
|
|
||||||
"You may rely on it.",
|
|
||||||
"As I see it, yes.",
|
|
||||||
"Most likely.",
|
|
||||||
"Outlook good.",
|
|
||||||
"Yes.",
|
|
||||||
"Signs point to yes.",
|
|
||||||
"Reply hazy, try again.",
|
|
||||||
"Ask again later.",
|
|
||||||
"Better not tell you now.",
|
|
||||||
"Cannot predict now.",
|
|
||||||
"Concentrate and ask again.",
|
|
||||||
"Don't count on it.",
|
|
||||||
"My reply is no.",
|
|
||||||
"My sources say no.",
|
|
||||||
"Outlook not so good.",
|
|
||||||
"Very doubtful."
|
|
||||||
];
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please ask a question.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, $"🎱 Thinking...");
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
await Task.Delay(2000);
|
|
||||||
string response = Responses[Random.Shared.Next(Responses.Length)];
|
|
||||||
await MessageHelper.EditAsync(channel, result.Data, $"🎱 {response}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Choose : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "choose";
|
|
||||||
public string[] Aliases => ["pick"];
|
|
||||||
public string Description => "Picks one of the given options.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "choose <option1> <option2> ...";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (ctx.Args.Length < 2)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide at least two options.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, "🤔 Choosing...");
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
string choice = args[Random.Shared.Next(args.Length)];
|
|
||||||
await MessageHelper.EditAsync(channel, result.Data, $"I choose **{choice}**!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class CoinFlip : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "coinflip";
|
|
||||||
public string[] Aliases => ["cf"];
|
|
||||||
public string Description => "Flips a coin.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "coinflip";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, "🪙 Flipping...");
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
await Task.Delay(3000);
|
|
||||||
|
|
||||||
string outcome = Random.Shared.Next(2) == 0 ? "Heads" : "Tails";
|
|
||||||
await MessageHelper.EditAsync(channel, result.Data, $"🪙 {outcome}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Runtime.InteropServices.Marshalling;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Dice : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "dice";
|
|
||||||
public string[] Aliases => ["roll"];
|
|
||||||
public string Description => "Rolls dice.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "roll <dice> (e.g. 2d6, d20, 3d8)";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string input = args.Length > 0 ? args[0].ToLower() : "1d6";
|
|
||||||
|
|
||||||
string[] parts = input.Split('d');
|
|
||||||
if (parts.Length != 2 || !int.TryParse(parts[1], out int sides) || sides < 2)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid dice format. Use something like `2d6` or `d20`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = 1;
|
|
||||||
if (!string.IsNullOrWhiteSpace(parts[0]) && !int.TryParse(parts[0], out count))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid dice format. Use something like `2d6` or `d20`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count < 1 || count > 100)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "You can only roll between 1 and 100 dice at a time.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<int> rolls = Enumerable.Range(0, count).Select(_ => Random.Shared.Next(1, sides+1)).ToList();
|
|
||||||
int total = rolls.Sum();
|
|
||||||
|
|
||||||
TaskResult<Message> rolling = await MessageHelper.ReplyAsync(ctx, channel, "🎲 Rolling...");
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
await Task.Delay(2000);
|
|
||||||
|
|
||||||
string rollDisplay = count > 1 ? $"({string.Join(" + ", rolls)}) = **{total}**" : $"**{total}**";
|
|
||||||
await MessageHelper.EditAsync(channel, rolling.Data, $"🎲 Rolled {input}: {rollDisplay}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Echo : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "echo";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Echos what you said through the bot.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "echo <message>";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
|
|
||||||
string reply = string.Join(" ", args);
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
if (string.IsNullOrWhiteSpace(reply)) await MessageHelper.ReplyAsync(ctx, channel, $"Enter a message to echo.");
|
|
||||||
|
|
||||||
reply = $"{member.Name} » {reply}";
|
|
||||||
if (reply.Length > 2048)
|
|
||||||
{
|
|
||||||
reply = reply.Substring(0, 2048);
|
|
||||||
}
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, reply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Hangman : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "hangman";
|
|
||||||
public string[] Aliases => ["hm"];
|
|
||||||
public string Description => "Starts a channel-wide game of hangman. Optionally specify a category.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "hangman [category] | hangman end";
|
|
||||||
|
|
||||||
private record HangmanSession(
|
|
||||||
string Word,
|
|
||||||
string? Category,
|
|
||||||
HashSet<char> Guessed,
|
|
||||||
HashSet<char> Wrong,
|
|
||||||
HashSet<string> Contributors,
|
|
||||||
long StarterId,
|
|
||||||
Message BotMessage,
|
|
||||||
Channel Channel,
|
|
||||||
ValourClient Client);
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<long, HangmanSession> _sessions = new();
|
|
||||||
private static readonly HttpClient _http = new();
|
|
||||||
|
|
||||||
private const int MaxWrong = 6;
|
|
||||||
|
|
||||||
private static readonly string[] Topics =
|
|
||||||
[
|
|
||||||
"animals", "food", "sports", "music", "science", "geography",
|
|
||||||
"movies", "nature", "technology", "history", "mythology", "space",
|
|
||||||
"ocean", "weather", "games", "art", "clothing", "vehicles",
|
|
||||||
];
|
|
||||||
|
|
||||||
private static readonly string[] FallbackWords =
|
|
||||||
[
|
|
||||||
"APPLE", "BRIDGE", "CASTLE", "DRAGON", "ELEPHANT", "FOREST", "GUITAR",
|
|
||||||
"HARBOR", "ISLAND", "JUNGLE", "KNIGHT", "LEMON", "MANGO", "OCEAN",
|
|
||||||
"PLANET", "ROBOT", "SNAKE", "TIGER", "UMBRELLA", "WIZARD", "ANCHOR",
|
|
||||||
"BUTTER", "CANDLE", "DONKEY", "ENGINE", "FALCON", "GOBLIN", "HAMMER",
|
|
||||||
"IGLOO", "JACKET", "KITTEN", "LADDER", "MIRROR", "NEEDLE", "ORANGE",
|
|
||||||
"PENCIL", "RABBIT", "SILVER", "TEMPLE", "TURTLE", "VALLEY", "WALRUS",
|
|
||||||
"ZIPPER", "BLANKET", "CACTUS", "DAISY", "GLOVES", "HOCKEY", "INSECT",
|
|
||||||
"JELLY", "KETTLE", "LOBSTER", "MARBLE", "NAPKIN", "OYSTER", "PEPPER",
|
|
||||||
"QUARTZ", "ROCKET", "SALMON", "THRONE", "VELVET", "WINDOW", "YOGURT",
|
|
||||||
"ZOMBIE", "ALMOND", "BISON", "COBRA", "DAGGER", "EMBER", "FROST",
|
|
||||||
"GHOST", "HONEY", "IVORY", "JEWEL", "KOALA", "MAPLE", "NINJA",
|
|
||||||
"OLIVE", "PIRATE", "RAVEN", "SPHINX", "TORNADO", "UNICORN", "VENOM",
|
|
||||||
"WITCH", "PIXEL", "STORM", "CLOUD", "FLAME", "COMET", "DUSK",
|
|
||||||
"ECHO", "FABLE", "GLYPH", "HAZE", "JINX", "KNACK", "LUNAR",
|
|
||||||
"MYTH", "NEON", "ORBIT", "PRISM", "QUEST", "RIDGE", "SHARD",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 7 stages: 0 wrong → 6 wrong (using +--+ to avoid markdown eating underscores)
|
|
||||||
private static readonly string[] Stages =
|
|
||||||
[
|
|
||||||
"```\n +--------+\n | |\n | \n | \n | \n | \n==+==\n```",
|
|
||||||
"```\n +--------+\n | |\n | O\n | \n | \n | \n==+==\n```",
|
|
||||||
"```\n +--------+\n | |\n | O\n | |\n | \n | \n==+==\n```",
|
|
||||||
"```\n +--------+\n | |\n | O\n | /|\n | \n | \n==+==\n```",
|
|
||||||
"```\n +--------+\n | |\n | O\n | /|\\\n | \n | \n==+==\n```",
|
|
||||||
"```\n +--------+\n | |\n | O\n | /|\\\n | / \n | \n==+==\n```",
|
|
||||||
"```\n +--------+\n | |\n | O\n | /|\\\n | / \\\n | \n==+==\n```",
|
|
||||||
];
|
|
||||||
|
|
||||||
private static async Task DeleteBotMessageAsync(CommandContext ctx, Message botMessage)
|
|
||||||
{
|
|
||||||
if (ctx.Client.Cache.Messages.TryGet(botMessage.Id, out var cached) && cached is not null)
|
|
||||||
try { await cached.DeleteAsync(); } catch { }
|
|
||||||
else
|
|
||||||
try { await botMessage.DeleteAsync(); } catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<Message?> RepostAsync(CommandContext ctx, Channel channel, Message botMessage, string content)
|
|
||||||
{
|
|
||||||
await DeleteBotMessageAsync(ctx, botMessage);
|
|
||||||
var result = await MessageHelper.ReplyAsync(ctx, channel, content);
|
|
||||||
if (!result.Success || result.Data is null) return null;
|
|
||||||
return ctx.Client.Cache.Messages.TryGet(result.Data.Id, out var cached) ? cached : result.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> FetchWord(string topic)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string url = $"https://api.datamuse.com/words?ml={Uri.EscapeDataString(topic)}&topic={Uri.EscapeDataString(topic)}&max=500";
|
|
||||||
string json = await _http.GetStringAsync(url);
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var words = doc.RootElement.EnumerateArray()
|
|
||||||
.Select(e => e.GetProperty("word").GetString() ?? "")
|
|
||||||
.Where(w => w.Length >= 4 && w.Length <= 12 && w.All(char.IsLetter))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (words.Count > 0)
|
|
||||||
return words[Random.Shared.Next(words.Count)].ToUpper();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return FallbackWords[Random.Shared.Next(FallbackWords.Length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildDisplay(string word, string? category, HashSet<char> guessed, HashSet<char> wrong)
|
|
||||||
{
|
|
||||||
string wordDisplay = string.Join(" ", word.Select(c => guessed.Contains(c) ? c.ToString() : "_"));
|
|
||||||
string wrongLetters = wrong.Count > 0 ? string.Join(", ", wrong.OrderBy(c => c)) : "none";
|
|
||||||
string categoryLine = category is not null ? $"📂 **Category**: {category.ToTitleCase()}\n" : "";
|
|
||||||
|
|
||||||
return string.Join("\n",
|
|
||||||
$"🎮 **HANGMAN**",
|
|
||||||
categoryLine,
|
|
||||||
$"`{wordDisplay}`",
|
|
||||||
"",
|
|
||||||
Stages[wrong.Count],
|
|
||||||
"",
|
|
||||||
$"❌ Wrong ({wrong.Count}/{MaxWrong}): {wrongLetters}",
|
|
||||||
"",
|
|
||||||
$"*Use `{Config.Prefix}hg <letter or word>` to guess!*"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
// sd/hangman end — end the current game
|
|
||||||
if (args.Length >= 1 && args[0].ToLower() == "end")
|
|
||||||
{
|
|
||||||
if (!_sessions.TryGetValue(channelId, out var session))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active hangman game in this channel.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isStarter = member.UserId == session.StarterId;
|
|
||||||
bool isMod = await PermissionHelper.HasPermAsync(member, channel, [ChatChannelPermissions.ManageMessages]);
|
|
||||||
|
|
||||||
if (!isStarter && !isMod)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Only the person who started the game (or a moderator) can end it.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
await DeleteBotMessageAsync(ctx, session.BotMessage);
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"🛑 Hangman ended by {member.Name}. The word was `{session.Word}`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sd/hangman [category] — start a new game
|
|
||||||
if (_sessions.ContainsKey(channelId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's already an active hangman game in this channel!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? category = args.Length >= 1
|
|
||||||
? args[0].ToLower()
|
|
||||||
: Topics[Random.Shared.Next(Topics.Length)];
|
|
||||||
|
|
||||||
string word = await FetchWord(category);
|
|
||||||
|
|
||||||
var guessed = new HashSet<char>();
|
|
||||||
var wrong = new HashSet<char>();
|
|
||||||
var contributors = new HashSet<string>();
|
|
||||||
|
|
||||||
string display = BuildDisplay(word, category, guessed, wrong);
|
|
||||||
var sent = await MessageHelper.ReplyAsync(ctx, channel, display);
|
|
||||||
if (!sent.Success || sent.Data is null) return;
|
|
||||||
|
|
||||||
_sessions[channelId] = new HangmanSession(word, category, guessed, wrong, contributors, member.UserId, sent.Data, channel, ctx.Client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ProcessGuessAsync(CommandContext ctx, Channel channel, string rawGuess)
|
|
||||||
{
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!_sessions.TryGetValue(channelId, out var session))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active hangman game in this channel.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string guess = rawGuess.ToUpper();
|
|
||||||
string memberName = ctx.Member.Name ?? "Unknown";
|
|
||||||
|
|
||||||
// Full word guess
|
|
||||||
if (guess.Length > 1)
|
|
||||||
{
|
|
||||||
if (guess == session.Word)
|
|
||||||
{
|
|
||||||
foreach (char c in session.Word) session.Guessed.Add(c);
|
|
||||||
session.Contributors.Add(memberName);
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
|
|
||||||
string contributorList = string.Join(", ", session.Contributors);
|
|
||||||
await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
|
||||||
+ $"\n\n🎉 **{memberName} guessed the word! The word was `{session.Word}`!**\nContributors: {contributorList}");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"🎉 **{memberName} guessed the word!** The word was `{session.Word}`!");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
session.Wrong.Add(guess[0]);
|
|
||||||
|
|
||||||
if (session.Wrong.Count >= MaxWrong)
|
|
||||||
{
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
|
||||||
+ $"\n\n💀 **Game over!** The word was `{session.Word}`.");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"💀 **Game Over!** Out of guesses. The word was `{session.Word}`");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newMsg = await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong));
|
|
||||||
if (newMsg is not null)
|
|
||||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"❌ `{guess}` is not the word!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single letter guess
|
|
||||||
char letter = guess[0];
|
|
||||||
|
|
||||||
if (!char.IsLetter(letter))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please guess a letter or a full word.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Guessed.Contains(letter) || session.Wrong.Contains(letter))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"`{letter}` has already been guessed!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Word.Contains(letter))
|
|
||||||
{
|
|
||||||
session.Guessed.Add(letter);
|
|
||||||
session.Contributors.Add(memberName);
|
|
||||||
|
|
||||||
bool won = session.Word.All(c => session.Guessed.Contains(c));
|
|
||||||
if (won)
|
|
||||||
{
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
string contributorList = string.Join(", ", session.Contributors);
|
|
||||||
await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
|
||||||
+ $"\n\n🎉 **The channel wins! The word was `{session.Word}`!**\nContributors: {contributorList}");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"🎉 **The channel wins!** The word was `{session.Word}`!");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newMsg = await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong));
|
|
||||||
if (newMsg is not null)
|
|
||||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"✅ `{letter}` is in the word!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
session.Wrong.Add(letter);
|
|
||||||
|
|
||||||
if (session.Wrong.Count >= MaxWrong)
|
|
||||||
{
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong)
|
|
||||||
+ $"\n\n💀 **Game over!** The word was `{session.Word}`.");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"💀 **Game over!** The word was `{session.Word}`.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newMsg = await RepostAsync(ctx, channel, session.BotMessage,
|
|
||||||
BuildDisplay(session.Word, session.Category, session.Guessed, session.Wrong));
|
|
||||||
if (newMsg is not null)
|
|
||||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"❌ No `{letter}` in the word!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HangmanGuess : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "hg";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Guess a letter or word in the active Hangman game.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "hg <letter or word>";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
if (!ctx.ChannelCache.TryGetValue(ctx.ChannelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (ctx.Args.Length == 0 || string.IsNullOrWhiteSpace(ctx.Args[0]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a letter or word to guess.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Hangman.ProcessGuessAsync(ctx, channel, ctx.Args[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Image : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "image";
|
|
||||||
public string[] Aliases => ["img"];
|
|
||||||
public string Description => "Fetches a random image matching your search.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "image <query>";
|
|
||||||
|
|
||||||
private static readonly HttpClient _http = new();
|
|
||||||
private static readonly Random _rng = new();
|
|
||||||
|
|
||||||
private record ImageResult(string Url, int Width, int Height, string Mime);
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Usage: `{Config.Prefix}image <query>`");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string query = string.Join(" ", args);
|
|
||||||
await channel.SendIsTyping();
|
|
||||||
|
|
||||||
var result = await FetchPixabayAsync(query) ?? await FetchWikimediaAsync(query);
|
|
||||||
|
|
||||||
if (result is null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"🔍 No images found for **{query}**.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string ext = Path.GetExtension(result.Url.Split('?')[0]).ToLowerInvariant();
|
|
||||||
string fileName = $"image{(string.IsNullOrEmpty(ext) ? ".jpg" : ext)}";
|
|
||||||
|
|
||||||
byte[] imageBytes;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
imageBytes = await _http.GetByteArrayAsync(result.Url);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "🖼️ Could not download the image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string cdnUrl;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var form = new MultipartFormDataContent();
|
|
||||||
using var fileContent = new ByteArrayContent(imageBytes);
|
|
||||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(result.Mime);
|
|
||||||
form.Add(fileContent, "file", fileName);
|
|
||||||
|
|
||||||
var uploadResult = await ctx.Planet.Node.PostMultipartDataWithResponse<string>("upload/image", form);
|
|
||||||
if (!uploadResult.Success)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "🖼️ Could not upload the image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cdnUrl = uploadResult.Data;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "🖼️ Could not upload the image. Try again later.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attachment = new MessageAttachment(MessageAttachmentType.Image)
|
|
||||||
{
|
|
||||||
Location = cdnUrl,
|
|
||||||
MimeType = result.Mime,
|
|
||||||
FileName = fileName,
|
|
||||||
Width = result.Width,
|
|
||||||
Height = result.Height
|
|
||||||
};
|
|
||||||
|
|
||||||
await channel.SendMessageAsync($"🖼️ **{query}**", attachments: [attachment]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<ImageResult?> FetchPixabayAsync(string query)
|
|
||||||
{
|
|
||||||
string? key = Environment.GetEnvironmentVariable("PIXABAY_API_KEY");
|
|
||||||
if (string.IsNullOrWhiteSpace(key)) return null;
|
|
||||||
|
|
||||||
string url = $"https://pixabay.com/api/?key={key}" +
|
|
||||||
$"&q={Uri.EscapeDataString(query)}&image_type=photo&per_page=20&safesearch=true";
|
|
||||||
|
|
||||||
HttpResponseMessage response;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response = await _http.GetAsync(url);
|
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
|
|
||||||
// 429 = rate limited — fall through to Wikimedia
|
|
||||||
if (!response.IsSuccessStatusCode) return null;
|
|
||||||
|
|
||||||
string json = await response.Content.ReadAsStringAsync();
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var hits = doc.RootElement.GetProperty("hits");
|
|
||||||
if (hits.GetArrayLength() == 0) return null;
|
|
||||||
|
|
||||||
var hit = hits[_rng.Next(hits.GetArrayLength())];
|
|
||||||
string imgUrl = hit.GetProperty("webformatURL").GetString()!;
|
|
||||||
int width = hit.TryGetProperty("webformatWidth", out var w) ? w.GetInt32() : 0;
|
|
||||||
int height = hit.TryGetProperty("webformatHeight", out var h) ? h.GetInt32() : 0;
|
|
||||||
string ext = Path.GetExtension(imgUrl.Split('?')[0]).ToLowerInvariant();
|
|
||||||
string mime = ext == ".png" ? "image/png" : ext == ".gif" ? "image/gif" : "image/jpeg";
|
|
||||||
|
|
||||||
return new ImageResult(imgUrl, width, height, mime);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<ImageResult?> FetchWikimediaAsync(string query)
|
|
||||||
{
|
|
||||||
string url = "https://commons.wikimedia.org/w/api.php" +
|
|
||||||
$"?action=query&generator=search&gsrsearch=intitle:{Uri.EscapeDataString(query)}" +
|
|
||||||
"&gsrnamespace=6&gsrlimit=30&prop=imageinfo&iiprop=url|size|mime&format=json";
|
|
||||||
|
|
||||||
string json;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "SkyBot/1.0 (https://github.com/SkyJoshua/SkyBot)");
|
|
||||||
json = await _http.GetStringAsync(url);
|
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
if (!doc.RootElement.TryGetProperty("query", out var queryEl) ||
|
|
||||||
!queryEl.TryGetProperty("pages", out var pages)) return null;
|
|
||||||
|
|
||||||
var candidates = new List<ImageResult>();
|
|
||||||
foreach (var page in pages.EnumerateObject())
|
|
||||||
{
|
|
||||||
if (!page.Value.TryGetProperty("imageinfo", out var infoArr)) continue;
|
|
||||||
var info = infoArr[0];
|
|
||||||
|
|
||||||
string mime = info.TryGetProperty("mime", out var m) ? m.GetString() ?? "" : "";
|
|
||||||
if (mime is not ("image/jpeg" or "image/png" or "image/gif" or "image/webp")) continue;
|
|
||||||
|
|
||||||
string imgUrl = info.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
|
|
||||||
int width = info.TryGetProperty("width", out var w) ? w.GetInt32() : 0;
|
|
||||||
int height = info.TryGetProperty("height", out var h) ? h.GetInt32() : 0;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(imgUrl))
|
|
||||||
candidates.Add(new ImageResult(imgUrl, width, height, mime));
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates.Count == 0 ? null : candidates[_rng.Next(candidates.Count)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Mock : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "mock";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Mock text";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "mock [text] (Or reply to a message)";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string text;
|
|
||||||
|
|
||||||
if (message.ReplyToId.HasValue)
|
|
||||||
{
|
|
||||||
var replyMessage = await message.FetchReplyMessageAsync();
|
|
||||||
text = replyMessage?.Content ?? "";
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide some text to mock or reply to a message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
text = string.Join(" ", args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "No text to mock.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string mocked = new string(text.Select((c, i) => i % 2 == 0 ? char.ToLower(c) : char.ToUpper(c)).ToArray());
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, mocked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Reverse : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "reverse";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Reverses yours or a replied text.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "reverse [text] (Or reply to a message)";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string text;
|
|
||||||
|
|
||||||
if (message.ReplyToId.HasValue)
|
|
||||||
{
|
|
||||||
var replyMessage = await message.FetchReplyMessageAsync();
|
|
||||||
text = replyMessage?.Content ?? "";
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide some text to reverse or reply to a message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
text = string.Join(" ", args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "No text to reverse.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string reversed = new string(text.Reverse().ToArray());
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Reversed: {reversed}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class RockPaperScissors : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "rps";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Play Rock Paper Scissors against the bot.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "rps <rock|paper|scissors>";
|
|
||||||
|
|
||||||
private static readonly string[] Choices = ["rock", "paper", "scissors"];
|
|
||||||
private static readonly string[] Emojis = ["🪨", "📄", "✂️"];
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please choose `Rock`, `Paper`, or `Scissors`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string input = args[0].ToLower();
|
|
||||||
int playerIndex = Array.IndexOf(Choices, input);
|
|
||||||
|
|
||||||
if (playerIndex == -1)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid choice. Please choose `Rock`, `Paper`, or `Scissors`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskResult<Message> result = await MessageHelper.ReplyAsync(ctx, channel, "🤔 Thinking...");
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
int botIndex = Random.Shared.Next(3);
|
|
||||||
|
|
||||||
string playerChoice = $"{Emojis[playerIndex]} {Choices[playerIndex].ToTitleCase()}";
|
|
||||||
string botChoice = $"{Emojis[botIndex]} {Choices[botIndex].ToTitleCase()}";
|
|
||||||
|
|
||||||
string outcome;
|
|
||||||
if (playerIndex == botIndex) outcome = "It's a tie!";
|
|
||||||
else if ((playerIndex == 0 && botIndex == 2) ||
|
|
||||||
(playerIndex == 1 && botIndex == 0) ||
|
|
||||||
(playerIndex == 2 && botIndex == 1)) outcome = "You win! 🎉";
|
|
||||||
else outcome = "You Lose! 🥲";
|
|
||||||
|
|
||||||
await MessageHelper.EditAsync(channel, result.Data, $"**You**: {playerChoice}\nvs\n**Bot**: {botChoice}\n[]()\n{outcome}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class MultiTap : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "t9decode";
|
|
||||||
public string[] Aliases => ["t9d"];
|
|
||||||
public string Description => "Decodes old phone keypad multi-tap input into text.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "multitap <digits> (e.g. 44 3 555 555 666 or reply to a message)";
|
|
||||||
|
|
||||||
private static readonly Dictionary<char, string> Keymap = new()
|
|
||||||
{
|
|
||||||
['2'] = "ABC",
|
|
||||||
['3'] = "DEF",
|
|
||||||
['4'] = "GHI",
|
|
||||||
['5'] = "JKL",
|
|
||||||
['6'] = "MNO",
|
|
||||||
['7'] = "PQRS",
|
|
||||||
['8'] = "TUV",
|
|
||||||
['9'] = "WXYZ",
|
|
||||||
};
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string raw;
|
|
||||||
|
|
||||||
if (message.ReplyToId.HasValue)
|
|
||||||
{
|
|
||||||
var replyMessage = await message.FetchReplyMessageAsync();
|
|
||||||
raw = replyMessage?.Content ?? "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide digits to decode, or reply to a message. Example: `multitap 44 3 555 555 666`");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
raw = string.Join("", args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip anything that isn't a digit
|
|
||||||
string input = new([..raw.Where(char.IsDigit)]);
|
|
||||||
|
|
||||||
var result = new StringBuilder();
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
while (i < input.Length)
|
|
||||||
{
|
|
||||||
char digit = input[i];
|
|
||||||
|
|
||||||
if (digit == '0')
|
|
||||||
{
|
|
||||||
result.Append(' ');
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 = silent same-key separator if surrounded by the same digit, otherwise a space
|
|
||||||
if (digit == '1')
|
|
||||||
{
|
|
||||||
int j = i;
|
|
||||||
while (j < input.Length && input[j] == '1') j++;
|
|
||||||
|
|
||||||
char before = i > 0 ? input[i - 1] : '\0';
|
|
||||||
char after = j < input.Length ? input[j] : '\0';
|
|
||||||
bool sameKey = before >= '2' && before <= '9' && before == after;
|
|
||||||
|
|
||||||
if (!sameKey) result.Append(' ');
|
|
||||||
i = j;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Keymap.TryGetValue(digit, out string? letters))
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count consecutive presses of the same digit
|
|
||||||
int count = 0;
|
|
||||||
while (i + count < input.Length && input[i + count] == digit)
|
|
||||||
count++;
|
|
||||||
|
|
||||||
int letterIndex = (count - 1) % letters.Length;
|
|
||||||
result.Append(letters[letterIndex]);
|
|
||||||
i += count;
|
|
||||||
}
|
|
||||||
|
|
||||||
string decoded = result.ToString().Trim();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(decoded))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Couldn't decode anything from that input.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"📱 **{decoded}**");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class T9Encode : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "t9encode";
|
|
||||||
public string[] Aliases => ["t9e"];
|
|
||||||
public string Description => "Encodes text into old phone keypad multi-tap digits.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "t9encode <text> (or reply to a message)";
|
|
||||||
|
|
||||||
// Maps each letter to (key digit, press count)
|
|
||||||
private static readonly Dictionary<char, (char Key, int Presses)> Charmap = BuildCharmap();
|
|
||||||
|
|
||||||
private static Dictionary<char, (char Key, int Presses)> BuildCharmap()
|
|
||||||
{
|
|
||||||
Dictionary<char, string> keymap = new()
|
|
||||||
{
|
|
||||||
['2'] = "ABC",
|
|
||||||
['3'] = "DEF",
|
|
||||||
['4'] = "GHI",
|
|
||||||
['5'] = "JKL",
|
|
||||||
['6'] = "MNO",
|
|
||||||
['7'] = "PQRS",
|
|
||||||
['8'] = "TUV",
|
|
||||||
['9'] = "WXYZ",
|
|
||||||
};
|
|
||||||
|
|
||||||
var map = new Dictionary<char, (char, int)>();
|
|
||||||
foreach (var (key, letters) in keymap)
|
|
||||||
for (int i = 0; i < letters.Length; i++)
|
|
||||||
map[letters[i]] = (key, i + 1);
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string raw;
|
|
||||||
|
|
||||||
if (message.ReplyToId.HasValue)
|
|
||||||
{
|
|
||||||
var replyMessage = await message.FetchReplyMessageAsync();
|
|
||||||
raw = replyMessage?.Content ?? "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide text to encode, or reply to a message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
raw = string.Join(" ", args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(raw))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "No text to encode.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new StringBuilder();
|
|
||||||
char prevKey = '\0';
|
|
||||||
|
|
||||||
foreach (char c in raw.ToUpper())
|
|
||||||
{
|
|
||||||
if (c == ' ')
|
|
||||||
{
|
|
||||||
result.Append('0');
|
|
||||||
prevKey = '\0';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Charmap.TryGetValue(c, out var entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// If this letter uses the same key as the previous one, insert a 1 separator
|
|
||||||
if (entry.Key == prevKey)
|
|
||||||
result.Append('1');
|
|
||||||
|
|
||||||
result.Append(entry.Key, entry.Presses);
|
|
||||||
prevKey = entry.Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
string encoded = result.ToString().Trim('0');
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(encoded))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Couldn't encode anything from that input.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"📱 `{encoded}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Trivia : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "trivia";
|
|
||||||
public string[] Aliases => ["triv"];
|
|
||||||
public string Description => "Starts a channel-wide trivia question. Everyone has 30 seconds to guess.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "trivia [easy|medium|hard] [topic] | trivia topics";
|
|
||||||
|
|
||||||
private record GuessEntry(char Letter, string MemberName);
|
|
||||||
|
|
||||||
private record TriviaSession(
|
|
||||||
char CorrectLetter,
|
|
||||||
string QuestionText,
|
|
||||||
List<string> Answers,
|
|
||||||
Message BotMessage,
|
|
||||||
Channel Channel,
|
|
||||||
ValourClient Client,
|
|
||||||
ConcurrentDictionary<long, GuessEntry> Guesses);
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<long, TriviaSession> _sessions = new();
|
|
||||||
private static readonly HttpClient _http = new();
|
|
||||||
|
|
||||||
private static readonly string[] Difficulties = ["easy", "medium", "hard"];
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, int> Categories = new()
|
|
||||||
{
|
|
||||||
["general"] = 9,
|
|
||||||
["books"] = 10,
|
|
||||||
["film"] = 11,
|
|
||||||
["movies"] = 11,
|
|
||||||
["music"] = 12,
|
|
||||||
["musicals"] = 13,
|
|
||||||
["tv"] = 14,
|
|
||||||
["television"] = 14,
|
|
||||||
["games"] = 15,
|
|
||||||
["videogames"] = 15,
|
|
||||||
["boardgames"] = 16,
|
|
||||||
["science"] = 17,
|
|
||||||
["nature"] = 17,
|
|
||||||
["computers"] = 18,
|
|
||||||
["tech"] = 18,
|
|
||||||
["math"] = 19,
|
|
||||||
["maths"] = 19,
|
|
||||||
["mythology"] = 20,
|
|
||||||
["sports"] = 21,
|
|
||||||
["geography"] = 22,
|
|
||||||
["geo"] = 22,
|
|
||||||
["history"] = 23,
|
|
||||||
["politics"] = 24,
|
|
||||||
["art"] = 25,
|
|
||||||
["celebrities"] = 26,
|
|
||||||
["animals"] = 27,
|
|
||||||
["vehicles"] = 28,
|
|
||||||
["cars"] = 28,
|
|
||||||
["comics"] = 29,
|
|
||||||
["anime"] = 31,
|
|
||||||
["manga"] = 31,
|
|
||||||
["cartoons"] = 32,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static async Task ProcessGuessAsync(CommandContext ctx, Channel channel, string rawGuess)
|
|
||||||
{
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!_sessions.TryGetValue(channelId, out var session))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active trivia question in this channel.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Guesses.ContainsKey(ctx.Message.AuthorUserId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "You've already submitted an answer!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rawGuess))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a letter. `A, B, C, or D`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char given = char.ToUpper(rawGuess[0]);
|
|
||||||
if (given < 'A' || given > 'D')
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid choice. Please guess A, B, C, or D.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Guesses[ctx.Message.AuthorUserId] = new GuessEntry(given, ctx.Member.Name ?? "Unknown");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "📬 Answer submitted!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
// sd/trivia topics
|
|
||||||
if (args.Length >= 1 && (args[0].ToLower() == "topics" || args[0].ToLower() == "t"))
|
|
||||||
{
|
|
||||||
string topicList = string.Join(", ", Categories.Keys.Order());
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"**Available topics:** {topicList}\n**Difficulties:** easy, medium, hard");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sd/trivia — fetch a new question
|
|
||||||
if (_sessions.ContainsKey(channelId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's already an active trivia question in this channel!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? difficulty = null;
|
|
||||||
int? categoryId = null;
|
|
||||||
|
|
||||||
foreach (string arg in args.Select(a => a.ToLower()))
|
|
||||||
{
|
|
||||||
if (Difficulties.Contains(arg))
|
|
||||||
difficulty = arg;
|
|
||||||
else if (Categories.TryGetValue(arg, out int id))
|
|
||||||
categoryId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick a random category if none was specified
|
|
||||||
if (categoryId is null)
|
|
||||||
{
|
|
||||||
var ids = Categories.Values.Distinct().ToArray();
|
|
||||||
categoryId = ids[Random.Shared.Next(ids.Length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
string url = "https://opentdb.com/api.php?amount=1&type=multiple";
|
|
||||||
if (difficulty is not null) url += $"&difficulty={difficulty}";
|
|
||||||
if (categoryId is not null) url += $"&category={categoryId}";
|
|
||||||
|
|
||||||
string rawJson;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
rawJson = await _http.GetStringAsync(url);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Failed to fetch a trivia question. Try again in a moment.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(rawJson);
|
|
||||||
var result = doc.RootElement.GetProperty("results")[0];
|
|
||||||
|
|
||||||
string question = WebUtility.HtmlDecode(result.GetProperty("question").GetString()!);
|
|
||||||
string correct = WebUtility.HtmlDecode(result.GetProperty("correct_answer").GetString()!);
|
|
||||||
string category = WebUtility.HtmlDecode(result.GetProperty("category").GetString()!);
|
|
||||||
string fetchedDifficulty = result.GetProperty("difficulty").GetString() ?? "unknown";
|
|
||||||
|
|
||||||
List<string> answers = result.GetProperty("incorrect_answers").EnumerateArray()
|
|
||||||
.Select(x => WebUtility.HtmlDecode(x.GetString()!))
|
|
||||||
.Append(correct)
|
|
||||||
.OrderBy(_ => Random.Shared.Next())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
char correctLetter = (char)('A' + answers.IndexOf(correct));
|
|
||||||
|
|
||||||
string questionText = string.Join("\n",
|
|
||||||
$"**Category**: {category} | **Difficulty**: {fetchedDifficulty.ToTitleCase()}",
|
|
||||||
"",
|
|
||||||
$"**{question}**",
|
|
||||||
"",
|
|
||||||
string.Join("\n", answers.Select((a, i) => $"{(char)('A' + i)}) {a}")),
|
|
||||||
"",
|
|
||||||
$"*Use `{Config.Prefix}tg <A/B/C/D>` — you have 30 seconds!*"
|
|
||||||
);
|
|
||||||
|
|
||||||
var sent = await MessageHelper.ReplyAsync(ctx, channel, questionText);
|
|
||||||
if (!sent.Success || sent.Data is null) return;
|
|
||||||
|
|
||||||
Message botMessage = ctx.Client.Cache.Messages.TryGet(sent.Data.Id, out var cachedSent) && cachedSent is not null
|
|
||||||
? cachedSent : sent.Data;
|
|
||||||
|
|
||||||
var newSession = new TriviaSession(correctLetter, questionText, answers, botMessage, channel, ctx.Client, new());
|
|
||||||
_sessions[channelId] = newSession;
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(30_000);
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
|
|
||||||
List<string> corrects = [..newSession.Guesses
|
|
||||||
.Where(kv => kv.Value.Letter == newSession.CorrectLetter)
|
|
||||||
.Select(kv => kv.Value.MemberName)];
|
|
||||||
|
|
||||||
List<string> wrongs = [..newSession.Guesses
|
|
||||||
.Where(kv => kv.Value.Letter != newSession.CorrectLetter)
|
|
||||||
.Select(kv => kv.Value.MemberName)];
|
|
||||||
|
|
||||||
string correctAnswer = newSession.Answers[newSession.CorrectLetter - 'A'];
|
|
||||||
string resultsText = $"⏰ **Time's up!** The answer was **{newSession.CorrectLetter}) {correctAnswer}**\n";
|
|
||||||
|
|
||||||
if (corrects.Count > 0)
|
|
||||||
resultsText += $"\n✅ **Correct:** {string.Join(", ", corrects)}";
|
|
||||||
if (wrongs.Count > 0)
|
|
||||||
resultsText += $"\n❌ **Wrong:** {string.Join(", ", wrongs)}";
|
|
||||||
if (newSession.Guesses.IsEmpty)
|
|
||||||
resultsText += "\n😶 Nobody answered!";
|
|
||||||
|
|
||||||
var resultMsg = new Message(newSession.Client)
|
|
||||||
{
|
|
||||||
Content = resultsText,
|
|
||||||
ChannelId = newSession.Channel.Id,
|
|
||||||
PlanetId = newSession.Channel.Planet!.Id,
|
|
||||||
AuthorUserId = newSession.Client.Me.Id,
|
|
||||||
AuthorMemberId = newSession.Channel.Planet?.MyMember.Id,
|
|
||||||
ReplyToId = newSession.BotMessage.Id,
|
|
||||||
Fingerprint = Guid.NewGuid().ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
await newSession.Client.MessageService.SendMessage(resultMsg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TriviaGuess : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "tg";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Submit your answer to the active Trivia question.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "tg <A/B/C/D>";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
if (!ctx.ChannelCache.TryGetValue(ctx.ChannelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (ctx.Args.Length == 0 || string.IsNullOrWhiteSpace(ctx.Args[0]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a letter. `A, B, C, or D`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Trivia.ProcessGuessAsync(ctx, channel, ctx.Args[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Wordle : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "wordle";
|
|
||||||
public string[] Aliases => ["wd"];
|
|
||||||
public string Description => "Starts a channel-wide game of Wordle. Guess the 5-letter word in 6 tries!";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "wordle | wordle board";
|
|
||||||
|
|
||||||
private record WordleSession(
|
|
||||||
string Word,
|
|
||||||
List<string> Guesses,
|
|
||||||
List<string[]> Feedback,
|
|
||||||
HashSet<string> Contributors,
|
|
||||||
long StarterId,
|
|
||||||
Message BotMessage,
|
|
||||||
Channel Channel,
|
|
||||||
ValourClient Client);
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<long, WordleSession> _sessions = new();
|
|
||||||
private static readonly HttpClient _http = new();
|
|
||||||
private static readonly SemaphoreSlim _fetchLock = new(1, 1);
|
|
||||||
private static string[]? _cachedWords;
|
|
||||||
|
|
||||||
private const int MaxGuesses = 6;
|
|
||||||
private const int WordLength = 5;
|
|
||||||
|
|
||||||
private static readonly string[] WordList =
|
|
||||||
[
|
|
||||||
"ABOUT", "ABUSE", "ACUTE", "ADMIT", "ADOPT", "AFTER", "AGENT", "AGREE",
|
|
||||||
"AHEAD", "ALIKE", "ALIVE", "ALONE", "ALONG", "ALTER", "ANGEL", "ANGER",
|
|
||||||
"ANGLE", "APART", "APPLE", "APPLY", "ARGUE", "ARISE", "ARMOR", "ASIDE",
|
|
||||||
"ASSET", "AVOID", "AWARD", "AWARE", "BADLY", "BASIC", "BEACH", "BEARD",
|
|
||||||
"BEGAN", "BEGIN", "BEING", "BELOW", "BENCH", "BLACK", "BLADE", "BLAME",
|
|
||||||
"BLANK", "BLAST", "BLAZE", "BLEED", "BLESS", "BLIND", "BLOCK", "BLOOD",
|
|
||||||
"BLOOM", "BLUNT", "BOARD", "BOOST", "BOUND", "BRAND", "BRAVE", "BREAD",
|
|
||||||
"BREAK", "BREED", "BRICK", "BRIDE", "BRIEF", "BRING", "BROAD", "BROOK",
|
|
||||||
"BROWN", "BRUSH", "BUILD", "BUILT", "BURST", "BUYER", "CABIN", "CABLE",
|
|
||||||
"CANDY", "CARRY", "CHAIN", "CHAIR", "CHAOS", "CHARM", "CHEAP", "CHECK",
|
|
||||||
"CHESS", "CHEST", "CHIEF", "CHILD", "CHILL", "CIVIC", "CIVIL", "CLAIM",
|
|
||||||
"CLASS", "CLEAN", "CLEAR", "CLIMB", "CLOCK", "CLOSE", "CLOUD", "COACH",
|
|
||||||
"COAST", "COUNT", "COURT", "COVER", "CRACK", "CRAFT", "CRANE", "CRAZY",
|
|
||||||
"CREAM", "CREEK", "CRIME", "CROSS", "CROWD", "CRUSH", "CURVE", "CYCLE",
|
|
||||||
"DAILY", "DANCE", "DEATH", "DEBUT", "DENSE", "DEPOT", "DEPTH", "DERBY",
|
|
||||||
"DINER", "DIRTY", "DISCO", "DITCH", "DIZZY", "DOUBT", "DOUGH", "DRAFT",
|
|
||||||
"DRAIN", "DRAMA", "DRANK", "DREAM", "DRESS", "DRIFT", "DRINK", "DRIVE",
|
|
||||||
"DRONE", "DROVE", "DROWN", "DRUNK", "DYING", "EAGER", "EAGLE", "EARLY",
|
|
||||||
"EARTH", "EIGHT", "ELITE", "EMPTY", "ENEMY", "ENJOY", "ENTER", "ENTRY",
|
|
||||||
"EQUAL", "ERROR", "ESSAY", "EVENT", "EVERY", "EXACT", "EXIST", "EXTRA",
|
|
||||||
"FABLE", "FAINT", "FAITH", "FALSE", "FANCY", "FATAL", "FAULT", "FEAST",
|
|
||||||
"FENCE", "FEVER", "FIELD", "FIERY", "FIFTH", "FIFTY", "FIGHT", "FINAL",
|
|
||||||
"FIRST", "FIXED", "FLAME", "FLASH", "FLEET", "FLESH", "FLOAT", "FLOCK",
|
|
||||||
"FLOOD", "FLOOR", "FLUSH", "FOCUS", "FORCE", "FORGE", "FORTH", "FOUND",
|
|
||||||
"FRAME", "FRANK", "FRAUD", "FRESH", "FRONT", "FROST", "FRUIT", "FULLY",
|
|
||||||
"FUNNY", "GIANT", "GIVEN", "GLASS", "GLEAM", "GLOBE", "GLOOM", "GLORY",
|
|
||||||
"GLOVE", "GOING", "GRACE", "GRADE", "GRAIN", "GRAND", "GRANT", "GRAPE",
|
|
||||||
"GRASP", "GRASS", "GRAVE", "GREAT", "GREEN", "GREET", "GRIEF", "GRIND",
|
|
||||||
"GROAN", "GROOM", "GROSS", "GROUP", "GROVE", "GROWN", "GUARD", "GUESS",
|
|
||||||
"GUIDE", "GUILT", "GUISE", "HARSH", "HEART", "HEAVY", "HENCE", "HERBS",
|
|
||||||
"HINGE", "HONEY", "HONOR", "HORSE", "HOTEL", "HOUSE", "HUMAN", "HURRY",
|
|
||||||
"IDEAL", "IMAGE", "INNER", "INPUT", "ISSUE", "IVORY", "JEWEL", "JOINT",
|
|
||||||
"JUDGE", "JUICE", "JUICY", "JUMBO", "KARMA", "KNACK", "KNEEL", "KNIFE",
|
|
||||||
"KNOCK", "KNOWN", "LABEL", "LANCE", "LARGE", "LASER", "LATER", "LAUGH",
|
|
||||||
"LAYER", "LEARN", "LEASE", "LEAST", "LEGAL", "LEMON", "LEVEL", "LIGHT",
|
|
||||||
"LIMIT", "LIVER", "LOCAL", "LODGE", "LOGIC", "LOOSE", "LOVER", "LOWER",
|
|
||||||
"LUCKY", "LUNAR", "MAGIC", "MAJOR", "MAKER", "MANOR", "MAPLE", "MARCH",
|
|
||||||
"MATCH", "MAYOR", "MEDIA", "MERCY", "MERIT", "METAL", "MIGHT", "MINOR",
|
|
||||||
"MINUS", "MODEL", "MONEY", "MONTH", "MORAL", "MOTOR", "MOUNT", "MOUSE",
|
|
||||||
"MOUTH", "MOVIE", "MUSIC", "NAIVE", "NERVE", "NEVER", "NIGHT", "NINJA",
|
|
||||||
"NOBLE", "NOISE", "NORTH", "NOVEL", "NURSE", "NYMPH", "OCEAN", "OFFER",
|
|
||||||
"OFTEN", "OLIVE", "ONSET", "OPERA", "ORDER", "OTHER", "OUTER", "OWNED",
|
|
||||||
"OWNER", "OZONE", "PAINT", "PANIC", "PAPER", "PARTY", "PEACE", "PEACH",
|
|
||||||
"PEARL", "PENNY", "PHASE", "PHONE", "PHOTO", "PIANO", "PIECE", "PILOT",
|
|
||||||
"PIXEL", "PLACE", "PLAIN", "PLANE", "PLANT", "PLATE", "PLAZA", "PLEAD",
|
|
||||||
"PLUMB", "PLUMP", "POINT", "POLAR", "POPPY", "POWER", "PRESS", "PRICE",
|
|
||||||
"PRIDE", "PRIME", "PRINT", "PRIOR", "PRISM", "PROBE", "PROOF", "PROSE",
|
|
||||||
"PROUD", "PROVE", "PULSE", "PUPIL", "QUEEN", "QUERY", "QUEST", "QUEUE",
|
|
||||||
"QUIET", "QUOTA", "QUOTE", "RADAR", "RADIO", "RAISE", "RALLY", "RANGE",
|
|
||||||
"RAPID", "RATIO", "REACH", "READY", "REALM", "REBEL", "REFER", "REIGN",
|
|
||||||
"RELAX", "REPLY", "RIDER", "RIDGE", "RISKY", "RIVER", "ROBIN", "ROBOT",
|
|
||||||
"ROCKY", "ROUGH", "ROUND", "ROUTE", "ROYAL", "RULER", "RURAL", "SADLY",
|
|
||||||
"SAINT", "SALAD", "SAUCE", "SCALE", "SCENE", "SCENT", "SCOUT", "SENSE",
|
|
||||||
"SEVEN", "SHADE", "SHAFT", "SHALL", "SHAME", "SHAPE", "SHARE", "SHARK",
|
|
||||||
"SHARP", "SHEEP", "SHEER", "SHELF", "SHELL", "SHIFT", "SHINE", "SHIRT",
|
|
||||||
"SHOCK", "SHOOT", "SHORT", "SHOUT", "SIEGE", "SIGHT", "SILLY", "SINCE",
|
|
||||||
"SIXTH", "SIXTY", "SKILL", "SKULL", "SLATE", "SLAVE", "SLEEP", "SLICE",
|
|
||||||
"SLIDE", "SLOPE", "SMALL", "SMART", "SMELL", "SMILE", "SMOKE", "SNAKE",
|
|
||||||
"SOLAR", "SOLID", "SOLVE", "SORRY", "SOUND", "SOUTH", "SPACE", "SPARE",
|
|
||||||
"SPARK", "SPEAK", "SPEAR", "SPEED", "SPEND", "SPICE", "SPINE", "SPITE",
|
|
||||||
"SPLIT", "SPOKE", "SPOON", "SPORT", "SPRAY", "SQUAD", "STACK", "STAFF",
|
|
||||||
"STAGE", "STAIN", "STAIR", "STAKE", "STAND", "STARK", "STATE", "STEAM",
|
|
||||||
"STEEL", "STEEP", "STEER", "STERN", "STICK", "STIFF", "STILL", "STOCK",
|
|
||||||
"STOMP", "STONE", "STORE", "STORM", "STORY", "STRAP", "STRAW", "STRAY",
|
|
||||||
"STRIP", "STUCK", "STUDY", "STUFF", "STYLE", "SUGAR", "SUITE", "SUNNY",
|
|
||||||
"SUPER", "SURGE", "SWAMP", "SWEAR", "SWEEP", "SWEET", "SWIFT", "SWIPE",
|
|
||||||
"SWORD", "TABLE", "TASTE", "TEACH", "TEARS", "TEMPT", "TENSE", "TENTH",
|
|
||||||
"THEFT", "THEIR", "THERE", "THICK", "THING", "THINK", "THIRD", "THREE",
|
|
||||||
"THREW", "THROW", "TIGER", "TIGHT", "TIMER", "TIRED", "TITAN", "TITLE",
|
|
||||||
"TOKEN", "TOPIC", "TOTAL", "TOUCH", "TOUGH", "TOWER", "TOXIC", "TRACE",
|
|
||||||
"TRACK", "TRADE", "TRAIL", "TRAIN", "TRAIT", "TRASH", "TREND", "TRIAL",
|
|
||||||
"TRIBE", "TRICK", "TRIED", "TROOP", "TROUT", "TRUCK", "TRULY", "TRUNK",
|
|
||||||
"TRUST", "TRUTH", "TWIST", "ULTRA", "UNCLE", "UNDER", "UNION", "UNITY",
|
|
||||||
"UNTIL", "UPPER", "UPSET", "URBAN", "USAGE", "USHER", "USUAL", "UTTER",
|
|
||||||
"VAGUE", "VALID", "VALUE", "VAPOR", "VAULT", "VIGOR", "VIRAL", "VISIT",
|
|
||||||
"VITAL", "VIVID", "VOCAL", "VOICE", "VOTER", "WAGER", "WATCH", "WATER",
|
|
||||||
"WEARY", "WEAVE", "WEDGE", "WEIRD", "WHERE", "WHILE", "WHITE", "WHOLE",
|
|
||||||
"WIDER", "WITCH", "WOMAN", "WOMEN", "WORLD", "WORRY", "WORSE", "WORST",
|
|
||||||
"WORTH", "WOULD", "WRATH", "WRITE", "WRONG", "YACHT", "YIELD", "YOUNG",
|
|
||||||
"YOUTH", "ZEBRA",
|
|
||||||
];
|
|
||||||
|
|
||||||
private static async Task<string[]> GetWordPoolAsync()
|
|
||||||
{
|
|
||||||
if (_cachedWords is not null) return _cachedWords;
|
|
||||||
|
|
||||||
await _fetchLock.WaitAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_cachedWords is not null) return _cachedWords;
|
|
||||||
|
|
||||||
string json = await _http.GetStringAsync(
|
|
||||||
"https://api.datamuse.com/words?sp=?????&max=1000&md=f");
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var words = doc.RootElement.EnumerateArray()
|
|
||||||
.Select(e => e.GetProperty("word").GetString() ?? "")
|
|
||||||
.Where(w => w.Length == WordLength && w.All(char.IsLetter))
|
|
||||||
.Select(w => w.ToUpper())
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (words.Length > 0)
|
|
||||||
{
|
|
||||||
_cachedWords = words;
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_fetchLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
return WordList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<bool> IsValidWordAsync(string word)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string json = await _http.GetStringAsync(
|
|
||||||
$"https://api.datamuse.com/words?sp={word.ToLower()}&max=1");
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
var arr = doc.RootElement;
|
|
||||||
if (arr.GetArrayLength() > 0)
|
|
||||||
{
|
|
||||||
string? returned = arr[0].GetProperty("word").GetString();
|
|
||||||
return string.Equals(returned, word, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns per-letter emoji feedback using standard Wordle rules
|
|
||||||
private static string[] GetFeedback(string guess, string word)
|
|
||||||
{
|
|
||||||
var result = new string[WordLength];
|
|
||||||
var wordLeft = word.ToCharArray();
|
|
||||||
var guessLeft = guess.ToCharArray();
|
|
||||||
|
|
||||||
// First pass: greens
|
|
||||||
for (int i = 0; i < WordLength; i++)
|
|
||||||
{
|
|
||||||
if (guessLeft[i] == wordLeft[i])
|
|
||||||
{
|
|
||||||
result[i] = "🟩";
|
|
||||||
wordLeft[i] = '\0';
|
|
||||||
guessLeft[i] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: yellows and grays
|
|
||||||
for (int i = 0; i < WordLength; i++)
|
|
||||||
{
|
|
||||||
if (guessLeft[i] == '\0') continue;
|
|
||||||
|
|
||||||
int idx = Array.IndexOf(wordLeft, guessLeft[i]);
|
|
||||||
if (idx >= 0)
|
|
||||||
{
|
|
||||||
result[i] = "🟨";
|
|
||||||
wordLeft[idx] = '\0';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result[i] = "⬛";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildDisplay(WordleSession session)
|
|
||||||
{
|
|
||||||
var rows = new List<string>();
|
|
||||||
|
|
||||||
for (int i = 0; i < MaxGuesses; i++)
|
|
||||||
{
|
|
||||||
if (i < session.Guesses.Count)
|
|
||||||
{
|
|
||||||
string emojis = string.Join("", session.Feedback[i]);
|
|
||||||
string letters = string.Join(" ", session.Guesses[i].ToCharArray());
|
|
||||||
rows.Add($"{emojis} {letters}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
rows.Add("⬜⬜⬜⬜⬜");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join("\n",
|
|
||||||
"🟩 **WORDLE** — Guess the 5-letter word!",
|
|
||||||
"",
|
|
||||||
string.Join("\n", rows),
|
|
||||||
"",
|
|
||||||
$"Guesses: {session.Guesses.Count}/{MaxGuesses}",
|
|
||||||
"",
|
|
||||||
$"*Use `{Config.Prefix}wg <word>` to guess!*"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ProcessGuessAsync(CommandContext ctx, Channel channel, string rawGuess)
|
|
||||||
{
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
if (!_sessions.TryGetValue(channelId, out var session)) return;
|
|
||||||
|
|
||||||
string guess = rawGuess.ToUpper();
|
|
||||||
string memberName = ctx.Member.Name ?? "Unknown";
|
|
||||||
|
|
||||||
if (guess.Length != WordLength || !guess.All(char.IsLetter))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Your guess must be exactly {WordLength} letters with no numbers or symbols.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await IsValidWordAsync(guess))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"`{guess}` isn't a valid word!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Guesses.Contains(guess))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"`{guess}` has already been guessed!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var feedback = GetFeedback(guess, session.Word);
|
|
||||||
session.Guesses.Add(guess);
|
|
||||||
session.Feedback.Add(feedback);
|
|
||||||
session.Contributors.Add(memberName);
|
|
||||||
|
|
||||||
bool won = guess == session.Word;
|
|
||||||
bool lost = !won && session.Guesses.Count >= MaxGuesses;
|
|
||||||
|
|
||||||
if (won)
|
|
||||||
{
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
string contributorList = string.Join(", ", session.Contributors);
|
|
||||||
await RepostBoardAsync(ctx, channel, session,
|
|
||||||
BuildDisplay(session)
|
|
||||||
+ $"\n\n🎉 **{memberName} got it in {session.Guesses.Count}!**\nContributors: {contributorList}");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"🎉 **The word was `{session.Word}`!** Got it in {session.Guesses.Count}/{MaxGuesses}!");
|
|
||||||
}
|
|
||||||
else if (lost)
|
|
||||||
{
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
await RepostBoardAsync(ctx, channel, session,
|
|
||||||
BuildDisplay(session)
|
|
||||||
+ $"\n\n💀 **Game over!** The word was `{session.Word}`.");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"💀 **Game over!** The word was `{session.Word}`.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newMsg = await RepostBoardAsync(ctx, channel, session, BuildDisplay(session));
|
|
||||||
if (newMsg is not null)
|
|
||||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<Message?> RepostBoardAsync(CommandContext ctx, Channel channel, WordleSession session, string content)
|
|
||||||
{
|
|
||||||
if (ctx.Client.Cache.Messages.TryGet(session.BotMessage.Id, out var old) && old is not null)
|
|
||||||
try { await old.DeleteAsync(); } catch { }
|
|
||||||
else
|
|
||||||
try { await session.BotMessage.DeleteAsync(); } catch { }
|
|
||||||
|
|
||||||
var result = await MessageHelper.ReplyAsync(ctx, channel, content);
|
|
||||||
if (!result.Success || result.Data is null) return null;
|
|
||||||
return ctx.Client.Cache.Messages.TryGet(result.Data.Id, out var cached) && cached is not null ? cached : result.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
// sd/wordle end — end the current game
|
|
||||||
if (args.Length >= 1 && args[0].ToLower() == "end")
|
|
||||||
{
|
|
||||||
if (!_sessions.TryGetValue(channelId, out var session))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active Wordle game in this channel.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isStarter = member.UserId == session.StarterId;
|
|
||||||
bool isMod = await PermissionHelper.HasPermAsync(member, channel, [ChatChannelPermissions.ManageMessages]);
|
|
||||||
|
|
||||||
if (!isStarter && !isMod)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Only the person who started the game (or a moderator) can end it.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sessions.TryRemove(channelId, out _);
|
|
||||||
if (ctx.Client.Cache.Messages.TryGet(session.BotMessage.Id, out var old) && old is not null)
|
|
||||||
try { await old.DeleteAsync(); } catch { }
|
|
||||||
else
|
|
||||||
try { await session.BotMessage.DeleteAsync(); } catch { }
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"🛑 Wordle ended by {member.Name}. The word was `{session.Word}`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sd/wordle board — repost the current board
|
|
||||||
if (args.Length >= 1 && args[0].ToLower() == "board")
|
|
||||||
{
|
|
||||||
if (!_sessions.TryGetValue(channelId, out var session))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's no active Wordle game in this channel.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newMsg = await RepostBoardAsync(ctx, channel, session, BuildDisplay(session));
|
|
||||||
if (newMsg is not null)
|
|
||||||
_sessions[channelId] = session with { BotMessage = newMsg };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sd/wordle — start a new game
|
|
||||||
if (_sessions.ContainsKey(channelId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "There's already an active Wordle game in this channel!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pool = await GetWordPoolAsync();
|
|
||||||
string word = pool[Random.Shared.Next(pool.Length)];
|
|
||||||
|
|
||||||
var newSession = new WordleSession(word, [], [], [], member.UserId, null!, channel, ctx.Client);
|
|
||||||
string display = BuildDisplay(newSession);
|
|
||||||
var sent = await MessageHelper.ReplyAsync(ctx, channel, display);
|
|
||||||
if (!sent.Success || sent.Data is null) return;
|
|
||||||
|
|
||||||
_sessions[channelId] = newSession with { BotMessage = sent.Data };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WordleGuess : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "wg";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Guess a word in the active Wordle game.";
|
|
||||||
public string Section => "Fun";
|
|
||||||
public string Usage => "wg <word>";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
if (!ctx.ChannelCache.TryGetValue(ctx.ChannelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (ctx.Args.Length == 0 || string.IsNullOrWhiteSpace(ctx.Args[0]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a word to guess.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Wordle.ProcessGuessAsync(ctx, channel, ctx.Args[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class BotGuide : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "botguide";
|
|
||||||
public string[] Aliases => ["bot", "bguide"];
|
|
||||||
public string Description => "Sends a link the a bot guide that SkyJoshua has made.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "botguide";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ValourClient Client = ctx.Client;
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
PlanetMember Member = ctx.Member;
|
|
||||||
Message Message = ctx.Message;
|
|
||||||
Planet Planet = ctx.Planet;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] Args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
string msg = @"Here is a link to a Bot Guide that SkyJoshua has made (WIP):
|
|
||||||
https://git.skyjoshua.xyz/SkyJoshua/Valour-Bot-Guide";
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Devcentral : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "devcentral";
|
|
||||||
public string[] Aliases => ["dev"];
|
|
||||||
public string Description => "Sends an invite link to the Dev Central Planet.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "devcentral";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = $"you can join the Dev Central (ID: 42439954653511681) planet here: https://app.valour.gg/I/k2tz9c4i";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Help : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "help";
|
|
||||||
public string[] Aliases => ["h"];
|
|
||||||
public string Description => "Shows all the commands and their descriptions.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "help [section] [page]";
|
|
||||||
private const int PageSize = 5;
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
// Show all sections.
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("**Available Categories**");
|
|
||||||
foreach (var section in CommandRegistry.Sections.Keys)
|
|
||||||
{
|
|
||||||
if (section == "template") continue;
|
|
||||||
if (section == "dev" && !PermissionHelper.IsOwner(member)) continue;
|
|
||||||
if (section == "mod" && !PermissionHelper.HasPerm(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles])) continue;
|
|
||||||
sb.AppendLine($"- `{section.ToTitleCase()}` ({CommandRegistry.Sections[section].Count})");
|
|
||||||
}
|
|
||||||
sb.AppendLine($"\nUse `{Config.Prefix}help <category>` to see commands in a category.");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// section [page]
|
|
||||||
string sectionName = args[0].ToLower();
|
|
||||||
if (!CommandRegistry.Sections.TryGetValue(sectionName, out var commands))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sectionName == "dev" && !PermissionHelper.IsOwner(member))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sectionName == "mod" && !PermissionHelper.HasPerm(member, [PlanetPermissions.Kick, PlanetPermissions.Ban, PlanetPermissions.ManageRoles]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown category `{sectionName}`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int page = 1;
|
|
||||||
if (args.Length >= 2 && int.TryParse(args[1], out int parsedPage))
|
|
||||||
{
|
|
||||||
page = parsedPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalPages = (int)Math.Ceiling(commands.Count / (double)PageSize);
|
|
||||||
page = Math.Clamp(page, 1, totalPages);
|
|
||||||
|
|
||||||
var pageCommands = commands.Skip((page - 1) * PageSize).Take(PageSize);
|
|
||||||
|
|
||||||
var sb2 = new StringBuilder();
|
|
||||||
sb2.AppendLine($"**{sectionName.ToTitleCase()} commands** (Page {page}/{totalPages}):");
|
|
||||||
foreach (var cmd in pageCommands)
|
|
||||||
{
|
|
||||||
var name = cmd.Aliases.Length > 0
|
|
||||||
? $"{cmd.Name}|{string.Join("|", cmd.Aliases)}"
|
|
||||||
: cmd.Name;
|
|
||||||
sb2.AppendLine($"`{Config.Prefix}{name}` - {cmd.Description}");
|
|
||||||
}
|
|
||||||
sb2.AppendLine($"\nUse `{Config.Prefix}help {sectionName} <page>` to see more.");
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, sb2.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Info : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "info";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Shows the info about a User or the Planet.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "info <user|planet> [mention|memberid]";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please specify `user` or `planet`.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (args[0].ToLower())
|
|
||||||
{
|
|
||||||
case "user":
|
|
||||||
case "u":
|
|
||||||
await HandleUserInfo(ctx, channel);
|
|
||||||
break;
|
|
||||||
case "planet":
|
|
||||||
case "p":
|
|
||||||
await HandlePlanetInfo(ctx, channel);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "invalid option. Use `user` or `planet`.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleUserInfo(CommandContext ctx, Channel channel)
|
|
||||||
{
|
|
||||||
Message message = ctx.Message;
|
|
||||||
Planet planet = ctx.Planet;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember? target;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (message.Mentions != null && message.Mentions.Any())
|
|
||||||
{
|
|
||||||
target = await planet.FetchMemberAsync(message.Mentions.First().TargetId);
|
|
||||||
}
|
|
||||||
else if (args.Length > 1 && long.TryParse(args[1], out long memberid))
|
|
||||||
{
|
|
||||||
target = await planet.FetchMemberAsync(memberid);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
target = ctx.Member;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Exception: {ex.Message}");
|
|
||||||
target = ctx.Member;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not find that member.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"**{target.Name.Trim()}**");
|
|
||||||
sb.AppendLine($"User ID: `{target.UserId}`");
|
|
||||||
sb.AppendLine($"Member ID: `{target.Id}`");
|
|
||||||
sb.AppendLine($"Nickname: `{(string.IsNullOrWhiteSpace(target.Nickname) ? "None" : target.Nickname)}`");
|
|
||||||
sb.AppendLine($"Subscription: `{(string.IsNullOrWhiteSpace(target.User.SubscriptionType) ? "None" : target.User.SubscriptionType)}`");
|
|
||||||
sb.AppendLine($"Status: `{(string.IsNullOrWhiteSpace(target.Status) ? "None" : target.Status)}`");
|
|
||||||
sb.AppendLine($"Primary Role: `{target.PrimaryRole?.Name ?? "None"}`");
|
|
||||||
sb.AppendLine($"Roles: `{string.Join(", ", target.Roles.Select(r => r.Name))}`");
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandlePlanetInfo(CommandContext ctx, Channel channel)
|
|
||||||
{
|
|
||||||
var planet = ctx.Planet;
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"**{planet.Name}**");
|
|
||||||
sb.AppendLine($"Planet ID: `{planet.Id}`");
|
|
||||||
sb.AppendLine($"Owner ID: `{planet.OwnerId}`");
|
|
||||||
sb.AppendLine($"Member Count: `{planet.Members?.Count ?? 0}`");
|
|
||||||
sb.AppendLine($"Channel Count: `{planet.Channels?.Count ?? 0}`");
|
|
||||||
sb.AppendLine($"Role Count: `{planet.Roles?.Count ?? 0}`");
|
|
||||||
sb.AppendLine($"Description: `{(string.IsNullOrWhiteSpace(planet.Description) ? "None" : planet.Description)}`");
|
|
||||||
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class JoinSite : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "joinsite";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Links to a site to help your bots join a planet.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "joinsite";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = $"You can use this website to easily add your bot to a planet: https://skyjoshua.xyz/planetjoiner";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Minecraft : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "minecraft";
|
|
||||||
public string[] Aliases => ["mc"];
|
|
||||||
public string Description => "Sends the Unofficial ValourSMP IPs";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "minecraft";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = @$"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 (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Ping : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "ping";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Shows the bot's response time.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "ping";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
DateTime start = DateTime.UtcNow;
|
|
||||||
TaskResult<Message> message = await MessageHelper.ReplyAsync(ctx, channel, "🏓 Pinging...");
|
|
||||||
double elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
|
|
||||||
|
|
||||||
await MessageHelper.EditAsync(channel, message.Data, $"🏓 Pong! `{elapsed:F0}ms`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Source : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "source";
|
|
||||||
public string[] Aliases => ["src"];
|
|
||||||
public string Description => "Shows the source code for this bot.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "source";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = $"You can find my source code here: {Config.SourceLink}";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Suggest : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "suggest";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Sends a link to where you can suggest commands.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "source";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = $"You can suggest a command to be added here: https://docs.google.com/spreadsheets/d/1CzcpLAuMiPL_RODrZ5x25cPj8yE-rR3mEnqrd_2Fbmk";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class SwaggerAPI : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "swagger";
|
|
||||||
public string[] Aliases => ["api"];
|
|
||||||
public string Description => "Sends a link to the Valour.gg Swagger API.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "swagger";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = $"Here is a link to the Swagger API: https://api.valour.gg/swagger";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Uptime : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "uptime";
|
|
||||||
public string[] Aliases => ["up"];
|
|
||||||
public string Description => "Shows how long the bot has been running.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "uptime";
|
|
||||||
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
if (!channelCache.TryGetValue(channelId, out var channel)) return;
|
|
||||||
|
|
||||||
TimeSpan uptime = DateTime.UtcNow - SkyBot.StartTime;
|
|
||||||
string formatted = $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s";
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"⏱️ Uptime: `{formatted}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class UserCount : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "usercount";
|
|
||||||
public string[] Aliases => ["users"];
|
|
||||||
public string Description => "Shows the user count of Valour.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "usercount";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
string message = @$"Current Valour user count is: {ValourUsercountHelper.ValourUsercount:N0}
|
|
||||||
You can see a graph of the user count here: /meow";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Version : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "version";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Shows the current version of the Bot and Valour.";
|
|
||||||
public string Section => "Info";
|
|
||||||
public string Usage => "version";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
string message = @$"Bot Version: {typeof(Version).Assembly.GetName().Version}
|
|
||||||
Valour Version: {typeof(Channel).Assembly.GetName().Version}";
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Ban : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "ban";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Bans a user from the planet.";
|
|
||||||
public string Section => "Mod";
|
|
||||||
public string Usage => "ban <mention|memberid> [reason] [length (y=year, M=month, w=week, d=day, h=hour, m=minute)]";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
Planet planet = ctx.Planet;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
PlanetMember bot = await planet.FetchMemberByUserAsync(ctx.Client.Me.Id);
|
|
||||||
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Ban]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PermissionHelper.HasPerm(bot, [PlanetPermissions.Ban]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"I don't have permission to ban members.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.Mentions.Any() && args.Length < 1)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please mention someone or user their id.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
long targetId;
|
|
||||||
if (message.Mentions.Any())
|
|
||||||
{
|
|
||||||
targetId = message.Mentions.First().TargetId;
|
|
||||||
}
|
|
||||||
else if (!long.TryParse(args[0], out targetId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid member ID.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PlanetMember victim = await planet.FetchMemberAsync(targetId);
|
|
||||||
|
|
||||||
if (victim == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not find that member.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? expires = null;
|
|
||||||
List<string> remainingArgs = args[1..].ToList();
|
|
||||||
|
|
||||||
for (int i = 0; i < remainingArgs.Count; i++)
|
|
||||||
{
|
|
||||||
var parsed = MessageHelper.ParseDuration(remainingArgs[i]);
|
|
||||||
if (parsed != null)
|
|
||||||
{
|
|
||||||
expires = parsed;
|
|
||||||
remainingArgs.RemoveAt(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string reason = remainingArgs.Count > 0 ? string.Join(" ", remainingArgs) : "No reason provided.";
|
|
||||||
|
|
||||||
PlanetBan ban = new PlanetBan(ctx.Client)
|
|
||||||
{
|
|
||||||
PlanetId = planet.Id,
|
|
||||||
TargetId = victim.UserId,
|
|
||||||
IssuerId = ctx.Client.Me.Id,
|
|
||||||
Reason = reason,
|
|
||||||
TimeCreated = DateTime.UtcNow,
|
|
||||||
TimeExpires = expires
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TaskResult<PlanetBan> result = await ban.CreateAsync();
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Failed to ban {victim.Name}: {result.Message}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expires == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Permanently Banned `{victim.Name}`. Reason: `{reason}`");
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Banned `{victim.Name}` until `{expires}`. Reason: `{reason}`");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class GetBans : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "bans";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Lists all bans in the planet.";
|
|
||||||
public string Section => "Mod";
|
|
||||||
public string Usage => "bans [page]";
|
|
||||||
|
|
||||||
private const int PageSize = 10;
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
Planet planet = ctx.Planet;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Ban]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int page = 1;
|
|
||||||
if (args.Length > 0 && int.TryParse(args[0], out int parsedPage))
|
|
||||||
page = parsedPage;
|
|
||||||
|
|
||||||
int skip = (page - 1) * PageSize;
|
|
||||||
|
|
||||||
var queryResult = await planet.Node.PostAsyncWithResponse<QueryResponse<PlanetBan>>(
|
|
||||||
$"api/planets/{planet.Id}/bans/query",
|
|
||||||
new { skip, take = PageSize, options = new { } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!queryResult.Success || queryResult.Data?.Items == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Failed to fetch bans.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!queryResult.Data.Items.Any())
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "No bans found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalPages = (int)Math.Ceiling(queryResult.Data.TotalCount / (double)PageSize);
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"**Bans** (Page {page}/{totalPages}):");
|
|
||||||
|
|
||||||
IEnumerable<PlanetBan> activeBans = queryResult.Data.Items.Where(b => b.Permanent || b.TimeExpires > DateTime.UtcNow);
|
|
||||||
|
|
||||||
if (!activeBans.Any())
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "No active bans found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var ban in activeBans)
|
|
||||||
{
|
|
||||||
var user = await ctx.Client.UserService.FetchUserAsync(ban.TargetId);
|
|
||||||
string username = user?.NameAndTag ?? "Unknown";
|
|
||||||
string expires = ban.TimeExpires.HasValue
|
|
||||||
? $"{ban.TimeExpires}"
|
|
||||||
: "Never";
|
|
||||||
sb.AppendLine($"**{username}** `{user?.Id}` - {ban.Reason} (Expires: `{expires}`)");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine($"\nUse `{Config.Prefix}bans <page>` to see more.");
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, sb.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Kick : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "kick";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Kicks a user from the planet.";
|
|
||||||
public string Section => "Mod";
|
|
||||||
public string Usage => "kick <user> [reason]";
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
Planet planet = ctx.Planet;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
PlanetMember bot = await planet.FetchMemberByUserAsync(ctx.Client.Me.Id);
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Kick]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"You don't have permission to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PermissionHelper.HasPerm(bot, [PlanetPermissions.Kick]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"I don't have permission to kick members.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.Mentions.Any() && args.Length < 2)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please mention someone or user their id.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
long targetId;
|
|
||||||
if (message.Mentions.Any())
|
|
||||||
{
|
|
||||||
targetId = message.Mentions.First().TargetId;
|
|
||||||
}
|
|
||||||
else if (!long.TryParse(args[1], out targetId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid member ID.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PlanetMember victim = await planet.FetchMemberAsync(targetId);
|
|
||||||
|
|
||||||
if (victim == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not find that member.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string reason = args.Length > 1 && !message.Mentions.Any()
|
|
||||||
? string.Join(" ", args[2..])
|
|
||||||
: args.Length > 1
|
|
||||||
? string.Join(" ", args[1..])
|
|
||||||
: "No reason provided.";
|
|
||||||
|
|
||||||
await victim.DeleteAsync();
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Kicked {victim.Name}. Reason: {reason}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Commands
|
|
||||||
{
|
|
||||||
public class Unban : ICommand
|
|
||||||
{
|
|
||||||
public string Name => "unban";
|
|
||||||
public string[] Aliases => [];
|
|
||||||
public string Description => "Unbans a user from the planet.";
|
|
||||||
public string Section => "Mod";
|
|
||||||
public string Usage => "unban <id>";
|
|
||||||
|
|
||||||
// public async Task Execute(CommandContext ctx)
|
|
||||||
// {
|
|
||||||
// ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
// long channelId = ctx.ChannelId;
|
|
||||||
|
|
||||||
// if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
// {
|
|
||||||
// await MessageHelper.ReplyAsync(ctx, channel, "Unbanning is currently unavailable due to a Valour server bug.");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
public async Task Execute(CommandContext ctx)
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache = ctx.ChannelCache;
|
|
||||||
long channelId = ctx.ChannelId;
|
|
||||||
Planet planet = ctx.Planet;
|
|
||||||
Message message = ctx.Message;
|
|
||||||
string[] args = ctx.Args;
|
|
||||||
PlanetMember member = ctx.Member;
|
|
||||||
PlanetMember bot = await planet.FetchMemberByUserAsync(ctx.Client.Me.Id);
|
|
||||||
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
if (!PermissionHelper.HasPerm(member, [PlanetPermissions.Ban]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "You don't have permission to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PermissionHelper.HasPerm(bot, [PlanetPermissions.Ban]))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "I don't have permission to unban members.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.Length < 1)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Please provide a user ID.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!long.TryParse(args[0], out long targetUserId))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Invalid user ID.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlanetBan? ban = null;
|
|
||||||
int skip = 0;
|
|
||||||
int take = 50;
|
|
||||||
|
|
||||||
while (ban == null)
|
|
||||||
{
|
|
||||||
var queryResult = await planet.Node.PostAsyncWithResponse<QueryResponse<PlanetBan>>(
|
|
||||||
$"api/planets/{planet.Id}/bans/query",
|
|
||||||
new {skip, take, options = new { }}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!queryResult.Success || queryResult.Data?.Items == null || !queryResult.Data.Items.Any())
|
|
||||||
break;
|
|
||||||
|
|
||||||
ban = queryResult.Data.Items.FirstOrDefault(b => b.TargetId == targetUserId && (b.Permanent || b.TimeExpires > DateTime.UtcNow));
|
|
||||||
|
|
||||||
if (queryResult.Data.Items.Count < take)
|
|
||||||
break;
|
|
||||||
|
|
||||||
skip += take;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ban == null)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Could not find a ban for that user.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ban.TimeExpires = DateTime.UtcNow.AddSeconds(-1);
|
|
||||||
TaskResult<PlanetBan> result = await planet.Node.PutAsyncWithResponse<PlanetBan>($"api/bans/{ban.Id}", ban);
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, "Failed to unban.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
User user = await ctx.Client.UserService.FetchUserAsync(targetUserId);
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Unbanned `{user?.NameAndTag ?? targetUserId.ToString()}`.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace SkyBot
|
|
||||||
{
|
|
||||||
|
|
||||||
public static class Config {
|
|
||||||
public static readonly long OwnerId = 15652354820931584;
|
|
||||||
public static readonly string Prefix = "s/";
|
|
||||||
public static readonly string SourceLink = "https://github.com/SkyJoshua/SkyBot";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using SkyBot.Models;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared;
|
|
||||||
|
|
||||||
namespace SkyBot.Helpers
|
|
||||||
{
|
|
||||||
public static class MessageHelper
|
|
||||||
{
|
|
||||||
|
|
||||||
public static string Mention(this PlanetMember member) => $"«@m-{member.Id}»";
|
|
||||||
public static string Mention(this User user) => $"«@u-{user.Id}»";
|
|
||||||
public static string ToTitleCase(this string str) => System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str);
|
|
||||||
|
|
||||||
public static async Task<TaskResult<Message>> ReplyAsync(CommandContext ctx, Channel channel, string content)
|
|
||||||
{
|
|
||||||
long? replyToId = ctx.Message.ReplyToId.HasValue ? ctx.Message.ReplyToId : ctx.Message.Id;
|
|
||||||
|
|
||||||
var msg = new Message(ctx.Client)
|
|
||||||
{
|
|
||||||
Content = content,
|
|
||||||
ChannelId = channel.Id,
|
|
||||||
PlanetId = ctx.Planet.Id,
|
|
||||||
AuthorUserId = ctx.Client.Me.Id,
|
|
||||||
AuthorMemberId = channel.Planet?.MyMember.Id,
|
|
||||||
ReplyToId = replyToId,
|
|
||||||
Fingerprint = Guid.NewGuid().ToString()
|
|
||||||
};
|
|
||||||
return await ctx.Client.MessageService.SendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<TaskResult<Message>> EditAsync(Channel channel, Message message, string content)
|
|
||||||
{
|
|
||||||
message.Content = content;
|
|
||||||
return await channel.Planet.Node.PutAsyncWithResponse<Message>($"api/messages/{message.Id}", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DateTime? ParseDuration(string input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input)) return null;
|
|
||||||
|
|
||||||
var unit = input[^1];
|
|
||||||
if (!int.TryParse(input[..^1], out int value)) return null;
|
|
||||||
|
|
||||||
return unit switch
|
|
||||||
{
|
|
||||||
'm' => DateTime.UtcNow.AddMinutes(value),
|
|
||||||
'h' => DateTime.UtcNow.AddHours(value),
|
|
||||||
'd' => DateTime.UtcNow.AddDays(value),
|
|
||||||
'w' => DateTime.UtcNow.AddDays(value * 7),
|
|
||||||
'M' => DateTime.UtcNow.AddMonths(value),
|
|
||||||
'y' => DateTime.UtcNow.AddYears(value),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Authorization;
|
|
||||||
|
|
||||||
namespace SkyBot.Helpers
|
|
||||||
{
|
|
||||||
public static class PermissionHelper
|
|
||||||
{
|
|
||||||
// Planet-level permissions
|
|
||||||
public static bool HasPerm(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(p => member.HasPermission(p))
|
|
||||||
: permissions.Any(p => member.HasPermission(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat channel permissions
|
|
||||||
public static async Task<bool> HasPermAsync(PlanetMember member, Channel channel, ChatChannelPermission[] 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;
|
|
||||||
|
|
||||||
if (requireAll)
|
|
||||||
{
|
|
||||||
foreach (var p in permissions)
|
|
||||||
if (!await channel.HasPermissionAsync(member, p)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var p in permissions)
|
|
||||||
if (await channel.HasPermissionAsync(member, p)) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Voice channel permissions
|
|
||||||
public static async Task<bool> HasPermAsync(PlanetMember member, Channel channel, VoiceChannelPermission[] 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;
|
|
||||||
|
|
||||||
if (requireAll)
|
|
||||||
{
|
|
||||||
foreach (var p in permissions)
|
|
||||||
if (!await channel.HasPermissionAsync(member, p)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var p in permissions)
|
|
||||||
if (await channel.HasPermissionAsync(member, p)) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsOwner(PlanetMember member)
|
|
||||||
{
|
|
||||||
if (member == null) return false;
|
|
||||||
return member.UserId == Config.OwnerId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace SkyBot.Helpers
|
|
||||||
{
|
|
||||||
public static class ValourUsercountHelper {
|
|
||||||
private static readonly HttpClient _http = new HttpClient();
|
|
||||||
private static long _valourUsercount;
|
|
||||||
public static long ValourUsercount => _valourUsercount;
|
|
||||||
|
|
||||||
public static async Task UpdateUsercount()
|
|
||||||
{
|
|
||||||
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 StartUpdater()
|
|
||||||
{
|
|
||||||
var timer = new System.Timers.Timer(300_000);
|
|
||||||
timer.Elapsed += async (_, _) => await UpdateUsercount();
|
|
||||||
timer.AutoReset = true;
|
|
||||||
timer.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Models
|
|
||||||
{
|
|
||||||
public class CommandContext
|
|
||||||
{
|
|
||||||
public required ValourClient Client{ get; set; }
|
|
||||||
public required ConcurrentDictionary<long, Channel> ChannelCache { get; set; }
|
|
||||||
public required PlanetMember Member { get; set; }
|
|
||||||
public required Message Message { get; set; }
|
|
||||||
public required Planet Planet { get; set; }
|
|
||||||
public required long ChannelId { get; set; }
|
|
||||||
public required string[] Args { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace SkyBot.Models
|
|
||||||
{
|
|
||||||
public interface ICommand
|
|
||||||
{
|
|
||||||
string Name { get; }
|
|
||||||
string[] Aliases { get; }
|
|
||||||
string Description { get; }
|
|
||||||
string Section { get; }
|
|
||||||
string Usage { get; }
|
|
||||||
Task Execute(CommandContext ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using DotNetEnv;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
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 ValourUsercountHelper.UpdateUsercount();
|
|
||||||
ValourUsercountHelper.StartUpdater();
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using Valour.Shared.Models;
|
|
||||||
|
|
||||||
namespace SkyBot.Services
|
|
||||||
{
|
|
||||||
public static class ChannelService
|
|
||||||
{
|
|
||||||
public static async Task InitializeChannelsAsync(
|
|
||||||
ConcurrentDictionary<long, Channel> channelCache,
|
|
||||||
Planet planet)
|
|
||||||
{
|
|
||||||
foreach (var channel in planet.Channels)
|
|
||||||
{
|
|
||||||
channelCache[channel.Id] = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
foreach (var channel in planet.Channels.Where(c => c.ChannelType == ChannelTypeEnum.PlanetChat)){
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await channel.OpenWithResult("SkyBot");
|
|
||||||
Console.WriteLine($"Realtime opened for: {planet.Name} (ID: {planet.Id}) -> {channel.Name} (ID: {channel.Id})");
|
|
||||||
} catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error opening realtime for {channel.Id}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"All channels opened for {planet.Name}.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Commands;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
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,
|
|
||||||
Message message
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (message.AuthorUserId == client.Me.Id) return;
|
|
||||||
string prefix = Config.Prefix;
|
|
||||||
string content = message.Content ?? "";
|
|
||||||
if (string.IsNullOrWhiteSpace(content)) return;
|
|
||||||
long channelId = message.ChannelId;
|
|
||||||
PlanetMember member = await message.FetchAuthorMemberAsync();
|
|
||||||
noPrefixMessages(message, content);
|
|
||||||
var parts = content.Substring(prefix.Length).Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (parts.Length == 0) return;
|
|
||||||
string command = parts[0].ToLower();
|
|
||||||
string[] args = parts[1..];
|
|
||||||
CommandContext ctx = new CommandContext
|
|
||||||
{
|
|
||||||
ChannelCache = channelCache,
|
|
||||||
ChannelId = channelId,
|
|
||||||
Member = member,
|
|
||||||
Planet = message.Planet,
|
|
||||||
Args = args,
|
|
||||||
Message = message,
|
|
||||||
Client = client
|
|
||||||
};
|
|
||||||
|
|
||||||
async void noPrefixMessages(Message message, string content)
|
|
||||||
{
|
|
||||||
if (message.AuthorUserId == Config.OwnerId)
|
|
||||||
{
|
|
||||||
if (MessageHelper.IsSingleEmoji(content))
|
|
||||||
{
|
|
||||||
await message.AddReactionAsync(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!content.ToLower().StartsWith(prefix)) return;
|
|
||||||
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);
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
if (channelCache.TryGetValue(channelId, out var channel))
|
|
||||||
{
|
|
||||||
await MessageHelper.ReplyAsync(ctx, channel, $"Unknown command `{command}`.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.ModelLogic;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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 _ =>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using Valour.Sdk.Client;
|
|
||||||
using Valour.Sdk.Models;
|
|
||||||
using SkyBot.Services;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using SkyBot.Helpers;
|
|
||||||
|
|
||||||
namespace SkyBot
|
|
||||||
{
|
|
||||||
public class SkyBot
|
|
||||||
{
|
|
||||||
private readonly ValourClient _client;
|
|
||||||
private readonly ConcurrentDictionary<long, Channel> _channelCache = new();
|
|
||||||
private readonly ConcurrentDictionary<long, bool> _initializedPlanets = new();
|
|
||||||
public static DateTime StartTime;
|
|
||||||
|
|
||||||
public SkyBot()
|
|
||||||
{
|
|
||||||
_client = new ValourClient("https://api.valour.gg/");
|
|
||||||
_client.SetupHttpClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StartAsync()
|
|
||||||
{
|
|
||||||
StartTime = DateTime.UtcNow;
|
|
||||||
await DatabaseHelper.InitializeAsync();
|
|
||||||
await WelcomeService.InitializeAsync();
|
|
||||||
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 (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Fatal error: {ex.Message}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
138
utils.cs
Normal file
138
utils.cs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user