-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Team registration for tournaments (#205)
* initial team model and stuff * a few team endpoints, teams discordcmd, refresh token functionality * update migration * ok i made all the team endpoints * npm run audit * update migration * mode ID filters for bws * fix build * discord commands and restructure db function locations * rename save to upload * test fixes * more fixes and add public folder * no support for gifs (yet) * more fixes and also better image cropping * more fixes, more discord cmds, create getTeamInvites * change team ava url to explicit api url due to proxy * ok 10 mb kinda ridiculous * extra security (dont use discord ID lookup), more test fixes * tournament registration cron * cron fixes * ease on cron queries * fix missing inv accept check + rewrite for loop * optimize discord invite q (and support actionRow limit) * use r2 for team avatars * feat: add base for osu irc bot (#208) * feat: add base for osu irc bot * fix: connection log typo * style: simplify self-message check * feat: add botAccount boolean * style: whitespace * feat: multiplayer commands, listen to channel message event * style: remove unnecessary parentheses * fix: placement of parentheses * feat: reduce command handler duplication * fix: whitespace * fix: more whitespace * Match Infrastructure (#207) * initial match model creation * initial match migration and models and routing * pickban order(maporder) entity * additional properties for match specs + update migration + change match name (sql reserved keyword) * remove discriminators in discord + errors * rename match to matchup everywhere * matchup generator endpoint (remove stages) * fix migration also add a first property * npm run lintfix * update readme for ircbot * intiial cron setup * add matchupGroup to models for qualis * feat: require a qualifier date when a team registers for a tournament with a qualifier stage (#211) * feat: require a qualifier date on team register if tourney has qualifiers, create matchup * fix: wtf * Auto-running Qualifiers (#213) * renaming irc to bancho * more bancho renaming * some pool check fixes for create/publish * Create match creation logic stuff * model + migration fixes * missing checks on slot map count * finish initial setup for auto-running qualifier * 🚑 ⬆️ Upgrade nodesu and bancho.js, fixing typings compilation (#214) * import osu bot * fixes to miscellaneous stuff + build * finish qualis bot * add missing leftjoin in register endpoint * more refbot fixes * change cron delay to 10 seconds * more fixes and also actually make the timers matter * check all players in lobby before running autostart * final fixes before i lose my mind refactoring * REFACTOR!!!!! * poon fixes * nicer log storage Yey * fix matchup running in cron-runner instead of bancho (+ dupe bancho client instances) * mod function fix * wrote wrong publicurl name in docker-compose.json * actually end match * remove unnecessary saves keep matchup save for match end * poon suggestion * Bancho Message Storage (#215) * matchupmessages model and migration * add matchupmessage logic to ref bot * testing * batch save and clear timer on end * date save --------- Co-authored-by: Hugo Denizart <[email protected]> * update koa-session to circumvent koa-passport 6 issues rkusa/koa-passport#181 * fixes and refactors to osu apiv2 * add typing to badgefilter * annoying build fixes and mode type --------- Co-authored-by: James <[email protected]> Co-authored-by: Hugo Denizart <[email protected]>
- Loading branch information
1 parent
a43d716
commit 1c0b83e
Showing
152 changed files
with
6,596 additions
and
1,731 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Placeholder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { BanchoMessage } from "bancho.js"; | ||
import { Command } from "./index"; | ||
|
||
async function run (message: BanchoMessage, ...args: string[]) { | ||
await message.user.sendMessage("Invoked example command!"); | ||
} | ||
|
||
const example: Command = { | ||
name: "example", | ||
aliases: ["test"], | ||
multiplayerCommand: false, | ||
run, | ||
}; | ||
|
||
export default example; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { BanchoMessage } from "bancho.js"; | ||
import example from "./example"; | ||
import exampleMulti from "./multiplayer/example"; | ||
import { Multi } from "nodesu"; | ||
|
||
interface GlobalCommand { | ||
name: string; | ||
aliases?: string[]; | ||
multiplayerCommand: false; | ||
run: (message: BanchoMessage, ...args: string[]) => Promise<void>; | ||
} | ||
|
||
interface MultiplayerCommand { | ||
name: string; | ||
aliases?: string[]; | ||
multiplayerCommand: true; | ||
run: (message: BanchoMessage, multi: Multi, ...args: string[]) => Promise<void>; | ||
} | ||
|
||
type Command = GlobalCommand | MultiplayerCommand; | ||
|
||
const commands: Command[] = []; | ||
|
||
// all commands | ||
commands.push(example); | ||
commands.push(exampleMulti); | ||
|
||
function handleCommand( | ||
commandName: string, | ||
message: BanchoMessage, | ||
...args: string[] | ||
): Promise<void>; | ||
|
||
function handleCommand( | ||
commandName: string, | ||
message: BanchoMessage, | ||
multi: Multi, | ||
...args: string[] | ||
): Promise<void>; | ||
|
||
async function handleCommand (commandName: string, message: BanchoMessage, multiOrArg?: Multi | string, ...args: string[]) { | ||
const command = commands.find( | ||
(cmd) => cmd.name == commandName.toLowerCase() | ||
|| cmd.aliases?.includes(commandName.toLowerCase()) | ||
); | ||
|
||
if (!command) | ||
return; | ||
|
||
if (command.multiplayerCommand) { | ||
if (!(multiOrArg instanceof Multi)) { | ||
console.error("Invoked multiplayer command without a multi match"); | ||
return; | ||
} | ||
|
||
await command.run(message, multiOrArg, ...args); | ||
} else { | ||
const allArgs = multiOrArg ? [multiOrArg as string, ...args] : args; | ||
await command.run(message, ...allArgs); | ||
} | ||
|
||
console.log(`${message.user.ircUsername} executed command ${command.name}`); | ||
} | ||
|
||
export { Command, handleCommand }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { BanchoMessage } from "bancho.js"; | ||
import { Command } from "../index"; | ||
import { Multi } from "nodesu"; | ||
|
||
async function run (message: BanchoMessage, multi: Multi, ...args: string[]) { | ||
await message.user.sendMessage(`Invoked example multi command in multi ID ${multi.match.id}!`); | ||
} | ||
|
||
const exampleMulti: Command = { | ||
name: "example_multi", | ||
aliases: ["test_multi"], | ||
multiplayerCommand: true, | ||
run, | ||
}; | ||
|
||
export default exampleMulti; |
28 changes: 28 additions & 0 deletions
28
BanchoBot/functions/tournaments/matchup/allAllowedUsersForMatchup.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Matchup } from "../../../../Models/tournaments/matchup"; | ||
import { StageType } from "../../../../Models/tournaments/stage"; | ||
import getUser from "../../../../Server/functions/get/getUser"; | ||
|
||
export default async function allAllowedUsersForMatchup (matchup: Matchup) { | ||
if (!matchup.stage) | ||
throw new Error("Matchup has no stage"); | ||
|
||
const botUsers = await Promise.all([ | ||
{ osu: { username: "Corsace", userID: "29191632" } }, | ||
{ osu: { username: "BanchoBot", userID: "3" } }, | ||
].map((user) => getUser(user.osu.userID, "osu", true))); | ||
|
||
if (matchup.stage.stageType === StageType.Qualifiers) | ||
return matchup.teams! | ||
.flatMap(team => team.members.concat(team.manager)) | ||
.concat(botUsers) | ||
.filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i); | ||
|
||
return matchup.team1!.members | ||
.concat( | ||
matchup.team1!.manager, | ||
matchup.team2!.members, | ||
matchup.team2!.manager, | ||
botUsers | ||
) | ||
.filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i); | ||
} |
16 changes: 16 additions & 0 deletions
16
BanchoBot/functions/tournaments/matchup/allPlayersInMatchup.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { BanchoLobbyPlayer } from "bancho.js"; | ||
import { Matchup } from "../../../../Models/tournaments/matchup"; | ||
import { StageType } from "../../../../Models/tournaments/stage"; | ||
import { Team } from "../../../../Models/tournaments/team"; | ||
|
||
export default function allPlayersInMatchup (matchup: Matchup, playersInLobby: BanchoLobbyPlayer[]) { | ||
const numMembersInLobby = (team: Team) => team.members.filter(m => | ||
playersInLobby.some(p => p.user.id.toString() === m.osu.userID) | ||
).length; | ||
|
||
if (matchup.stage!.stageType === StageType.Qualifiers) | ||
return matchup.teams!.map(numMembersInLobby).every(n => n === matchup.stage!.tournament.matchupSize); | ||
|
||
return numMembersInLobby(matchup.team1!) === matchup.stage!.tournament.matchupSize && | ||
numMembersInLobby(matchup.team2!) === matchup.stage!.tournament.matchupSize; | ||
} |
5 changes: 5 additions & 0 deletions
5
BanchoBot/functions/tournaments/matchup/areAllPlayersInAssignedSlots.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { BanchoLobby, BanchoLobbyPlayer } from "bancho.js"; | ||
|
||
export default function areAllPlayersInAssignedSlots (mpLobby: BanchoLobby, playersPlaying: BanchoLobbyPlayer[] | undefined) { | ||
return !playersPlaying || playersPlaying.every(p => mpLobby.slots.some(s => s !== null && s.user.id === p.user.id)); | ||
} |
18 changes: 18 additions & 0 deletions
18
BanchoBot/functions/tournaments/matchup/doAllPlayersHaveCorrectMods.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { BanchoLobby } from "bancho.js"; | ||
import { MappoolSlot } from "../../../../Models/tournaments/mappools/mappoolSlot"; | ||
import getMappoolSlotMods from "./getMappoolSlotMods"; | ||
|
||
export default function doAllPlayersHaveCorrectMods (mpLobby: BanchoLobby, slotMod: MappoolSlot) { | ||
if (typeof slotMod.userModCount !== "number" && typeof slotMod.uniqueModCount !== "number") | ||
return true; | ||
|
||
const allowedMods = getMappoolSlotMods(slotMod.allowedMods); | ||
if ( | ||
mpLobby.slots.some(slot => | ||
slot.mods.some(mod => | ||
!allowedMods.some(allowedMod => allowedMod.enumValue === mod.enumValue) | ||
) | ||
) | ||
) | ||
return false; | ||
} |
7 changes: 7 additions & 0 deletions
7
BanchoBot/functions/tournaments/matchup/getMappoolSlotMods.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { BanchoMod, BanchoMods } from "bancho.js"; | ||
|
||
export default function getMappoolSlotMods (allowedModsEnum: number | null | undefined): BanchoMod[] { | ||
const modParse = BanchoMods.parseBitFlags(allowedModsEnum || 0, true).concat(BanchoMods.NoFail); // Default NoFail requirements | ||
|
||
return modParse.filter((v, i, a) => a.findIndex(m => m.enumValue === v.enumValue) === i); // If a mappool slot's allowedMods has NoFail already, then it will filter it out. | ||
} |
20 changes: 20 additions & 0 deletions
20
BanchoBot/functions/tournaments/matchup/getUserInMatchup.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { BanchoMessage } from "bancho.js"; | ||
import { User } from "../../../../Models/user"; | ||
|
||
const bots = { | ||
"Corsace": "29191632", | ||
"BanchoBot": "3", | ||
}; | ||
|
||
export default async function getUserInMatchup (users: User[], message: BanchoMessage): Promise<User> { | ||
if (!message.user || !message.user.id) { | ||
const id = bots[message.user?.ircUsername || "BanchoBot"]; | ||
return users.find(user => user.osu.userID === id)!; | ||
} | ||
|
||
const user = users.find(user => user.osu.userID === message.user!.id.toString()); | ||
if (user) | ||
return user; | ||
|
||
throw new Error("User not found"); | ||
} |
13 changes: 13 additions & 0 deletions
13
BanchoBot/functions/tournaments/matchup/invitePlayersToLobby.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { BanchoLobby } from "bancho.js"; | ||
import { Matchup } from "../../../../Models/tournaments/matchup"; | ||
import { StageType } from "../../../../Models/tournaments/stage"; | ||
|
||
export default async function invitePlayersToLobby (matchup: Matchup, mpLobby: BanchoLobby) { | ||
if (matchup.stage!.stageType === StageType.Qualifiers) { | ||
const users = matchup.teams!.flatMap(team => team.members.concat(team.manager).filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i)); | ||
await Promise.all(users.map(user => mpLobby.invitePlayer(`#${user.osu.userID}`))); | ||
} else { | ||
const users = matchup.team1!.members.concat(matchup.team1!.manager).concat(matchup.team2!.members).concat(matchup.team2!.manager).filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i); | ||
await Promise.all(users.map(m => mpLobby.invitePlayer(`#${m.osu.userID}`))); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
BanchoBot/functions/tournaments/matchup/isPlayerInMatchup.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Matchup } from "../../../../Models/tournaments/matchup"; | ||
import { StageType } from "../../../../Models/tournaments/stage"; | ||
import { Team } from "../../../../Models/tournaments/team"; | ||
|
||
export default function isPlayerInMatchup (matchup: Matchup, playerID: string, checkManager: boolean): boolean { | ||
const isPlayerInTeam = (team: Team) => | ||
(checkManager && team.manager.osu.userID === playerID) || | ||
team.members.some(player => player.osu.userID === playerID); | ||
|
||
if (matchup.stage!.stageType === StageType.Qualifiers) | ||
return matchup.teams!.some(isPlayerInTeam); | ||
else | ||
return isPlayerInTeam(matchup.team1!) || isPlayerInTeam(matchup.team2!); | ||
} |
128 changes: 128 additions & 0 deletions
128
BanchoBot/functions/tournaments/matchup/loadNextBeatmap.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { BanchoChannel, BanchoLobby, BanchoMessage } from "bancho.js"; | ||
import { Beatmap } from "../../../../Models/beatmap"; | ||
import { Mappool } from "../../../../Models/tournaments/mappools/mappool"; | ||
import { MappoolMap } from "../../../../Models/tournaments/mappools/mappoolMap"; | ||
import { MappoolSlot } from "../../../../Models/tournaments/mappools/mappoolSlot"; | ||
import { Matchup } from "../../../../Models/tournaments/matchup"; | ||
import { leniencyTime, StageType } from "../../../../Models/tournaments/stage"; | ||
import getMappoolSlotMods from "./getMappoolSlotMods"; | ||
|
||
async function getNextBeatmap (matchup: Matchup, mpLobby: BanchoLobby, mpChannel: BanchoChannel, pools: Mappool[]): Promise<[Beatmap, number | null | undefined, boolean] | null> { | ||
return new Promise((resolve, reject) => { | ||
if (matchup.stage!.stageType === StageType.Qualifiers) { | ||
const pool = pools[0]; | ||
const beatmaps = pool.slots | ||
.flatMap(slot => slot.maps | ||
.sort((a, b) => a.order - b.order) | ||
) | ||
.filter(map => !matchup.maps! | ||
.some(matchMap => matchMap.map.beatmap?.ID === map.beatmap?.ID) | ||
); | ||
|
||
if (!matchup.stage!.qualifierTeamChooseOrder || beatmaps.length === 1) { | ||
if (beatmaps.length === 0) | ||
return resolve(null); | ||
|
||
if (!beatmaps[0].beatmap) | ||
return reject(new Error("Beatmap doesn't exist CONTACT CORSACE IMMEDIATELY")); | ||
|
||
const slotMod = pool.slots.find(slot => slot.maps.some(map => map.beatmap?.ID === beatmaps[0].beatmap?.ID))!; | ||
return resolve([beatmaps[0].beatmap, slotMod.allowedMods, typeof slotMod.allowedMods !== "number" || typeof slotMod.uniqueModCount === "number" || typeof slotMod.userModCount === "number"]); | ||
} | ||
let gotBeatmap = false; | ||
const messageHandler = async (message: BanchoMessage) => { | ||
if (message.user.ircUsername === "BanchoBot" && message.content === "Countdown finished") { | ||
setTimeout(() => { | ||
if (gotBeatmap) | ||
return; | ||
if (beatmaps.length === 0) | ||
return resolve(null); | ||
if (!beatmaps[0].beatmap) | ||
return reject(new Error("Beatmap doesn't exist CONTACT CORSACE IMMEDIATELY")); | ||
|
||
mpChannel.sendMessage("OK U GUYS ARE TAKING TOO LON g im picking a random map for all of u to play now GL"); | ||
const slotMod = pool.slots.find(slot => slot.maps.some(map => map.beatmap?.ID === beatmaps[0].beatmap?.ID))!; | ||
mpChannel.removeListener("message", messageHandler); | ||
return resolve([beatmaps[0].beatmap, slotMod.allowedMods, typeof slotMod.allowedMods !== "number" || typeof slotMod.uniqueModCount === "number" || typeof slotMod.userModCount === "number"]); | ||
}, leniencyTime); | ||
} | ||
|
||
const isManagerMessage = !message.self && message.user.id && ( | ||
( | ||
matchup.stage?.stageType === StageType.Qualifiers && | ||
matchup.teams?.some(team => team.manager.osu.userID === message.user.id.toString()) | ||
) || | ||
[matchup.team1, matchup.team2].some(team => team?.manager.osu.userID === message.user.id.toString())); | ||
|
||
if (!isManagerMessage) | ||
return; | ||
|
||
const contentParts = message.content.split(" "); | ||
const command = contentParts[0]; | ||
const param = ["!map", "!pick"].includes(command) ? contentParts[1] : command; | ||
|
||
if (param.length > 4) | ||
return; | ||
|
||
const mapById = (id: number) => beatmaps.find(map => map.beatmap!.ID === id); | ||
const mapBySlotOrder = (slot: MappoolSlot, order: number) => slot.maps.find(map => map.order === order); | ||
|
||
const id = parseInt(param); | ||
let map: MappoolMap | undefined; | ||
|
||
if (isNaN(id)) { | ||
const nums = param.match(/\d+/g) || []; | ||
const order = parseInt(nums[nums.length - 1]); | ||
const slot = pool.slots.find(slot => param.toLowerCase().includes(slot.acronym.toLowerCase())); | ||
|
||
if (!slot) | ||
return; | ||
if (isNaN(order) && slot.maps.length > 1) | ||
return await mpChannel.sendMessage(`Slot ${slot.acronym} has more than 1 map, specify a map #`); | ||
|
||
map = mapBySlotOrder(slot, isNaN(order) ? 1 : order); | ||
} else | ||
map = mapById(id); | ||
|
||
if (!map) | ||
return await mpChannel.sendMessage("The map ID or slot provided is INVALID ."); | ||
if (!map.beatmap) | ||
return reject(new Error("Map is missing beatmap CONTACT CORSACE IMMEDIATELY")); | ||
|
||
const slotMod = pool.slots.find(slot => slot.maps.some(slotMap => slotMap.beatmap!.ID === map!.beatmap!.ID))!; | ||
|
||
if (!beatmaps.some(beatmap => beatmap.beatmap!.ID === map!.beatmap!.ID)) | ||
return await mpChannel.sendMessage(`${slotMod.acronym}${map.order} is ALREADY PLAYED .`); | ||
|
||
gotBeatmap = true; | ||
mpChannel.removeListener("message", messageHandler); | ||
return resolve([map.beatmap, slotMod.allowedMods, typeof slotMod.allowedMods !== "number" || typeof slotMod.uniqueModCount === "number" || typeof slotMod.userModCount === "number"]); | ||
}; | ||
|
||
mpChannel.sendMessage("It's time to pick a map!!11!1"); | ||
mpLobby.startTimer(matchup.stage!.tournament.mapTimer || 90); | ||
mpChannel.on("message", messageHandler); | ||
} else { | ||
// TODO: implement this | ||
reject(new Error("Not implemented")); | ||
} | ||
}); | ||
} | ||
|
||
export default async function loadNextBeatmap (matchup: Matchup, mpLobby: BanchoLobby, mpChannel: BanchoChannel, pools: Mappool[], possibleEnd: boolean): Promise<boolean> { | ||
const nextBeatmapInfo = await getNextBeatmap(matchup, mpLobby, mpChannel, pools); | ||
if (!nextBeatmapInfo) { | ||
if (!possibleEnd) | ||
throw new Error("No maps found? This is probably a mistake CONTACT CORSACE IMMEDIATELY"); | ||
|
||
return true; | ||
} | ||
|
||
const mods = getMappoolSlotMods(nextBeatmapInfo[1]); | ||
await Promise.all([ | ||
mpLobby.setMap(nextBeatmapInfo[0].ID), | ||
mpLobby.setMods(mods, nextBeatmapInfo[2]), | ||
]); | ||
await mpLobby.startTimer(matchup.stage!.tournament.readyTimer || 90); | ||
return false; | ||
} |
Oops, something went wrong.