4 using System.Collections.Immutable;
7 using System.Net.Cache;
8 using System.Threading.Tasks;
10 using System.Xml.Linq;
35 internal readonly record
struct SpamFilter(ImmutableHashSet<(
SpamServerFilterType Type, string Value)> Filters)
39 if (!Filters.Any()) {
return false; }
41 foreach (var (type, value) in Filters)
43 if (!IsFiltered(info, type, value)) {
return false; }
54 int.TryParse(value, out
int parsedInt);
79 static bool CompareEquals(
string a,
string b)
80 => a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b);
82 static bool CompareContains(
string a,
string b)
83 => a.Contains(b, StringComparison.OrdinalIgnoreCase);
86 public XElement Serialize()
88 var element =
new XElement(
"Filter");
90 foreach (var (type, value) in Filters)
92 element.Add(
new XAttribute(type.ToString().ToLowerInvariant(), value));
98 public static bool TryParse(XElement element, out SpamFilter filter)
101 foreach (var attribute
in element.Attributes())
105 DebugConsole.ThrowError($
"Failed to parse spam filter attribute \"{attribute.Name}\"");
109 builder.Add((e, attribute.Value));
114 filter =
new SpamFilter(builder.ToImmutable());
122 public override string ToString()
124 return !Filters.Any() ?
"Invalid Filter" :
string.Join(
", ", Filters.Select(
static f => $
"{f.Type}: {f.Value}"));
128 internal sealed
class SpamServerFilter
130 public readonly ImmutableArray<SpamFilter> Filters;
134 foreach (var f
in Filters)
136 if (f.IsFiltered(info)) {
return true; }
142 public SpamServerFilter(XElement element)
144 var builder = ImmutableArray.CreateBuilder<SpamFilter>();
145 foreach (var subElement
in element.Elements())
147 if (SpamFilter.TryParse(subElement, out var filter))
152 Filters = builder.ToImmutable();
155 public SpamServerFilter(ImmutableArray<SpamFilter> filters)
156 => Filters = filters;
158 public readonly
static string SavePath = Path.Combine(
"Data",
"serverblacklist.xml");
160 public void Save(
string path)
162 var comment =
new XComment(SpamServerFilters.LocalFilterComment);
163 var doc =
new XDocument(comment,
new XElement(
"Filters"));
164 foreach (var filter
in Filters)
166 doc.Root?.Add(filter.Serialize());
171 using var writer =
XmlWriter.
Create(path,
new XmlWriterSettings { Indent =
true });
172 doc.SaveSafe(writer);
176 DebugConsole.ThrowError(
"Saving spam filter failed.", e);
181 internal static class SpamServerFilters
183 public static Option<SpamServerFilter> LocalSpamFilter;
184 public static Option<SpamServerFilter> GlobalSpamFilter;
186 public const string LocalFilterComment =
@"
187 This file contains a list of filters that can be used to hide servers from the server list.
188 You can add filters by right-clicking a server in the server list and selecting ""Hide server"" or by reporting the server and choosing ""Report and hide server"".
189 The filters are saved in this file, which you can edit manually if you want to.
191 The available filter types are:
192 - NameEquals: The server name must equal the specified value. Homoglyphs are also checked.
193 - NameContains: The server name must contain the specified value.
194 - MessageEquals: The server description must equal the specified value. Homoglyphs are also checked.
195 - MessageContains: The server description must contain the specified value.
196 - PlayerCountLarger: The player count must be larger than the specified value.
197 - PlayerCountExact: The player count must match the specified value exactly.
198 - MaxPlayersLarger: The max player count must be larger than the specified value.
199 - MaxPlayersExact: The max player count must match the specified value exactly.
200 - GameModeEquals: The game mode identifier must match the specified value exactly.
201 - PlayStyleEquals: The play style must match the specified value exactly.
202 - Endpoint: The server endpoint, which is a Steam ID or an IP address, must match the specified value exactly. Steam ID is in the format of STEAM_X:Y:Z.
203 - LanguageEquals: The server language must match the specified value exactly.
205 The filter values are case-insensitive and adding multiple conditions on one filter will require all of them to be met.
206 Homoglyph comparison is used for NameEquals and MessageEquals filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal.
210 <Filter namecontains=""discord.gg"" />
211 <Filter messagecontains=""discord.gg"" />
212 <Filter nameequals=""get good get lmaobox"" maxplayersexact=""999"" />
214 These will hide all servers that have a discord.gg link in their name or description and servers with the name ""get good get lmaobox"" that have 999 max players.
216 static SpamServerFilters()
219 if (!File.Exists(SpamServerFilter.SavePath))
221 var comment =
new XComment(LocalFilterComment);
223 doc =
new XDocument(comment,
new XElement(
"Filters"));
227 using var writer =
XmlWriter.
Create(SpamServerFilter.SavePath,
new XmlWriterSettings { Indent = true });
228 doc.SaveSafe(writer);
232 DebugConsole.ThrowError(
"Saving spam filter failed.", e);
237 doc = XMLExtensions.TryLoadXml(SpamServerFilter.SavePath);
240 if (doc?.Root is { } root)
242 LocalSpamFilter = Option.Some(
new SpamServerFilter(root));
246 public static bool IsFiltered(
ServerInfo info)
248 if (LocalSpamFilter.TryUnwrap(out var localFilter) && localFilter.IsFiltered(info)) {
return true; }
249 if (GlobalSpamFilter.TryUnwrap(out var globalFilter) && globalFilter.IsFiltered(info)) {
return true; }
253 public static void AddServerToLocalSpamList(
ServerInfo info)
255 if (!LocalSpamFilter.TryUnwrap(out var localFilter)) {
return; }
256 if (localFilter.IsFiltered(info)) {
return; }
259 var newFilter =
new SpamServerFilter(filters);
260 newFilter.Save(SpamServerFilter.SavePath);
261 LocalSpamFilter = Option.Some(newFilter);
264 public static void ClearLocalSpamFilter()
266 var newFilter =
new SpamServerFilter(ImmutableArray<SpamFilter>.Empty);
267 newFilter.Save(SpamServerFilter.SavePath);
268 LocalSpamFilter = Option.Some(newFilter);
271 public static void RequestGlobalSpamFilter()
273 if (GameSettings.CurrentConfig.DisableGlobalSpamList) {
return; }
275 string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl;
276 if (
string.IsNullOrEmpty(remoteContentUrl)) {
return; }
280 var client =
new RestClient($
"{remoteContentUrl}spamfilter")
282 CachePolicy =
new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore)
284 client.AddDefaultHeader(
"Cache-Control",
"no-cache");
285 client.AddDefaultHeader(
"Pragma",
"no-cache");
286 var request =
new RestRequest(
"serve_spamlist.php", Method.GET);
287 TaskPool.Add(
"RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived);
292 DebugConsole.ThrowError(
"Fetching global spam list failed.", e);
294 GameAnalyticsManager.AddErrorEventOnce(
"SpamServerFilters.RequestGlobalSpamFilter:Exception", GameAnalyticsManager.ErrorSeverity.Error,
295 "Fetching global spam list failed. " + e.Message);
298 static void RemoteContentReceived(Task t)
302 if (!t.TryGetResult(out IRestResponse? remoteContentResponse)) {
throw new Exception(
"Task did not return a valid result"); }
303 if (remoteContentResponse.StatusCode != HttpStatusCode.OK)
305 DebugConsole.AddWarning(
306 "Failed to receive global spam filter." +
307 "There may be an issue with your internet connection, or the master server might be temporarily unavailable " +
308 $
"(error code: {remoteContentResponse.StatusCode})");
311 string data = remoteContentResponse.Content;
312 if (
string.IsNullOrWhiteSpace(data)) {
return; }
314 if (XDocument.Parse(data).Root is { } root)
316 GlobalSpamFilter = Option.Some(
new SpamServerFilter(root));
322 DebugConsole.ThrowError(
"Reading received global spam filter failed.", e);
324 GameAnalyticsManager.AddErrorEventOnce(
"SpamServerFilters.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error,
325 "Reading received global spam filter failed. " + e.Message);
static XmlWriter Create(string path, System.Xml.XmlWriterSettings settings)
ImmutableArray< Endpoint > Endpoints
LanguageIdentifier Language
readonly Identifier Value