From 781b4d19d44e7e3e94eee9226e62e7e4c7374ad8 Mon Sep 17 00:00:00 2001 From: Skyedra <22365940+Skyedra@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:47:38 -0800 Subject: [PATCH] init --- .gitignore | 4 ++ AllRooms.cs | 25 ++++++++++++ LocalConfig.cs | 34 +++++++++++++++++ Menu.cs | 60 +++++++++++++++++++++++++++++ Program.cs | 7 ++++ Query.cs | 75 ++++++++++++++++++++++++++++++++++++ README.md | 6 +++ Room.cs | 86 ++++++++++++++++++++++++++++++++++++++++++ skyeMatrixTools.csproj | 14 +++++++ 9 files changed, 311 insertions(+) create mode 100644 .gitignore create mode 100644 AllRooms.cs create mode 100644 LocalConfig.cs create mode 100644 Menu.cs create mode 100644 Program.cs create mode 100644 Query.cs create mode 100644 README.md create mode 100644 Room.cs create mode 100644 skyeMatrixTools.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dbb761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +localSecrets.json +bin +obj +.vscode diff --git a/AllRooms.cs b/AllRooms.cs new file mode 100644 index 0000000..59bb1e6 --- /dev/null +++ b/AllRooms.cs @@ -0,0 +1,25 @@ +using System.Numerics; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +public class AllRooms +{ + public Dictionary roomList { get; private set; } = new Dictionary(); + public async Task PopulateRooms() + { + string result = await Query.Get("_matrix/client/r0/joined_rooms", true); + if (result != null) + { + JObject json = JObject.Parse(result); + JArray rooms = (JArray) json["joined_rooms"]; + + foreach (string internalID in rooms) + { + //Console.WriteLine("Room: " + roomName); + var room = new Room(internalID); + await room.GetName(); + roomList.Add(internalID, room); + } + } + } +} \ No newline at end of file diff --git a/LocalConfig.cs b/LocalConfig.cs new file mode 100644 index 0000000..381c0d8 --- /dev/null +++ b/LocalConfig.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json.Linq; + +public class LocalConfig +{ + public static string authToken + { + get + { + if (string.IsNullOrEmpty(_authToken)) + { + JObject localSecrets = JObject.Parse(File.ReadAllText("localSecrets.json")); + _authToken = (string) ((JObject) localSecrets["matrix"])["auth_token"]; + } + + return _authToken; + } + } + private static string _authToken; + + public static string homeserver + { + get + { + if (string.IsNullOrEmpty(_homeserver)) + { + JObject localSecrets = JObject.Parse(File.ReadAllText("localSecrets.json")); + _homeserver = (string) ((JObject) localSecrets["matrix"])["homeserver"]; + } + + return _homeserver; + } + } + private static string _homeserver; +} \ No newline at end of file diff --git a/Menu.cs b/Menu.cs new file mode 100644 index 0000000..f83f601 --- /dev/null +++ b/Menu.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; + +public class Menu +{ + public async Task ShowMenu(AllRooms allRooms) + { + bool quit = false; + while (!quit) + { + Console.WriteLine("Choose room to kick t2bots"); + foreach ((string internalID, Room room) in allRooms.roomList) + { + Console.WriteLine($"{room.name} -- {room.internalID}"); + } + Console.WriteLine("Type/paste internal ID:"); + + string? selectedInternalID = Console.ReadLine(); + if (selectedInternalID == null) + continue; + selectedInternalID = selectedInternalID.Trim(); + if (selectedInternalID == "") + continue; + + if (allRooms.roomList.ContainsKey(selectedInternalID)) + { + var room = allRooms.roomList[selectedInternalID]; + if (room != null) + { + Console.WriteLine("Enter 'd' for dry run or 'r' for real run"); + string key = Console.ReadLine(); + if (key == "d") + { + await room.KickT2Bots(true); + + Console.WriteLine("Do you want to kick for real? Enter 'r' or 'y'"); + string forReal = Console.ReadLine(); + if (forReal == "r" || forReal == "y") + { + await room.KickT2Bots(false); + + Console.WriteLine("Press enter to continue."); + Console.ReadLine(); + } + } + else if (key == "r") + { + await room.KickT2Bots(false); + Console.WriteLine("Press enter to continue."); + Console.ReadLine(); + } + else + Console.WriteLine("unknown input"); + Console.WriteLine("\n\n"); + } + } else { + Console.WriteLine("No such ID\n\n"); + } + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..83ca166 --- /dev/null +++ b/Program.cs @@ -0,0 +1,7 @@ +Console.WriteLine("Loading..."); +var allRooms = new AllRooms(); +await allRooms.PopulateRooms(); + +Console.WriteLine("Load done."); +var menu = new Menu(); +await menu.ShowMenu(allRooms); \ No newline at end of file diff --git a/Query.cs b/Query.cs new file mode 100644 index 0000000..eeb3806 --- /dev/null +++ b/Query.cs @@ -0,0 +1,75 @@ +using System.Threading.Tasks; + +public class Query +{ + public static async Task Get(string urlPath, bool withAuth, Dictionary extraParameters = null) + { + return await Call("GET", urlPath, withAuth, extraParameters); + } + + public static async Task Post(string urlPath, bool withAuth, string postData, Dictionary extraParameters = null) + { + return await Call("POST", urlPath, withAuth, extraParameters, postData); + } + + public static async Task Call(string method, string urlPath, bool withAuth, + Dictionary extraParameters = null, string postData = null) + { + string finalPath = LocalConfig.homeserver + urlPath; + + for (int i=0; i<10; i++) + { + using (var client = new HttpClient()) + { + if (withAuth) + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + LocalConfig.authToken); + + if (extraParameters != null) + { + foreach ((string key, string value) in extraParameters) + { + client.DefaultRequestHeaders.Add(key, value); + } + } + + HttpResponseMessage response; + if (method == "POST") + { + response = await client.PostAsync(finalPath, new StringContent(postData)); + } else { + response = await client.GetAsync(finalPath); + } + + if (response.IsSuccessStatusCode) + return await response.Content.ReadAsStringAsync(); + + // Retry logic + // ex: https://spec.matrix.org/v1.14/client-server-api/#rate-limiting + if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests) + { + if (i != 9) + await Task.Delay(i*1000 + 100); + else + Console.WriteLine("FATAL - too many requests even though we delayed"); + } else + { + Console.WriteLine("Problem with API request:" + response.StatusCode); + + if ((int) response.StatusCode >= 400 && (int) response.StatusCode < 500) + { + Console.WriteLine("Giving up on this one due to status code."); + return null; + } + + if (i != 9) + Console.WriteLine("Retry..."); + else + Console.WriteLine("FATAL"); + } + + } + } + + return null; + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1523c65 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Skye's Matrix Tools + +Tools for moderating Matrix rooms. Current features: + +- Ability to batch kick all t2 bots (for after turning off a discord server) +- End of list. diff --git a/Room.cs b/Room.cs new file mode 100644 index 0000000..c11fd25 --- /dev/null +++ b/Room.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +public class Room +{ + public string internalID { get; private set; } = ""; + public string name { get; private set; } = ""; + public List users { get; private set; } = new List(); + + public Room(string internalID) + { + this.internalID = internalID; + } + + public async Task GetName() + { + // https://rawrly.com/_matrix/client/r0/rooms/!IhFsOrvvOXcSaoMqDB:rawrly.com/state/m.room.name + string result = await Query.Get("_matrix/client/r0/rooms/" + internalID + "/state/m.room.name", true); + if (result != null) + { + JObject json = JObject.Parse(result); + name = (string) json["name"]; + } + } + + public void GetSpaceName() + { + + } + + public async Task GetMembers() + { + // https://rawrly.com/_matrix/client/r0/rooms/!IhFsOrvvOXcSaoMqDB:rawrly.com/members + string at = ""; + while (true) + { + Dictionary extraParameters = new Dictionary(); + if (at != "") + { + extraParameters.Add("at", at); + } + extraParameters.Add("membership", "join"); + + string result = await Query.Get($"_matrix/client/r0/rooms/{internalID}/members", true, extraParameters); + + JObject json = JObject.Parse(result); + if (json.ContainsKey("chunk")) + { + foreach (JObject member in (JArray) json["chunk"]) + { + string userID = (string) member["user_id"]; + if (!users.Contains(userID)) + users.Add(userID); + } + } + + // if (json.ContainsKey("")) + // (Not sure how pagination is handled, seems to just send everything for this one?) + return; + } + } + + internal async Task KickT2Bots(bool dryRun) + { + // Clear members list + users.Clear(); + await GetMembers(); + + foreach (var user in users) + { + if (user.EndsWith(":t2bot.io")) + { + if (dryRun) + Console.WriteLine($"Would kick: {user}"); + else + { + // KICK code + Console.WriteLine($"Kicking: {user}"); + JObject kickData = new JObject(); + kickData.Add("user_id", user); + string result = await Query.Post($"_matrix/client/v3/rooms/{internalID}/kick", true, kickData.ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/skyeMatrixTools.csproj b/skyeMatrixTools.csproj new file mode 100644 index 0000000..1ddcc23 --- /dev/null +++ b/skyeMatrixTools.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + +