-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
526 lines (422 loc) · 18.7 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
import cProfile
import json
import os
import time
import urllib.parse
import boto3
import requests
import ts3
from botocore.exceptions import ClientError
from sqlalchemy import MetaData
from sqlalchemy import create_engine
from sqlalchemy import select, insert, update
from common.init_logging import setup_logger
# BEFORE YOU JUDGE THIS SCRIPT... I wrote it while drinking, i swear....
# Test Comment Here
# Total hours wasted on this script: 15 (as of 2024-01-23)
# Update: 2024-01-15: HOLY FUCK TREVOR!? WHY DO WE DO THE SAME THING TWICE IN DIFFERENT BLOCKS???
# YES it took me like 10 hours to realize that fact.
# Get the logger
logger = setup_logger(__name__)
# Get DEBUG environment variable
DEBUG = os.environ.get("DEBUG", False)
def get_secret():
"""
Retrieves the database password from Secrets Manager.
"""
secret_name = "prod/lambda/database_write"
region_name = "us-east-1"
# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
# For a list of exceptions thrown, see
# https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
raise e
# Parse the JSON string into a Python dictionary
secret = json.loads(get_secret_value_response['SecretString'])
db_user = secret['username']
db_host = secret["host"]
db_name = "nyartcco_nyartcc"
db_pass = secret["password"]
db_string = f"mysql+pymysql://{db_user}:{db_pass}@{db_host}/{db_name}"
logger.info(f"Database connection string: mysql+pymysql://{db_user}:***@{db_host}/{db_name}")
return db_string
# Get the environment variables
tsUsername = os.environ["tsUsername"]
tsPass = os.environ["tsPass"]
tsHostname = os.environ["tsHostname"]
# Counters
# Count successful updates
updateCount = 0
# Count failed updates
failCount = 0
# TS3 Server Group IDs - This is the base group that is cloned to create each position group
sourceGroup = 227
# Database connection
db_string = get_secret()
engine = create_engine(db_string)
meta = MetaData(engine, reflect=True)
table = meta.tables["callsigns"]
table2 = meta.tables["online"]
table3 = meta.tables["controllers"]
ts_ids = meta.tables["ts_user"]
ts_MessageLog = meta.tables["ts_message_log"]
def incrementUpdateCount():
"""
Increment the update count.
"""
global updateCount
updateCount += 1
def incrementFailCount():
global failCount
failCount += 1
class TimeoutException(Exception):
pass
def timeout_handler(signum, frame):
raise TimeoutException("Timeout handler triggered!")
zny_web_instance = "https://nyartcc.org"
### PILOT BLOCK ###
def fetch_vatsim_pilots():
""" Fetches active pilots from VATSIM data feed. """
response = requests.get('http://data.vatsim.net/v3/vatsim-data.json')
data = response.json()
return {pilot['cid']: pilot for pilot in data['pilots']}
tracked_users = {
'908962': 'kRhqR59V3/Ekbq1dpCr+QV8xAXo='
}
def updatePilots(ts3conn, conn):
"""
Update pilot groups in Teamspeak based on active pilots from VATSIM that are tracked.
"""
active_pilots = fetch_vatsim_pilots() # Fetches current active pilots
# Fetch existing TS3 groups related to pilots
ts3_groups = {group["name"]: int(group["sgid"]) for group in ts3conn.servergrouplist() if "Pilot_" in group["name"]}
for cid, pilot in active_pilots.items():
if cid in tracked_users:
pilot_callsign = pilot['callsign']
ts3uid = tracked_users[cid]
group_name = f"Pilot_{pilot_callsign}"
if group_name not in ts3_groups:
# Create new group if not exists
resp = ts3conn.servergroupcopy(ssgid=sourceGroup, tsgid=0, name=group_name, type_=1)
ts3_groups[group_name] = int(resp.parsed[0]["sgid"])
try:
# Add the pilot to the respective group
dbid = ts3conn.clientgetdbidfromuid(cluid=ts3uid).parsed[0]["cldbid"]
ts3conn.servergroupaddclient(sgid=ts3_groups[group_name], cldbid=dbid)
logger.info(f"Added pilot {cid} ({pilot_callsign}) to {group_name}")
incrementUpdateCount()
except Exception as e:
logger.error(f"Failed to add/update pilot {cid} in TS3: {e}")
incrementFailCount()
else:
logger.debug(f"Pilot {cid} not tracked. Skipping...")
# Cleanup old pilot groups if no longer active
for group_name, sgid in ts3_groups.items():
if group_name not in [f"Pilot_{p['callsign']}" for p in active_pilots.values() if p['cid'] in tracked_users]:
ts3conn.servergroupdel(sgid=sgid, force=1)
logger.info(f"Deleted unused pilot group {group_name}")
### CONTROLLER BLOCK ###
def updatePos(ts3conn):
"""
Update the positions of all online controllers.
:param ts3conn:
:return:
"""
# List of all positions
positions = []
# Dictionary of all TS3 groups
ts3_groups = {}
# Dictionary of all online controllers
onlineController = {}
# Dictionary of all the users currently connected to the TS3 server
trackedUsers = {}
# Get the list of all online controllers from the ZNY website
try:
positionResponse = requests.get(zny_web_instance + '/api/positions/online')
positionResponse.raise_for_status() # Raises an error for 4xx or 5xx responses
if not positionResponse.content:
raise ValueError("Empty response received from positions API.")
positionInfo = positionResponse.json()
except requests.RequestException as e:
logger.error(f"Request failed: {e}")
raise
except ValueError as e:
logger.error(f"Invalid response: {e}")
raise
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON response: {e}")
logger.error(f"Response content: {positionResponse.text}")
raise
# Parse the list of online controllers
for position in positionInfo['data']:
# If the position is not in the dictionary of online controllers, add it
if position['identifier'] not in onlineController:
onlineController[position['identifier']] = []
# Get the user info for the controller from the ZNY website
try:
user_info_response = requests.get(
zny_web_instance + '/api/teamspeak/userIdentity?cid={}'.format(position['cid']))
user_info_response.raise_for_status()
if not user_info_response.content:
raise ValueError("Empty response received from user info API.")
userInfo = user_info_response.json()
except requests.RequestException as e:
logger.error(f"Request failed: {e}")
raise
except ValueError as e:
logger.error(f"Invalid response: {e}")
raise
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON response: {e}")
logger.error(f"Response content: {user_info_response.text}")
raise
# Add the user to the position in the dictionary for that position
for uid in userInfo:
onlineController[position['identifier']].append(uid)
# Connect to the database
try:
conn = engine.connect()
except Exception as e:
# Log the error with as much detail as possible
logger.error(f"Database connection failed: {e}")
# Sanitize the database URL before logging it
sanitized_db_url = re.sub(r'//(.*):(.*)@', '//***:***@', str(db))
# Log the sanitized database URL
logger.error(f"Failed to connect to database at {sanitized_db_url}")
# Raise the exception
raise
# Get the list of all positions from the database
positionsAll = conn.execute(select([table])).fetchall()
# Add all positions to the list of positions
for position in positionsAll:
positions.append(position["identifier"])
# Get the list of all TS3 groups
resp = ts3conn.servergrouplist()
# Add all TS3 groups to the dictionary of groups
for group in resp:
ts3_groups[group["name"]] = int(group["sgid"])
for group in ts3_groups:
if group in positions and group not in onlineController:
try:
logger.info(f"Removing {group} from TS3 server group")
ts3conn.servergroupdel(sgid=ts3_groups[group], force=1)
except:
pass
for position in onlineController:
trackedUsers[position] = []
logger.info(f"Currently tracked users: {trackedUsers[position]}")
time.sleep(.1)
if position not in ts3_groups:
resp = ts3conn.servergroupcopy(
ssgid=sourceGroup, tsgid=0, name=position, type_=1
)
ts3_groups[position] = int(resp.parsed[0]["sgid"])
for controller in onlineController[position]:
logger.info(f"Current controller info: {controller}")
resp = ts3conn.clientgetdbidfromuid(cluid=controller)
dibUser = resp.parsed[0]["cldbid"]
try:
time.sleep(.1)
logger.info(f"dibUser: {dibUser}")
logger.info(f"Add {controller} to {position}")
ts3conn.servergroupaddclient(
sgid=ts3_groups[position], cldbid=dibUser
)
incrementUpdateCount()
except:
logger.error(f"FAILED to add '{position}' to {controller}")
incrementFailCount()
finally:
trackedUsers[position].append(dibUser)
resp = ts3conn.servergroupclientlist(sgid=ts3_groups[position])
for user in resp.parsed:
logger.info(f"USER: {user}")
if user["cldbid"] not in trackedUsers[position]:
ts3conn.servergroupdelclient(
sgid=ts3_groups[position], cldbid=user["cldbid"]
)
incrementUpdateCount()
logger.info(f"Removed {user['cldbid']} from {position}")
def updateUsers(ts3conn, conn):
"""
Update the positions of all online controllers.
:param ts3conn: The TS3 connection.
:param conn: The database connection.
:return:
"""
def sendMessageReg(client_unique_identifier, clid):
"""
Send a message to a user to register on the ZNY website.
:param client_unique_identifier: The client unique identifier from TS3.
:param clid: The client ID from TS3.
:return:
"""
# give UID send message to user (DON'T RESEND FOR X TIME)
# This works. Only issue is you Laravel cant accept a slash in Unicode and treats it as a
# normal slash in the url.
# Instead, we need to pass with a get param or within a post. I am
# down for either, Post may be easier as it allows it to forward through.
ts3conn.sendtextmessage(
targetmode=1,
target=clid,
msg="Your teamspeak account is not registered with NYARTCC. In order to update Your positions, ratings and ARTCC status please click here https://nyartcc.org/ts/reg/set?uidStringID={}".format(
urllib.parse.quote_plus(client_unique_identifier)
),
)
logger.debug(f"Sent welcome message to {clid}")
return True
def sendBadUsername():
"""
Send a message to a user to change their username.
:return:
"""
# give UID send mmessage to user (DONT RESEND FOR X TIME)
pass
# get all users in TS and return as resp
# getAllActiveUsers select from TS users link to users via CID. Allows for multiple Idents
# get all users in TS and return as resp
# getAllActiveUsers select from TS users link to users via CID. Allows for multiple Idents
def checkLastMessage(uid, messageType):
"""
Check the last message time for a user
:param uid:
:param messageType:
:return:
"""
dbuserInfo = conn.execute(
select([ts_MessageLog]).where(ts_MessageLog.c.uid == uid)
).fetchone()
if dbuserInfo:
return dbuserInfo["time"]
else:
conn.execute(
insert(ts_MessageLog),
[
{"uid": uid, "type": messageType, "time": 0},
],
)
return 0
def updateLastMessage(uid, messageType, time):
"""
Update the last message time for a user
:param uid:
:param messageType:
:param time:
:return:
"""
conn.execute(
update(ts_MessageLog)
.where(ts_MessageLog.c.uid == uid)
.values(time=time)
)
return True
resp = ts3conn.clientlist()
rawTsIds = conn.execute(select([ts_ids])).fetchall()
allTeamspeakIds = []
for ts_id in rawTsIds:
allTeamspeakIds.append(ts_id["uid"])
artccInfo = requests.get(zny_web_instance + '/api/teamspeak/serverinfo').json()
groupsTracked = artccInfo['data']['tagsTracked']
for user in resp.parsed:
logger.info(f"Variable USER is currently: {user}")
userInfo = ts3conn.clientinfo(clid=user["clid"])
if userInfo.parsed[0]["client_unique_identifier"] in allTeamspeakIds:
# Query the ZNY website API to get information about the user
# Including CID, isStaff, isBoardMember, and other tags
userInfoWebsite = requests.get(
zny_web_instance + '/api/teamspeak/userinfo?uid={}'.format(
urllib.parse.quote_plus(userInfo.parsed[0]['client_unique_identifier']))).json()
userGroupsTS = userInfo.parsed[0]['client_servergroups'].split(',')
userGroupsTracked = list(set(groupsTracked) & set(userGroupsTS))
userGroupsWebsite = userInfoWebsite['data']['tags']
# FIXME: Here we should handle normal users and deal with position tags
# Handle staff members
# If user is a staff member, remove the 'NY Controller' tag - we're not supposed
# to have both NY Controller and the Staff tag.
if userInfoWebsite['data']['isStaff'] and '11' in userGroupsWebsite:
userGroupsWebsite.remove('11')
# If user is a board member, remove the 'NY Controller' tag and add the
# 'Board Member' tag instead.
if userInfoWebsite['data']['isBoardMember']:
# Don't assign the "NY Controller" tag.
logger.info("Found a board member!")
logger.info(f"userInfoWebsite['data'] is currently: {userInfoWebsite['data']}")
# Remove the 'NY Controller' tag
if '11' in userGroupsWebsite:
logger.info("User has id 11 (NY Controller) in list. Removing it.")
try:
userGroupsWebsite.remove('11')
except error as e:
logger.error(f"Failed to remove tag 11. Error: {e}")
logger.info("Removed id 11 successfully!")
# Add the 'Board Member' tag
if '17401' in userGroupsWebsite:
userGroupsWebsite.append('17401')
logger.info(f"Successfully added id 17401 (Board Member) to user {userInfoWebsite['data']['cid']}")
# Ignore server groups for 'KM'
# Check if user is KM and if he has the 'I1' tag if so, remove it and add C3.
if '73' in userGroupsWebsite and userInfoWebsite['data']['cid'] == 908962:
logger.info("Found user KM and he has id 73. He's a fake I1! Remove it!")
try:
userGroupsWebsite.remove('73')
except error as e:
logger.error(f"Failed to remove group 73. Error: {e}")
incrementFailCount()
userGroupsWebsite.append('72')
logger.info("Added group 72 instead.")
incrementUpdateCount()
userAddGroups = list(set(userGroupsWebsite) - set(userGroupsTracked))
userRemoveGroups = list(set(userGroupsTracked) - set(userGroupsWebsite))
for groupId in userRemoveGroups:
ts3conn.servergroupdelclient(sgid=groupId, cldbid=userInfo.parsed[0]['client_database_id'])
incrementUpdateCount()
for groupId in userAddGroups:
ts3conn.servergroupaddclient(sgid=groupId, cldbid=userInfo.parsed[0]['client_database_id'])
incrementUpdateCount()
else:
if checkLastMessage(
userInfo.parsed[0]["client_unique_identifier"], "reg"
) < int(userInfo.parsed[0]["client_lastconnected"]):
updateLastMessage(
userInfo.parsed[0]["client_unique_identifier"],
"reg",
int(userInfo.parsed[0]["client_lastconnected"]),
)
sendMessageReg(
userInfo.parsed[0]["client_unique_identifier"], user["clid"]
)
incrementUpdateCount()
def lambda_handler(event, context):
"""
The main function that is executed by AWS Lambda when the function is triggered.
:param event:
:param context:
:return:
"""
try:
with ts3.query.TS3Connection(tsHostname, "10011") as ts3conn:
ts3conn.login(client_login_name=tsUsername, client_login_password=tsPass)
ts3conn.use(sid=1)
conn = engine.connect()
updatePos(ts3conn)
updateUsers(ts3conn, conn)
updatePilots(ts3conn, conn)
return {
"statusCode": 200,
"headers": {},
"body": json.dumps({
"message": f"Ran successfully! {updateCount} updates were made. {failCount} failed.",
}),
}
except Exception as e:
# Instead of returning, raise an exception to signal failure
raise RuntimeError(f"Epic fail! {e}")