mirror of
https://github.com/ARK-Unity/ark-unity.git
synced 2024-07-12 15:30:12 +02:00
chore: initial commit
This commit is contained in:
commit
003f0ca307
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
build
|
||||
.next
|
||||
coverage
|
||||
node_modules
|
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
|||
# <https://editorconfig.org/>
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_size = 2
|
4
.eslintrc.json
Normal file
4
.eslintrc.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["@ark-unity/eslint-config-custom"]
|
||||
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.npm
|
||||
|
||||
# next.js
|
||||
.next
|
||||
out
|
||||
|
||||
# production
|
||||
build
|
||||
dist
|
||||
.swc
|
||||
|
||||
# testing
|
||||
coverage
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/downloads
|
||||
|
||||
# envs
|
||||
.env
|
||||
.env.production
|
||||
.env.development
|
||||
secrets
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.turbo
|
||||
bin/
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
}
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"prisma.prisma",
|
||||
"mikestead.dotenv",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
17
.vscode/settings.json
vendored
Normal file
17
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "auto"
|
||||
}
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
},
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
}
|
||||
}
|
62
README.md
Normal file
62
README.md
Normal file
|
@ -0,0 +1,62 @@
|
|||
# ARK Unity
|
||||
|
||||
## About
|
||||
|
||||
Prominent presence across the [ARK Franchise](https://playark.com)! 🦖
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 10.0.0
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
# Go to the project root
|
||||
cd ark-unity
|
||||
|
||||
# Install dependencies
|
||||
npm clean-install
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
##### Commands
|
||||
|
||||
```sh
|
||||
# Build, Lint and Test
|
||||
npm run build
|
||||
npm run lint
|
||||
npm run lint:editorconfig
|
||||
npm run lint:prettier
|
||||
npm run test
|
||||
|
||||
# Test only one workspace (e.g: `apps/ark-ascended-discord-bot`)
|
||||
npm run test --workspace=apps/ark-ascended-discord-bot
|
||||
|
||||
# Register commands for `DISCORD_GUILD_ID` provided in `.env`
|
||||
npm run discord:register-commands
|
||||
|
||||
# Deploy to Production
|
||||
npx railway login
|
||||
npx railway link
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### Production environment (with [Docker](https://www.docker.com/))
|
||||
|
||||
```sh
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
## Useful Links
|
||||
|
||||
[Discord Documentation](https://discord.com/developers/applications).
|
||||
|
||||
[Discord.js Documentation](https://discord.js.org/) and [Discord.js Guide](https://discordjs.guide/).
|
40
apps/ark-ascended-discord-bot/.env.example
Normal file
40
apps/ark-ascended-discord-bot/.env.example
Normal file
|
@ -0,0 +1,40 @@
|
|||
NODE_ENV=development
|
||||
|
||||
COMPOSE_PROJECT_NAME=ark-ascended-discord-bot
|
||||
DISCORD_GUILD_ID=1010010
|
||||
|
||||
DISCORD_SURVIVOR_ROLE_ID=101010
|
||||
DISCORD_SERVER_BOOSTER_ROLE_ID=101010101
|
||||
DISCORD_CONTENT_CREATOR_ROLE_ID=101010101
|
||||
DISCORD_ADMINISTRATOR_ROLE_ID=101010101
|
||||
DISCORD_BLOCK_CONTENT_CREATOR_ROLE_ID=101010101
|
||||
|
||||
DISCORD_VOICE_LOBBY_CATEGORY_ID=101010101
|
||||
DISCORD_VOICE_LOBBY_CHANNEL_ID=101010101
|
||||
|
||||
DISCORD_NEWCOMERS_CHANNEL_ID=101010
|
||||
DISCORD_BOT_COMMANDS_CHANNEL_ID=101010101
|
||||
DISCORD_RULES_CHANNEL_ID=101010101
|
||||
DISCORD_ASA_NEWS_CHANNEL_ID=101010101
|
||||
DISCORD_TWEET_CHANNEL_ID=101010101
|
||||
|
||||
DISCORD_CLIENT_ID=clientId
|
||||
DISCORD_TOKEN=token
|
||||
DISCORD_TOKEN_USER=token
|
||||
|
||||
GOOGLE_API_KEY=key
|
||||
TWITCH_CLIENT_ID=clientId
|
||||
TWITCH_CLIENT_SECRET=clientSecret
|
||||
STEAM_API_KEY=key
|
||||
|
||||
DISCORD_ADMIN_COMMANDS_CHANNEL_ID=101010101
|
||||
|
||||
DISCORD_QOTD_REACT_CHANNEL_ID=101010101
|
||||
DISCORD_QOTD_ROLE_ID=101010101
|
||||
DISCORD_QOTD_QUESTIONS_CHANNEL_ID=101010101
|
||||
DISCORD_QOTD_ANSWERS_CHANNEL_ID=101010101
|
||||
DISCORD_QUESTIONS_IDEAS_CHANNEL_ID=101010101
|
||||
|
||||
DISCORD_SUPPORT_TICKETS_CHANNEL_ID=101010101
|
||||
DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID=101010101
|
||||
DISCORD_SUPPORT_TICKETS_CLOSED_CATEGORY_ID=101010101
|
10
apps/ark-ascended-discord-bot/.eslintrc.json
Normal file
10
apps/ark-ascended-discord-bot/.eslintrc.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["@ark-unity/eslint-config-custom"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"import/extensions": ["error", "always"]
|
||||
}
|
||||
}
|
13
apps/ark-ascended-discord-bot/.swcrc
Normal file
13
apps/ark-ascended-discord-bot/.swcrc
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"dynamicImport": true
|
||||
},
|
||||
"target": "esnext"
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
38
apps/ark-ascended-discord-bot/package.json
Normal file
38
apps/ark-ascended-discord-bot/package.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "@ark-unity/ark-ascended-discord-bot",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#src/*": "./build/*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf ./build && swc ./src --out-dir ./build && tsc",
|
||||
"start": "node --enable-source-maps build/index.js",
|
||||
"dev:build": "swc ./src --out-dir ./build --watch",
|
||||
"dev:tsc": "tsc --watch --preserveWatchOutput",
|
||||
"dev": "concurrently --names \"swc,tsc\" \"npm run dev:build\" \"npm run dev:tsc\"",
|
||||
"lint": "eslint ./src --max-warnings 0 --report-unused-disable-directives",
|
||||
"test": "cross-env NODE_ENV=test node --enable-source-maps --test build/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ark-unity/utils": "*",
|
||||
"@googleapis/youtube": "12.0.0",
|
||||
"axios": "1.5.1",
|
||||
"discord.js": "14.13.0",
|
||||
"dotenv": "16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ark-unity/eslint-config-custom": "*",
|
||||
"@ark-unity/tsconfig": "*",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.93",
|
||||
"@types/node": "20.8.6",
|
||||
"chokidar": "3.5.3",
|
||||
"concurrently": "8.2.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.51.0",
|
||||
"rimraf": "5.0.5",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
BIN
apps/ark-ascended-discord-bot/public/ARKCommunity.jpg
Normal file
BIN
apps/ark-ascended-discord-bot/public/ARKCommunity.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 MiB |
21
apps/ark-ascended-discord-bot/src/commands/asa.ts
Normal file
21
apps/ark-ascended-discord-bot/src/commands/asa.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { SlashCommandBuilder, channelMention } from 'discord.js'
|
||||
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
import { DISCORD_ASA_NEWS_CHANNEL_ID } from '#src/configuration/global.js'
|
||||
|
||||
const asa: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('asa')
|
||||
.setDescription('ASA news and updates!')
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
await interaction.reply(`
|
||||
**ARK: Survival Ascended** is an Unreal Engine 5 remake of the original ARK game.
|
||||
Planned to be released in **October, 2023**, on PC (Steam), Xbox Series X|S, and PlayStation 5 for **$39.99 USD** at launch.
|
||||
|
||||
More information and updates in ${channelMention(DISCORD_ASA_NEWS_CHANNEL_ID)}.
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
export default asa
|
|
@ -0,0 +1,123 @@
|
|||
import { GuildMember, SlashCommandBuilder, userMention, bold } from 'discord.js'
|
||||
import { LOCALE } from '@ark-unity/utils'
|
||||
|
||||
import { getYouTubeSubscriberCount } from '#src/services/google.js'
|
||||
import { getTwitchFollowerCount } from '#src/services/twitch.js'
|
||||
import {
|
||||
DISCORD_BLOCK_CONTENT_CREATOR_ROLE_ID,
|
||||
DISCORD_CONTENT_CREATOR_ROLE_ID
|
||||
} from '#src/configuration/global.js'
|
||||
import type { DiscordGetUserProfileResponse } from '#src/services/discord/DiscordClient.js'
|
||||
import { discordAPI } from '#src/services/discord/DiscordClient.js'
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
|
||||
const MINIMUM_CONTENT_CREATOR_ROLE = 5_000
|
||||
|
||||
const DISCORD_CONNECTIONS_MORE_INFORMATION =
|
||||
'See: <https://support.discord.com/hc/en-us/articles/8063233404823-Connections-Community-Members> for more information.'
|
||||
|
||||
const medias = [
|
||||
{ name: 'YouTube', value: 'youtube', fansType: 'subscribers' },
|
||||
{ name: 'Twitch', value: 'twitch', fansType: 'followers' }
|
||||
] as const
|
||||
|
||||
const contentCreatorRole: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('content-creator-role')
|
||||
.setDescription(
|
||||
`Adds the Content Creator role to the user who executed the command.`
|
||||
)
|
||||
.addStringOption((option) => {
|
||||
return option
|
||||
.setName('media')
|
||||
.setDescription('The media you create content for.')
|
||||
.addChoices(...medias)
|
||||
.setRequired(true)
|
||||
})
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
if (
|
||||
interaction.member == null ||
|
||||
!(interaction.member instanceof GuildMember)
|
||||
) {
|
||||
throw new Error('Member is null')
|
||||
}
|
||||
if (interaction.member.roles.cache.has(DISCORD_CONTENT_CREATOR_ROLE_ID)) {
|
||||
await interaction.reply(
|
||||
`You already have the Content Creator role. :star_struck:`
|
||||
)
|
||||
return
|
||||
}
|
||||
if (
|
||||
interaction.member.roles.cache.has(DISCORD_BLOCK_CONTENT_CREATOR_ROLE_ID)
|
||||
) {
|
||||
await interaction.reply(
|
||||
`You can't have the Content Creator role. Refer to a moderator for more information.`
|
||||
)
|
||||
return
|
||||
}
|
||||
const media = interaction.options.get('media')
|
||||
const mediaChoice = medias.find((mediasChoice) => {
|
||||
return mediasChoice.value === media?.value
|
||||
})
|
||||
if (mediaChoice == null) {
|
||||
throw new Error('Media invalid')
|
||||
}
|
||||
const { data } = await discordAPI.get<DiscordGetUserProfileResponse>(
|
||||
`/users/${interaction.user.id}/profile`
|
||||
)
|
||||
const account = data.connected_accounts.find((connectedAccount) => {
|
||||
return mediaChoice.value === connectedAccount.type
|
||||
})
|
||||
if (account == null) {
|
||||
await interaction.reply(
|
||||
`${userMention(interaction.user.id)} You need to connect your ${
|
||||
mediaChoice.name
|
||||
} account to your Discord account to use this command.\n${DISCORD_CONNECTIONS_MORE_INFORMATION}`
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!account.verified) {
|
||||
await interaction.reply(
|
||||
`${userMention(interaction.user.id)} You need to verify your ${
|
||||
mediaChoice.name
|
||||
} account on Discord to use this command.\n${DISCORD_CONNECTIONS_MORE_INFORMATION}`
|
||||
)
|
||||
return
|
||||
}
|
||||
let mediaURL = ''
|
||||
let fansCount = 0
|
||||
if (account.type === 'youtube') {
|
||||
mediaURL = `https://www.youtube.com/channel/${account.id}`
|
||||
fansCount = await getYouTubeSubscriberCount(account)
|
||||
} else {
|
||||
mediaURL = `https://www.twitch.tv/${account.name}`
|
||||
fansCount = await getTwitchFollowerCount(account)
|
||||
}
|
||||
const fansCountString = `You currently have ${bold(
|
||||
`${fansCount.toLocaleString(LOCALE)} ${mediaChoice.fansType} on ${
|
||||
mediaChoice.name
|
||||
}`
|
||||
)}`
|
||||
if (fansCount < MINIMUM_CONTENT_CREATOR_ROLE) {
|
||||
await interaction.reply(
|
||||
`${userMention(
|
||||
interaction.user.id
|
||||
)} You need at least ${MINIMUM_CONTENT_CREATOR_ROLE.toLocaleString(
|
||||
LOCALE
|
||||
)} ${
|
||||
mediaChoice.fansType
|
||||
} to use this command.\n${fansCountString}, you can make it. :muscle:\n${mediaURL}`
|
||||
)
|
||||
return
|
||||
}
|
||||
await interaction.member.roles.add(DISCORD_CONTENT_CREATOR_ROLE_ID)
|
||||
await interaction.reply(
|
||||
`${userMention(
|
||||
interaction.user.id
|
||||
)} You have been given the Content Creator role. :tada:\n${fansCountString}.\n${mediaURL}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default contentCreatorRole
|
|
@ -0,0 +1,24 @@
|
|||
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
import { invitationsLeaderboardReply } from '#src/commands/invitations-leaderboard.js'
|
||||
|
||||
const invitationsLeaderboardPing: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('invitations-leaderboard-ping')
|
||||
.setDescription(
|
||||
'Replies with the leaderboard of the members with the most invites (with ping).'
|
||||
)
|
||||
.setDMPermission(false)
|
||||
.setDefaultMemberPermissions(
|
||||
new PermissionsBitField('Administrator').valueOf()
|
||||
)
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
await interaction.reply({
|
||||
content: await invitationsLeaderboardReply(interaction)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default invitationsLeaderboardPing
|
|
@ -0,0 +1,62 @@
|
|||
import type { CacheType, ChatInputCommandInteraction } from 'discord.js'
|
||||
import {
|
||||
bold,
|
||||
GuildMember,
|
||||
SlashCommandBuilder,
|
||||
underscore,
|
||||
userMention
|
||||
} from 'discord.js'
|
||||
import { pluralizeWord } from '@ark-unity/utils'
|
||||
|
||||
import { DiscordClient } from '#src/services/discord/DiscordClient.js'
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
|
||||
const LEADERBOARD_SIZE = 10
|
||||
|
||||
export const invitationsLeaderboardReply = async (
|
||||
interaction: ChatInputCommandInteraction<CacheType>
|
||||
): Promise<string> => {
|
||||
if (
|
||||
interaction.member == null ||
|
||||
!(interaction.member instanceof GuildMember)
|
||||
) {
|
||||
throw new Error('Member is null')
|
||||
}
|
||||
const discordClient = await DiscordClient.getInstance()
|
||||
const invitations = await interaction.guild?.invites.fetch()
|
||||
const usersInvitations = await discordClient.getUsersInvitations(invitations)
|
||||
const sortedInvitationsLeaderboard = Array.from(
|
||||
usersInvitations.values()
|
||||
).sort((a, b) => {
|
||||
return b.invitationsCount - a.invitationsCount
|
||||
})
|
||||
const leaderboard = sortedInvitationsLeaderboard
|
||||
.slice(0, LEADERBOARD_SIZE)
|
||||
.map((userInvitations, index) => {
|
||||
return `${index + 1}. ${userMention(userInvitations.inviter.id)}: ${bold(
|
||||
userInvitations.invitationsCount.toString()
|
||||
)} ${pluralizeWord('member', userInvitations.invitationsCount)} invited.`
|
||||
})
|
||||
return `${underscore(
|
||||
bold(`Invitations Leaderboard - Top ${LEADERBOARD_SIZE}`)
|
||||
)}\n\n${leaderboard.join('\n')}`
|
||||
}
|
||||
|
||||
const invitationsLeaderboard: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('invitations-leaderboard')
|
||||
.setDescription(
|
||||
'Replies with the leaderboard of the members with the most invites.'
|
||||
)
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
await interaction.reply({
|
||||
content: await invitationsLeaderboardReply(interaction),
|
||||
allowedMentions: {
|
||||
users: []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default invitationsLeaderboard
|
32
apps/ark-ascended-discord-bot/src/commands/invitations.ts
Normal file
32
apps/ark-ascended-discord-bot/src/commands/invitations.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { SlashCommandBuilder, userMention, bold } from 'discord.js'
|
||||
import { pluralizeWord } from '@ark-unity/utils'
|
||||
|
||||
import { DiscordClient } from '#src/services/discord/DiscordClient.js'
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
|
||||
const invitations: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('invitations')
|
||||
.setDescription(
|
||||
'Replies with the number of invitations made by the user who executed the command.'
|
||||
)
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
if (interaction.member == null) {
|
||||
throw new Error('Member is null')
|
||||
}
|
||||
const discordClient = await DiscordClient.getInstance()
|
||||
const invitations = await interaction.guild?.invites.fetch()
|
||||
const usersInvitations =
|
||||
await discordClient.getUsersInvitations(invitations)
|
||||
const userInvitations = usersInvitations.get(interaction.member.user.id)
|
||||
const invitationsCount = userInvitations?.invitationsCount ?? 0
|
||||
await interaction.reply({
|
||||
content: `${userMention(interaction.user.id)} You invited ${bold(
|
||||
invitationsCount.toString()
|
||||
)} ${pluralizeWord('member', invitationsCount)}.`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default invitations
|
16
apps/ark-ascended-discord-bot/src/commands/ping.ts
Normal file
16
apps/ark-ascended-discord-bot/src/commands/ping.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
|
||||
const ping: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with Pong!')
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
const latency = Math.round(interaction.client.ws.ping)
|
||||
await interaction.reply(`:ping_pong: Pong! Latency is ${latency}ms.`)
|
||||
}
|
||||
}
|
||||
|
||||
export default ping
|
|
@ -0,0 +1,33 @@
|
|||
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
import { DISCORD_QUESTIONS_IDEAS_CHANNEL_ID } from '#src/configuration/qotd.js'
|
||||
|
||||
const randomQuestionIdea: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('random-question-idea')
|
||||
.setDescription('Pick a random question idea for QOTD!')
|
||||
.setDMPermission(false)
|
||||
.setDefaultMemberPermissions(
|
||||
new PermissionsBitField('MuteMembers').valueOf()
|
||||
)
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
const channel = await interaction.guild?.channels.fetch(
|
||||
DISCORD_QUESTIONS_IDEAS_CHANNEL_ID
|
||||
)
|
||||
if (channel == null || !channel.isTextBased()) {
|
||||
await interaction.reply('Questions ideas channel not found.')
|
||||
return
|
||||
}
|
||||
const messages = await channel.messages.fetch()
|
||||
const message = messages.random()
|
||||
if (message == null) {
|
||||
await interaction.reply('No question idea found.')
|
||||
return
|
||||
}
|
||||
await interaction.reply(message.content)
|
||||
}
|
||||
}
|
||||
|
||||
export default randomQuestionIdea
|
|
@ -0,0 +1,37 @@
|
|||
import { bold, SlashCommandBuilder } from 'discord.js'
|
||||
import { LOCALE } from '@ark-unity/utils'
|
||||
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
import type { SteamGetNumberOfCurrentPlayersResponse } from '#src/services/steam.js'
|
||||
import { ARK_SURVIVAL_EVOLVED_APP_ID, steamAPI } from '#src/services/steam.js'
|
||||
|
||||
const steamARKSurvivalEvolvedCurrentPlayersCount: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('steam-ark-se-players-count')
|
||||
.setDescription(
|
||||
'Replies with the current number of players on Steam on ARK: Survival Evolved.'
|
||||
)
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
let numberOfCurrentPlayers: number = 0
|
||||
try {
|
||||
const { data } =
|
||||
await steamAPI.get<SteamGetNumberOfCurrentPlayersResponse>(
|
||||
'/ISteamUserStats/GetNumberOfCurrentPlayers/v1',
|
||||
{
|
||||
params: {
|
||||
appid: ARK_SURVIVAL_EVOLVED_APP_ID
|
||||
}
|
||||
}
|
||||
)
|
||||
numberOfCurrentPlayers = data.response.player_count
|
||||
} catch {}
|
||||
await interaction.reply(
|
||||
`There are currently ${bold(
|
||||
numberOfCurrentPlayers.toLocaleString(LOCALE)
|
||||
)} players on ARK: Survival Evolved (Steam).\nhttps://steamcharts.com/app/${ARK_SURVIVAL_EVOLVED_APP_ID}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default steamARKSurvivalEvolvedCurrentPlayersCount
|
|
@ -0,0 +1,37 @@
|
|||
import { bold, SlashCommandBuilder } from 'discord.js'
|
||||
import { LOCALE } from '@ark-unity/utils'
|
||||
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
import type { SteamGetNumberOfCurrentPlayersResponse } from '#src/services/steam.js'
|
||||
import { ARK2_APP_ID, steamAPI } from '#src/services/steam.js'
|
||||
|
||||
const steamARK2CurrentPlayersCount: DiscordCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('steam-ark2-current-players-count')
|
||||
.setDescription(
|
||||
'Replies with the current number of players on Steam on ARK 2.'
|
||||
)
|
||||
.toJSON(),
|
||||
execute: async (interaction) => {
|
||||
let numberOfCurrentPlayers: number = 0
|
||||
try {
|
||||
const { data } =
|
||||
await steamAPI.get<SteamGetNumberOfCurrentPlayersResponse>(
|
||||
'/ISteamUserStats/GetNumberOfCurrentPlayers/v1',
|
||||
{
|
||||
params: {
|
||||
appid: ARK2_APP_ID
|
||||
}
|
||||
}
|
||||
)
|
||||
numberOfCurrentPlayers = data.response.player_count
|
||||
} catch {}
|
||||
await interaction.reply(
|
||||
`There are currently ${bold(
|
||||
numberOfCurrentPlayers.toLocaleString(LOCALE)
|
||||
)} players on ARK 2 (Steam).\nhttps://steamcharts.com/app/${ARK2_APP_ID}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default steamARK2CurrentPlayersCount
|
48
apps/ark-ascended-discord-bot/src/configuration/global.ts
Normal file
48
apps/ark-ascended-discord-bot/src/configuration/global.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export const NODE_ENV = process.env['NODE_ENV'] ?? 'development'
|
||||
export const DISCORD_GUILD_ID = process.env['DISCORD_GUILD_ID'] ?? '101010101'
|
||||
|
||||
export const DISCORD_SURVIVOR_ROLE_ID =
|
||||
process.env['DISCORD_SURVIVOR_ROLE_ID'] ?? '101010101'
|
||||
export const DISCORD_SERVER_BOOSTER_ROLE_ID =
|
||||
process.env['DISCORD_SERVER_BOOSTER_ROLE_ID'] ?? '101010101'
|
||||
export const DISCORD_CONTENT_CREATOR_ROLE_ID =
|
||||
process.env['DISCORD_CONTENT_CREATOR_ROLE_ID'] ?? '101010101'
|
||||
export const DISCORD_ADMINISTRATOR_ROLE_ID =
|
||||
process.env['DISCORD_ADMINISTRATOR_ROLE_ID'] ?? '101010101'
|
||||
export const DISCORD_BLOCK_CONTENT_CREATOR_ROLE_ID =
|
||||
process.env['DISCORD_BLOCK_CONTENT_CREATOR_ROLE_ID'] ?? '101010101'
|
||||
|
||||
export const DISCORD_VOICE_LOBBY_CATEGORY_ID =
|
||||
process.env['DISCORD_VOICE_LOBBY_CATEGORY_ID'] ?? '101010101'
|
||||
export const DISCORD_VOICE_LOBBY_CHANNEL_ID =
|
||||
process.env['DISCORD_VOICE_LOBBY_CHANNEL_ID'] ?? '101010101'
|
||||
|
||||
export const DISCORD_NEWCOMERS_CHANNEL_ID =
|
||||
process.env['DISCORD_NEWCOMERS_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_BOT_COMMANDS_CHANNEL_ID =
|
||||
process.env['DISCORD_BOT_COMMANDS_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_RULES_CHANNEL_ID =
|
||||
process.env['DISCORD_RULES_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_ASA_NEWS_CHANNEL_ID =
|
||||
process.env['DISCORD_ASA_NEWS_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_TWEET_CHANNEL_ID =
|
||||
process.env['DISCORD_TWEET_CHANNEL_ID'] ?? '101010101'
|
||||
|
||||
export const DISCORD_CLIENT_ID = process.env['DISCORD_CLIENT_ID'] ?? 'clientId'
|
||||
export const DISCORD_TOKEN = process.env['DISCORD_TOKEN'] ?? 'token'
|
||||
export const DISCORD_TOKEN_USER = process.env['DISCORD_TOKEN_USER'] ?? 'token'
|
||||
|
||||
export const GOOGLE_API_KEY = process.env['GOOGLE_API_KEY'] ?? 'key'
|
||||
export const TWITCH_CLIENT_ID = process.env['TWITCH_CLIENT_ID'] ?? 'clientId'
|
||||
export const TWITCH_CLIENT_SECRET =
|
||||
process.env['TWITCH_CLIENT_SECRET'] ?? 'clientSecret'
|
||||
export const STEAM_API_KEY = process.env['STEAM_API_KEY'] ?? 'key'
|
||||
|
||||
export const DISCORD_ADMIN_COMMANDS_CHANNEL_ID =
|
||||
process.env['DISCORD_ADMIN_COMMANDS_CHANNEL_ID'] ?? '101010101'
|
||||
|
||||
export const SURVIVOR_BUTTON_ID = 'survivor-button-id'
|
18
apps/ark-ascended-discord-bot/src/configuration/qotd.ts
Normal file
18
apps/ark-ascended-discord-bot/src/configuration/qotd.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* QOTD (Question Of The Day)
|
||||
*/
|
||||
export const DISCORD_QOTD_REACT_CHANNEL_ID =
|
||||
process.env['DISCORD_QOTD_REACT_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_QOTD_ROLE_ID =
|
||||
process.env['DISCORD_QOTD_ROLE_ID'] ?? '101010101'
|
||||
export const DISCORD_QOTD_QUESTIONS_CHANNEL_ID =
|
||||
process.env['DISCORD_QOTD_QUESTIONS_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_QOTD_ANSWERS_CHANNEL_ID =
|
||||
process.env['DISCORD_QOTD_ANSWERS_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_QUESTIONS_IDEAS_CHANNEL_ID =
|
||||
process.env['DISCORD_QUESTIONS_IDEAS_CHANNEL_ID'] ?? '101010101'
|
||||
export const QOTD_BUTTON_ID = 'qotd-button-id'
|
|
@ -0,0 +1,163 @@
|
|||
import { TextInputBuilder, TextInputStyle } from 'discord.js'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* Support Tickets
|
||||
*/
|
||||
export const DISCORD_SUPPORT_TICKETS_CHANNEL_ID =
|
||||
process.env['DISCORD_SUPPORT_TICKETS_CHANNEL_ID'] ?? '101010101'
|
||||
export const DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID =
|
||||
process.env['DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID'] ?? '101010101'
|
||||
export const DISCORD_SUPPORT_TICKETS_CLOSED_CATEGORY_ID =
|
||||
process.env['DISCORD_SUPPORT_TICKETS_CLOSED_CATEGORY_ID'] ?? '101010101'
|
||||
|
||||
export const SUPPORT_TICKETS_IMAGE_URL = 'https://i.imgur.com/6Amtfsx.png'
|
||||
export const SUPPORT_TICKET_BUTTON_CLOSE_ID = 'support-ticket-button-close-id'
|
||||
export const SUPPORT_TICKET_BUTTON_DELETE_ID = 'support-ticket-button-delete-id'
|
||||
export const SUPPORT_TICKETS_SELECT_MENU_ID = 'support-tickets-select-menu-id'
|
||||
|
||||
export const SUPPORT_TICKETS_MEMBER_REPORT_ID =
|
||||
'support-tickets-member-report-id'
|
||||
export const SUPPORT_TICKETS_STAFF_ID = 'support-tickets-staff-id'
|
||||
export const SUPPORT_TICKETS_PARTNER_ID = 'support-tickets-partner-id'
|
||||
export const SUPPORT_TICKETS_ID_LIST = [
|
||||
SUPPORT_TICKETS_MEMBER_REPORT_ID,
|
||||
SUPPORT_TICKETS_STAFF_ID,
|
||||
SUPPORT_TICKETS_PARTNER_ID
|
||||
] as const
|
||||
|
||||
export type SupportTicketId = (typeof SUPPORT_TICKETS_ID_LIST)[number]
|
||||
|
||||
export interface SupportTicketCategory {
|
||||
title: string
|
||||
description: string
|
||||
id: string
|
||||
emoji: string
|
||||
textInputs: TextInputBuilder[]
|
||||
channelName: string
|
||||
}
|
||||
|
||||
export const SUPPORT_TICKETS_CATEGORIES: ReadonlyMap<
|
||||
SupportTicketId,
|
||||
SupportTicketCategory
|
||||
> = new Map([
|
||||
[
|
||||
SUPPORT_TICKETS_MEMBER_REPORT_ID,
|
||||
{
|
||||
title: 'Member Report',
|
||||
description: 'Want to report a member?',
|
||||
id: SUPPORT_TICKETS_MEMBER_REPORT_ID,
|
||||
emoji: '❗',
|
||||
textInputs: [
|
||||
new TextInputBuilder()
|
||||
.setCustomId('reportedMemberDiscordUsername')
|
||||
.setLabel('Reported Member Discord Username')
|
||||
.setPlaceholder('tieko.')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Short),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('rulesViolation')
|
||||
.setLabel('Which rules did this member violate?')
|
||||
.setPlaceholder('e.g: aggressive / toxic behaviour')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
],
|
||||
channelName: 'member-report'
|
||||
}
|
||||
],
|
||||
[
|
||||
SUPPORT_TICKETS_STAFF_ID,
|
||||
{
|
||||
title: 'Staff Application',
|
||||
description: 'Want to apply for staff?',
|
||||
id: SUPPORT_TICKETS_STAFF_ID,
|
||||
emoji: '🛡️',
|
||||
textInputs: [
|
||||
new TextInputBuilder()
|
||||
.setCustomId('name')
|
||||
.setLabel('What is your name?')
|
||||
.setPlaceholder('Tieko')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Short),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('timezone')
|
||||
.setLabel('What is your time zone?')
|
||||
.setPlaceholder('CEST')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Short),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('age')
|
||||
.setLabel('How old are you?')
|
||||
.setPlaceholder('18')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Short),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('alreadyStaff')
|
||||
.setLabel('Have you been staff on a server before?')
|
||||
.setPlaceholder('Yes/No...')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('motivations')
|
||||
.setLabel('Tell us what motivates you for this role?')
|
||||
.setPlaceholder('I would like to help the ARK community...')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
],
|
||||
channelName: 'staff-apply'
|
||||
}
|
||||
],
|
||||
[
|
||||
SUPPORT_TICKETS_PARTNER_ID,
|
||||
{
|
||||
title: 'Partnerships',
|
||||
description: 'Want to become a partner?',
|
||||
id: SUPPORT_TICKETS_PARTNER_ID,
|
||||
emoji: '🤝',
|
||||
textInputs: [
|
||||
new TextInputBuilder()
|
||||
.setCustomId('interests')
|
||||
.setLabel('Why are you interested in becoming a partner?')
|
||||
.setPlaceholder(
|
||||
'I believe that we can each contribute something valuable...'
|
||||
)
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('partnerType')
|
||||
.setLabel('What kind of partnership are you looking for?')
|
||||
.setPlaceholder(
|
||||
'A channel in our Discord with a link to your website.'
|
||||
)
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Short),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('getReturns')
|
||||
.setLabel('What do we get in return?')
|
||||
.setPlaceholder('A link to our Discord on your website.')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Short),
|
||||
new TextInputBuilder()
|
||||
.setCustomId('otherInfo')
|
||||
.setLabel('Other information')
|
||||
.setPlaceholder('Other information you would like to share.')
|
||||
.setMinLength(1)
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
],
|
||||
channelName: 'partnership'
|
||||
}
|
||||
]
|
||||
])
|
46
apps/ark-ascended-discord-bot/src/events/guildMemberAdd.ts
Normal file
46
apps/ark-ascended-discord-bot/src/events/guildMemberAdd.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import type { TextChannel } from 'discord.js'
|
||||
import { bold, channelMention, Events, userMention } from 'discord.js'
|
||||
import { formatNumberOrdinals } from '@ark-unity/utils'
|
||||
|
||||
import {
|
||||
DISCORD_RULES_CHANNEL_ID,
|
||||
DISCORD_NEWCOMERS_CHANNEL_ID,
|
||||
DISCORD_SURVIVOR_ROLE_ID
|
||||
} from '#src/configuration/global.js'
|
||||
import type { DiscordEvent } from '#src/services/discord/DiscordEvent.js'
|
||||
import { DiscordClient } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const guildMemberAdd: DiscordEvent<Events.GuildMemberAdd> = {
|
||||
name: Events.GuildMemberAdd,
|
||||
execute: async (member) => {
|
||||
await member.roles.add(DISCORD_SURVIVOR_ROLE_ID)
|
||||
const discordClient = await DiscordClient.getInstance()
|
||||
let channel = discordClient.channels.cache.get(DISCORD_NEWCOMERS_CHANNEL_ID)
|
||||
if (channel != null && channel.isTextBased()) {
|
||||
channel = channel as TextChannel
|
||||
const content = `Welcome to the ${bold(
|
||||
member.guild.name
|
||||
)} Discord server! ${userMention(member.user.id)}
|
||||
You are the ${bold(
|
||||
formatNumberOrdinals(member.guild.memberCount)
|
||||
)} member! :star_struck:
|
||||
Please read and accept the ${channelMention(
|
||||
DISCORD_RULES_CHANNEL_ID
|
||||
)} and most importantly, have fun!`
|
||||
await channel.send({
|
||||
content,
|
||||
files: [
|
||||
{
|
||||
attachment: fileURLToPath(
|
||||
new URL('../../public/ARKCommunity.jpg', import.meta.url)
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default guildMemberAdd
|
|
@ -0,0 +1,49 @@
|
|||
import { Events } from 'discord.js'
|
||||
import { firstTruePromise } from '@ark-unity/utils'
|
||||
|
||||
import { DiscordClient } from '#src/services/discord/DiscordClient.js'
|
||||
import type { DiscordEvent } from '#src/services/discord/DiscordEvent.js'
|
||||
import { handleButtonInteractionRoles } from '#src/interactionCreate/handleButtonInteractionRoles.js'
|
||||
import { handleSupportTicketsSelectMenu } from '#src/interactionCreate/handleSupportTicketsSelectMenu.js'
|
||||
import { handleSupportTicketsModalSubmit } from '#src/interactionCreate/handleSupportTicketsModalSubmit.js'
|
||||
import { handleSupportTicketsButtonCloseDelete } from '#src/interactionCreate/handleSupportTicketsButtonCloseDelete.js'
|
||||
|
||||
const interactionCreate: DiscordEvent<Events.InteractionCreate> = {
|
||||
name: Events.InteractionCreate,
|
||||
execute: async (interaction) => {
|
||||
if (interaction.isStringSelectMenu()) {
|
||||
await handleSupportTicketsSelectMenu(interaction)
|
||||
return
|
||||
}
|
||||
if (interaction.isButton()) {
|
||||
await firstTruePromise([
|
||||
handleButtonInteractionRoles(interaction),
|
||||
handleSupportTicketsButtonCloseDelete(interaction)
|
||||
])
|
||||
return
|
||||
}
|
||||
if (interaction.isModalSubmit()) {
|
||||
await handleSupportTicketsModalSubmit(interaction)
|
||||
return
|
||||
}
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
return
|
||||
}
|
||||
const discordClient = await DiscordClient.getInstance()
|
||||
const command = discordClient.commands.get(interaction.commandName)
|
||||
if (command == null) {
|
||||
console.error(`Command \`${interaction.commandName}\` not found.`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await command.execute(interaction)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
await interaction.reply(
|
||||
'There was an error while executing this command. Please try again later.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default interactionCreate
|
20
apps/ark-ascended-discord-bot/src/events/ready.ts
Normal file
20
apps/ark-ascended-discord-bot/src/events/ready.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Events } from 'discord.js'
|
||||
|
||||
import type { DiscordEvent } from '#src/services/discord/DiscordEvent.js'
|
||||
|
||||
const ready: DiscordEvent<Events.ClientReady> = {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
execute: async (discordClient) => {
|
||||
if (discordClient.user == null) {
|
||||
return
|
||||
}
|
||||
await discordClient.user.setUsername('ARK Ascended Bot')
|
||||
console.log(`Logged in as \`${discordClient.user.tag}\`!`)
|
||||
console.log(
|
||||
`Invite link: <https://discord.com/oauth2/authorize?client_id=${discordClient.user.id}&permissions=8&scope=bot>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ready
|
54
apps/ark-ascended-discord-bot/src/events/voiceStateUpdate.ts
Normal file
54
apps/ark-ascended-discord-bot/src/events/voiceStateUpdate.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import type { VoiceChannel } from 'discord.js'
|
||||
import { ChannelType, Events, GuildMember } from 'discord.js'
|
||||
|
||||
import {
|
||||
DISCORD_VOICE_LOBBY_CATEGORY_ID,
|
||||
DISCORD_VOICE_LOBBY_CHANNEL_ID
|
||||
} from '#src/configuration/global.js'
|
||||
import type { DiscordEvent } from '#src/services/discord/DiscordEvent.js'
|
||||
|
||||
const deleteVoiceChannel = async (channel: VoiceChannel): Promise<void> => {
|
||||
if (channel.deletable) {
|
||||
try {
|
||||
await channel.delete()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const voiceStateUpdate: DiscordEvent<Events.VoiceStateUpdate> = {
|
||||
name: Events.VoiceStateUpdate,
|
||||
execute: async (oldState, newState) => {
|
||||
if (newState.channelId === oldState.channelId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
newState.channelId === DISCORD_VOICE_LOBBY_CHANNEL_ID &&
|
||||
newState.member instanceof GuildMember
|
||||
) {
|
||||
const channel = await newState.guild.channels.create({
|
||||
name: `${newState.member.displayName}'s Channel`,
|
||||
type: ChannelType.GuildVoice,
|
||||
parent: newState.channel?.parentId,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
id: newState.member.id,
|
||||
allow: ['ManageChannels', 'MoveMembers']
|
||||
}
|
||||
]
|
||||
})
|
||||
await newState.member.voice.setChannel(channel.id)
|
||||
}
|
||||
|
||||
if (
|
||||
oldState.channelId !== DISCORD_VOICE_LOBBY_CHANNEL_ID &&
|
||||
oldState.channel?.parentId === DISCORD_VOICE_LOBBY_CATEGORY_ID &&
|
||||
oldState.channel?.type === ChannelType.GuildVoice &&
|
||||
oldState.channel?.members.size === 0
|
||||
) {
|
||||
await deleteVoiceChannel(oldState.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default voiceStateUpdate
|
4
apps/ark-ascended-discord-bot/src/index.ts
Normal file
4
apps/ark-ascended-discord-bot/src/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { DiscordClient } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const discordClient = await DiscordClient.getInstance()
|
||||
await discordClient.login()
|
|
@ -0,0 +1,108 @@
|
|||
import { GuildMember } from 'discord.js'
|
||||
import type { ButtonInteraction, CacheType } from 'discord.js'
|
||||
import { firstTruePromise } from '@ark-unity/utils'
|
||||
|
||||
import {
|
||||
DISCORD_SURVIVOR_ROLE_ID,
|
||||
SURVIVOR_BUTTON_ID
|
||||
} from '#src/configuration/global.js'
|
||||
import {
|
||||
DISCORD_QOTD_ROLE_ID,
|
||||
QOTD_BUTTON_ID
|
||||
} from '#src/configuration/qotd.js'
|
||||
|
||||
interface HandleButtonInteractionRoleOptions {
|
||||
interaction: ButtonInteraction<CacheType>
|
||||
buttonId: string
|
||||
roleId: string
|
||||
survivorRoleNeeded?: boolean
|
||||
addRoleMessage: string
|
||||
removeRoleMessage: string
|
||||
shouldDisableRoleRemoval?: boolean
|
||||
disableRoleRemovalMessage?: string
|
||||
}
|
||||
|
||||
const handleButtonInteractionRole = async (
|
||||
options: HandleButtonInteractionRoleOptions
|
||||
): Promise<boolean> => {
|
||||
const {
|
||||
interaction,
|
||||
buttonId,
|
||||
roleId,
|
||||
survivorRoleNeeded = false,
|
||||
addRoleMessage,
|
||||
removeRoleMessage,
|
||||
shouldDisableRoleRemoval = false,
|
||||
disableRoleRemovalMessage = 'You already have this role.'
|
||||
} = options
|
||||
if (interaction.replied) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.member instanceof GuildMember)) {
|
||||
return false
|
||||
}
|
||||
if (interaction.customId !== buttonId) {
|
||||
return false
|
||||
}
|
||||
if (interaction.member.roles.cache.has(roleId)) {
|
||||
if (shouldDisableRoleRemoval) {
|
||||
await interaction.reply({
|
||||
content: disableRoleRemovalMessage,
|
||||
ephemeral: true
|
||||
})
|
||||
return true
|
||||
}
|
||||
await interaction.member.roles.remove(roleId)
|
||||
await interaction.reply({
|
||||
content: removeRoleMessage,
|
||||
ephemeral: true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
survivorRoleNeeded &&
|
||||
!interaction.member.roles.cache.has(DISCORD_SURVIVOR_ROLE_ID)
|
||||
) {
|
||||
await interaction.reply({
|
||||
content:
|
||||
'You must have the Survivor role first before getting this role. <:Bob:1045749690022494208>',
|
||||
ephemeral: true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
await interaction.member.roles.add(roleId)
|
||||
await interaction.reply({
|
||||
content: addRoleMessage,
|
||||
ephemeral: true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
export const handleButtonInteractionRoles = async (
|
||||
interaction: ButtonInteraction<CacheType>
|
||||
): Promise<boolean> => {
|
||||
return await firstTruePromise([
|
||||
handleButtonInteractionRole({
|
||||
interaction,
|
||||
buttonId: SURVIVOR_BUTTON_ID,
|
||||
roleId: DISCORD_SURVIVOR_ROLE_ID,
|
||||
survivorRoleNeeded: false,
|
||||
addRoleMessage: 'You are now a Survivor! <:Bob:1083009379890106471>',
|
||||
disableRoleRemovalMessage:
|
||||
'You are already a Survivor. <:Bob:1083009379890106471>',
|
||||
removeRoleMessage:
|
||||
'You are no longer a Survivor. <:dinocry:1085546900226318336>',
|
||||
shouldDisableRoleRemoval: true
|
||||
}),
|
||||
handleButtonInteractionRole({
|
||||
interaction,
|
||||
buttonId: QOTD_BUTTON_ID,
|
||||
roleId: DISCORD_QOTD_ROLE_ID,
|
||||
survivorRoleNeeded: true,
|
||||
addRoleMessage: 'You will now be notified for questions.',
|
||||
removeRoleMessage: 'You will no longer be notified for questions.'
|
||||
})
|
||||
])
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
GuildMember,
|
||||
type ButtonInteraction,
|
||||
type CacheType,
|
||||
GuildChannel,
|
||||
userMention,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
EmbedBuilder,
|
||||
bold
|
||||
} from 'discord.js'
|
||||
import { firstTruePromise } from '@ark-unity/utils'
|
||||
|
||||
import {
|
||||
DISCORD_SUPPORT_TICKETS_CLOSED_CATEGORY_ID,
|
||||
DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID,
|
||||
SUPPORT_TICKET_BUTTON_CLOSE_ID,
|
||||
SUPPORT_TICKET_BUTTON_DELETE_ID
|
||||
} from '#src/configuration/support-tickets.js'
|
||||
|
||||
const handleSupportTicketsButtonClose = async (
|
||||
interaction: ButtonInteraction<CacheType>
|
||||
): Promise<boolean> => {
|
||||
if (interaction.replied) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.member instanceof GuildMember)) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.channel instanceof GuildChannel)) {
|
||||
return false
|
||||
}
|
||||
if (interaction.customId !== SUPPORT_TICKET_BUTTON_CLOSE_ID) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
interaction.channel.parentId !== DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID
|
||||
) {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`⚠️ This ticket is ${bold(
|
||||
'already closed'
|
||||
)} or not in the correct category.`
|
||||
)
|
||||
.setColor([255, 97, 97])
|
||||
]
|
||||
})
|
||||
return true
|
||||
}
|
||||
await interaction.channel.edit({
|
||||
parent: DISCORD_SUPPORT_TICKETS_CLOSED_CATEGORY_ID,
|
||||
lockPermissions: true
|
||||
})
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`Ticket Closed by ${userMention(interaction.member.id)}.`
|
||||
)
|
||||
.setColor([255, 97, 97])
|
||||
],
|
||||
components: [
|
||||
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(SUPPORT_TICKET_BUTTON_DELETE_ID)
|
||||
.setLabel('Delete')
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
)
|
||||
]
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSupportTicketsButtonDelete = async (
|
||||
interaction: ButtonInteraction<CacheType>
|
||||
): Promise<boolean> => {
|
||||
if (interaction.replied) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.member instanceof GuildMember)) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.channel instanceof GuildChannel)) {
|
||||
return false
|
||||
}
|
||||
if (interaction.customId !== SUPPORT_TICKET_BUTTON_DELETE_ID) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
interaction.channel.parentId !== DISCORD_SUPPORT_TICKETS_CLOSED_CATEGORY_ID
|
||||
) {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`⚠️ This ticket is ${bold(
|
||||
'already open'
|
||||
)} or not in the correct category.`
|
||||
)
|
||||
.setColor([255, 97, 97])
|
||||
]
|
||||
})
|
||||
return true
|
||||
}
|
||||
await interaction.channel.delete()
|
||||
return true
|
||||
}
|
||||
|
||||
export const handleSupportTicketsButtonCloseDelete = async (
|
||||
interaction: ButtonInteraction<CacheType>
|
||||
): Promise<boolean> => {
|
||||
return await firstTruePromise([
|
||||
handleSupportTicketsButtonClose(interaction),
|
||||
handleSupportTicketsButtonDelete(interaction)
|
||||
])
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import {
|
||||
GuildMember,
|
||||
type CacheType,
|
||||
type ModalSubmitInteraction,
|
||||
Guild,
|
||||
ChannelType,
|
||||
userMention,
|
||||
EmbedBuilder,
|
||||
channelMention,
|
||||
codeBlock,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle
|
||||
} from 'discord.js'
|
||||
|
||||
import type { SupportTicketId } from '#src/configuration/support-tickets.js'
|
||||
import {
|
||||
DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID,
|
||||
SUPPORT_TICKETS_CATEGORIES,
|
||||
SUPPORT_TICKETS_IMAGE_URL,
|
||||
SUPPORT_TICKET_BUTTON_CLOSE_ID
|
||||
} from '#src/configuration/support-tickets.js'
|
||||
|
||||
export const handleSupportTicketsModalSubmit = async (
|
||||
interaction: ModalSubmitInteraction<CacheType>
|
||||
): Promise<boolean> => {
|
||||
if (interaction.replied) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.member instanceof GuildMember)) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.guild instanceof Guild)) {
|
||||
return false
|
||||
}
|
||||
const category = SUPPORT_TICKETS_CATEGORIES.get(
|
||||
interaction.customId as SupportTicketId
|
||||
)
|
||||
if (category == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const channelName = `${category.channelName}-${interaction.member.user.username}`
|
||||
const channel = await interaction.guild.channels.create({
|
||||
name: channelName,
|
||||
type: ChannelType.GuildText,
|
||||
parent: DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID
|
||||
})
|
||||
await channel.lockPermissions()
|
||||
await channel.permissionOverwrites.edit(interaction.member.user.id, {
|
||||
ViewChannel: true,
|
||||
SendMessages: true
|
||||
})
|
||||
await channel.send({
|
||||
content: userMention(interaction.member.id),
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setTitle('ARK: Survival Ascended Community Discord Support')
|
||||
.setDescription(
|
||||
`
|
||||
Support will be with you shortly.
|
||||
To close this ticket react with the button below.
|
||||
`
|
||||
)
|
||||
.setImage(SUPPORT_TICKETS_IMAGE_URL)
|
||||
.setColor([56, 142, 60]),
|
||||
new EmbedBuilder().setDescription(
|
||||
category.textInputs
|
||||
.map((textInput, index) => {
|
||||
const label = textInput.data.label ?? `Field ${index}`
|
||||
const value = interaction.fields.getTextInputValue(
|
||||
textInput.data.custom_id ?? 'custom_id'
|
||||
)
|
||||
return `**${label}**\n${codeBlock(value)}`
|
||||
})
|
||||
.join('\n')
|
||||
)
|
||||
],
|
||||
components: [
|
||||
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(SUPPORT_TICKET_BUTTON_CLOSE_ID)
|
||||
.setLabel('Close')
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
)
|
||||
]
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
content: `✅ Ticket submitted successfully. Please check ${channelMention(
|
||||
channel.id
|
||||
)} for updates.`,
|
||||
ephemeral: true
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
GuildMember,
|
||||
ModalBuilder,
|
||||
ActionRowBuilder,
|
||||
Guild,
|
||||
CategoryChannel,
|
||||
channelMention
|
||||
} from 'discord.js'
|
||||
import type {
|
||||
CacheType,
|
||||
ModalActionRowComponentBuilder,
|
||||
StringSelectMenuInteraction
|
||||
} from 'discord.js'
|
||||
|
||||
import type { SupportTicketId } from '#src/configuration/support-tickets.js'
|
||||
import {
|
||||
DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID,
|
||||
SUPPORT_TICKETS_CATEGORIES,
|
||||
SUPPORT_TICKETS_SELECT_MENU_ID
|
||||
} from '#src/configuration/support-tickets.js'
|
||||
|
||||
export const handleSupportTicketsSelectMenu = async (
|
||||
interaction: StringSelectMenuInteraction<CacheType>
|
||||
): Promise<boolean> => {
|
||||
if (interaction.replied) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.member instanceof GuildMember)) {
|
||||
return false
|
||||
}
|
||||
if (!(interaction.guild instanceof Guild)) {
|
||||
return false
|
||||
}
|
||||
if (interaction.customId !== SUPPORT_TICKETS_SELECT_MENU_ID) {
|
||||
return false
|
||||
}
|
||||
const value = interaction.values[0]
|
||||
const category = SUPPORT_TICKETS_CATEGORIES.get(value as SupportTicketId)
|
||||
if (category == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const channelName = `${category.channelName}-${interaction.member.user.username}`
|
||||
const openCategory = await interaction.guild.channels.fetch(
|
||||
DISCORD_SUPPORT_TICKETS_OPEN_CATEGORY_ID
|
||||
)
|
||||
if (!(openCategory instanceof CategoryChannel)) {
|
||||
return false
|
||||
}
|
||||
const openTicket = openCategory.children.cache.find((channel) => {
|
||||
return channel.name === channelName
|
||||
})
|
||||
const hasAlreadyTicketOpen = openTicket != null
|
||||
if (hasAlreadyTicketOpen) {
|
||||
await interaction.reply({
|
||||
content: `⚠️ You already have a ticket open in this category. Please check ${channelMention(
|
||||
openTicket.id
|
||||
)} for updates.`,
|
||||
ephemeral: true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
await interaction.showModal(
|
||||
new ModalBuilder()
|
||||
.setCustomId(category.id)
|
||||
.setTitle(category.title)
|
||||
.addComponents(
|
||||
...category.textInputs.map((textInput) => {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
||||
textInput
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Routes, EmbedBuilder } from 'discord.js'
|
||||
|
||||
// import { DISCORD_ADMIN_COMMANDS_CHANNEL_ID } from '#src/configuration/qotd.js'
|
||||
import { DISCORD_QOTD_ANSWERS_CHANNEL_ID } from '#src/configuration/qotd.js'
|
||||
import { discordRest } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const QOTD_DAY_URL = 'https://i.imgur.com/JOTjHva.png'
|
||||
|
||||
// await discordRest.post(Routes.channelMessages(DISCORD_ADMIN_COMMANDS_CHANNEL_ID), {
|
||||
await discordRest.post(
|
||||
Routes.channelMessages(DISCORD_QOTD_ANSWERS_CHANNEL_ID),
|
||||
{
|
||||
body: {
|
||||
embeds: [
|
||||
new EmbedBuilder().setImage(QOTD_DAY_URL).setColor([255, 97, 97])
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log('Successfully sent QOTD Day message.')
|
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
Routes,
|
||||
roleMention,
|
||||
EmbedBuilder,
|
||||
channelMention,
|
||||
bold
|
||||
} from 'discord.js'
|
||||
|
||||
// import { DISCORD_QOTD_ANSWERS_CHANNEL_ID, DISCORD_ADMIN_COMMANDS_CHANNEL_ID, DISCORD_QOTD_ROLE_ID } from '#src/configuration/qotd.js'
|
||||
import {
|
||||
DISCORD_QOTD_ANSWERS_CHANNEL_ID,
|
||||
DISCORD_QOTD_QUESTIONS_CHANNEL_ID,
|
||||
DISCORD_QOTD_ROLE_ID
|
||||
} from '#src/configuration/qotd.js'
|
||||
import { discordRest } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const QOTD_QUESTION_URL = 'https://i.imgur.com/ist5BrW.jpg'
|
||||
|
||||
// await discordRest.post(Routes.channelMessages(DISCORD_ADMIN_COMMANDS_CHANNEL_ID), {
|
||||
await discordRest.post(
|
||||
Routes.channelMessages(DISCORD_QOTD_QUESTIONS_CHANNEL_ID),
|
||||
{
|
||||
body: {
|
||||
content: `${roleMention(DISCORD_QOTD_ROLE_ID)}`,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`
|
||||
## Question Of The Day #18
|
||||
|
||||
Following yesterday's question (day 17), about taming creatures.
|
||||
Which ${bold(
|
||||
'type of creature'
|
||||
)} will be your favorite on ARK: Survival Ascended:
|
||||
- 🦖 ${bold('Land')}
|
||||
- 🐦 ${bold('Air')}
|
||||
- 🐟 ${bold('Water')}
|
||||
|
||||
Discuss about it with other survivors in ${channelMention(
|
||||
DISCORD_QOTD_ANSWERS_CHANNEL_ID
|
||||
)}.
|
||||
|
||||
*(feel free to suggest questions ideas in ${channelMention(
|
||||
'1082438215790772374'
|
||||
)})*
|
||||
`
|
||||
)
|
||||
.setImage(QOTD_QUESTION_URL)
|
||||
.setColor([255, 97, 97])
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log('Successfully sent QOTD Question message.')
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
Routes,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
roleMention,
|
||||
EmbedBuilder
|
||||
} from 'discord.js'
|
||||
|
||||
import {
|
||||
QOTD_BUTTON_ID,
|
||||
DISCORD_QOTD_REACT_CHANNEL_ID,
|
||||
DISCORD_QOTD_ROLE_ID
|
||||
} from '#src/configuration/qotd.js'
|
||||
import { discordRest } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const QOTD_URL = 'https://i.imgur.com/kB67d7N.png'
|
||||
|
||||
await discordRest.post(Routes.channelMessages(DISCORD_QOTD_REACT_CHANNEL_ID), {
|
||||
body: {
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`
|
||||
## Question Of The Day
|
||||
|
||||
If you want to be notified for questions, polls, quizzes, please click on the button below to get the ${roleMention(
|
||||
DISCORD_QOTD_ROLE_ID
|
||||
)} role.
|
||||
|
||||
You can always unassign the role by clicking again on the button.
|
||||
`
|
||||
)
|
||||
.setImage(QOTD_URL)
|
||||
.setColor([255, 97, 97])
|
||||
],
|
||||
components: [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(QOTD_BUTTON_ID)
|
||||
.setEmoji('<:QOTD:1155553559144841236>')
|
||||
.setLabel('I want to be notified for questions.')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
console.log('Successfully sent QOTD message.')
|
|
@ -0,0 +1,5 @@
|
|||
import { DiscordClient } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const discordClient = await DiscordClient.getInstance()
|
||||
await discordClient.registerCommands()
|
||||
console.log('Successfully reloaded application (/) commands.')
|
136
apps/ark-ascended-discord-bot/src/scripts/send-rules-message.ts
Normal file
136
apps/ark-ascended-discord-bot/src/scripts/send-rules-message.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { EmbedBuilder } from '@discordjs/builders'
|
||||
import {
|
||||
roleMention,
|
||||
channelMention,
|
||||
Routes,
|
||||
bold,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle
|
||||
} from 'discord.js'
|
||||
|
||||
import {
|
||||
DISCORD_ADMINISTRATOR_ROLE_ID,
|
||||
DISCORD_BOT_COMMANDS_CHANNEL_ID,
|
||||
DISCORD_CONTENT_CREATOR_ROLE_ID,
|
||||
DISCORD_RULES_CHANNEL_ID,
|
||||
DISCORD_SERVER_BOOSTER_ROLE_ID,
|
||||
SURVIVOR_BUTTON_ID
|
||||
} from '#src/configuration/global.js'
|
||||
import { discordRest } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
const LOGO_URL = 'https://i.imgur.com/qfyWYkE.png'
|
||||
const DISCORD_RULES_IMAGE_URL = 'https://imgur.com/HznoDDv.png'
|
||||
|
||||
await discordRest.post(Routes.channelMessages(DISCORD_RULES_CHANNEL_ID), {
|
||||
body: {
|
||||
content: 'https://discord.gg/playAscended',
|
||||
embeds: [
|
||||
new EmbedBuilder().setImage(LOGO_URL),
|
||||
new EmbedBuilder()
|
||||
.setTitle(
|
||||
'Hello Survivors and welcome to the ARK Ascended Community Discord server!'
|
||||
)
|
||||
.setDescription(
|
||||
`
|
||||
Survive the past. Tame the future. Suddenly awakened on a primal world filled with dinosaurs and humans struggling for dominance, you must team-up with legendary heroes to confront powerful dark forces. Saddle up, and join the definitive next-gen survival experience!
|
||||
|
||||
This is your destination to discuss all things about ARK, stay up to date with the latest news, and find other players to play with and most importantly, have fun!
|
||||
|
||||
${bold('Not affiliated with Studio Wildcard. :bow_and_arrow:')}
|
||||
`
|
||||
)
|
||||
.setFields(
|
||||
{
|
||||
name: bold('Roles'),
|
||||
value: `
|
||||
${roleMention(
|
||||
DISCORD_SERVER_BOOSTER_ROLE_ID
|
||||
)} - Boost the server to get this role and we're grateful for your support!
|
||||
${roleMention(
|
||||
DISCORD_CONTENT_CREATOR_ROLE_ID
|
||||
)} - Use the \`/content-creator-role\` command to get this role.
|
||||
|
||||
Commands should be executed in the ${channelMention(
|
||||
DISCORD_BOT_COMMANDS_CHANNEL_ID
|
||||
)} channel.
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'Voice Channels',
|
||||
value: `
|
||||
${bold('•')} The voice channels are ${bold('self-moderated')}.
|
||||
${bold('•')} You can create your ${bold(
|
||||
'own voice channel'
|
||||
)} by joining the \`Lobby\` voice channel.
|
||||
You will be granted ownership of a new voice channel that you can rename and manage (only possible with 2FA enabled).
|
||||
${bold(
|
||||
'•'
|
||||
)} Voice channels will automatically be deleted if the channel is empty.
|
||||
`
|
||||
},
|
||||
{
|
||||
name: bold('Links'),
|
||||
value: `
|
||||
${bold('•')} [ARKCountdown Twitch](https://www.twitch.tv/arkcountdown)
|
||||
${bold('•')} [ARK Ascended News Twitter](https://twitter.com/ARKAscendedNews)
|
||||
${bold(
|
||||
'•'
|
||||
)} [ARK Ascended News Instagram](https://www.instagram.com/arkascendednews)
|
||||
${bold(
|
||||
'•'
|
||||
)} [ARK 2 Steam page](https://store.steampowered.com/app/2050420/ARK_2/)
|
||||
`
|
||||
}
|
||||
),
|
||||
new EmbedBuilder().setImage(DISCORD_RULES_IMAGE_URL),
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`
|
||||
We want everyone to enjoy their time in the community, so we've laid down a few rules to keep this server a safe and positive environment for all.
|
||||
|
||||
These rules will be enforced by our team of ${roleMention(
|
||||
DISCORD_ADMINISTRATOR_ROLE_ID
|
||||
)} volunteers, so please read them carefully and adhere to them at all times, and follow the direction of our moderation team. Failure to do so could result in a mute or ban.`
|
||||
)
|
||||
.setFields({
|
||||
name: bold('General Rules:'),
|
||||
value: `
|
||||
${bold('•')} Use common sense.
|
||||
${bold('•')} The primary language of this server is English.
|
||||
${bold('•')} Do not publicly accuse other users/players of misconduct.
|
||||
${bold('•')} Do not spam messages, images, emoji, commands, and @mentions.
|
||||
${bold(
|
||||
'•'
|
||||
)} Trading, selling, begging, boosting and account sharing are not allowed.
|
||||
${bold('•')} Aggressive or 'toxic' behaviour is forbidden.
|
||||
${bold('•')} Multiple accounts are not permitted.
|
||||
${bold(
|
||||
'•'
|
||||
)} No posting content related to piracy, cheats, cracks, exploits along with NSFW.
|
||||
|
||||
The following documents are also strictly enforced:
|
||||
• ${bold('[Discord Community Guidelines](https://dis.gd/guidelines)')}
|
||||
• ${bold('[Discord Terms of Service](https://dis.gd/terms)')}
|
||||
`
|
||||
}),
|
||||
new EmbedBuilder()
|
||||
.setColor([220, 148, 22])
|
||||
.setDescription(
|
||||
bold(
|
||||
':warning: To access the community channels, after reading and agreeing to the rules, click on the button below to receive the Survivor role.'
|
||||
)
|
||||
)
|
||||
],
|
||||
components: [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(SURVIVOR_BUTTON_ID)
|
||||
.setEmoji('✅')
|
||||
.setLabel('I am a survivor and accept the rules!')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
console.log('Successfully sent rules message.')
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
Routes,
|
||||
bold,
|
||||
EmbedBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
ActionRowBuilder
|
||||
} from 'discord.js'
|
||||
|
||||
import {
|
||||
DISCORD_SUPPORT_TICKETS_CHANNEL_ID,
|
||||
SUPPORT_TICKETS_IMAGE_URL,
|
||||
SUPPORT_TICKETS_SELECT_MENU_ID,
|
||||
SUPPORT_TICKETS_CATEGORIES
|
||||
} from '#src/configuration/support-tickets.js'
|
||||
import { discordRest } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
await discordRest.post(
|
||||
Routes.channelMessages(DISCORD_SUPPORT_TICKETS_CHANNEL_ID),
|
||||
{
|
||||
body: {
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setTitle('ARK: Survival Ascended Community Discord Support')
|
||||
.setDescription(
|
||||
`
|
||||
We appreciate you reaching out to our ${bold(
|
||||
'ARK: Survival Ascended'
|
||||
)} support channel.
|
||||
To help us better understand you and provide you with the most appropriate assistance, please provide as much detail as possible in your support ticket.
|
||||
|
||||
Our team is dedicated to delivering timely and effective support and we will do our utmost to respond as quickly as possible.
|
||||
`
|
||||
)
|
||||
.setImage(SUPPORT_TICKETS_IMAGE_URL)
|
||||
.setColor([56, 142, 60])
|
||||
],
|
||||
components: [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId(SUPPORT_TICKETS_SELECT_MENU_ID)
|
||||
.setPlaceholder('Select an option to create a ticket')
|
||||
.setMaxValues(1)
|
||||
.addOptions(
|
||||
[...SUPPORT_TICKETS_CATEGORIES.values()].map((category) => {
|
||||
return new StringSelectMenuOptionBuilder()
|
||||
.setLabel(category.title)
|
||||
.setDescription(category.description)
|
||||
.setValue(category.id)
|
||||
.setEmoji(category.emoji)
|
||||
})
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log('Successfully sent Support Tickets message.')
|
|
@ -0,0 +1,165 @@
|
|||
import fs from 'node:fs'
|
||||
|
||||
import axios from 'axios'
|
||||
import type { Invite, User } from 'discord.js'
|
||||
import { Client, Collection, GatewayIntentBits, REST, Routes } from 'discord.js'
|
||||
|
||||
import type { DiscordEvent } from '#src/services/discord/DiscordEvent.js'
|
||||
import type { DiscordCommand } from '#src/services/discord/DiscordCommand.js'
|
||||
import {
|
||||
DISCORD_CLIENT_ID,
|
||||
DISCORD_GUILD_ID,
|
||||
DISCORD_TOKEN,
|
||||
DISCORD_TOKEN_USER
|
||||
} from '#src/configuration/global.js'
|
||||
|
||||
const DISCORD_API_VERSION = '10'
|
||||
|
||||
export const discordAPI = axios.create({
|
||||
baseURL: `https://discord.com/api/v${DISCORD_API_VERSION}/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: DISCORD_TOKEN_USER
|
||||
}
|
||||
})
|
||||
|
||||
export const discordRest = new REST({ version: DISCORD_API_VERSION }).setToken(
|
||||
DISCORD_TOKEN
|
||||
)
|
||||
|
||||
const filesOnlyGetJavaScriptFiles = (files: string[]): string[] => {
|
||||
return files.filter((file) => {
|
||||
return file.endsWith('.js')
|
||||
})
|
||||
}
|
||||
|
||||
export class DiscordClient extends Client {
|
||||
private static instance: DiscordClient | null = null
|
||||
|
||||
public static COMMANDS_URL = new URL('../../commands/', import.meta.url)
|
||||
public static EVENTS_URL = new URL('../../events/', import.meta.url)
|
||||
|
||||
public commands: Collection<string, DiscordCommand> = new Collection()
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates
|
||||
]
|
||||
})
|
||||
this.token = DISCORD_TOKEN
|
||||
}
|
||||
|
||||
private async saveCommands(): Promise<void> {
|
||||
const commandsFiles = filesOnlyGetJavaScriptFiles(
|
||||
await fs.promises.readdir(DiscordClient.COMMANDS_URL)
|
||||
)
|
||||
for (const commandFile of commandsFiles) {
|
||||
const { default: command } = await import(
|
||||
new URL(`./${commandFile}`, DiscordClient.COMMANDS_URL).href
|
||||
)
|
||||
this.commands.set(command.data.name, command)
|
||||
}
|
||||
}
|
||||
|
||||
public static async getInstance(): Promise<DiscordClient> {
|
||||
if (DiscordClient.instance == null) {
|
||||
DiscordClient.instance = new DiscordClient()
|
||||
await DiscordClient.instance.saveCommands()
|
||||
await DiscordClient.instance.loadEvents()
|
||||
}
|
||||
return DiscordClient.instance
|
||||
}
|
||||
|
||||
public async loadEvents(): Promise<void> {
|
||||
const eventsFiles = filesOnlyGetJavaScriptFiles(
|
||||
await fs.promises.readdir(DiscordClient.EVENTS_URL)
|
||||
)
|
||||
for (const eventFile of eventsFiles) {
|
||||
const { default: event } = (await import(
|
||||
new URL(`./${eventFile}`, DiscordClient.EVENTS_URL).href
|
||||
)) as { default: DiscordEvent<any> }
|
||||
const eventExecute = async (...parameters: unknown[]): Promise<void> => {
|
||||
try {
|
||||
await event.execute(...parameters)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
if (event.once ?? false) {
|
||||
this.once(event.name, eventExecute)
|
||||
} else {
|
||||
this.on(event.name, eventExecute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async registerCommands(): Promise<void> {
|
||||
await discordRest.put(
|
||||
Routes.applicationGuildCommands(DISCORD_CLIENT_ID, DISCORD_GUILD_ID),
|
||||
{
|
||||
body: this.commands.map((command) => {
|
||||
return command.data
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param invitations
|
||||
* @returns
|
||||
*/
|
||||
public async getUsersInvitations(
|
||||
invitations?: Collection<string, Invite>
|
||||
): Promise<UsersInvitations> {
|
||||
if (invitations == null) {
|
||||
throw new Error('Failed to fetch invitations')
|
||||
}
|
||||
const usersInvitations: UsersInvitations = new Map()
|
||||
for (const invitation of invitations.values()) {
|
||||
if (invitation.inviter == null) {
|
||||
continue
|
||||
}
|
||||
const userInvitations = usersInvitations.get(invitation.inviter.id)
|
||||
if (userInvitations == null) {
|
||||
usersInvitations.set(invitation.inviter.id, {
|
||||
invitationsCount: invitation.uses ?? 0,
|
||||
inviter: invitation.inviter
|
||||
})
|
||||
} else {
|
||||
userInvitations.invitationsCount += invitation.uses ?? 0
|
||||
}
|
||||
}
|
||||
return usersInvitations
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key: User Id
|
||||
*
|
||||
* Value: Number of invitations
|
||||
*/
|
||||
export type UsersInvitations = Map<string, UserInvitations>
|
||||
|
||||
export interface UserInvitations {
|
||||
invitationsCount: number
|
||||
inviter: User
|
||||
}
|
||||
|
||||
export interface ConnectedAccount {
|
||||
type: 'github' | 'steam' | 'twitch' | 'twitter' | 'youtube'
|
||||
id: string
|
||||
name: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* /users/{id}/profile
|
||||
*/
|
||||
export interface DiscordGetUserProfileResponse {
|
||||
connected_accounts: ConnectedAccount[]
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import type {
|
||||
CacheType,
|
||||
ChatInputCommandInteraction,
|
||||
RESTPostAPIChatInputApplicationCommandsJSONBody
|
||||
} from 'discord.js'
|
||||
|
||||
export interface DiscordCommand {
|
||||
data: RESTPostAPIChatInputApplicationCommandsJSONBody
|
||||
execute: (
|
||||
interaction: ChatInputCommandInteraction<CacheType>
|
||||
) => Promise<void>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import type { ClientEvents, Events } from 'discord.js'
|
||||
|
||||
export interface DiscordEvent<
|
||||
K extends keyof ClientEvents = Events.ClientReady
|
||||
> {
|
||||
name: K
|
||||
once?: boolean
|
||||
execute: (..._arguments: ClientEvents[K]) => Promise<void>
|
||||
}
|
30
apps/ark-ascended-discord-bot/src/services/google.ts
Normal file
30
apps/ark-ascended-discord-bot/src/services/google.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { youtube as youtubeAPI } from '@googleapis/youtube'
|
||||
|
||||
import { GOOGLE_API_KEY } from '#src/configuration/global.js'
|
||||
import type { ConnectedAccount } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
export const youtube = youtubeAPI({
|
||||
version: 'v3',
|
||||
auth: GOOGLE_API_KEY
|
||||
})
|
||||
|
||||
export const getYouTubeSubscriberCount = async (
|
||||
account: ConnectedAccount
|
||||
): Promise<number> => {
|
||||
const channels = await youtube.channels.list({
|
||||
part: ['statistics'],
|
||||
id: [account.id]
|
||||
})
|
||||
if (channels.data.items == null || channels.data.items.length === 0) {
|
||||
throw new Error('No YouTube channels found')
|
||||
}
|
||||
const channel = channels.data.items[0]
|
||||
if (channel?.statistics == null) {
|
||||
throw new Error('No YouTube channel statistics found')
|
||||
}
|
||||
const { subscriberCount } = channel.statistics
|
||||
if (subscriberCount == null) {
|
||||
throw new Error('No YouTube channel subscriber count found')
|
||||
}
|
||||
return Number.parseInt(subscriberCount, 10)
|
||||
}
|
52
apps/ark-ascended-discord-bot/src/services/steam.ts
Normal file
52
apps/ark-ascended-discord-bot/src/services/steam.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import axios from 'axios'
|
||||
|
||||
import { STEAM_API_KEY } from '#src/configuration/global.js'
|
||||
|
||||
/**
|
||||
* Documentation: <https://partner.steamgames.com/doc/webapi>
|
||||
*/
|
||||
export const steamAPI = axios.create({
|
||||
baseURL: 'https://api.steampowered.com/',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
export const ARK2_APP_ID = 2050420
|
||||
export const ARK_SURVIVAL_EVOLVED_APP_ID = 346110
|
||||
|
||||
/**
|
||||
* /ISteamUserStats/GetNumberOfCurrentPlayers/v1/
|
||||
*/
|
||||
export interface SteamGetNumberOfCurrentPlayersResponse {
|
||||
response: {
|
||||
player_count: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /ISteamUser/GetPlayerSummaries/v2/
|
||||
*/
|
||||
export interface SteamGetPlayerSummaries {
|
||||
response: {
|
||||
players: Array<{
|
||||
steamid: string
|
||||
personaname: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const steamCheckPlayerSteamId = async (
|
||||
steamId: string
|
||||
): Promise<boolean> => {
|
||||
const { data } = await steamAPI.get<SteamGetPlayerSummaries>(
|
||||
'/ISteamUser/GetPlayerSummaries/v2/',
|
||||
{
|
||||
params: {
|
||||
key: STEAM_API_KEY,
|
||||
steamids: steamId
|
||||
}
|
||||
}
|
||||
)
|
||||
return data.response.players.length === 1
|
||||
}
|
78
apps/ark-ascended-discord-bot/src/services/twitch.ts
Normal file
78
apps/ark-ascended-discord-bot/src/services/twitch.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
TWITCH_CLIENT_ID,
|
||||
TWITCH_CLIENT_SECRET
|
||||
} from '#src/configuration/global.js'
|
||||
import type { ConnectedAccount } from '#src/services/discord/DiscordClient.js'
|
||||
|
||||
/**
|
||||
* Documentation: <https://partner.steamgames.com/doc/webapi>
|
||||
*/
|
||||
export const twitchAPI = axios.create({
|
||||
baseURL: 'https://api.twitch.tv/helix/',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
export const twitchOAuthAPI = axios.create({
|
||||
baseURL: 'https://id.twitch.tv/oauth2/',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* /users/follows
|
||||
*/
|
||||
export interface TwitchGetUsersFollowsResponse {
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* /token
|
||||
*/
|
||||
export interface TwitchCredentials {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
token_type: 'bearer'
|
||||
}
|
||||
|
||||
export interface TwitchCredendialsExtended extends TwitchCredentials {
|
||||
access_token_age: number
|
||||
}
|
||||
|
||||
let credentials: TwitchCredendialsExtended | null = null
|
||||
|
||||
export const getTwitchFollowerCount = async (
|
||||
account: ConnectedAccount
|
||||
): Promise<number> => {
|
||||
const isValidCredentials =
|
||||
credentials !== null &&
|
||||
credentials.access_token_age + credentials.expires_in > Date.now()
|
||||
if (credentials == null || !isValidCredentials) {
|
||||
const parameters = new URLSearchParams()
|
||||
parameters.append('client_id', TWITCH_CLIENT_ID)
|
||||
parameters.append('client_secret', TWITCH_CLIENT_SECRET)
|
||||
parameters.append('grant_type', 'client_credentials')
|
||||
const { data: twitchCredentials } =
|
||||
await twitchOAuthAPI.post<TwitchCredentials>('/token', parameters)
|
||||
credentials = {
|
||||
...twitchCredentials,
|
||||
access_token_age: Date.now()
|
||||
}
|
||||
}
|
||||
const { data: twitchData } =
|
||||
await twitchAPI.get<TwitchGetUsersFollowsResponse>('/channels/followers', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.access_token}`,
|
||||
'Client-ID': TWITCH_CLIENT_ID
|
||||
},
|
||||
params: {
|
||||
broadcaster_id: account.id
|
||||
}
|
||||
})
|
||||
const followerCount = twitchData.total
|
||||
return followerCount
|
||||
}
|
17
apps/ark-ascended-discord-bot/tsconfig.json
Normal file
17
apps/ark-ascended-discord-bot/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "@ark-unity/tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#src/*": ["./src/*"]
|
||||
},
|
||||
"checkJs": false
|
||||
},
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
6707
package-lock.json
generated
Normal file
6707
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "ark-unity",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"test": "turbo run test",
|
||||
"lint": "turbo run lint",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:prettier": "prettier . --check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ark-unity/eslint-config-custom": "*",
|
||||
"editorconfig-checker": "5.1.1",
|
||||
"prettier": "3.0.3",
|
||||
"prettier-plugin-tailwindcss": "0.5.6",
|
||||
"turbo": "1.10.15",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
12
packages/eslint-config-custom/.eslintrc.json
Normal file
12
packages/eslint-config-custom/.eslintrc.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": ["conventions", "turbo", "prettier"],
|
||||
"plugins": ["prettier"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
22
packages/eslint-config-custom/package.json
Normal file
22
packages/eslint-config-custom/package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "@ark-unity/eslint-config-custom",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": ".eslintrc.json",
|
||||
"files": [
|
||||
".eslintrc.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||
"@typescript-eslint/parser": "6.8.0",
|
||||
"eslint": "8.51.0",
|
||||
"eslint-config-conventions": "11.0.1",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-config-turbo": "1.10.15",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-unicorn": "48.0.1",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
8
packages/tsconfig/package.json
Normal file
8
packages/tsconfig/package.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@ark-unity/tsconfig",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"tsconfig.json"
|
||||
]
|
||||
}
|
19
packages/tsconfig/tsconfig.json
Normal file
19
packages/tsconfig/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
10
packages/utils/.eslintrc.json
Normal file
10
packages/utils/.eslintrc.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["@ark-unity/eslint-config-custom"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"import/extensions": ["error", "always"]
|
||||
}
|
||||
}
|
13
packages/utils/.swcrc
Normal file
13
packages/utils/.swcrc
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"dynamicImport": true
|
||||
},
|
||||
"target": "esnext"
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
35
packages/utils/package.json
Normal file
35
packages/utils/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@ark-unity/utils",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"files": [
|
||||
"build",
|
||||
"!**/*.test.js",
|
||||
"!**/*.test.d.ts",
|
||||
"!**/*.map"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rimraf ./build && swc ./src --out-dir ./build && tsc",
|
||||
"dev:build": "swc ./src --out-dir ./build --watch",
|
||||
"dev:tsc": "tsc --watch --preserveWatchOutput",
|
||||
"dev": "concurrently --names \"swc,tsc\" \"npm run dev:build\" \"npm run dev:tsc\"",
|
||||
"lint": "eslint ./src --max-warnings 0 --report-unused-disable-directives",
|
||||
"test": "cross-env NODE_ENV=test node --enable-source-maps --test build/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ark-unity/eslint-config-custom": "*",
|
||||
"@ark-unity/tsconfig": "*",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.93",
|
||||
"@types/node": "20.8.6",
|
||||
"chokidar": "3.5.3",
|
||||
"concurrently": "8.2.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.51.0",
|
||||
"rimraf": "5.0.5",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
50
packages/utils/src/__test__/formatNumberOrdinals.test.ts
Normal file
50
packages/utils/src/__test__/formatNumberOrdinals.test.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { formatNumberOrdinals } from '../formatNumberOrdinals.js'
|
||||
|
||||
await test('utils/formatNumberOrdinals', async () => {
|
||||
assert.strictEqual(formatNumberOrdinals(1), '1st')
|
||||
assert.strictEqual(formatNumberOrdinals(2), '2nd')
|
||||
assert.strictEqual(formatNumberOrdinals(3), '3rd')
|
||||
assert.strictEqual(formatNumberOrdinals(4), '4th')
|
||||
assert.strictEqual(formatNumberOrdinals(5), '5th')
|
||||
assert.strictEqual(formatNumberOrdinals(6), '6th')
|
||||
assert.strictEqual(formatNumberOrdinals(7), '7th')
|
||||
assert.strictEqual(formatNumberOrdinals(8), '8th')
|
||||
assert.strictEqual(formatNumberOrdinals(9), '9th')
|
||||
assert.strictEqual(formatNumberOrdinals(10), '10th')
|
||||
assert.strictEqual(formatNumberOrdinals(11), '11th')
|
||||
assert.strictEqual(formatNumberOrdinals(12), '12th')
|
||||
assert.strictEqual(formatNumberOrdinals(13), '13th')
|
||||
assert.strictEqual(formatNumberOrdinals(14), '14th')
|
||||
assert.strictEqual(formatNumberOrdinals(15), '15th')
|
||||
assert.strictEqual(formatNumberOrdinals(16), '16th')
|
||||
assert.strictEqual(formatNumberOrdinals(17), '17th')
|
||||
assert.strictEqual(formatNumberOrdinals(18), '18th')
|
||||
assert.strictEqual(formatNumberOrdinals(19), '19th')
|
||||
assert.strictEqual(formatNumberOrdinals(20), '20th')
|
||||
assert.strictEqual(formatNumberOrdinals(21), '21st')
|
||||
assert.strictEqual(formatNumberOrdinals(22), '22nd')
|
||||
assert.strictEqual(formatNumberOrdinals(23), '23rd')
|
||||
assert.strictEqual(formatNumberOrdinals(24), '24th')
|
||||
assert.strictEqual(formatNumberOrdinals(25), '25th')
|
||||
assert.strictEqual(formatNumberOrdinals(26), '26th')
|
||||
assert.strictEqual(formatNumberOrdinals(27), '27th')
|
||||
assert.strictEqual(formatNumberOrdinals(28), '28th')
|
||||
assert.strictEqual(formatNumberOrdinals(29), '29th')
|
||||
assert.strictEqual(formatNumberOrdinals(30), '30th')
|
||||
assert.strictEqual(formatNumberOrdinals(31), '31st')
|
||||
assert.strictEqual(formatNumberOrdinals(32), '32nd')
|
||||
assert.strictEqual(formatNumberOrdinals(33), '33rd')
|
||||
assert.strictEqual(formatNumberOrdinals(34), '34th')
|
||||
assert.strictEqual(formatNumberOrdinals(35), '35th')
|
||||
assert.strictEqual(formatNumberOrdinals(501), '501st')
|
||||
assert.strictEqual(formatNumberOrdinals(502), '502nd')
|
||||
assert.strictEqual(formatNumberOrdinals(503), '503rd')
|
||||
assert.strictEqual(formatNumberOrdinals(504), '504th')
|
||||
assert.strictEqual(formatNumberOrdinals(571), '571st')
|
||||
assert.strictEqual(formatNumberOrdinals(572), '572nd')
|
||||
assert.strictEqual(formatNumberOrdinals(573), '573rd')
|
||||
assert.strictEqual(formatNumberOrdinals(574), '574th')
|
||||
})
|
13
packages/utils/src/__test__/pluralizeWord.test.ts
Normal file
13
packages/utils/src/__test__/pluralizeWord.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { pluralizeWord } from '../pluralizeWord.js'
|
||||
|
||||
await test('utils/pluralizeWord', async () => {
|
||||
assert.strictEqual(pluralizeWord('word', 0), 'word')
|
||||
assert.strictEqual(pluralizeWord('word', 1), 'word')
|
||||
assert.strictEqual(pluralizeWord('word', 2), 'words')
|
||||
assert.strictEqual(pluralizeWord('word', 3), 'words')
|
||||
assert.strictEqual(pluralizeWord('member', 1), 'member')
|
||||
assert.strictEqual(pluralizeWord('member', 571), 'members')
|
||||
})
|
11
packages/utils/src/firstTruePromise.ts
Normal file
11
packages/utils/src/firstTruePromise.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const firstTruePromise = async (
|
||||
promises: Array<Promise<boolean>>
|
||||
): Promise<boolean> => {
|
||||
for (const promise of promises) {
|
||||
const result = await promise
|
||||
if (result) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
18
packages/utils/src/formatNumberOrdinals.ts
Normal file
18
packages/utils/src/formatNumberOrdinals.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export const LOCALE = 'en-US' as const
|
||||
|
||||
const pluralRules = new Intl.PluralRules(LOCALE, { type: 'ordinal' })
|
||||
|
||||
const suffixes = {
|
||||
zero: 'th',
|
||||
one: 'st',
|
||||
two: 'nd',
|
||||
few: 'rd',
|
||||
many: 'th',
|
||||
other: 'th'
|
||||
} as const
|
||||
|
||||
export const formatNumberOrdinals = (number: number): string => {
|
||||
const rule = pluralRules.select(number)
|
||||
const suffix = suffixes[rule]
|
||||
return `${number.toLocaleString(LOCALE)}${suffix}`
|
||||
}
|
9
packages/utils/src/getRandomNumber.ts
Normal file
9
packages/utils/src/getRandomNumber.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Get a random number between a minimum (inclusive) and maximum (inclusive) value.
|
||||
* @param minimum
|
||||
* @param maximum
|
||||
* @returns
|
||||
*/
|
||||
export const getRandomNumber = (minimum: number, maximum: number): number => {
|
||||
return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum
|
||||
}
|
4
packages/utils/src/index.ts
Normal file
4
packages/utils/src/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './firstTruePromise.js'
|
||||
export * from './formatNumberOrdinals.js'
|
||||
export * from './getRandomNumber.js'
|
||||
export * from './pluralizeWord.js'
|
6
packages/utils/src/pluralizeWord.ts
Normal file
6
packages/utils/src/pluralizeWord.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const pluralizeWord = (word: string, count: number): string => {
|
||||
if (count <= 1) {
|
||||
return word
|
||||
}
|
||||
return `${word}s`
|
||||
}
|
14
packages/utils/tsconfig.json
Normal file
14
packages/utils/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "@ark-unity/tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
25
turbo.json
Normal file
25
turbo.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalEnv": ["NODE_ENV"],
|
||||
"globalDependencies": ["**/.env"],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", "build/**", ".next/**", "!.next/cache/**"],
|
||||
"inputs": ["**/*.tsx", "**/*.ts"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["build"],
|
||||
"inputs": ["**/*.tsx", "**/*.ts"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["build"],
|
||||
"outputs": [],
|
||||
"inputs": ["**/*.tsx", "**/*.ts"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user