DomainDeleted
81 |
82 | ## Missing domains
83 | Found a Discord/Steam phishing domain that isn't yet present in the database? Send it into the `#domain-reports` channel on our Discord server or open an **issue** in this repository.
84 |
85 | ## Resources
86 | Need help, want to discuss phishing or have a suggestion? Feel free to join our Discord server: https://discord.gg/d63pvY28HU (temporarily closed)
87 |
88 | - Official website: https://sinking.yachts
89 | - Email: admin@fishfish.gg, sinkingyachts@gmail.com
90 | - GitHub: https://github.com/SinkingYachts
91 | - Blog: https://sinking.yachts/blog/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sinking Yachts 🐟
2 |
3 |
4 |

5 |
6 |
7 |
8 | A C# library for detecting Discord/Steam phishing links using the Sinking Yachts API.
9 |
10 |
11 |
12 |
13 | > **Warning**
14 | > Sinking Yachts is currently in the process of being discontinued and replaced with the upstream [Fish Fish API](https://fishfish.gg).
15 | > Expect this library to eventually become archived and marked as deprecated.
16 | > Daily statistics have already been shut down.
17 |
18 | ## Usage
19 | Available on NuGet as `SinkingYachts`, methods are available under the public class `YachtsClient`.
20 |
21 | https://www.nuget.org/packages/SinkingYachts
22 |
23 | ## Features
24 | - Made with **.NET 6**
25 | - Fully async
26 | - Access to a Discord-related phishing database of over `15 500` confirmed malicious domains
27 | - Regex matching of domains and automatic phishing detection
28 | - Different modes for storing and loading phishing domains
29 | - Instant updates through **WebSocket events**
30 | - Domain whitelisting to decrease false positives
31 | - Customizable caching to decrease load
32 |
33 | ## Example Project
34 | Under the `Example` directory you can find a working demo Discord bot that implements this library.
35 | ```rust
36 | 07.09. 19:13:59 [Discord] Discord.Net v3.8.0 (API v9)
37 | 07.09. 19:13:59 [Gateway] Connecting
38 | 07.09. 19:14:01 [Gateway] Connected
39 | 07.09. 19:14:02 [Bot] Ready to protect your server from 15601 phishing domains
40 | 07.09. 19:14:02 [Bot] Domains added within the past day: 8
41 | 07.09. 19:14:02 [Bot] Domains deleted within the past day: 0
42 | 07.09. 19:14:02 [Gateway] Ready
43 | ```
44 |
45 | ## Code Samples
46 |
47 | ### Check message content
48 | ```csharp
49 | bool isPhishing = await Yachts.IsPhishing("hello https://hypesquadacademy-apply.ml");
50 | //👉 True
51 | ```
52 |
53 | ### Check a domain
54 | ```csharp
55 | bool isPhishing = await Yachts.IsPhishingDomain("warning-selectioneventhype.gq");
56 | //👉 True
57 | ```
58 |
59 | ### Get the latest domains
60 | ```csharp
61 | string[] domains = (await Yachts.GetRecent(TimeSpan.FromDays(1))).Where(x => x.Type == ChangeType.Add).SelectMany(x => x.Domains).ToArray();
62 | //👉 steamcommunitysiv.top, wvwww-roblox.com, discord-download.win, steamcoumunity.eu, streamcummonity.com, streamcommunity.org, join-event-hypesquad.com, steamcommunityzowe.top
63 | ```
64 |
65 | ### Get the database size
66 | ```csharp
67 | int size = await Yachts.GetDatabaseSize();
68 | //👉 15601
69 | ```
70 |
71 | ## Available methods
72 | - Task GetRecent(TimeSpan time)
73 | - Task GetRecent(int seconds)
74 | - Task IsPhishing(string content)
75 | - Task IsPhishingDomain(string domain)
76 | - Task GetDatabaseSize()
77 | - Task GetPhishingDomains()
78 |
79 | ## Available events (requires `StorageMode.LocalWS`)
80 | - EventHandler\ DomainAdded
81 | - EventHandler\ DomainDeleted
82 |
83 | ## Statistics from the past week
84 | | Date | New domains found |
85 | | :---: | :---: |
86 | | 10.11.2022 | + `37` |
87 | | 11.11.2022 | + `14` |
88 | | 12.11.2022 | + `25` |
89 | | 13.11.2022 | + `23` |
90 | | 14.11.2022 | + `21` |
91 | | 15.11.2022 | + `30` |
92 | | 16.11.2022 | + `14` |
93 |
94 | ## Recently flagged domains
95 | ```ruby
96 | stcommunity.click
97 | steamppowered.store
98 | steamcommunltu.online
99 | steamncommnuity.com
100 | staemconnunity.ru
101 | steamcomrunity.ru
102 | dlscordjbost.com
103 | steamcommynltiy.net.ru
104 | steamcommynltiy.pp.ru
105 | steancommunutty.ru
106 | hypesquad-gg.com
107 | ```
108 |
109 | ## Missing domains
110 | Found a Discord/Steam phishing domain that isn't yet present in the database? Send it into the `#domain-reports` channel on our Discord server or open an **issue** in this repository.
111 |
112 | ## Resources
113 | The official Sinking Yachts Discord server has disabled invites at the moment. This will be updated once the server is open to the public again.
114 |
115 | - Official website: https://sinking.yachts
116 | - Email: admin@fishfish.gg, sinkingyachts@gmail.com
117 | - GitHub: https://github.com/SinkingYachts
118 | - Blog: https://sinking.yachts/blog/
119 |
--------------------------------------------------------------------------------
/SinkingYachts/Connection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Net.WebSockets;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using System.Text.Json.Serialization;
8 |
9 | namespace SinkingYachts
10 | {
11 | ///
12 | /// The class for connecting and receiving messages from the real-time WebSocket server.
13 | ///
14 | public class Connection
15 | {
16 | ///
17 | /// The WebSocket URI to connect to.
18 | ///
19 | public static readonly Uri Feed = new("wss://phish.sinking.yachts/feed");
20 |
21 | ///
22 | /// How long to wait before reconnecting after a connection is lost.
23 | ///
24 | public const int ReconnectionDelay = 10000;
25 |
26 | private ClientWebSocket WS;
27 | private readonly CancellationTokenSource Source = new();
28 |
29 | ///
30 | /// Whether there is currently a connection to the phishing feed.
31 | ///
32 | public bool Connected = false;
33 |
34 | ///
35 | /// Executes whenever a phishing domain is added into the database.
36 | ///
37 | public EventHandler DomainAdded;
38 |
39 | ///
40 | /// Executes whenever a phishing domain is removed from the database.
41 | ///
42 | public EventHandler DomainDeleted;
43 |
44 | private readonly string Identity;
45 |
46 | ///
47 | /// Default constructor for the connection class.
48 | ///
49 | ///
50 | public Connection(string identity)
51 | {
52 | Identity = identity;
53 |
54 | Connect();
55 | }
56 |
57 | ///
58 | /// Connects to the remote WebSocket server to start receiving updates.
59 | ///
60 | public async void Connect()
61 | {
62 | Connected = false;
63 | WS = new ClientWebSocket();
64 | WS.Options.SetRequestHeader("X-Identity", Identity);
65 |
66 | try
67 | {
68 | await WS.ConnectAsync(Feed, Source.Token);
69 | }
70 | catch
71 | {
72 | Connected = false;
73 | await Task.Delay(ReconnectionDelay);
74 | Connect();
75 | return;
76 | }
77 |
78 | while (WS.State == WebSocketState.Open)
79 | {
80 | Connected = true;
81 | var receiveBuffer = new byte[1024];
82 | var offset = 0;
83 |
84 | while (true)
85 | {
86 | try
87 | {
88 | ArraySegment bytesReceived = new(receiveBuffer, offset, receiveBuffer.Length);
89 |
90 | WebSocketReceiveResult result = await WS.ReceiveAsync(bytesReceived, Source.Token);
91 | offset += result.Count;
92 |
93 | if (result.EndOfMessage) break;
94 | }
95 | catch { break; };
96 | }
97 |
98 | if (offset != 0) OnMessage(Encoding.UTF8.GetString(receiveBuffer, 0, offset));
99 | }
100 |
101 | Connected = false;
102 | await Task.Delay(ReconnectionDelay);
103 | Connect();
104 | }
105 |
106 | ///
107 | /// Called when a WebSocket message is received.
108 | ///
109 | public void OnMessage(string msg)
110 | {
111 | Change data;
112 |
113 | try
114 | {
115 | JsonSerializerOptions opt = new();
116 | opt.Converters.Add(new JsonStringEnumConverter());
117 |
118 | data = JsonSerializer.Deserialize(msg, opt);
119 | }
120 | catch (Exception ex)
121 | {
122 | throw new($"Failed to deserialize database change: {ex.GetType().Name} => {ex.Message}\nMessage: {msg}");
123 | }
124 |
125 | if (data.Domains is null) throw new($"Domains in the update event are null.\nMessage: {msg}");
126 |
127 | foreach (string domain in data.Domains)
128 | {
129 | if (data.Type == ChangeType.Add) DomainAdded.Invoke(this, domain);
130 | else if (data.Type == ChangeType.Delete) DomainDeleted.Invoke(this, domain);
131 | }
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/SinkingYachts/SinkingYachts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Reflection;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 | using System.Text.RegularExpressions;
9 | using System.Threading.Tasks;
10 | using System.Timers;
11 |
12 | namespace SinkingYachts
13 | {
14 | ///
15 | /// Storage Mode sets the different modes of storing and loading phishing domains.
16 | ///
is the easiest to use, but sends a lot of HTTP requests.
17 | ///
takes up a lot of memory, and some domains can be missed if they are aren't synced yet.
18 | ///
is the most powerful option if you want to precisely collect domains.
19 | ///
20 | public enum StorageMode
21 | {
22 | ///
23 | /// Domains are only cached after seen first. This option sends an API request for every non-cached domain.
24 | ///
25 | Remote,
26 | ///
27 | /// All domains are downloaded and cached immediately. The cache is updated every 15 minutes.
28 | ///
29 | Local,
30 | ///
31 | /// Same as , but persists a WebSocket connection to Sinking Yachts, allowing it to receive new domains in real time.
32 | ///
This makes sure that no domains slip through the detection due to not being synced yet.
33 | ///
34 | LocalWS
35 | }
36 |
37 | ///
38 | /// An enum holding the type of a change. Can be either add or delete.
39 | ///
40 | public enum ChangeType
41 | {
42 | ///
43 | /// Domain add event.
44 | ///
45 | [JsonPropertyName("add")]
46 | Add,
47 |
48 | ///
49 | /// Domain delete event.
50 | ///
51 | [JsonPropertyName("delete")]
52 | Delete
53 | }
54 |
55 | ///
56 | /// A little class that implements the domain add/update structure.
57 | ///
58 | public class Change
59 | {
60 | ///
61 | /// The type of the event. Can be either add or delete.
62 | ///
63 | [JsonPropertyName("type")]
64 | public ChangeType Type { get; set; }
65 |
66 | ///
67 | /// A list of domains that have changed in this event. This is always one domain, but it's sent in an array for potential bulk imports.
68 | ///
69 | [JsonPropertyName("domains")]
70 | public string[] Domains { get; set; }
71 | }
72 |
73 | ///
74 | /// The main class to run anti-phishing checks.
75 | ///
76 | public class YachtsClient
77 | {
78 | ///
79 | /// The official domains of Discord, Steam, Roblox and Github. If a sent domain is present in this array, it's immediately returned as safe.
80 | ///
81 | private static readonly string[] OfficialDomains = new[]
82 | {
83 | "discord.com",
84 | "discord.gg",
85 | "discordapp.com",
86 | "discordapp.net",
87 | "discord.media",
88 | "discordstatus.com",
89 | "steamcommunity.com",
90 | "steamgames.com",
91 | "steampowered.com",
92 | "valve.net",
93 | "valvesoftware.com",
94 | "roblox.com",
95 | "www.roblox.com",
96 | "github.com",
97 | "githubusercontent.com",
98 | "raw.githubusercontent.com"
99 | };
100 | ///
101 | /// A cache to store API response values.
102 | ///
103 | public readonly Dictionary Cache = new();
104 |
105 | private readonly HttpClient Client;
106 | private readonly Regex UrlRegex = new(@"(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])", RegexOptions.Compiled);
107 |
108 | private const int RefreshInterval = 1000 * 60 * 15;
109 | private static Timer Refresher;
110 | private const string Api = "https://phish.sinking.yachts";
111 | private const int Version = 2;
112 |
113 | private static Connection Con;
114 |
115 | private readonly string Identity;
116 | private readonly TimeSpan CachePeriod;
117 | private readonly StorageMode Mode;
118 |
119 | ///
120 | /// Executes whenever a phishing domain is added into the database.
121 | ///
122 | public EventHandler DomainAdded;
123 |
124 | ///
125 | /// Executes whenever a phishing domain is deleted from the database.
126 | ///
127 | public EventHandler DomainDeleted;
128 |
129 | ///
130 | /// Creates a new instance of the Sinking Yachts client.
131 | ///
132 | /// The domain storage mode to use.
133 | /// A short string identifying your bot application. By default this is the name of your project.
134 | /// How long in hours should be API responses cached for.
135 | public YachtsClient(StorageMode mode, string identity = null, int cachePeriodHours = 3)
136 | {
137 | Mode = mode;
138 | Identity = $"https://github.com/actually-akac/SinkingYachts | {identity ?? Assembly.GetEntryAssembly().GetName().Name}";
139 | CachePeriod = TimeSpan.FromHours(cachePeriodHours);
140 |
141 | Client = new();
142 | Client.DefaultRequestHeaders.Add("X-Identity", Identity);
143 |
144 | switch (Mode)
145 | {
146 | case StorageMode.Local:
147 | {
148 | UpdateCache();
149 |
150 | Refresher = new()
151 | {
152 | Interval = RefreshInterval
153 | };
154 | Refresher.Elapsed += (o, e) => UpdateCache();
155 | Refresher.Start();
156 |
157 | break;
158 | }
159 | case StorageMode.LocalWS:
160 | {
161 | UpdateCache();
162 |
163 | Refresher = new()
164 | {
165 | Interval = RefreshInterval
166 | };
167 | Refresher.Elapsed += (o, e) => UpdateCache();
168 | Refresher.Start();
169 |
170 | Con = new Connection(Identity);
171 |
172 | Con.DomainAdded += (sender, domain) => Cache[domain] = true;
173 | Con.DomainAdded += (sender, domain) => DomainAdded(sender, domain);
174 |
175 | Con.DomainDeleted += (sender, domain) => Cache[domain] = false;
176 | Con.DomainDeleted += (sender, domain) => DomainDeleted(sender, domain);
177 |
178 | break;
179 | }
180 | }
181 | }
182 |
183 | ///
184 | /// Updates the cache with fresh domains. Not used with .
185 | ///
186 | private async void UpdateCache()
187 | {
188 | string[] all = await GetPhishingDomains();
189 |
190 | foreach (string key in Cache.Keys)
191 | {
192 | if (all.Contains(key)) Cache[key] = true;
193 | else Cache[key] = false;
194 | }
195 |
196 | foreach (string domain in all)
197 | {
198 | bool exists = Cache.ContainsKey(domain);
199 |
200 | if (!exists) Cache[domain] = true;
201 | }
202 | }
203 |
204 | ///
205 | /// Checks whether a provided Discord message content contains phishing domains.
206 | ///
207 | /// The message content to check.
208 | ///
209 | public async Task IsPhishing(string content)
210 | {
211 | if (string.IsNullOrEmpty(content)) return false;
212 |
213 | MatchCollection matches = UrlRegex.Matches(content);
214 |
215 | foreach (Match match in matches)
216 | {
217 | bool success = Uri.TryCreate(match.Value, UriKind.Absolute, out Uri uri);
218 | if (!success) continue;
219 |
220 | if (await IsPhishingDomain(uri.Host)) return true;
221 | }
222 |
223 | return false;
224 | }
225 |
226 | ///
227 | /// Checks whether a provided domain is known to be a phish site.
228 | ///
229 | /// The domain to check.
230 | ///
231 | public async Task IsPhishingDomain(string domain)
232 | {
233 | if (OfficialDomains.Contains(domain)) return false;
234 | if (Cache.TryGetValue(domain, out bool output)) return output;
235 |
236 | HttpResponseMessage res = await Client.GetAsync($"{Api}/v{Version}/check/{domain}");
237 | string content = await res.Content.ReadAsStringAsync();
238 |
239 | if (!res.IsSuccessStatusCode) throw new($"Unexpected response while checking {domain}: {res.StatusCode}, {content}");
240 |
241 | output = bool.Parse(content);
242 | Cache[domain] = output;
243 |
244 | Task remover = Task.Delay(CachePeriod).ContinueWith(x =>
245 | {
246 | Cache.Remove(domain);
247 | });
248 |
249 | return output;
250 | }
251 |
252 | ///
253 | /// Gets the entire list of all known phishing domains.
254 | ///
255 | ///
256 | public async Task GetPhishingDomains()
257 | {
258 | HttpResponseMessage res = await Client.GetAsync($"{Api}/v{Version}/text");
259 | string content = await res.Content.ReadAsStringAsync();
260 |
261 | if (!res.IsSuccessStatusCode) throw new($"Unexpected response while fetching all phishing domains: {res.StatusCode}, {content}");
262 |
263 | return content.Split('\n');
264 | }
265 |
266 | ///
267 | /// Fetches the total amount of flagged domains in the database.
268 | ///
269 | public async Task GetDatabaseSize()
270 | {
271 | HttpResponseMessage res = await Client.GetAsync($"{Api}/v{Version}/dbsize");
272 | string content = await res.Content.ReadAsStringAsync();
273 |
274 | if (!res.IsSuccessStatusCode) throw new($"Unexpected response while fetching databse size: {res.StatusCode}, {content}");
275 |
276 | bool success = int.TryParse(content, out int result);
277 |
278 | if (!success)
279 | throw new($"Couldn't parse string {content} as domain count.");
280 |
281 | return result;
282 | }
283 |
284 | ///
285 | /// Fetches the domains added or deleted within the last X seconds.
286 | ///
287 | public async Task GetRecent(int seconds)
288 | {
289 | if (seconds > 604800) throw new ArgumentException("Maximum value is 604800 seconds (7 days).", nameof(seconds));
290 | if (seconds <= 0) throw new ArgumentException("Argument has to be positive.", nameof(seconds));
291 |
292 | HttpResponseMessage res = await Client.GetAsync($"{Api}/v{Version}/recent/{seconds}");
293 | string content = await res.Content.ReadAsStringAsync();
294 |
295 | if (!res.IsSuccessStatusCode) throw new($"Unexpected response while fetching recent changes: {res.StatusCode}, {content}");
296 |
297 | try
298 | {
299 | JsonSerializerOptions opt = new();
300 | opt.Converters.Add(new JsonStringEnumConverter());
301 |
302 | return JsonSerializer.Deserialize(content, opt);
303 | }
304 | catch (Exception ex)
305 | {
306 | throw new Exception($"Failed to deserialize database changes: {ex.GetType().Name} => {ex.Message}\nJSON: {content}");
307 | }
308 | }
309 |
310 | ///
311 | /// Fetches the domains added or deleted within the provided TimeSpan.
312 | ///
313 | public async Task GetRecent(TimeSpan time)
314 | {
315 | return await GetRecent((int)time.TotalSeconds);
316 | }
317 | }
318 | }
--------------------------------------------------------------------------------