commit 781b4d19d44e7e3e94eee9226e62e7e4c7374ad8 Author: Skyedra <22365940+Skyedra@users.noreply.github.com> Date: Wed Mar 4 15:47:38 2026 -0800 init 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 + + + + + + +