2016-08-12 13:55:59 +00:00
|
|
|
import asyncio
|
|
|
|
import discord
|
2016-08-12 15:23:45 +00:00
|
|
|
import json
|
2016-11-08 18:37:39 +00:00
|
|
|
|
|
|
|
import opendota
|
2017-01-09 07:37:09 +00:00
|
|
|
import overwatch
|
|
|
|
import league
|
2016-08-12 19:08:36 +00:00
|
|
|
import strings as s
|
2016-08-13 12:19:41 +00:00
|
|
|
import telegram
|
2016-08-16 20:16:22 +00:00
|
|
|
import bs4
|
2017-01-09 07:37:09 +00:00
|
|
|
import brawlhalla
|
2017-02-07 21:24:49 +00:00
|
|
|
import osu
|
2016-08-12 15:48:35 +00:00
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
2016-08-12 15:23:45 +00:00
|
|
|
d_client = discord.Client()
|
2016-08-13 13:36:08 +00:00
|
|
|
discord_is_ready = False
|
|
|
|
|
|
|
|
|
|
|
|
# When Discord is ready, set discord_is_ready to True
|
|
|
|
@d_client.event
|
|
|
|
async def on_ready():
|
|
|
|
global discord_is_ready
|
|
|
|
discord_is_ready = True
|
2016-08-12 15:23:45 +00:00
|
|
|
|
|
|
|
# Get player database from the db.json file
|
|
|
|
file = open("db.json")
|
|
|
|
db = json.load(file)
|
|
|
|
file.close()
|
2016-08-12 13:55:59 +00:00
|
|
|
|
2016-08-12 14:01:05 +00:00
|
|
|
# Get the discord bot token from "discordtoken.txt"
|
2016-08-12 19:08:36 +00:00
|
|
|
file = open("discordtoken.txt", "r")
|
|
|
|
token = file.read()
|
|
|
|
file.close()
|
2016-08-12 15:23:45 +00:00
|
|
|
|
2017-02-09 13:52:16 +00:00
|
|
|
def save_db():
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Save the current db object to the db.json file."""
|
2017-02-09 13:52:16 +00:00
|
|
|
f = open("db.json", "w")
|
|
|
|
json.dump(db, f)
|
|
|
|
f.close()
|
|
|
|
del f
|
|
|
|
|
|
|
|
|
2016-11-02 13:45:41 +00:00
|
|
|
async def overwatch_status_change(timeout):
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Check for Overwatch levelups and rank changes."""
|
2016-08-12 17:21:43 +00:00
|
|
|
while True:
|
2016-08-13 13:36:08 +00:00
|
|
|
if discord_is_ready:
|
2016-08-13 13:52:11 +00:00
|
|
|
print("[Overwatch] Starting check...")
|
2016-08-13 13:36:08 +00:00
|
|
|
# Update data for every player in list
|
|
|
|
for player in db:
|
|
|
|
if "overwatch" in db[player]:
|
2016-08-13 18:03:47 +00:00
|
|
|
try:
|
|
|
|
r = await overwatch.get_player_data(**db[player]["overwatch"])
|
|
|
|
except overwatch.NotFoundException:
|
|
|
|
print("[Overwatch] Player not found.")
|
2016-08-16 13:28:07 +00:00
|
|
|
except Exception:
|
|
|
|
# If some other error occours, skip the player
|
|
|
|
print("[Overwatch] Request returned an unhandled exception.")
|
2016-08-13 18:03:47 +00:00
|
|
|
else:
|
2016-11-02 13:45:41 +00:00
|
|
|
# Check for levelups
|
2016-11-07 21:29:37 +00:00
|
|
|
level = r["data"]["level"]
|
|
|
|
try:
|
|
|
|
oldlevel = db[player]["overwatch"]["level"]
|
|
|
|
except KeyError:
|
|
|
|
oldlevel = 0
|
|
|
|
if level > oldlevel:
|
2016-08-13 18:03:47 +00:00
|
|
|
# Send the message
|
2016-11-07 21:29:37 +00:00
|
|
|
loop.create_task(send_event(eventmsg=s.overwatch_level_up, player=player, level=level))
|
2016-08-13 18:03:47 +00:00
|
|
|
# Update database
|
2016-11-07 21:29:37 +00:00
|
|
|
db[player]["overwatch"]["level"] = level
|
2017-02-09 14:37:01 +00:00
|
|
|
save_db()
|
2016-11-02 13:45:41 +00:00
|
|
|
# Check for rank changes
|
2016-11-07 21:29:37 +00:00
|
|
|
rank = r["data"]["competitive"]["rank"]
|
|
|
|
if rank is not None:
|
|
|
|
rank = int(rank)
|
|
|
|
try:
|
|
|
|
oldrank = int(db[player]["overwatch"]["rank"])
|
|
|
|
except KeyError:
|
|
|
|
oldrank = 0
|
|
|
|
if rank != oldrank:
|
2016-11-02 13:45:41 +00:00
|
|
|
# Send the message
|
2017-01-09 07:37:09 +00:00
|
|
|
loop.create_task(send_event(eventmsg=s.overwatch_rank_change,
|
2017-01-09 07:39:13 +00:00
|
|
|
player=player, change=overwatch.format_rankchange(rank-oldrank),
|
2017-01-09 07:37:09 +00:00
|
|
|
rank=rank, medal=overwatch.url_to_medal(r["data"]["competitive"]["rank_img"])))
|
2016-11-02 13:45:41 +00:00
|
|
|
# Update database
|
2016-11-07 21:29:37 +00:00
|
|
|
db[player]["overwatch"]["rank"] = rank
|
2017-03-05 22:04:13 +00:00
|
|
|
else:
|
|
|
|
db[player]["overwatch"]["rank"] = 0
|
|
|
|
save_db()
|
2016-08-13 18:03:47 +00:00
|
|
|
finally:
|
2017-02-09 14:38:30 +00:00
|
|
|
await asyncio.sleep(1)
|
2016-08-13 13:52:11 +00:00
|
|
|
print("[Overwatch] Check completed successfully.")
|
2016-08-13 13:36:08 +00:00
|
|
|
# Wait for the timeout
|
|
|
|
await asyncio.sleep(timeout)
|
|
|
|
else:
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
2017-02-09 14:31:44 +00:00
|
|
|
|
2016-08-13 13:36:08 +00:00
|
|
|
async def league_rank_change(timeout):
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Check for League of Legends solo-duo ranked status changes."""
|
2016-08-13 13:36:08 +00:00
|
|
|
while True:
|
|
|
|
if discord_is_ready:
|
2016-08-13 18:03:47 +00:00
|
|
|
print("[League] Starting check for rank changes...")
|
2016-08-13 13:36:08 +00:00
|
|
|
# Update data for every player in list
|
|
|
|
for player in db:
|
|
|
|
if "league" in db[player]:
|
|
|
|
try:
|
|
|
|
r = await league.get_player_rank(**db[player]["league"])
|
|
|
|
except league.NoRankedGamesCompletedException:
|
|
|
|
# If the player has no ranked games completed, skip him
|
2016-08-16 13:28:07 +00:00
|
|
|
pass
|
2016-08-13 13:56:27 +00:00
|
|
|
except league.RateLimitException:
|
|
|
|
# If you've been ratelimited, skip the player and notify the console.
|
|
|
|
print("[League] Request rejected for rate limit.")
|
2016-08-16 13:28:07 +00:00
|
|
|
except Exception:
|
|
|
|
# If some other error occours, skip the player
|
|
|
|
print("[League] Request returned an unhandled exception.")
|
2016-08-13 13:36:08 +00:00
|
|
|
else:
|
|
|
|
# Convert tier into a number
|
|
|
|
tier_number = league.ranklist.index(r["tier"])
|
2016-11-08 14:03:46 +00:00
|
|
|
roman_number = league.roman.index(r["entries"][0]["division"]) # Potrebbe non funzionare
|
|
|
|
try:
|
|
|
|
old_tier_number = db[player]["league"]["tier"]
|
|
|
|
old_roman_number = db[player]["league"]["division"]
|
|
|
|
except KeyError:
|
|
|
|
# Bronze VI?
|
|
|
|
old_tier_number = 0
|
|
|
|
old_roman_number = 5
|
2016-08-13 13:36:08 +00:00
|
|
|
# Check for tier changes
|
2016-11-08 14:03:46 +00:00
|
|
|
if tier_number != old_tier_number or roman_number != old_roman_number:
|
2016-08-13 14:23:58 +00:00
|
|
|
# Send the message
|
2016-11-08 14:03:46 +00:00
|
|
|
loop.create_task(send_event(eventmsg=s.league_rank_up, player=player, tier=s.league_tier_list[tier_number], division=s.league_roman_list[roman_number],
|
|
|
|
oldtier=s.league_tier_list[old_tier_number], olddivision=s.league_roman_list[old_roman_number]))
|
2016-08-13 13:36:08 +00:00
|
|
|
# Update database
|
|
|
|
db[player]["league"]["tier"] = tier_number
|
|
|
|
db[player]["league"]["division"] = roman_number
|
2017-02-09 13:52:16 +00:00
|
|
|
save_db()
|
2016-08-13 13:57:38 +00:00
|
|
|
finally:
|
2016-08-13 13:52:11 +00:00
|
|
|
# Prevent getting ratelimited by Riot
|
2016-08-13 18:03:47 +00:00
|
|
|
await asyncio.sleep(2)
|
|
|
|
print("[League] Rank check completed.")
|
|
|
|
# Wait for the timeout
|
|
|
|
await asyncio.sleep(timeout)
|
|
|
|
else:
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
2017-02-09 14:31:44 +00:00
|
|
|
|
2016-08-13 18:03:47 +00:00
|
|
|
async def league_level_up(timeout):
|
2017-02-20 10:12:46 +00:00
|
|
|
"""Check for League of Legends profile level ups and name changes."""
|
2016-08-13 18:03:47 +00:00
|
|
|
while True:
|
|
|
|
if discord_is_ready:
|
|
|
|
print("[League] Starting check for level changes...")
|
|
|
|
# Update data for every player in list
|
|
|
|
for player in db:
|
|
|
|
if "league" in db[player]:
|
|
|
|
try:
|
|
|
|
r = await league.get_player_info(**db[player]["league"])
|
|
|
|
except league.RateLimitException:
|
|
|
|
# If you've been ratelimited, skip the player and notify the console.
|
|
|
|
print("[League] Request rejected for rate limit.")
|
2016-08-16 13:28:07 +00:00
|
|
|
except Exception:
|
|
|
|
# If some other error occours, skip the player
|
|
|
|
print("[League] Request returned an unhandled exception.")
|
2016-08-13 18:03:47 +00:00
|
|
|
else:
|
2017-02-20 10:12:46 +00:00
|
|
|
# Update summoner name
|
|
|
|
name = r["name"]
|
|
|
|
db[player]["league"]["name"] = name
|
2016-08-13 18:03:47 +00:00
|
|
|
# Check for level changes
|
2016-11-08 14:06:01 +00:00
|
|
|
level = r["summonerLevel"]
|
2016-11-08 14:03:46 +00:00
|
|
|
try:
|
2016-11-08 14:06:01 +00:00
|
|
|
old_level = db[player]["league"]["level"]
|
2016-11-08 14:03:46 +00:00
|
|
|
except KeyError:
|
2016-11-08 14:06:01 +00:00
|
|
|
old_level = 0
|
|
|
|
if level > old_level:
|
2016-08-13 18:03:47 +00:00
|
|
|
# Send the message
|
2016-11-08 14:06:01 +00:00
|
|
|
loop.create_task(send_event(eventmsg=s.league_level_up, player=player, level=level))
|
2016-08-13 18:03:47 +00:00
|
|
|
# Update database
|
2016-11-08 14:06:01 +00:00
|
|
|
db[player]["league"]["level"] = level
|
2017-02-20 10:12:46 +00:00
|
|
|
save_db()
|
2016-08-13 18:03:47 +00:00
|
|
|
finally:
|
|
|
|
# Prevent getting ratelimited by Riot
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
print("[League] Level check completed.")
|
2016-08-13 13:36:08 +00:00
|
|
|
# Wait for the timeout
|
|
|
|
await asyncio.sleep(timeout)
|
|
|
|
else:
|
|
|
|
await asyncio.sleep(1)
|
2016-08-12 17:21:43 +00:00
|
|
|
|
2017-02-09 14:31:44 +00:00
|
|
|
|
2016-08-16 20:16:22 +00:00
|
|
|
async def brawlhalla_update_mmr(timeout):
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Check for Brawlhalla MMR changes."""
|
2016-08-16 20:16:22 +00:00
|
|
|
while True:
|
|
|
|
if discord_is_ready:
|
|
|
|
print("[Brawlhalla] Starting check for mmr changes...")
|
|
|
|
# Update mmr for every player in list
|
|
|
|
for player in db:
|
|
|
|
if "brawlhalla" in db[player]:
|
|
|
|
try:
|
|
|
|
r = await brawlhalla.get_leaderboard_for(db[player]["brawlhalla"]["username"])
|
2016-08-17 21:46:58 +00:00
|
|
|
except None:
|
2016-08-16 20:16:22 +00:00
|
|
|
print("[Brawlhalla] Request returned an unhandled exception.")
|
|
|
|
else:
|
|
|
|
# Parse the page
|
2016-08-17 21:46:58 +00:00
|
|
|
bs = bs4.BeautifulSoup(r.text, "html.parser")
|
2016-08-16 20:16:22 +00:00
|
|
|
# Divide the page into rows
|
|
|
|
rows = bs.find_all("tr")
|
|
|
|
# Find the row containing the rank
|
|
|
|
for row in rows:
|
|
|
|
# Skip header rows
|
2016-08-17 21:46:58 +00:00
|
|
|
if row.has_attr('id') and row['id'] == "rheader":
|
2016-08-16 20:16:22 +00:00
|
|
|
continue
|
|
|
|
# Check if the row belongs to the correct player
|
|
|
|
# (Brawlhalla searches aren't case sensitive)
|
2016-08-17 21:46:58 +00:00
|
|
|
columns = list(row.children)
|
|
|
|
for column in columns:
|
2016-08-16 20:16:22 +00:00
|
|
|
# Find the player name column
|
2016-08-17 21:46:58 +00:00
|
|
|
if column.has_attr('class') and column['class'][0] == "pnameleft":
|
2016-08-16 20:16:22 +00:00
|
|
|
# Check if the name matches the parameter
|
|
|
|
if column.string == db[player]["brawlhalla"]["username"]:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
# Get the current mmr
|
2016-08-17 21:46:58 +00:00
|
|
|
mmr = int(list(row.children)[7].string)
|
2016-11-08 14:20:46 +00:00
|
|
|
try:
|
|
|
|
old_mmr = db[player]["brawlhalla"]["mmr"]
|
|
|
|
except KeyError:
|
|
|
|
old_mmr = 0
|
2016-08-16 20:16:22 +00:00
|
|
|
# Compare the mmr with the value saved in the database
|
2016-11-08 14:20:46 +00:00
|
|
|
if mmr != old_mmr:
|
2016-08-16 20:16:22 +00:00
|
|
|
# Send a message
|
2016-11-08 18:37:39 +00:00
|
|
|
loop.create_task(send_event(s.brawlhalla_new_mmr, player=player, mmr=mmr, oldmmr=old_mmr))
|
2016-08-16 20:16:22 +00:00
|
|
|
# Update database
|
|
|
|
db[player]["brawlhalla"]["mmr"] = mmr
|
2017-02-09 13:52:16 +00:00
|
|
|
save_db()
|
2016-08-16 20:16:22 +00:00
|
|
|
break
|
|
|
|
finally:
|
|
|
|
await asyncio.sleep(1)
|
2016-08-17 21:46:58 +00:00
|
|
|
print("[Brawlhalla] Request returned an unhandled exception.")
|
2016-08-17 18:26:20 +00:00
|
|
|
await asyncio.sleep(timeout)
|
|
|
|
else:
|
|
|
|
await asyncio.sleep(1)
|
2016-08-16 20:16:22 +00:00
|
|
|
|
2017-02-09 14:31:44 +00:00
|
|
|
|
2016-11-08 18:37:39 +00:00
|
|
|
async def opendota_last_match(timeout):
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Check for new played Dota 2 matches using the OpenDota API."""
|
2016-11-08 18:37:39 +00:00
|
|
|
while True:
|
|
|
|
if discord_is_ready:
|
|
|
|
print("[OpenDota] Starting last match check...")
|
|
|
|
# Check for new dota match for every player in the database
|
|
|
|
for player in db:
|
|
|
|
try:
|
|
|
|
# TODO: Se uno non ha mai giocato a dota, cosa succede? Aggiungere handling
|
2016-11-08 20:07:23 +00:00
|
|
|
r = await opendota.get_latest_match(db[player]["steam"]["steamid"])
|
2016-11-08 18:37:39 +00:00
|
|
|
except KeyError:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
old_last = db[player]["dota"]["lastmatch"]
|
|
|
|
except KeyError:
|
|
|
|
old_last = 0
|
|
|
|
last = r["match_id"]
|
|
|
|
if last > old_last:
|
2016-12-19 18:56:37 +00:00
|
|
|
# Get player team
|
|
|
|
# 0 if radiant
|
|
|
|
# 1 if dire
|
|
|
|
team = r["player_slot"] & 0b10000000 >> 7
|
|
|
|
# Get victory status
|
|
|
|
victory = (bool(team) == r["radiant_win"])
|
|
|
|
# Prepare format map
|
|
|
|
f = {
|
|
|
|
"k": r["kills"],
|
|
|
|
"d": r["deaths"],
|
|
|
|
"a": r["assists"],
|
|
|
|
"player": player,
|
|
|
|
"result": s.won if victory else s.lost,
|
|
|
|
"hero": opendota.get_hero_name(r["hero_id"])
|
|
|
|
}
|
2016-11-08 18:37:39 +00:00
|
|
|
# Send a message
|
2016-12-19 18:56:37 +00:00
|
|
|
loop.create_task(send_event(s.dota_new_match, **f))
|
2016-11-08 18:37:39 +00:00
|
|
|
# Update database
|
2016-12-19 18:56:37 +00:00
|
|
|
try:
|
|
|
|
db[player]["dota"]["lastmatch"] = last
|
|
|
|
except KeyError:
|
|
|
|
db[player]["dota"] = {
|
|
|
|
"lastmatch": last
|
|
|
|
}
|
2017-02-09 13:52:16 +00:00
|
|
|
save_db()
|
2016-11-08 18:37:39 +00:00
|
|
|
finally:
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
print("[OpenDota] Check successful.")
|
|
|
|
await asyncio.sleep(timeout)
|
|
|
|
else:
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
|
2017-02-07 21:24:49 +00:00
|
|
|
async def osu_pp(timeout):
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Check for changes in Osu! pp."""
|
2017-02-07 21:24:49 +00:00
|
|
|
while True:
|
|
|
|
if discord_is_ready:
|
|
|
|
print("[Osu!] Starting pp check...")
|
|
|
|
for mode in range(0, 4):
|
|
|
|
for player in db:
|
|
|
|
try:
|
|
|
|
r = await osu.get_user(db[player]["osu"]["id"], mode)
|
|
|
|
except KeyError:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
if r["pp_raw"] is not None:
|
|
|
|
pp = float(r["pp_raw"])
|
|
|
|
else:
|
|
|
|
pp = 0
|
|
|
|
if pp != 0:
|
|
|
|
try:
|
|
|
|
old = db[player]["osu"][str(mode)]
|
|
|
|
except KeyError:
|
|
|
|
old = 0
|
|
|
|
if pp != old:
|
|
|
|
db[player]["osu"][str(mode)] = pp
|
|
|
|
f = {
|
|
|
|
"player": player,
|
|
|
|
"mode": s.osu_modes[mode],
|
|
|
|
"pp": int(pp),
|
|
|
|
"change": int(pp - old)
|
|
|
|
}
|
|
|
|
loop.create_task(send_event(s.osu_pp_change, **f))
|
|
|
|
else:
|
|
|
|
db[player]["osu"][str(mode)] = 0.0
|
2017-02-09 13:52:16 +00:00
|
|
|
save_db()
|
2017-02-07 21:24:49 +00:00
|
|
|
finally:
|
2017-02-07 21:27:50 +00:00
|
|
|
await asyncio.sleep(5)
|
2017-02-07 21:24:49 +00:00
|
|
|
print("[Osu!] Check successful.")
|
|
|
|
await asyncio.sleep(timeout)
|
|
|
|
else:
|
2017-02-07 21:27:50 +00:00
|
|
|
await asyncio.sleep(1)
|
2017-02-07 21:24:49 +00:00
|
|
|
|
|
|
|
|
2016-08-13 14:23:58 +00:00
|
|
|
async def send_event(eventmsg: str, player: str, **kwargs):
|
2017-02-09 14:31:44 +00:00
|
|
|
"""Send a message about a new event on both Telegram and Discord"""
|
2016-08-13 14:23:58 +00:00
|
|
|
# Create arguments dict
|
|
|
|
mapping = kwargs.copy()
|
|
|
|
mapping["eventmsg"] = None
|
|
|
|
# Discord
|
|
|
|
# The user id is the player argument; convert that into a mention
|
|
|
|
mapping["player"] = "<@" + player + ">"
|
|
|
|
# Format the event message
|
|
|
|
msg = eventmsg.format(**mapping)
|
|
|
|
# Send the message
|
|
|
|
loop.create_task(d_client.send_message(d_client.get_channel("213655027842154508"), msg))
|
|
|
|
# Telegram
|
|
|
|
# Find the matching Telegram username inside the db
|
|
|
|
mapping["player"] = "@" + db[player]["telegram"]["username"]
|
|
|
|
# Convert the Discord Markdown to Telegram Markdown
|
|
|
|
msg = eventmsg.replace("**", "*")
|
|
|
|
# Format the event message
|
|
|
|
msg = msg.format(**mapping)
|
|
|
|
# Send the message
|
|
|
|
loop.create_task(telegram.send_message(msg, -2141322))
|
|
|
|
|
2017-02-07 21:26:53 +00:00
|
|
|
loop.create_task(overwatch_status_change(900))
|
|
|
|
print("[Overwatch] Added level up check to the queue.")
|
2017-02-07 21:24:49 +00:00
|
|
|
|
2017-02-07 21:26:53 +00:00
|
|
|
loop.create_task(league_rank_change(900))
|
|
|
|
print("[League] Added rank change check to the queue.")
|
2016-08-13 13:52:11 +00:00
|
|
|
|
2017-02-07 21:26:53 +00:00
|
|
|
loop.create_task(league_level_up(900))
|
|
|
|
print("[League] Added level change check to the queue.")
|
2016-08-12 20:53:12 +00:00
|
|
|
|
2017-02-07 21:24:49 +00:00
|
|
|
#loop.create_task(brawlhalla_update_mmr(7200))
|
|
|
|
#print("[Brawlhalla] Added mmr change check to the queue.")
|
2016-08-13 18:03:47 +00:00
|
|
|
|
2017-02-07 21:24:49 +00:00
|
|
|
#loop.create_task(opendota_last_match(600))
|
|
|
|
#print("[OpenDota] Added last match check to the queue.")
|
2016-08-17 18:26:20 +00:00
|
|
|
|
2017-02-07 21:26:53 +00:00
|
|
|
loop.create_task(osu_pp(1800))
|
2017-02-07 21:24:49 +00:00
|
|
|
print("[Osu!] Added pp change check to the queue.")
|
2016-08-17 18:26:20 +00:00
|
|
|
|
2017-02-09 13:52:16 +00:00
|
|
|
# Run until ^C
|
2016-08-12 20:53:12 +00:00
|
|
|
try:
|
|
|
|
loop.run_until_complete(d_client.start(token))
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
loop.run_until_complete(d_client.logout())
|
|
|
|
finally:
|
|
|
|
loop.close()
|