kahoot-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,637 @@
1
+ # Used by module
2
+ from selenium import webdriver
3
+ from selenium.webdriver.chrome.options import Options
4
+ from selenium.webdriver.common.by import By
5
+ import time
6
+ # Used by CLI
7
+ from termcolor import colored
8
+ import os
9
+ from readchar import readkey, key
10
+ import random
11
+
12
+ def cls(): # Source - https://stackoverflow.com/a/684344
13
+ os.system('cls' if os.name=='nt' else 'clear')
14
+
15
+ class InvalidPinError(Exception):
16
+ """Exception raised when the provided pin did not correspond to any current game, or link provided is not a link to kahoot.it"""
17
+
18
+ class InvalidNameError(Exception):
19
+ """Exception raised when the provided nickname was deemed invalid. This can be due to 3 reasons:
20
+ - The nickname was empty (This does not apply if a random name generator is used.)
21
+ - The nickname was longer than 15 characters.
22
+ - The nickname was taken."""
23
+
24
+ class InvalidFunctionUsage(Exception):
25
+ """Exception raised when a function was improperly used.
26
+ Example: Using Namerator() without a name generator being present."""
27
+
28
+ class NameratorError(Exception):
29
+ """Errors related to the random name generator function. These are usually:
30
+ - You haven't rolled at least once
31
+ - You have zero rolls left"""
32
+
33
+ class KahootClient():
34
+ def __init__(self) -> None:
35
+ """A KahootClient() object, used to interact with the actuall Kahoot Client.
36
+ """
37
+ return
38
+ def JoinGame(self, pin: int | str) -> str:
39
+ # Note: The pin can also be the kahoot.it invitation website. It will only accept a string if it contains "https://kahoot.it"
40
+ """The joining function. Joins a game, and waits for a nickname.
41
+
42
+ Args:
43
+ pin (int | str): The pin to the corresponding game. This can also be a URL.
44
+
45
+ Raises:
46
+ InvalidPinError: If the provided pin was not a valid link, or it did not lead to a valid session.
47
+
48
+ Returns:
49
+ str: "ok" if the pin was correct, and the client is currently waiting for a nickname
50
+ Note: It can also return "namerator" if a name generator is present.
51
+ """
52
+
53
+ self.pin = pin
54
+
55
+ # Set up and launch selenium driver
56
+ options = Options()
57
+ optionsToAdd = ["--disable-component-extensions-with-background-pages", "--disable-default-apps", "--disable-extensions",
58
+ "--disable-features=InterestFeedContentSuggestions", "--disable-features=Translate", "--mute-audio",
59
+ "--no-default-browser-check", "--no-first-run", "--ash-no-nudges", "--disable-search-engine-choice-screen",
60
+ "--propagate-iph-for-testing", "--disable-back-forward-cache", "--disable-features=BackForwardCache",
61
+ "--disable-features=HeavyAdPrivacyMitigations", "--no-process-per-site", "--disable-background-networking",
62
+ "--disable-component-update", "--disable-domain-reliability", "--disable-features=AutofillServerCommunication",
63
+ "--disable-features=CertificateTransparencyComponentUpdater", "--disable-sync", "--metrics-recording-only",
64
+ "--disable-features=OptimizationHints", "--single-process", "--headless=new"]
65
+ for arg in optionsToAdd:
66
+ options.add_argument(arg)
67
+ global _driver
68
+ _driver = webdriver.Chrome(options=options)
69
+ _driver.implicitly_wait(0.5)
70
+
71
+ # Get webpage and join session
72
+ if type(pin) == str:
73
+ if "kahoot.it" in self.pin:
74
+ _driver.get(self.pin)
75
+ else:
76
+ raise InvalidPinError("The provided link does not lead to kahoot.it")
77
+ elif type(pin) == int:
78
+ _driver.get("https://kahoot.it/")
79
+ time.sleep(0.5)
80
+ pin_box = _driver.find_element(by=By.NAME, value="gameId")
81
+ pin_submit = _driver.find_element(By.CSS_SELECTOR, ("button[type='submit']"))
82
+
83
+ pin_box.send_keys(self.pin)
84
+ pin_submit.click()
85
+
86
+ time.sleep(1.5)
87
+ # Check if pin led to a valid session, raise InvalidPinError if not
88
+ if _driver.current_url == "https://kahoot.it/join" or _driver.current_url == "https://kahoot.it/namerator":
89
+ pass
90
+ else:
91
+ raise InvalidPinError(f"The pin ({self.pin}) does not lead to any current game.")
92
+
93
+ if _driver.current_url == "https://kahoot.it/join":
94
+ return "ok"
95
+
96
+ elif _driver.current_url == "https://kahoot.it/namerator":
97
+ return "namerator"
98
+
99
+ time.sleep(1)
100
+ # Set language to English, to avoid missing elements due to language differences
101
+ lang = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='kahoot-settings__language-picker']")
102
+ lang.click()
103
+ langEN = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='option-with-value-en']")
104
+ langEN.click()
105
+
106
+ def SetNickname(self, nickname: str) -> str:
107
+ """Sets the nickname. This can only be used if JoinGame() was successful and no name generator is present
108
+
109
+ Args:
110
+ nickname (str): The desired nickname
111
+
112
+ Raises:
113
+ InvalidFunctionUsage: If this function was called without being able to set a nickname
114
+ InvalidNameError: If the provided nickname is invalid (see InvalidNameError exception for reasons why).
115
+
116
+ Returns:
117
+ str: Basically a status code. It'll return **"ok"** if the nickname was valid, **"twoauth"** if 2FA is needed, **"started"** if the game joined has already started, and if the name was changed, it'll return the new name.
118
+ """
119
+ if _driver.current_url != "https://kahoot.it/join":
120
+ raise InvalidFunctionUsage("SetNickname() was called without being able to set a nickname.")
121
+ if len(nickname) > 15:
122
+ raise InvalidNameError("The nickname provided is longer than 15 characters.")
123
+ elif nickname == "":
124
+ raise InvalidNameError("The nickname provided is empty.")
125
+ self.nickname = nickname
126
+ name_box = _driver.find_element(by=By.NAME, value="nickname")
127
+ name_submit = _driver.find_element(By.CSS_SELECTOR, ("button[type='submit']"))
128
+
129
+ name_box.send_keys(self.nickname)
130
+ name_submit.click()
131
+ time.sleep(1.5)
132
+ if _driver.current_url == "https://kahoot.it/join":
133
+ try:
134
+ error_name_box = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='duplicate-name-error-notification']") # This variable is not made to be accessed, it was just made to check if the error box is there or not
135
+ except:
136
+ pass
137
+ else:
138
+ raise InvalidNameError("The provided nickname was taken.")
139
+ elif _driver.current_url == "https://kahoot.it/instructions":
140
+ nicknameElement = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='nickname']")
141
+ if nicknameElement.text != nickname:
142
+ self.nickname = nicknameElement.text
143
+ return nicknameElement.text
144
+ else:
145
+ return "ok"
146
+ elif _driver.current_url == "https://kahoot.it/twoauth":
147
+ return "twoauth"
148
+ elif _driver.current_url == "https://kahoot.it/gameblock":
149
+ return "started"
150
+ def Namerator(self, roll: bool = False, keep: bool = False) -> str:
151
+ """A function to interact with the random name generator, if there's one
152
+
153
+ Args:
154
+ roll (bool, optional): If it should roll a new nickname. Defaults to False.
155
+ keep (bool, optional): If it should select the current nickname. Defaults to False.
156
+ Note: These arguments are **exclusive**, meaning they cannot be used with each other.
157
+
158
+ Raises:
159
+ InvalidFunctionUsage: If this function was called without a name generator present, or both arguments are used.
160
+ NameratorError: Errors related to the random name generator.
161
+
162
+ Returns:
163
+ str: The current nickname.
164
+ Note: This will also be returned if "keep" is True.
165
+ Note:
166
+ This function keeps track of the remaining rolls by itself. You can access this by using "self.namerator_spins" You have a total of 3 rolls.
167
+ """
168
+ if _driver.current_url != "https://kahoot.it/namerator":
169
+ raise InvalidFunctionUsage("Namerator() was used without a random name generator.")
170
+ try:
171
+ if _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-continue-button']") == "": # Basically check if the user has rolled before, without the user knowing
172
+ pass
173
+ except:
174
+ global namerator_spins
175
+ namerator_spins = 3
176
+ self.namerator_spins = namerator_spins
177
+ else:
178
+ nickname = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='name-spinner-selected-name']")
179
+ name_submit = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-continue-button']")
180
+ finally:
181
+ try:
182
+ spin_button = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-spin-button']")
183
+ except:
184
+ pass
185
+ if roll == True and keep == True:
186
+ raise InvalidFunctionUsage("Namerator() was called with both flags set to True.")
187
+ elif roll == True:
188
+ if namerator_spins > 0:
189
+ namerator_spins -= 1
190
+ spin_button.click()
191
+ while True:
192
+ try:
193
+ if namerator_spins > 0:
194
+ spin_button = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-spin-button']")
195
+ nickname = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='name-spinner-selected-name']")
196
+ else:
197
+ name_submit = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-continue-button']")
198
+ nickname = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='name-spinner-selected-name']")
199
+ except:
200
+ pass
201
+ else:
202
+ self.nickname = nickname.text
203
+ return nickname.text
204
+ else:
205
+ raise NameratorError("Tried to roll with 0 rolls left.")
206
+ elif keep == True:
207
+ try:
208
+ if _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-continue-button']") == "": # Basically check if the user has rolled before, without the user knowing
209
+ pass
210
+ except:
211
+ raise NameratorError("Tried to keep a username without rolling first.")
212
+ else:
213
+ name_submit = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='namerator-continue-button']")
214
+ self.nickname = nickname.text
215
+ name_submit.click()
216
+ return nickname
217
+ def TwoAuth(self, order: list) -> bool:
218
+ """A function to interact with the 2FA stage, if there is one.
219
+
220
+ Args:
221
+ order (list): A 4 item list, containing the code. The code should be:
222
+ - "b" for blue
223
+ - "r" for red
224
+ - "g" for green
225
+ - "y" for yellow.
226
+
227
+ Raises:
228
+ InvalidFunctionUsage: If function was called without a random name generator, or "order" doesn't follow the ettiquete above.
229
+
230
+ Returns:
231
+ bool: Returns "True" if the code was correct, and "False" if it wasn't.
232
+ """
233
+ if _driver.current_url != "https://kahoot.it/twoauth":
234
+ raise InvalidFunctionUsage("TwoAuth function was called without 2FA needed.")
235
+
236
+ try:
237
+ if red_auth == "":
238
+ print("", end="\r") # This is supposed to check if red_auth exists, AKA if it's the first time inputting the code
239
+ except NameError:
240
+ red_auth = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='two-factor-cards__triangle-button']")
241
+ blue_auth = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='two-factor-cards__diamond-button']")
242
+ yellow_auth = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='two-factor-cards__circle-button']")
243
+ green_auth = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='two-factor-cards__square-button']")
244
+ combo = order
245
+ while True:
246
+ if len(combo) > 4:
247
+ raise InvalidFunctionUsage("The provided code is invalid.")
248
+ else:
249
+ for char in combo:
250
+ if char not in ["r", "b", "y", "g", "R", "B", "Y", "G"]:
251
+ raise InvalidFunctionUsage("The provided code is invalid.")
252
+ if len(combo) == 4: # Not the best solution, but I guess it works?
253
+ break
254
+ for char in combo:
255
+ if char == "r" or char == "R":
256
+ red_auth.click()
257
+ time.sleep(0.25)
258
+ elif char == "b" or char == "B":
259
+ blue_auth.click()
260
+ time.sleep(0.25)
261
+ elif char == "y" or char == "Y":
262
+ yellow_auth.click()
263
+ time.sleep(0.25)
264
+ elif char == "g" or char == "G":
265
+ green_auth.click()
266
+ time.sleep(0.25)
267
+ time.sleep(0.5)
268
+ if _driver.current_url == "https://kahoot.it/twoauth":
269
+ return False
270
+ elif _driver.current_url == "https://kahoot.it/instructions":
271
+ return True
272
+ def ClientState(self, fast: bool = False) -> str:
273
+ """Returns where the client is (a question, podium, results, etc.). Note: The function waits 1 second before returning. This is to account for loading times. (This can be skipped by setting fast to True)
274
+
275
+ Returns:
276
+ str: Returns:
277
+ - "Quiz" if in a question
278
+ - "Start" if the game is just starting
279
+ - "Res" if it's on the result page (of a question)
280
+ - "End" if it's in the end of the game
281
+ - "Wait" if it's on the question loading screen
282
+ - "Lobby" if the game hasn't started yet
283
+ - "Out" if you've been kicked out
284
+ - "TwoAuth" if 2FA is required
285
+ - "Namerator" if there's a random name generator present
286
+ """
287
+ if fast == False:
288
+ time.sleep(1)
289
+ if _driver.current_url == "https://kahoot.it/gameblock":
290
+ return "Quiz"
291
+ elif _driver.current_url == "https://kahoot.it/answer/result":
292
+ return "Res"
293
+ elif _driver.current_url == "https://kahoot.it/instructions":
294
+ return "Lobby"
295
+ elif _driver.current_url == "https://kahoot.it/":
296
+ return "Out"
297
+ elif _driver.current_url == "https://kahoot.it/getready":
298
+ return "Wait"
299
+ elif _driver.current_url == "https://kahoot.it/ranking":
300
+ return "End"
301
+ elif _driver.current_url == "https://kahoot.it/twoauth":
302
+ return "TwoAuth"
303
+ elif _driver.current_url == "https://kahoot.it/start":
304
+ return "Start"
305
+ elif _driver.current_url == "https://kahoot.it/namerator":
306
+ return "Namerator"
307
+ def GetQuestion(self) -> list:
308
+ """Returns the current question type, title, and answers, if possible.
309
+
310
+ Raises:
311
+ InvalidFunctionUsage: If function was called outside a question
312
+
313
+ Returns:
314
+ list: A list of ["the question type", "the question title", question number (as an int), "top left answer", "top right answer", "bottom left answer", "bottom right answer"]
315
+
316
+ The question types are:
317
+ - "Quiz" for a regular quiz
318
+ - "Multi" for a multiple choice quiz
319
+ - "TorF" for a True or False quiz (This is separated from "Quiz" because in True or False, red and blue are swapped)
320
+
321
+ Note: If the host hid the options to users, it'll return ["the question type", "", question number (as an int), "Red triangle", "Blue diamond", "Yellow circle", "Green square"]
322
+ If it's a true or false question (with the answers hidden to users) it'll return ["(true or false)", "", "Blue diamond", "Red triangle"]
323
+ """
324
+
325
+ if _driver.current_url != "https://kahoot.it/gameblock":
326
+ raise InvalidFunctionUsage("GetQuestion() was called outside a question.")
327
+
328
+ # Question type identificator thingy
329
+ try:
330
+ question_type = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='question-type-heading-trueOrFalseTitle']")
331
+ except:
332
+ question_type = "Quiz"
333
+ else:
334
+ question_type = "TorF"
335
+
336
+ try:
337
+ submit_multi = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='multi-select-submit-button']") # This variable is not supposed to be used *here*, it should just be used to check the question type
338
+ except:
339
+ pass
340
+ else:
341
+ question_type = "Multi"
342
+
343
+ # Title identification thingy
344
+ try:
345
+ question_title = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='block-title']")
346
+ except:
347
+ question_title = ""
348
+
349
+ # Red and blue options identificator
350
+ ans_red = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-0']")
351
+ ans_blue = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-1']")
352
+
353
+ # Check if yellow option exists
354
+ try:
355
+ ans_yellow = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-2']")
356
+ amountOptions = 2
357
+ except:
358
+ ans_yellow = ""
359
+ amountOptions = 1
360
+
361
+ # Check if green option exists
362
+ try:
363
+ ans_green = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-3']")
364
+ amountOptions = 3
365
+ except:
366
+ ans_green = ""
367
+ if ans_yellow != "":
368
+ amountOptions = 2
369
+ else:
370
+ amountOptions = 1
371
+
372
+ # Checking question number
373
+ question_no = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='question-index-counter']")
374
+ question_no = int(question_no.text)
375
+
376
+ # Returns
377
+ if question_title == "":
378
+ if amountOptions == 3:
379
+ return [question_type, "", question_no, ans_red.text, ans_blue.text, ans_yellow.text, ans_green.text]
380
+ elif amountOptions == 2:
381
+ return [question_type, "", question_no, ans_red.text, ans_blue.text, ans_yellow.text]
382
+ elif amountOptions == 1:
383
+ return [question_type, "", question_no, ans_red.text, ans_blue.text]
384
+ else:
385
+ if amountOptions == 3:
386
+ return [question_type, question_title.text, question_no, ans_red.text, ans_blue.text, ans_yellow.text, ans_green.text]
387
+ elif amountOptions == 2:
388
+ return [question_type, question_title.text, question_no, ans_red.text, ans_blue.text, ans_yellow.text]
389
+ elif amountOptions == 1:
390
+ return [question_type, question_title.text, question_no, ans_red.text, ans_blue.text]
391
+ def AnswerQuestion(self, options: int | list):
392
+ """Answers the current question.
393
+
394
+ Args:
395
+ options (int | list): The index of the desired response, or a list in the case of multiple choice questions.
396
+ 0 for top right, 1 for top left, 2 for bottom right, and 3 for bottom left.
397
+ **Note**: For a regular quiz or true or false quiz, an interger is required. On the other hand, a multiple choice quiz requires a list, even if it's for one element.
398
+
399
+ Raises:
400
+ InvalidFunctionUsage: If the function was called outside a question, or the question was called incorrectly (for example, a list for a single choice question)
401
+ """
402
+ if _driver.current_url != "https://kahoot.it/gameblock":
403
+ raise InvalidFunctionUsage("AnswerQuestion() was called outside a question.")
404
+
405
+ # Question type identificator thingy
406
+ try:
407
+ submit_multi = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='multi-select-submit-button']")
408
+ except:
409
+ try:
410
+ question_type = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='question-type-heading-trueOrFalseTitle']")
411
+ except:
412
+ question_type = "Quiz"
413
+ else:
414
+ question_type = "TorF"
415
+ else:
416
+ question_type = "Multi"
417
+
418
+
419
+
420
+ # Options identificator thingy
421
+ if type(options) == int:
422
+ if 0 == options:
423
+ ans_red = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-0']")
424
+ elif 1 == options:
425
+ ans_blue = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-1']")
426
+
427
+ elif 2 == options:
428
+ # Check if yellow option exists
429
+ try:
430
+ ans_yellow = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-2']")
431
+ except:
432
+ ans_yellow = ""
433
+
434
+ elif 3 == options:
435
+ # Check if green option exists
436
+ try:
437
+ ans_green = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-3']")
438
+ except:
439
+ ans_green = ""
440
+ elif type(options) == list:
441
+ if 0 in options:
442
+ ans_red = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-0']")
443
+ elif 1 in options:
444
+ ans_blue = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-1']")
445
+
446
+ elif 2 in options:
447
+ # Check if yellow option exists
448
+ try:
449
+ ans_yellow = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-2']")
450
+ except:
451
+ ans_yellow = ""
452
+
453
+ elif 3 in options:
454
+ # Check if green option exists
455
+ try:
456
+ ans_green = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='answer-3']")
457
+ except:
458
+ ans_green = ""
459
+
460
+
461
+ if type(options) == list and question_type == "Quiz":
462
+ raise InvalidFunctionUsage("A list was used to define the options of a regular quiz.")
463
+ elif type(options) == list and question_type == "TorF":
464
+ raise InvalidFunctionUsage("A list was used to define the options of a True or False quiz.")
465
+ elif type(options) == int and question_type == "Multi":
466
+ raise InvalidFunctionUsage("An interger was used to define the options of a Multiple Choice quiz.")
467
+
468
+ if _driver.current_url == "https://kahoot.it/answer/result":
469
+ return
470
+ if question_type == "Quiz" or question_type == "TorF":
471
+ if options == 0:
472
+ ans_red.click()
473
+ elif options == 1:
474
+ ans_blue.click()
475
+ elif options == 2 and ans_yellow != "":
476
+ ans_yellow.click()
477
+ elif options == 3 and ans_green != "":
478
+ ans_green.click()
479
+ return
480
+ elif question_type == "Multi":
481
+ if 0 in options:
482
+ ans_red.click()
483
+ if 1 in options:
484
+ ans_blue.click()
485
+ if 2 in options and ans_yellow != "":
486
+ ans_yellow.click()
487
+ if 3 in options and ans_green != "":
488
+ ans_green.click()
489
+ submit_multi.click()
490
+ return
491
+ def GetResult(self) -> str | list:
492
+ """Checks the result of the latest question, and returns the result and points (if possible).
493
+
494
+ Raises:
495
+ InvalidFunctionUsage: If the function was called outside the results page.
496
+
497
+ Returns:
498
+ str|list: If you haven't won, it'll return a string. It will be either:
499
+ - "Lose" (if you've lost)
500
+ - "NoTime" (if you ran out of time)
501
+ However, if you have won, it will return a list, containing: ["Win", "+(the amount of points you've gained)"]
502
+ (If it's partially correct, it'll return ["(correct)/(total)", "+(the amount of points you gained"])
503
+ """
504
+ if _driver.current_url != "https://kahoot.it/answer/result":
505
+ raise InvalidFunctionUsage("GetResult() was called outside the results page.")
506
+ try:
507
+ ranOutOfTime = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='time-up-answer']")
508
+ except:
509
+ ranOutOfTime = False
510
+ else:
511
+ ranOutOfTime = True
512
+ if ranOutOfTime == False:
513
+ result_logo = _driver.find_element(By.CSS_SELECTOR, "circle[cx='40']")
514
+ if result_logo.get_attribute("fill") == "#66BF39":
515
+ result = "Win"
516
+ points_increment = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='score-increment']")
517
+ elif result_logo.get_attribute("fill") == "#F35":
518
+ result = "Lose"
519
+ else:
520
+ result_logo = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='partial-correct-answer']")
521
+ result = result_logo.text
522
+ final_res = ""
523
+ temp_res = ""
524
+ save_line = False
525
+ for char in result:
526
+ temp_res += char
527
+ if char == "/" and final_res == "":
528
+ save_line = True
529
+ if char == "\n" and save_line == True:
530
+ final_res = temp_res
531
+ break
532
+ elif char == "\n" and save_line == False:
533
+ temp_res = ""
534
+ final_res = final_res.replace("\n", "")
535
+ result = final_res
536
+ if result == "Lose" or result == "NoTime":
537
+ return result
538
+ else:
539
+ return [result, points_increment.text]
540
+ else:
541
+ return "NoTime"
542
+ def GetLeaderboardPos(self) -> list:
543
+ """Returns the current leaderboard rank, and nemesis (if possible)
544
+
545
+ Raises:
546
+ InvalidFunctionUsage: Raised when this function was called outside the results page.
547
+
548
+ Returns:
549
+ list: If the user is not in the podium, it'll return [(position as int), "x points behind nemesis"].
550
+ If the user IS in the podium, it'll return ["Podium"]
551
+ """
552
+ if _driver.current_url != "https://kahoot.it/answer/result":
553
+ raise InvalidFunctionUsage("GetLeaderboardPos() was called outside the results page.")
554
+
555
+ leaderboard_pos = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='player-rank']")
556
+ try:
557
+ nemesis = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='nemesis']")
558
+ except:
559
+ nemesis = ""
560
+ else:
561
+ nemesis_text = nemesis.text
562
+ leaderboard_pos_int = ""
563
+ for char in nemesis_text:
564
+ nemesis_text = nemesis_text.replace(char, "", 1)
565
+ if char == "\n":
566
+ break
567
+ for char in leaderboard_pos.text:
568
+ if char in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]:
569
+ leaderboard_pos_int += char
570
+ try:
571
+ leaderboard_pos_int = int(leaderboard_pos_int)
572
+ except:
573
+ pass
574
+ if nemesis != "":
575
+ return [leaderboard_pos_int, nemesis_text]
576
+ else:
577
+ return ["Podium"]
578
+ def GetPoints(self) -> int:
579
+ """Returns the total amount of points.
580
+ _(for returning the point increment, see GetResult())_
581
+
582
+ Raises:
583
+ InvalidFunctionUsage: If this function was called outside a game.
584
+
585
+ Returns:
586
+ int: The total amount of points.
587
+ """
588
+ if _driver.current_url in ["https://kahoot.it/", "https://kahoot.it/join"]:
589
+ raise InvalidFunctionUsage("GetPoints() was called outside a game.")
590
+ total_points_element = _driver.find_element(By.CSS_SELECTOR, "[data-functional-selector='bottom-bar-score']")
591
+ total_points = int(total_points_element.text)
592
+ return total_points
593
+ def GetFinalRanking(self) -> int:
594
+ """Returns the final ranking, as an interger.
595
+ - If the position is not in the podium, it will return a number from 4 to the total amount of players (inclusive)
596
+ - If the position is in the podium, but hasn't been shown yet it'll return 0.
597
+ - If the position is in the podium, and has been shown, it'll return a number from 1 to 3 (inclusive)
598
+
599
+ Raises:
600
+ InvalidFunctionUsage: If the function was called outside the ranking phase
601
+
602
+ Returns:
603
+ int: The ranking.
604
+ """
605
+ if _driver.current_url != "https://kahoot.it/ranking":
606
+ raise InvalidFunctionUsage("GetFinalRanking() was called outside ranking phase.")
607
+ try:
608
+ non_podium = _driver.find_element(By.TAG_NAME, "text")
609
+ except:
610
+ non_podium = False
611
+
612
+ if non_podium != False:
613
+ ranking = non_podium.text
614
+ final_str = ""
615
+ for char in ranking:
616
+ if char in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]:
617
+ final_str += char
618
+ else:
619
+ pass
620
+ return final_str
621
+ else:
622
+ try:
623
+ podiumPlace = _driver.find_element(By.CSS_SELECTOR, "path[fill='#FFC00A']")
624
+ except:
625
+ try:
626
+ podiumPlace = _driver.find_element(By.CSS_SELECTOR, "path[fill='#CCC']")
627
+ except:
628
+ try:
629
+ podiumPlace = _driver.find_element(By.CSS_SELECTOR, "path[fill='#EB670F']")
630
+ except:
631
+ return 0
632
+ else:
633
+ return 3
634
+ else:
635
+ return 2
636
+ else:
637
+ return 1
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: kahoot_cli
3
+ Version: 1.0.0
4
+ Summary: A module/CLI tool to interact with a kahoot client (via Selenium) and play a quiz.
5
+ Author-email: Aquaticsanti <aquaticsanti@gmail.com>
6
+ License-Expression: GPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/Aquaticsanti/kahoot_cli
8
+ Project-URL: Issues, https://github.com/Aquaticsanti/kahoot_cli/issues
9
+ Project-URL: Documentation, https://github.com/Aquaticsanti/kahoot_cli/wiki
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # Kahoot_CLI
18
+
19
+ Interact with any Kahoot quiz right from your terminal! Or import it and use it on your own scripts! The possibilities are endless! (I hope)
20
+
21
+ # Features:
22
+ - [X] Joining a game
23
+ - [ ] Reactions
24
+ - [ ] Avatars
25
+ - [X] Answering single choice questions
26
+ - [X] Answering multiple choice questions
27
+ - [X] Answering True or False questions
28
+ - [X] Displaying points
29
+ - [X] Displaying ranking
30
+ - [X] Finishing a game
31
+ - [X] Error handling (this name is taken, pin is invalid, kicked out, etc.)
32
+
33
+ # Current Bugs:
34
+ - [ ] Won't detect irregular gamemodes (that it can't play)
35
+
36
+ # Why would anyone need this, since this just uses Selenium?
37
+
38
+ Kahoot-CLI is designed in mind of devices that can't show a browser tab, like handheld like devices. With Kahoot-CLI, you could make something like a Kahoot handheld, something you couldn't make before.