1+ using System . Net . Http . Json ;
2+ using System . Text . Json ;
3+ using System . Text . Json . Serialization ;
4+ using CounterStrikeSharp . API ;
5+ using CounterStrikeSharp . API . Core ;
6+ using CounterStrikeSharp . API . Core . Attributes . Registration ;
7+ using CounterStrikeSharp . API . Modules . Commands ;
8+ using CounterStrikeSharp . API . Modules . Timers ;
9+ using Microsoft . Extensions . Logging ;
10+
11+ namespace AzLink . CounterStrikeSharp ;
12+
13+ public class AzLink : BasePlugin , IPluginConfig < AzLinkConfig >
14+ {
15+ private const string AzLinkVersion = "1.0.0" ;
16+
17+ private HttpClient client = new ( ) ;
18+
19+ private DateTime lastFullSent = DateTime . Now ;
20+ private DateTime lastSent = DateTime . Now ;
21+
22+ public override string ModuleName => "AzLink" ;
23+ public override string ModuleAuthor => "Azuriom" ;
24+ public override string ModuleVersion => AzLinkVersion ;
25+ public override string ModuleDescription => "Link your Azuriom website with an Counter-Strike 2 server." ;
26+
27+ public AzLinkConfig Config { get ; set ; } = new ( ) ;
28+
29+ public void OnConfigParsed ( AzLinkConfig config )
30+ {
31+ Config = config ;
32+
33+ InitHttpClient ( ) ;
34+
35+ if ( config . SiteKey == null || config . Url == null )
36+ {
37+ Logger . LogWarning ( "AzLink is not configured yet." ) ;
38+ }
39+ }
40+
41+ public override void Load ( bool hotReload )
42+ {
43+ AddTimer ( 60 , TryFetch , TimerFlags . REPEAT ) ;
44+ }
45+
46+ [ ConsoleCommand ( "azlink_setup" , "Setup AzLink" ) ]
47+ [ CommandHelper ( whoCanExecute : CommandUsage . SERVER_ONLY ) ]
48+ public void OnSetupCommand ( CCSPlayerController ? player , CommandInfo commandInfo )
49+ {
50+ if ( commandInfo . ArgCount < 3 )
51+ {
52+ commandInfo . ReplyToCommand (
53+ "You must first add this server in your Azuriom admin dashboard, in the 'Servers' section." ) ;
54+ return ;
55+ }
56+
57+ Config . Url = commandInfo . GetArg ( 1 ) ;
58+ Config . SiteKey = commandInfo . GetArg ( 2 ) ;
59+
60+ InitHttpClient ( ) ;
61+
62+ PingWebsite ( ( ) =>
63+ {
64+ commandInfo . ReplyToCommand ( "Linked to the website successfully." ) ;
65+ SaveConfig ( ) ;
66+ } , code =>
67+ {
68+ commandInfo . ReplyToCommand ( $ "An error occurred, code { code } ") ;
69+ Config . Url = null ;
70+ } ) ;
71+ }
72+
73+ [ ConsoleCommand ( "azlink_status" , "Check the status of AzLink" ) ]
74+ [ CommandHelper ( whoCanExecute : CommandUsage . SERVER_ONLY ) ]
75+ public void OnStatusCommand ( CCSPlayerController ? player , CommandInfo commandInfo )
76+ {
77+ if ( Config . Url == null )
78+ {
79+ commandInfo . ReplyToCommand ( "AzLink is not configured yet, use the 'setup' subcommand first." ) ;
80+ return ;
81+ }
82+
83+ PingWebsite ( ( ) => commandInfo . ReplyToCommand ( "Connected to the website successfully." ) ,
84+ code => commandInfo . ReplyToCommand ( $ "An error occurred, code { code } ") ) ;
85+ }
86+
87+ [ ConsoleCommand ( "azlink_fetch" , "Fetch data from the website" ) ]
88+ [ CommandHelper ( whoCanExecute : CommandUsage . SERVER_ONLY ) ]
89+ public void OnFetchCommand ( CCSPlayerController ? player , CommandInfo commandInfo )
90+ {
91+ if ( Config . Url == null )
92+ {
93+ commandInfo . ReplyToCommand ( "AzLink is not configured yet, use the 'setup' subcommand first." ) ;
94+ return ;
95+ }
96+
97+ RunFetch ( res =>
98+ {
99+ DispatchCommands ( res . Commands ) ;
100+
101+ commandInfo . ReplyToCommand ( "Data has been fetched successfully." ) ;
102+ } , code => commandInfo . ReplyToCommand ( $ "An error occurred, code { code } ") , true ) ;
103+ }
104+
105+ private void TryFetch ( )
106+ {
107+ var now = DateTime . Now ;
108+
109+ if ( Config . Url == null || Config . SiteKey == null )
110+ {
111+ return ;
112+ }
113+
114+ if ( ( now - lastSent ) . TotalSeconds < 15 )
115+ {
116+ return ;
117+ }
118+
119+ lastSent = now ;
120+
121+ var full = now . Minute % 15 == 0 && ( now - lastFullSent ) . TotalSeconds >= 60 ;
122+
123+ if ( full )
124+ {
125+ lastFullSent = now ;
126+ }
127+
128+ RunFetch ( res => DispatchCommands ( res . Commands ) ,
129+ code => Logger . LogError ( "Unable to send data to the website (code {code})" , code ) , full ) ;
130+ }
131+
132+ private void RunFetch ( Action < FetchResponse > callback , Action < int > errorHandler , bool sendFullData )
133+ {
134+ //Server.NextFrameAsync(() =>
135+ //{
136+ var data = GetServerData ( sendFullData ) ;
137+
138+ FetchAsync ( callback , errorHandler , data ) ;
139+ //});
140+ }
141+
142+ private async void FetchAsync < T > ( Action < FetchResponse > callback , Action < int > errorHandler , T data )
143+ {
144+ try
145+ {
146+ var res = await client . PostAsJsonAsync ( "/api/azlink" , data ) ;
147+
148+ if ( ! res . IsSuccessStatusCode )
149+ {
150+ await Server . NextFrameAsync ( ( ) => errorHandler ( ( int ) res . StatusCode ) ) ;
151+ return ;
152+ }
153+
154+ var fetchRes = await res . Content . ReadFromJsonAsync < FetchResponse > ( ) ;
155+
156+ if ( fetchRes == null )
157+ {
158+ throw new ApplicationException ( "Unable to parse the response from the website." ) ;
159+ }
160+
161+ await Server . NextFrameAsync ( ( ) => callback ( fetchRes ) ) ;
162+ }
163+ catch ( Exception e )
164+ {
165+ Logger . LogError ( "An error occurred while fetching data from the website: {error}" , e . Message ) ;
166+
167+ Console . WriteLine ( e ) ;
168+ }
169+ }
170+
171+ private async void PingWebsite ( Action onSuccess , Action < int > errorHandler )
172+ {
173+ if ( Config . Url == null || Config . SiteKey == null )
174+ {
175+ throw new ApplicationException ( "AzLink is not configured yet." ) ;
176+ }
177+
178+ try
179+ {
180+ var res = await client . GetAsync ( "/api/azlink" ) ;
181+
182+ if ( ! res . IsSuccessStatusCode )
183+ {
184+ await Server . NextFrameAsync ( ( ) => errorHandler ( ( int ) res . StatusCode ) ) ;
185+
186+ return ;
187+ }
188+
189+ await Server . NextFrameAsync ( onSuccess ) ;
190+ }
191+ catch ( Exception e )
192+ {
193+ Logger . LogError ( "An error occurred while pinging the website: {error}" , e . Message ) ;
194+
195+ Console . WriteLine ( e ) ;
196+ }
197+ }
198+
199+ private void DispatchCommands ( ICollection < PendingCommand > commands )
200+ {
201+ if ( commands . Count == 0 )
202+ {
203+ return ;
204+ }
205+
206+ foreach ( var info in commands )
207+ {
208+ var player = Utilities . GetPlayerFromSteamId ( ulong . Parse ( info . UserId ) ) ;
209+ var name = player ? . PlayerName ?? info . UserName ;
210+ var id = player ? . UserId ? . ToString ( ) ?? info . UserId ;
211+
212+ foreach ( var command in info . Values )
213+ {
214+ var cmd = command . Replace ( "{player}" , name )
215+ . Replace ( "{id}" , id )
216+ . Replace ( "{steam_id}" , info . UserId ) ;
217+
218+ Logger . LogInformation ( "Dispatching command to {Name} ({User}): {Command}" , name , info . UserId , cmd ) ;
219+
220+ Server . ExecuteCommand ( cmd ) ;
221+ }
222+ }
223+
224+ Logger . LogInformation ( "Dispatched commands to {Count} players." , commands . Count ) ;
225+ }
226+
227+ private Dictionary < string , object > GetServerData ( bool includeFullData )
228+ {
229+ var online = Utilities . GetPlayers ( ) . Select ( player => new Dictionary < string , string >
230+ {
231+ { "name" , player . PlayerName } , { "uid" , player . SteamID . ToString ( ) }
232+ } ) ;
233+ var data = new Dictionary < string , object >
234+ {
235+ {
236+ "platform" , new Dictionary < string , string >
237+ {
238+ { "type" , "COUNTER_STRIKE_SHARP" } ,
239+ { "name" , "CounterStrikeSharp" } ,
240+ { "version" , Api . GetVersionString ( ) } ,
241+ { "key" , "uid" }
242+ }
243+ } ,
244+ { "version" , ModuleVersion } ,
245+ { "players" , online . ToArray ( ) } ,
246+ { "maxPlayers" , Server . MaxPlayers } ,
247+ { "full" , includeFullData }
248+ } ;
249+
250+ if ( includeFullData )
251+ {
252+ data . Add ( "ram" , GC . GetTotalMemory ( false ) / 1024 / 1024 ) ;
253+ }
254+
255+ return data ;
256+ }
257+
258+ private void InitHttpClient ( )
259+ {
260+ if ( Config . Url == null || Config . SiteKey == null )
261+ {
262+ return ;
263+ }
264+
265+ client = new ( ) ;
266+ client . BaseAddress = new Uri ( Config . Url ) ;
267+ client . DefaultRequestHeaders . Clear ( ) ;
268+ client . DefaultRequestHeaders . Add ( "Azuriom-Link-Token" , Config . SiteKey ) ;
269+ client . DefaultRequestHeaders . Add ( "Accept" , "application/json" ) ;
270+ client . DefaultRequestHeaders . Add ( "User-Agent" , $ "AzLink CounterStrikeSource v{ AzLinkVersion } ") ;
271+ }
272+
273+ private void SaveConfig ( )
274+ {
275+ var baseName = Path . GetFileName ( ModuleDirectory ) ;
276+ var configsPath = Path . Combine ( ModuleDirectory , ".." , ".." , "configs" , "plugins" ) ;
277+ var jsonConfigPath = Path . Combine ( configsPath , baseName , $ "{ baseName } .json") ;
278+ var json = JsonSerializer . Serialize ( Config , new JsonSerializerOptions
279+ {
280+ WriteIndented = true
281+ } ) ;
282+
283+ File . WriteAllText ( jsonConfigPath , json ) ;
284+ }
285+ }
286+
287+ public class AzLinkConfig : BasePluginConfig
288+ {
289+ public string ? Url { get ; set ; }
290+
291+ public string ? SiteKey { get ; set ; }
292+ }
293+
294+ internal class FetchResponse
295+ {
296+ [ JsonPropertyName ( "commands" ) ] public List < PendingCommand > Commands { get ; set ; } = [ ] ;
297+ }
298+
299+ internal class PendingCommand
300+ {
301+ [ JsonPropertyName ( "uid" ) ] public string UserId { get ; set ; } = "" ;
302+
303+ [ JsonPropertyName ( "name" ) ] public string UserName { get ; set ; } = "" ;
304+
305+ [ JsonPropertyName ( "values" ) ] public List < string > Values { get ; set ; } = [ ] ;
306+ }
0 commit comments