2024-06-30 14:09:50 +00:00
import type { User } from "@slack/web-api/dist/response/UsersInfoResponse" ;
2024-06-30 10:28:18 +00:00
const { App , ExpressReceiver } = ( await import ( "@slack/bolt" ) ) ;
import postgres from "postgres" ;
import "dotenv/config" ;
import bcrypt from "bcrypt" ;
2024-06-30 15:18:21 +00:00
import type { StaticSelectAction } from "@slack/bolt" ;
2024-06-30 09:18:57 +00:00
2024-06-30 10:28:18 +00:00
const sql = postgres ( {
host : '/var/run/postgresql' ,
database : 'haroon_osu' ,
username : 'haroon'
} )
2024-06-30 09:18:57 +00:00
2024-06-30 10:28:18 +00:00
const receiver = new ExpressReceiver ( { signingSecret : process.env.SLACK_SIGNING_SECRET ! } )
const app = new App ( {
token : process.env.SLACK_BOT_TOKEN ,
signingSecret : process.env.SLACK_SIGNING_SECRET ,
receiver ,
installerOptions : {
port : 41691
}
} ) ;
2024-06-30 17:36:38 +00:00
2024-06-30 10:28:18 +00:00
const states = new Map ( ) ;
app . command ( "/osu-link" , async ( ctx ) = > {
await ctx . ack ( ) ;
2024-06-30 15:45:48 +00:00
const [ exists = null ] = await sql ` SELECT osu_id FROM links WHERE slack_id = ${ ctx . context . userId ! } ` ;
2024-06-30 10:28:18 +00:00
if ( exists ) {
return ctx . respond ( {
text : "This slack account is already linked to an osu! account." ,
unfurl_links : true ,
blocks : [
{
type : 'section' ,
text : {
type : "mrkdwn" ,
text : ` This slack account is already linked to an <https://osu.ppy.sh/users/ ${ exists . osu_id } /|osu! account>. `
2024-06-30 09:18:57 +00:00
}
2024-06-30 10:28:18 +00:00
}
2024-06-30 11:33:54 +00:00
]
2024-06-30 09:18:57 +00:00
2024-06-30 10:28:18 +00:00
} )
return ;
}
const verifCode = ` OSULEADERBOARD- ${ ctx . context . userId } - ${ Date . now ( ) } ` ;
states . set ( ctx . context . userId , verifCode ) ;
const encodedCode = await bcrypt . hash ( verifCode , 10 ) ;
ctx . respond ( {
replace_original : true ,
text : "View this message in your client to verify!" ,
blocks : [
{
type : 'section' ,
text : {
type : "mrkdwn" ,
text : ` Hey <@ ${ ctx . context . userId } >! To link your osu! account to your Slack account, click this button: `
} ,
"accessory" : {
"type" : "button" ,
"text" : {
"type" : "plain_text" ,
"text" : "Link account" ,
"emoji" : true
} ,
"value" : "link" ,
"url" : ` https://osu.ppy.sh/oauth/authorize?client_id=33126&redirect_uri=https://osu.haroon.hackclub.app/osu/callback&response_type=code&state= ${ encodeURIComponent ( ctx . context . userId + ":" + encodedCode ) } ` ,
"action_id" : "link"
}
2024-06-30 09:18:57 +00:00
}
2024-06-30 10:28:18 +00:00
]
} )
} )
receiver . router . get ( "/osu/callback" , async ( req , res ) = > {
res . contentType ( "text/html" )
const code = req . query . code as string ;
const state = req . query . state as string ;
const [ userId , hash ] = state . split ( ':' ) ;
try {
const isValid = await bcrypt . compare ( states . get ( userId ) , hash ) ;
if ( ! isValid ) {
throw new Error ( ) ;
2024-06-30 09:18:57 +00:00
}
2024-06-30 10:28:18 +00:00
} catch ( err ) {
return res . send ( ` Something went wrong: <br><br>Your state was invalid. Please re-authenticate. (invalid_state)<br><br>This has been reported. ` )
}
states . delete ( userId ) ;
2024-06-30 09:18:57 +00:00
2024-06-30 10:28:18 +00:00
const data = await fetch ( "https://osu.ppy.sh/oauth/token" , {
method : "POST" ,
headers : {
"Content-Type" : "application/x-www-form-urlencoded"
} ,
body : ` client_id=33126&client_secret= ${ encodeURIComponent ( process . env . CLIENT_SECRET ! ) } &code= ${ code } &grant_type=authorization_code&redirect_uri= ${ encodeURIComponent ( "https://osu.haroon.hackclub.app/osu/callback" ) } `
} ) . then ( res = > res . json ( ) ) ;
if ( data . error ) {
console . log ( data )
return res . send ( ` Something went wrong: <br><br> ${ data . message } ( ${ data . error } )<br><br>This has been reported. ` )
} else {
const user = await fetch ( "https://osu.ppy.sh/api/v2/me" , {
headers : {
"Authorization" : ` Bearer ${ data . access_token } `
}
} ) . then ( res = > res . json ( ) ) ;
// {user.id} - osu! user ID
// userId - slack user ID
2024-06-30 11:33:54 +00:00
await sql ` INSERT INTO links VALUES ( ${ user . id } , ${ userId } ) `
2024-06-30 10:28:18 +00:00
2024-06-30 15:44:24 +00:00
getLeaderboard ( ) ;
2024-06-30 10:28:18 +00:00
return res . send ( ` Your osu! account ( ${ user . id } ) has been successfully linked to your Slack account ( ${ userId } )! ` )
2024-06-30 09:18:57 +00:00
}
2024-06-30 10:28:18 +00:00
} )
2024-06-30 14:09:50 +00:00
let _token : string | null ;
async function getTemporaryToken ( ) : Promise < string > {
if ( _token ) return _token ;
2024-06-30 11:33:54 +00:00
const data = await fetch ( "https://osu.ppy.sh/oauth/token" , {
method : "POST" ,
headers : {
"Content-Type" : "application/x-www-form-urlencoded"
} ,
body : ` client_id=33126&client_secret= ${ encodeURIComponent ( process . env . CLIENT_SECRET ! ) } &grant_type=client_credentials&scope=public `
} ) . then ( res = > res . json ( ) ) ;
2024-06-30 14:09:50 +00:00
_token = data . access_token ;
setTimeout ( ( ) = > {
_token = null ;
} , data . expires_in )
2024-06-30 11:33:54 +00:00
return data . access_token ;
}
/// GENERATED ///
function splitArray < T > ( arr : T [ ] , maxElements : number ) : T [ ] [ ] {
const result : T [ ] [ ] = [ ] ;
for ( let i = 0 ; i < arr . length ; i += maxElements ) {
result . push ( arr . slice ( i , i + maxElements ) ) ;
}
return result ;
}
/// GENERATED ///
2024-06-30 14:09:50 +00:00
const cache : {
username : string ,
id : number ,
slackId : string ,
2024-06-30 11:33:54 +00:00
score : {
osu : number ,
taiko : number
fruits : number ,
mania : number
2024-06-30 14:09:50 +00:00
}
2024-06-30 11:33:54 +00:00
} [ ] = [ ]
2024-06-30 14:09:50 +00:00
async function getLeaderboard ( ) : Promise < {
username : string ,
id : number ,
slackId : string ,
score : {
osu : number ,
taiko : number
fruits : number ,
mania : number
}
} [ ] >
async function getLeaderboard ( sortBy : "osu" | "taiko" | "fruits" | "mania" , asc? : boolean ) : Promise < {
username : string ,
id : number ,
slackId : string ,
score : {
osu : number ,
taiko : number
fruits : number ,
mania : number
}
} [ ] >
async function getLeaderboard ( sortBy ? : "osu" | "taiko" | "fruits" | "mania" , asc : boolean = true ) {
2024-06-30 11:33:54 +00:00
const token = await getTemporaryToken ( ) ;
const users = await sql ` SELECT * FROM links ` ;
2024-06-30 14:09:50 +00:00
let lb : {
username : string ,
id : number ,
slackId : string ,
score : {
osu : number ,
taiko : number
fruits : number ,
mania : number
}
} [ ] = [ ] ;
2024-06-30 11:33:54 +00:00
2024-06-30 14:09:50 +00:00
const osuUsers : string [ ] [ ] = users . map ( user = > [ user . osu_id , user . slack_id ] ) ;
2024-06-30 11:33:54 +00:00
2024-06-30 14:09:50 +00:00
for ( let list of splitArray < string [ ] > ( osuUsers , 50 ) ) {
const query = list . map ( ( user ) = > ` ids[]= ${ user [ 0 ] } ` ) . join ( "&" ) ;
2024-06-30 11:33:54 +00:00
const data = await fetch ( ` https://osu.ppy.sh/api/v2/users? ${ query } ` , {
headers : {
'Authorization' : ` Bearer ${ token } `
}
} ) . then ( res = > res . json ( ) ) ;
2024-06-30 15:45:48 +00:00
// @ts-ignore i can't be bothered to type this rn
2024-06-30 11:33:54 +00:00
lb . push ( . . . data . users . map ( user = > ( {
username : user.username ,
id : user.id ,
2024-06-30 14:09:50 +00:00
slackId : osuUsers.find ( v = > v [ 0 ] == user . id ) ! [ 1 ] ,
2024-06-30 11:33:54 +00:00
score : {
2024-06-30 15:18:21 +00:00
osu : user.statistics_rulesets.osu?.total_score || 0 ,
taiko : user.statistics_rulesets.taiko?.total_score || 0 ,
fruits : user.statistics_rulesets.fruits?.total_score || 0 ,
mania : user.statistics_rulesets.mania?.total_score || 0 ,
2024-06-30 11:33:54 +00:00
}
} ) ) )
}
2024-06-30 15:18:21 +00:00
cache . length = 0 ;
cache . push ( . . . lb ) ;
2024-06-30 14:09:50 +00:00
if ( sortBy ) {
lb = lb . sort ( ( a , b ) = > {
if ( asc ) return b . score [ sortBy ] - a . score [ sortBy ]
else return a . score [ sortBy ] - b . score [ sortBy ]
} )
}
2024-06-30 11:33:54 +00:00
return lb
}
2024-06-30 14:09:50 +00:00
async function generateProfile ( slackProfile : User ) {
const token = await getTemporaryToken ( ) ;
const osuProfile = await fetch ( ` https://osu.ppy.sh/api/v2/users/ ${ cache . find ( user = > user . slackId == slackProfile . id ) ! . id } ?key=id ` , {
headers : {
'Authorization' : ` Bearer ${ token } `
}
} ) . then ( res = > res . json ( ) ) ;
return [
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : ` *Slack Username*: <https://hackclub.slack.com/team/ ${ slackProfile . id } | ${ slackProfile . profile ! . display_name_normalized } > \ n*osu! username:* <https://osu.ppy.sh/users/ ${ osuProfile . id } | ${ osuProfile . username } > `
} ,
"accessory" : {
"type" : "image" ,
"image_url" : osuProfile . avatar_url ,
"alt_text" : ` ${ osuProfile . username } 's osu profile picture `
}
}
]
}
app . command ( '/osu-profile' , async ( ctx ) = > {
await ctx . ack ( ) ;
const userProfile = ( await ctx . client . users . info ( { user : ctx.context.userId ! } ) ) . user ! . profile ! ;
const arg = ctx . command . text . slice ( ) ;
let match ;
if ( match = arg . match ( /\<\@(.+)\|(.+)>/ ) ) {
// Slack user
const mentionedUser = match [ 1 ] ;
const slackProfile = ( await ctx . client . users . info ( { user : mentionedUser } ) ) . user ! ;
if ( ! cache . find ( u = > u . slackId == slackProfile . id ) ) {
return ctx . respond ( {
2024-06-30 16:39:38 +00:00
response_type : 'in_channel' ,
text : ` ${ userProfile . display_name_normalized } ran /osu-profile @ ${ slackProfile . profile ! . display_name_normalized } ` ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-profile \` | Matched by slack user `
}
]
} ,
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : ` *Slack Username*: <https://hackclub.slack.com/team/ ${ slackProfile . id } | ${ slackProfile . profile ! . display_name_normalized } > \ n*osu! username:* Not linked `
} ,
"accessory" : {
"type" : "image" ,
"image_url" : 'https://osu.ppy.sh/images/layout/avatar-guest@2x.png' ,
"alt_text" : ` default osu profile picture `
}
}
]
2024-06-30 14:09:50 +00:00
} )
}
return ctx . respond ( {
response_type : 'in_channel' ,
text : ` ${ userProfile . display_name_normalized } ran /osu-profile @ ${ slackProfile . profile ! . display_name_normalized } ` ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-profile \` | Matched by slack user `
}
]
} ,
. . . await generateProfile ( slackProfile )
]
} )
} else if ( arg ) {
// osu! user
const cached = cache . find ( u = > u . username . toLowerCase ( ) == arg . toLowerCase ( ) )
if ( ! cached ) {
2024-06-30 16:39:38 +00:00
const token = await getTemporaryToken ( ) ;
const osuProfile = await fetch ( ` https://osu.ppy.sh/api/v2/users/ ${ arg } ?key=username ` , {
headers : {
'Authorization' : ` Bearer ${ token } `
}
} ) . then ( res = > res . json ( ) ) ;
2024-06-30 14:09:50 +00:00
return ctx . respond ( {
2024-06-30 16:39:38 +00:00
response_type : 'in_channel' ,
text : ` ${ userProfile . display_name_normalized } ran /osu-profile ${ arg } ` ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-profile \` | Matched by osu! username `
}
]
} ,
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : ` *Slack Username*: Not linked \ n*osu! username:* <https://osu.ppy.sh/users/ ${ osuProfile . id } | ${ osuProfile . username } > `
} ,
"accessory" : {
"type" : "image" ,
"image_url" : osuProfile . avatar_url ,
"alt_text" : ` ${ osuProfile . username } 's osu profile picture `
}
}
]
2024-06-30 14:09:50 +00:00
} )
}
const slackProfile = ( await ctx . client . users . info ( { user : cached.slackId } ) ) . user ! ;
return ctx . respond ( {
response_type : 'in_channel' ,
text : ` ${ userProfile . display_name_normalized } ran /osu-profile ${ arg } ` ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-profile \` | Matched by osu! username `
}
]
} ,
. . . await generateProfile ( slackProfile )
]
} )
2024-06-30 15:18:21 +00:00
} else {
// User's own profile
const mentionedUser = ctx . context . userId ! ;
const slackProfile = ( await ctx . client . users . info ( { user : mentionedUser } ) ) . user ! ;
if ( ! cache . find ( u = > u . slackId == slackProfile . id ) ) {
return ctx . respond ( {
text : ` You don't seem to have an osu! account linked. You might have to wait a bit for my cache to reload though. `
} )
}
return ctx . respond ( {
response_type : 'in_channel' ,
text : ` ${ userProfile . display_name_normalized } ran /osu-profile ` ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-profile \` | Matched by no input `
}
]
} ,
. . . await generateProfile ( slackProfile )
]
} )
2024-06-30 14:09:50 +00:00
}
} )
2024-06-30 15:18:21 +00:00
app . command ( '/osu-leaderboard' , async ( ctx ) = > {
await ctx . ack ( ) ;
const cached = splitArray < any > ( cache , 10 ) [ 0 ] . sort ( ( a , b ) = > {
return b . score . osu - a . score . osu
} ) ;
const users = [ ] ;
for ( let i in cached ) {
const cachedU = cached [ i ] ;
const slackProfile = ( await ctx . client . users . info ( { user : cachedU.slackId } ) ) . user ! ;
users . push ( ` ${ parseInt ( i ) + 1 } . <https://hackclub.slack.com/team/ ${ slackProfile . id } | ${ slackProfile . profile ! . display_name_normalized } > / <https://osu.ppy.sh/users/ ${ cachedU . id } | ${ cachedU . username } > - ${ cachedU . score . osu . toLocaleString ( ) } ` )
}
ctx . respond ( {
response_type : 'in_channel' ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-leaderboard \` `
}
]
} ,
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : users . join ( '\n' )
}
} ,
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : "*Current leaderboard:* :osu-standard: osu!standard"
}
]
} ,
{
"type" : "section" ,
"block_id" : "select" ,
"text" : {
"type" : "mrkdwn" ,
"text" : "Change leaderboard:"
} ,
"accessory" : {
"type" : "static_select" ,
"placeholder" : {
"type" : "plain_text" ,
"text" : "Choose ruleset..." ,
"emoji" : true
} ,
"options" : [
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-standard: osu!standard" ,
"emoji" : true
} ,
"value" : "osu"
} ,
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-taiko: osu!taiko" ,
"emoji" : true
} ,
"value" : "taiko"
} ,
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-catch: osu!catch" ,
"emoji" : true
} ,
"value" : "fruits"
} ,
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-mania: osu!mania" ,
"emoji" : true
} ,
"value" : "mania"
}
] ,
2024-06-30 16:39:38 +00:00
"action_id" : "change-leaderboard|" + ctx . context . userId
2024-06-30 15:18:21 +00:00
}
}
]
} )
} )
app . action ( "link" , ( { ack } ) = > ack ( ) )
app . action ( /change-leaderboard\|.+/ , async ( ctx ) = > {
await ctx . ack ( ) ;
const action = ctx . action as StaticSelectAction
const [ _ , userId ] = action . action_id . split ( '|' ) ;
if ( userId != ctx . context . userId ) {
return ctx . respond ( { replace_original : false , response_type : "ephemeral" , text : ` This leaderboard was initialised by <@ ${ userId } >. Only they can manage it. ` } )
}
const selected = action . selected_option . value ;
const cached = splitArray < any > ( cache , 10 ) [ 0 ] . sort ( ( a , b ) = > {
return b . score [ selected ] - a . score [ selected ]
} ) ;
const users = [ ] ;
for ( let i in cached ) {
const cachedU = cached [ i ] ;
const slackProfile = ( await ctx . client . users . info ( { user : cachedU.slackId } ) ) . user ! ;
users . push ( ` ${ parseInt ( i ) + 1 } . <https://hackclub.slack.com/team/ ${ slackProfile . id } | ${ slackProfile . profile ! . display_name_normalized } > / <https://osu.ppy.sh/users/ ${ cachedU . id } | ${ cachedU . username } > - ${ cachedU . score [ selected ] . toLocaleString ( ) } ` )
}
ctx . respond ( {
response_type : 'in_channel' ,
blocks : [
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` <@ ${ ctx . context . userId } > ran \` /osu-leaderboard \` `
}
]
} ,
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : users . join ( '\n' )
}
} ,
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : ` *Current leaderboard:* ${ action . selected_option . text . text } `
}
]
} ,
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : "Change leaderboard:"
} ,
"accessory" : {
"type" : "static_select" ,
"placeholder" : {
"type" : "plain_text" ,
"text" : "Choose ruleset..." ,
"emoji" : true
} ,
"options" : [
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-standard: osu!standard" ,
"emoji" : true
} ,
"value" : "osu"
} ,
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-taiko: osu!taiko" ,
"emoji" : true
} ,
"value" : "taiko"
} ,
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-catch: osu!catch" ,
"emoji" : true
} ,
"value" : "fruits"
} ,
{
"text" : {
"type" : "plain_text" ,
"text" : ":osu-mania: osu!mania" ,
"emoji" : true
} ,
"value" : "mania"
}
] ,
2024-06-30 16:39:38 +00:00
"action_id" : "change-leaderboard|" + userId
2024-06-30 15:18:21 +00:00
}
}
]
} )
} )
2024-06-30 17:36:38 +00:00
receiver . router . get ( '*' , ( req , res ) = > {
res . redirect ( ` https://osu.ppy.sh ${ req . path } ` )
} )
2024-06-30 14:09:50 +00:00
; ( async ( ) = > {
await app . start ( 41691 ) ;
console . log ( '⚡️ Bolt app is running!' ) ;
2024-06-30 15:18:21 +00:00
getLeaderboard ( ) ;
2024-06-30 10:28:18 +00:00
2024-06-30 15:18:21 +00:00
setTimeout ( getLeaderboard , 5 * 60 * 1000 )
2024-06-30 14:09:50 +00:00
} ) ( ) ;