mirror of
https://github.com/Hessenuk/DiscordTickets.git
synced 2024-12-23 00:03:09 +02:00
feat!: v4 (merge pull request #425 from discord-tickets/v4)
This commit is contained in:
commit
12647decda
@ -4,13 +4,11 @@
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"CONTRIBUTORS.md",
|
||||
"README.md"
|
||||
"CONTRIBUTORS.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commit": true,
|
||||
"commitConvention": "angular",
|
||||
"badgeTemplate": "<a href=\"https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md\">\n<img src=\"https://img.shields.io/github/all-contributors/discord-tickets/bot?color=ee8449&style=flat-square\"alt=\"All contributors\">\n</a>",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "eartharoid",
|
||||
|
1
.commitlintrc.js
Normal file
1
.commitlintrc.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = { extends: ['@commitlint/config-conventional'] };
|
@ -1 +1,19 @@
|
||||
# only db, scripts, src, and user directories are needed
|
||||
.github
|
||||
.husky
|
||||
.vscode
|
||||
logs
|
||||
node_modules
|
||||
# prisma will generated by postinstall script
|
||||
prisma
|
||||
.all-contributorsrc
|
||||
.commitlintrc.js
|
||||
.dockerignore
|
||||
.env
|
||||
.eslintrc.json
|
||||
.gitattributes
|
||||
.gitignore
|
||||
CONTRIBUTORS.md
|
||||
Dockerfile
|
||||
jsconfig.json
|
||||
README.md
|
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
189
.eslintrc.js
189
.eslintrc.js
@ -1,189 +0,0 @@
|
||||
module.exports = {
|
||||
'env': {
|
||||
'commonjs': true,
|
||||
'es2021': true,
|
||||
'node': true
|
||||
},
|
||||
'extends': 'eslint:recommended',
|
||||
'parserOptions': { 'ecmaVersion': 12 },
|
||||
'rules': {
|
||||
'array-bracket-newline': [
|
||||
'error',
|
||||
'consistent'
|
||||
],
|
||||
'array-bracket-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'array-element-newline': [
|
||||
'error',
|
||||
'consistent'
|
||||
],
|
||||
'arrow-body-style': [
|
||||
'error',
|
||||
'as-needed'
|
||||
],
|
||||
'arrow-parens': [
|
||||
'error',
|
||||
'as-needed'
|
||||
],
|
||||
'block-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'brace-style': [
|
||||
'error',
|
||||
'1tbs'
|
||||
],
|
||||
'comma-dangle': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'comma-spacing': [
|
||||
'error',
|
||||
{
|
||||
'after': true,
|
||||
'before': false
|
||||
}
|
||||
],
|
||||
'comma-style': [
|
||||
'error',
|
||||
'last'
|
||||
],
|
||||
'computed-property-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'curly': [
|
||||
'error',
|
||||
'multi-line', // 'multi'
|
||||
'consistent'
|
||||
],
|
||||
'default-case-last': [
|
||||
'error'
|
||||
],
|
||||
'dot-location': [
|
||||
'error',
|
||||
'property'
|
||||
],
|
||||
'dot-notation': [
|
||||
'error'
|
||||
],
|
||||
'eqeqeq': [
|
||||
'error'
|
||||
],
|
||||
'func-call-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'indent': [
|
||||
'error',
|
||||
'tab'
|
||||
],
|
||||
'linebreak-style': [
|
||||
'off',
|
||||
'windows'
|
||||
],
|
||||
'max-depth': [
|
||||
'warn',
|
||||
{ 'max': 5 }
|
||||
],
|
||||
'max-len': [
|
||||
'warn',
|
||||
{
|
||||
'code': 150,
|
||||
'ignoreRegExpLiterals': true,
|
||||
'ignoreStrings': true,
|
||||
'ignoreTemplateLiterals': true,
|
||||
'ignoreTrailingComments': true,
|
||||
'ignoreUrls': true
|
||||
}
|
||||
],
|
||||
'max-lines': [
|
||||
'warn',
|
||||
500
|
||||
],
|
||||
'max-statements-per-line': [
|
||||
'error'
|
||||
],
|
||||
'multiline-comment-style': [
|
||||
'warn'
|
||||
],
|
||||
'no-console': [
|
||||
'warn'
|
||||
],
|
||||
'no-return-assign': [
|
||||
'error'
|
||||
],
|
||||
'no-template-curly-in-string': [
|
||||
'warn'
|
||||
],
|
||||
'no-trailing-spaces': [
|
||||
'error'
|
||||
],
|
||||
'no-underscore-dangle': [
|
||||
'error'
|
||||
],
|
||||
'no-unneeded-ternary': [
|
||||
'error'
|
||||
],
|
||||
'no-var': [
|
||||
'error'
|
||||
],
|
||||
'no-whitespace-before-property': [
|
||||
'error'
|
||||
],
|
||||
'object-curly-newline': [
|
||||
'error',
|
||||
{
|
||||
'minProperties': 2,
|
||||
'multiline': true
|
||||
}
|
||||
],
|
||||
'object-curly-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'object-property-newline': [
|
||||
'error'
|
||||
],
|
||||
'operator-linebreak': [
|
||||
'error'
|
||||
],
|
||||
'prefer-arrow-callback': [
|
||||
'error'
|
||||
],
|
||||
'prefer-const': [
|
||||
'error',
|
||||
{
|
||||
'destructuring': 'all',
|
||||
'ignoreReadBeforeAssign': false
|
||||
}
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'rest-spread-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'sort-keys': [
|
||||
'error',
|
||||
'asc',
|
||||
{ 'natural': true }
|
||||
],
|
||||
'space-in-parens': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'spaced-comment': [
|
||||
'error',
|
||||
'always'
|
||||
]
|
||||
}
|
||||
};
|
228
.eslintrc.json
Normal file
228
.eslintrc.json
Normal file
@ -0,0 +1,228 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"commonjs": false,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"unused-imports"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {
|
||||
"array-bracket-newline": [
|
||||
"error",
|
||||
"consistent"
|
||||
],
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"array-element-newline": [
|
||||
"error",
|
||||
"consistent"
|
||||
],
|
||||
"arrow-body-style": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"block-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"brace-style": [
|
||||
"error",
|
||||
"1tbs"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"exports": "always-multiline",
|
||||
"functions": "always-multiline",
|
||||
"imports": "always-multiline",
|
||||
"objects": "always-multiline"
|
||||
}
|
||||
],
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
"after": true,
|
||||
"before": false
|
||||
}
|
||||
],
|
||||
"comma-style": [
|
||||
"error",
|
||||
"last"
|
||||
],
|
||||
"computed-property-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"curly": [
|
||||
"error",
|
||||
"multi-line", // "multi"
|
||||
"consistent"
|
||||
],
|
||||
"default-case-last": [
|
||||
"error"
|
||||
],
|
||||
"dot-location": [
|
||||
"error",
|
||||
"property"
|
||||
],
|
||||
"dot-notation": [
|
||||
"error"
|
||||
],
|
||||
"eqeqeq": [
|
||||
"error"
|
||||
],
|
||||
"func-call-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"off",
|
||||
"windows"
|
||||
],
|
||||
"max-depth": [
|
||||
"warn",
|
||||
{
|
||||
"max": 5
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 180,
|
||||
"ignoreRegExpLiterals": true,
|
||||
"ignoreStrings": true,
|
||||
"ignoreTemplateLiterals": true,
|
||||
"ignoreTrailingComments": true,
|
||||
"ignoreUrls": true
|
||||
}
|
||||
],
|
||||
"max-lines": [
|
||||
"warn"
|
||||
],
|
||||
"max-statements-per-line": [
|
||||
"error"
|
||||
],
|
||||
"multiline-comment-style": [
|
||||
"off"
|
||||
],
|
||||
"no-console": [
|
||||
"warn"
|
||||
],
|
||||
"no-prototype-builtins": [
|
||||
"off"
|
||||
],
|
||||
"no-return-assign": [
|
||||
"error"
|
||||
],
|
||||
"no-template-curly-in-string": [
|
||||
"warn"
|
||||
],
|
||||
"no-trailing-spaces": [
|
||||
"error"
|
||||
],
|
||||
"no-underscore-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"allowAfterThis": true,
|
||||
"allowFunctionParams": true
|
||||
}
|
||||
],
|
||||
"no-unneeded-ternary": [
|
||||
"error"
|
||||
],
|
||||
"no-unused-expressions": [
|
||||
"error"
|
||||
],
|
||||
"no-var": [
|
||||
"error"
|
||||
],
|
||||
"no-whitespace-before-property": [
|
||||
"error"
|
||||
],
|
||||
"object-curly-newline": [
|
||||
"error",
|
||||
{
|
||||
"minProperties": 2,
|
||||
"multiline": true
|
||||
}
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-property-newline": [
|
||||
"error"
|
||||
],
|
||||
"operator-linebreak": [
|
||||
"error"
|
||||
],
|
||||
"prefer-arrow-callback": [
|
||||
"error"
|
||||
],
|
||||
"prefer-const": [
|
||||
"error",
|
||||
{
|
||||
"destructuring": "all",
|
||||
"ignoreReadBeforeAssign": false
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"rest-spread-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"sort-keys": [
|
||||
"warn",
|
||||
"asc",
|
||||
{
|
||||
"natural": true
|
||||
}
|
||||
],
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
6
.github/SECURITY.md
vendored
6
.github/SECURITY.md
vendored
@ -6,10 +6,10 @@ Release versions that will receive security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 4.x | ✅ |
|
||||
| 3.x | ✅ |
|
||||
| 2.x | ❌ |
|
||||
| 1.x | ❌ |
|
||||
| 2.x | ❌ |
|
||||
| 3.x | ❌ |
|
||||
| 4.x | ✅ |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
|
58
.github/workflows/docker.yml
vendored
Normal file
58
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
push_to_registries:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
eartharoid/discord-tickets
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
42
.github/workflows/lint.yml
vendored
42
.github/workflows/lint.yml
vendored
@ -1,30 +1,32 @@
|
||||
name: eslint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
commitlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Cache pnpm modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-pnpm-modules
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
fetch-depth: 0
|
||||
- uses: wagoid/commitlint-github-action@v5
|
||||
with:
|
||||
version: 6.3.0
|
||||
run_install: false
|
||||
- run: pnpm install eslint
|
||||
configFile: .commitlintrc.js
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm run lint
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,12 +1,14 @@
|
||||
# directories
|
||||
.history/
|
||||
.vscode/
|
||||
node_modules/
|
||||
logs/
|
||||
site/
|
||||
prisma/
|
||||
|
||||
# files
|
||||
.env
|
||||
version
|
||||
user/config.js
|
||||
user/database.sqlite
|
||||
user/plugins/*/
|
||||
*.env*
|
||||
*.db*
|
||||
*.log
|
||||
user/config.yml
|
||||
user/**/*.*
|
||||
!user/**/.gitkeep
|
||||
!user/templates/*
|
4
.husky/commit-msg
Normal file
4
.husky/commit-msg
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx --no -- commitlint --edit ${1}
|
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
3
Caddyfile
Normal file
3
Caddyfile
Normal file
@ -0,0 +1,3 @@
|
||||
tickets.example.com {
|
||||
reverse_proxy 127.0.0.1:8169
|
||||
}
|
39
Dockerfile
39
Dockerfile
@ -1,20 +1,23 @@
|
||||
# Use the alpine image of node 16
|
||||
FROM node:16-alpine
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Create a dir for the app and make it owned by a non-root user (node)
|
||||
RUN mkdir /tickets && \
|
||||
chown -R 1000:1000 /tickets
|
||||
WORKDIR /tickets
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /build
|
||||
# install python etc so node-gyp works for the optional dependencies
|
||||
RUN apk add --no-cache make gcc g++ python3
|
||||
# install pnpm to make dependency installation faster (because it has a lockfile)
|
||||
RUN npm install -g pnpm
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY --link scripts scripts
|
||||
RUN chmod +x ./scripts/start.sh
|
||||
# install dependencies, CI=true to skip pre/postinstall scripts
|
||||
RUN CI=true pnpm install --prod --frozen-lockfile
|
||||
COPY --link . .
|
||||
|
||||
# Change user to node
|
||||
USER node
|
||||
|
||||
# Install packages
|
||||
COPY --chown=1000:1000 package.json pnpm-lock.yaml ./
|
||||
RUN npx pnpm install --prod --frozen-lockfile
|
||||
|
||||
# Copy src folder
|
||||
COPY src ./src
|
||||
|
||||
# Set the command
|
||||
CMD ["node", "src/"]
|
||||
FROM node:18-alpine AS runner
|
||||
ENV NODE_ENV=production \
|
||||
HTTP_HOST=0.0.0.0 \
|
||||
HTTP_PORT=80
|
||||
WORKDIR /usr/bot
|
||||
COPY --from=builder /build ./
|
||||
EXPOSE ${HTTP_PORT}
|
||||
ENTRYPOINT [ "/usr/bot/scripts/start.sh" ]
|
||||
|
306
README.md
306
README.md
@ -1,244 +1,156 @@
|
||||
<div align="center">
|
||||
<img
|
||||
src="https://img.eartharoid.me/insecure/plain/https://static.eartharoid.me/discord-tickets/logo/wordmark/gradient-by-eartharoid.png@png"
|
||||
alt="Discord Tickets"
|
||||
/>
|
||||
<b>
|
||||
An open-source ticket management bot for Discord - a free alternative to the premium and white-label plans of other popular ticketing bots.
|
||||
</b>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<p>
|
||||
<a href="https://github.com/discord-tickets/bot/stargazers">
|
||||
<img
|
||||
src="https://img.shields.io/github/stars/discord-tickets/bot?style=flat-square"
|
||||
alt="GitHub stars">
|
||||
</a>
|
||||
<img
|
||||
src="https://img.shields.io/badge/dynamic/json?color=5865F2&label=bots&query=clients.total&url=https%3A%2F%2Fstats.discordtickets.app%2Fapi%2Fv3%2Fcurrent&logo=discord&logoColor=white&style=flat-square"
|
||||
alt="">
|
||||
<img
|
||||
src="https://img.shields.io/badge/dynamic/json?color=5865F2&label=tickets&query=tickets&url=https%3A%2F%2Fstats.discordtickets.app%2Fapi%2Fv3%2Fcurrent&logo=discord&logoColor=white&style=flat-square"
|
||||
alt="">
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md">
|
||||
<img src="https://img.shields.io/github/all-contributors/discord-tickets/bot?color=ee8449&style=flat-square"alt="All contributors">
|
||||
</a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
<a
|
||||
href="https://www.codacy.com/gh/discord-tickets/bot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=discord-tickets/bot&utm_campaign=Badge_Grade"><img
|
||||
src="https://img.shields.io/codacy/grade/b974eb5f984c40868e07d82c968bd02d?logo=codacy&style=flat-square"
|
||||
alt="Codacy">
|
||||
</a>
|
||||
<a
|
||||
href="https://lnk.earth/discord"><img
|
||||
src="https://img.shields.io/discord/451745464480432129?label=discord&color=7289DA&style=flat-square"
|
||||
alt="Discord">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<a
|
||||
|
||||
![bots](https://img.shields.io/badge/dynamic/json?color=5865F2&label=bots&query=clients.total&url=https%3A%2F%2Fstats.discordtickets.app%2Fapi%2Fv3%2Fcurrent&logo=discord&logoColor=white&style=for-the-badge)
|
||||
![tickets](https://img.shields.io/badge/dynamic/json?color=5865F2&label=tickets&query=tickets&url=https%3A%2F%2Fstats.discordtickets.app%2Fapi%2Fv3%2Fcurrent&logo=discord&logoColor=white&style=for-the-badge)
|
||||
[![GitHub stars](https://img.shields.io/github/stars/discord-tickets/bot?style=for-the-badge)](https://github.com/discord-tickets/bot/stargazers)
|
||||
[![Codacy](https://img.shields.io/codacy/grade/b974eb5f984c40868e07d82c968bd02d?logo=codacy&style=for-the-badge)](https://www.codacy.com/gh/discord-tickets/bot/dashboard)
|
||||
[![All Contributors](https://img.shields.io/github/all-contributors/discord-tickets/bot?color=ee8449&style=for-the-badge)](https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md)
|
||||
[![Discord](https://img.shields.io/discord/451745464480432129?label=discord&color=7289DA&style=for-the-badge)](https://lnk.earth/discord)
|
||||
|
||||
<br>
|
||||
|
||||
![Discord Tickets](https://static.eartharoid.me/discord-tickets/logo/wordmark/gradient-by-eartharoid.png)
|
||||
|
||||
**A free alternative to the premium and white-label plans of other ticket management bots.**
|
||||
|
||||
<br>
|
||||
<a
|
||||
href="https://www.producthunt.com/posts/discord-tickets?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-discord-tickets"
|
||||
target="_blank">
|
||||
<img
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=321112&theme=light"
|
||||
alt="Discord Tickets - A free ticketing solution | Product Hunt"
|
||||
style="width: 250px; height: 54px;"
|
||||
width="250"
|
||||
height="54"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="https://discordtickets.app/getting-started#pebblehost">
|
||||
<img src="https://img.eartharoid.me/insecure/rs:auto:180/plain/s3://eartharoid/sharex/21/10/pebblehost.webp"/>
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<b><a href="https://discordtickets.app/getting-started#pebblehost">Partnered with PebbleHost</a></b>
|
||||
<br>
|
||||
<sub>
|
||||
for affordable bot hosting
|
||||
</sub>
|
||||
</div>
|
||||
[![PebbleHost](https://img.eartharoid.me/insecure/rs:auto:180/plain/s3://eartharoid/sharex/21/10/pebblehost.webp)](https://pebble.host/discordtickets)
|
||||
|
||||
<a href="https://pebble.host/discordtickets">Partnered with PebbleHost</a>
|
||||
<br>
|
||||
<sub>for affordable bot hosting</sub>
|
||||
|
||||
---
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## 📖 Contents
|
||||
|
||||
- [📖 Contents](#-contents)
|
||||
- [✨ Features](#-features)
|
||||
- [Want to learn more?](#want-to-learn-more)
|
||||
- [⚡ Getting started](#-getting-started)
|
||||
- [🤑 Sponsors](#-sponsors)
|
||||
- [🎖️ Contributors](#️-contributors)
|
||||
- [💻 Contributing](#-contributing)
|
||||
- [🌎 Translating](#-translating)
|
||||
- [😕 Support](#-support)
|
||||
- [⭐ Star History](#-star-history)
|
||||
- [🥱 License](#-license)
|
||||
|
||||
|
||||
## What is this?
|
||||
## ✨ Features
|
||||
|
||||
Discord Tickets is a Discord bot for creating and managing ticket channels. It is a free and open-source alternative to the paid "premium" and "white-label" plans of popular ticketing bots, such as [Ticket Tool](https://tickettool.xyz/), [TicketsBot](https://ticketsbot.net/), [Tickety](https://tickety.net/), [Helper.gg](https://helper.gg/), [Helper](https://helper.wtf), and others.
|
||||
- 📖 [**Documentation**](https://discordtickets.app/getting-started/) - comprehensive documentation and guides to help you get started
|
||||
- ⚙️ **Simple settings** - configure your bot with the included and easy-to-use dashboard
|
||||
- 🎨 **Highly customisable** - tweak features, colours, messages, and more to your liking
|
||||
- 🛸 **Modern features** - including slash commands, buttons, select menus, and modals
|
||||
- 🤖 **Automation** - ease your staff team's workload with configurable automation
|
||||
- 🏷️ [**Tags**](https://v4--discordtickets.netlify.app/features/#tags) - resolve members' problems without escalating to tickets
|
||||
- 🎫 **Tickets** - close inactive tickets automatically
|
||||
- 📜 **Archiving** - store messages in the database and view transcripts later
|
||||
- ❓ **Context** - ask for a topic or up to 5 custom questions before creating a ticket, and see references to a message or previous ticket at a glance
|
||||
- 🗃️ **Organisation** - claim, release, move and transfer tickets between members and categories
|
||||
- 🌎 [**Internationalisation**](#-translating) - available in more than 10 languages
|
||||
- ⏱️ **Statistics** - analyse your staff members' performance
|
||||
- 🪓 **Battle-tested** - trusted by thousands of servers, with over [half a million tickets](https://stats.discordtickets.app/) created since 2019
|
||||
- 🐳 [**Docker**](https://discordtickets.app/self-hosting/installation/docker/) - reliable and quick deployment with Docker
|
||||
|
||||
Discord Tickets is feature-rich and much more customisable than many of the bots mentioned above. As it is intended for self-hosting, the bot can have your community/company's logo, for free.
|
||||
[*...and more!*](https://discordtickets.app/features/)
|
||||
|
||||
Although intended for use in a single Discord server, the bot can also function in multiple servers at once if you run more than one community.
|
||||
### Want to learn more?
|
||||
|
||||
### Features
|
||||
**Visit [the website](https://discordtickets.app/) for more features, details, and screenshots**,
|
||||
or skip to the [full feature tour](https://discordtickets.app/features/).
|
||||
|
||||
Discord Tickets is packed full of features, many of which were suggested by its users. If it's missing a feature you want, you can:
|
||||
## ⚡ Getting started
|
||||
|
||||
- Create a plugin for it, if you can code JavaScript
|
||||
- Request someone else to make a plugin
|
||||
- [Submit a feature request](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md#submitting-a-feature-request) if you think many other users would benefit from it
|
||||
> *🙏 Please read the [documentation](https://discordtickets.app/self-hosting/installation/) before you start.*
|
||||
|
||||
Here's some of the things that makes Discord Tickets awesome:
|
||||
There are 3 ways to get started with Discord Tickets:
|
||||
|
||||
1. **Highly customisable**
|
||||
Some messages can be configured for each server and for each ticket category. Every other message is set in the locale files, making it relatively easy to override the default messages.
|
||||
You can also configure the functionality of the bot to your liking and add commands with plugins.
|
||||
- ☁️ [Add the public bot](https://discordtickets.app/public/) - instant setup, but branded *(great for testing)*
|
||||
- 😴 [Get a managed bot](https://discordtickets.app/managed/) - your own bot, hosted by me *(easy and cheap)*
|
||||
- 🧑💻 [Install your own bot](https://discordtickets.app/self-hosting/) - host it yourself *(experience recommended)*
|
||||
|
||||
2. **Localisable**
|
||||
If the bot hasn't already been translated to your (community's) language, you can [translate](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md#translating) it yourself.
|
||||
Plugin authors are encouraged to support multiple languages as well.
|
||||
**[Read the documentation](https://discordtickets.app/getting-started/)** for more information.
|
||||
|
||||
> This button is here because it looks nice, but I will cry if you click it before reading the [documentation](https://discordtickets.app/getting-started/). :)
|
||||
|
||||
<details>
|
||||
<summary>Translation status</summary>
|
||||
<a href="https://i18n.capestar.net/engage/discord-tickets/">
|
||||
<img src="https://i18n.capestar.net/widgets/discord-tickets/-/bot/multi-auto.svg" alt="Weblate" />
|
||||
</a>
|
||||
</details>
|
||||
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/eB6TkX?referralCode=Z3aYd2)
|
||||
|
||||
3. **Multiple ticket categories**
|
||||
Each ticket category has its own settings for messages and the support team roles. There's also multiple methods of creating a ticket.
|
||||
<!-- [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/discord-tickets/bot) -->
|
||||
|
||||
4. **A beautiful ticket archives portal**
|
||||
**\[COMING SOON\] :** Add the official [Discord Tickets Portal](https://github.com/discord-tickets/portal) plugin for an instant ticket archives website.
|
||||
## 🤑 Sponsors
|
||||
|
||||
You can use [text transcripts](https://discordtickets.app/plugins/official/text-transcripts/) whilst you wait for the portal.
|
||||
Discord Tickets is made possible by these awesome people and organisations:
|
||||
|
||||
5. **Open-source and self-hosted**
|
||||
It's yours, you have full control.
|
||||
![Sponsors](https://cdn.jsdelivr.net/gh/eartharoid/sponsors/sponsorkit/sponsors.svg)
|
||||
|
||||
Please consider sponsoring the project if it adds value to your business/community.
|
||||
|
||||
## Getting started
|
||||
|
||||
| [**Host it yourself**](https://discordtickets.app/getting-started) | [**Managed bot**](https://discordtickets.app/getting-started#managed-hosting) | [**Public test bot**](https://discord.com/oauth2/authorize?permissions=8&scope=applications.commands%20bot&client_id=475371285531066368) |
|
||||
|:-:|:-:|:-:|
|
||||
| Recommended if you have some experience. | Recommended if you want a bot without hosting it. | Try out the bot for a few days. |
|
||||
| [Go to the docs »](https://discordtickets.app/getting-started) | [Learn more »](https://discordtickets.app/getting-started#managed-hosting) | [Add to Discord »](https://discord.com/oauth2/authorize?permissions=8&scope=applications.commands%20bot&client_id=475371285531066368) |
|
||||
|
||||
Discord Tickets is partnered with [PebbleHost](https://discordtickets.app/getting-started#pebblehost). Click on the logo below if you want to self host but you don't have a server.
|
||||
|
||||
[![PebbleHost](https://img.eartharoid.me/insecure/rs:auto:180/plain/s3://eartharoid/sharex/21/10/pebblehost.webp)](https://discordtickets.app/getting-started#pebblehost)
|
||||
|
||||
## Documentation
|
||||
|
||||
You will find most of information you need at [discordtickets.app](https://discordtickets.app).
|
||||
|
||||
## Support
|
||||
|
||||
If the [documentation](https://discordtickets.app) leaves you with questions, you can ask for help in the [discussions](https://github.com/discord-tickets/bot/discussions/categories/support-q-a) or join the support server on Discord.
|
||||
|
||||
[![Join the Discord server](https://img.eartharoid.me/insecure/rs:auto:440:200/plain/s3://eartharoid/images/join-discord.png@png)](https://lnk.earth/discord)
|
||||
|
||||
## Contributing
|
||||
|
||||
For contributing instructions, or to find out all of the ways you can contribute, read [CONTRIBUTING.md](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md). All contributions are welcome and encouraged, but please [read the information](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md) given before doing so.
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to everyone to has contributed to Discord Tickets, including everyone who has:
|
||||
|
||||
- Contributed code
|
||||
- Translated
|
||||
- Improved documentation
|
||||
- Reported bugs
|
||||
- Requested a feature
|
||||
- Given help or support to other users
|
||||
- Created a public plugin
|
||||
- Created resources such as tutorials
|
||||
|
||||
**A full list of contributors can be found in [CONTRIBUTORS.md](https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md).**
|
||||
|
||||
## Sponsors
|
||||
|
||||
*Does your community or company use Discord Tickets? [Sponsor the project](https://github.com/discord-tickets/bot/?sponsor=1) to get your logo shown here.*
|
||||
|
||||
**These awesome people and communities sponsor Discord Tickets:**
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://reskybounds.com">
|
||||
<img
|
||||
src="https://img.eartharoid.me/insecure/rs:auto:256/plain/s3://eartharoid/k/22/05/reskybounds.png"
|
||||
height="128px;"
|
||||
alt="" />
|
||||
<br />
|
||||
<sub><b>reSkybounds</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://darkhosting.club">
|
||||
<img
|
||||
src="https://cdn.discordapp.com/attachments/920423855636496387/943574596777549894/attachment.png"
|
||||
height="128px;"
|
||||
alt="" />
|
||||
<br />
|
||||
<sub><b>DarkHosting™️</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://simplyvanilla.net">
|
||||
<img
|
||||
src="https://i.imghut.com/2022/04/26/sv-2022-discord-static.png"
|
||||
height="128px;"
|
||||
alt="" />
|
||||
<br />
|
||||
<sub><b>Simply Vanilla</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://urhost.io/">
|
||||
<img
|
||||
src="https://static.eartharoid.me/k/22/05/urhost.png"
|
||||
height="128px;"
|
||||
alt="" />
|
||||
<br />
|
||||
<sub><b>URHOST</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://sunrisenode.com">
|
||||
<img
|
||||
src="https://i.imgur.com/0gHlN7L.png"
|
||||
height="128px;"
|
||||
alt="" />
|
||||
<br />
|
||||
<sub><b>SunriseNode</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Donate
|
||||
**[Sponsor on GitHub](https://github.com/discord-tickets/bot/?sponsor=1)**, or
|
||||
|
||||
[![Donate at ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/eartharoid)
|
||||
|
||||
## Star History
|
||||
> **Note**
|
||||
>
|
||||
> Your logo will only appear here if you sponsor through GitHub Sponsors.
|
||||
> [Create an organisation](https://github.com/account/organizations/new?plan=free) if you want to use your business/community logo.
|
||||
|
||||
[![Star History Chart](https://api.star-history.com/svg?repos=discord-tickets/bot&type=Date)](https://star-history.com/#discord-tickets/bot&Date)
|
||||
## 🎖️ Contributors
|
||||
|
||||
<!-- [![Contributors](https://contrib.rocks/image?repo=discord-tickets/bot)](https://github.com/discord-tickets/bot/graphs/contributors) -->
|
||||
|
||||
Discord Tickets is made possible by all of the people listed in [CONTRIBUTORS.md](https://github.com/discord-tickets/bot/blob/main/CONTRIBUTORS.md).
|
||||
|
||||
|
||||
## License
|
||||
### 💻 Contributing
|
||||
|
||||
Discord Tickets is licensed under the [GPLv3 license](https://github.com/discord-tickets/bot/blob/main/LICENSE).
|
||||
If you want to help translate, suggest a feature, submit a bug report,
|
||||
or contribute in any other way, please read the [contributing guidelines](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md).
|
||||
|
||||
> **Note**
|
||||
> You can add yourself to the list of contributors [here](https://github.com/discord-tickets/bot/issues/new/choose).
|
||||
|
||||
#### 🌎 Translating
|
||||
|
||||
[![Translation status](https://hosted.weblate.org/widgets/discord-tickets/-/open-graph.png)](https://hosted.weblate.org/engage/discord-tickets/)
|
||||
|
||||
## 😕 Support
|
||||
|
||||
[![Discord](https://discordapp.com/api/guilds/451745464480432129/widget.png?style=banner4)](https://lnk.earth/discord)
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
<details>
|
||||
<summary>Show graph</summary>
|
||||
|
||||
[![Star History Chart](https://api.star-history.com/svg?repos=discord-tickets/bot&type=Date)](https://star-history.com/#discord-tickets/bot&Date)
|
||||
|
||||
</details>
|
||||
|
||||
## 🥱 License
|
||||
|
||||
Discord Tickets by eartharoid™️ is licensed under the [GPLv3 license](https://github.com/discord-tickets/bot/blob/main/LICENSE).
|
||||
|
||||
This is not an official Discord product. It is not affiliated with nor endorsed by Discord Inc.
|
||||
|
||||
© 2021 Isaac Saunders
|
||||
© 2023 Isaac Saunders
|
||||
|
@ -1,20 +0,0 @@
|
||||
# Use the alpine image of node 16
|
||||
FROM node:16-alpine
|
||||
|
||||
# Create a dir for the app and make it owned by a non-root user (node)
|
||||
RUN mkdir /tickets && \
|
||||
chown -R 1000:1000 /tickets
|
||||
WORKDIR /tickets
|
||||
|
||||
# Change user to node
|
||||
USER node
|
||||
|
||||
# Install packages
|
||||
COPY --chown=1000:1000 package.json pnpm-lock.yaml ./
|
||||
RUN npx pnpm install --prod --frozen-lockfile --no-optional && \
|
||||
# Currently WIP since pnpm installs dev deps automatically when I don't want it to.
|
||||
# Quick fix is to add to main deps
|
||||
npx pnpm install mysql2
|
||||
|
||||
# Set the command
|
||||
CMD ["node", "src/"]
|
257
db/mysql/migrations/20230309144248_4_0_0/migration.sql
Normal file
257
db/mysql/migrations/20230309144248_4_0_0/migration.sql
Normal file
@ -0,0 +1,257 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `archivedChannels` (
|
||||
`channelId` VARCHAR(19) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`ticketId` VARCHAR(19) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `archivedChannels_ticketId_channelId_key`(`ticketId`, `channelId`),
|
||||
PRIMARY KEY (`ticketId`, `channelId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `archivedMessages` (
|
||||
`authorId` VARCHAR(19) NOT NULL,
|
||||
`content` TEXT NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`deleted` BOOLEAN NOT NULL DEFAULT false,
|
||||
`edited` BOOLEAN NOT NULL DEFAULT false,
|
||||
`external` BOOLEAN NOT NULL DEFAULT false,
|
||||
`id` VARCHAR(19) NOT NULL,
|
||||
`ticketId` VARCHAR(19) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `archivedRoles` (
|
||||
`colour` CHAR(6) NOT NULL DEFAULT '5865F2',
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`roleId` VARCHAR(19) NOT NULL,
|
||||
`ticketId` VARCHAR(19) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `archivedRoles_ticketId_roleId_key`(`ticketId`, `roleId`),
|
||||
PRIMARY KEY (`ticketId`, `roleId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `archivedUsers` (
|
||||
`avatar` VARCHAR(191) NULL,
|
||||
`bot` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`discriminator` CHAR(4) NULL,
|
||||
`displayName` TEXT NULL,
|
||||
`roleId` VARCHAR(19) NULL,
|
||||
`ticketId` VARCHAR(19) NOT NULL,
|
||||
`userId` VARCHAR(19) NOT NULL,
|
||||
`username` TEXT NULL,
|
||||
|
||||
UNIQUE INDEX `archivedUsers_ticketId_userId_key`(`ticketId`, `userId`),
|
||||
PRIMARY KEY (`ticketId`, `userId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `categories` (
|
||||
`channelName` VARCHAR(191) NOT NULL,
|
||||
`claiming` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`cooldown` INTEGER NULL,
|
||||
`customTopic` VARCHAR(191) NULL,
|
||||
`description` VARCHAR(191) NOT NULL,
|
||||
`discordCategory` VARCHAR(19) NOT NULL,
|
||||
`emoji` VARCHAR(191) NOT NULL,
|
||||
`enableFeedback` BOOLEAN NOT NULL DEFAULT false,
|
||||
`guildId` VARCHAR(19) NOT NULL,
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`image` VARCHAR(191) NULL,
|
||||
`memberLimit` INTEGER NOT NULL DEFAULT 1,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`openingMessage` TEXT NOT NULL,
|
||||
`pingRoles` JSON NOT NULL,
|
||||
`ratelimit` INTEGER NULL,
|
||||
`requiredRoles` JSON NOT NULL,
|
||||
`requireTopic` BOOLEAN NOT NULL DEFAULT false,
|
||||
`staffRoles` JSON NOT NULL,
|
||||
`totalLimit` INTEGER NOT NULL DEFAULT 50,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `feedback` (
|
||||
`comment` TEXT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`guildId` VARCHAR(19) NOT NULL,
|
||||
`rating` INTEGER NOT NULL,
|
||||
`ticketId` VARCHAR(19) NOT NULL,
|
||||
`userId` VARCHAR(19) NULL,
|
||||
|
||||
PRIMARY KEY (`ticketId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `guilds` (
|
||||
`autoClose` INTEGER NOT NULL DEFAULT 43200000,
|
||||
`autoTag` JSON NOT NULL,
|
||||
`archive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`blocklist` JSON NOT NULL,
|
||||
`claimButton` BOOLEAN NOT NULL DEFAULT false,
|
||||
`closeButton` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`errorColour` VARCHAR(191) NOT NULL DEFAULT 'Red',
|
||||
`footer` VARCHAR(191) NULL DEFAULT 'Discord Tickets by eartharoid',
|
||||
`id` VARCHAR(19) NOT NULL,
|
||||
`locale` VARCHAR(191) NOT NULL DEFAULT 'en-GB',
|
||||
`logChannel` VARCHAR(19) NULL,
|
||||
`primaryColour` VARCHAR(191) NOT NULL DEFAULT '#009999',
|
||||
`staleAfter` INTEGER NULL,
|
||||
`successColour` VARCHAR(191) NOT NULL DEFAULT 'Green',
|
||||
`workingHours` JSON NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `questions` (
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`categoryId` INTEGER NOT NULL,
|
||||
`label` VARCHAR(45) NOT NULL,
|
||||
`maxLength` INTEGER NULL DEFAULT 4000,
|
||||
`minLength` INTEGER NULL DEFAULT 0,
|
||||
`options` JSON NOT NULL,
|
||||
`order` INTEGER NOT NULL,
|
||||
`placeholder` VARCHAR(100) NULL,
|
||||
`required` BOOLEAN NOT NULL DEFAULT true,
|
||||
`style` INTEGER NOT NULL DEFAULT 2,
|
||||
`type` ENUM('MENU', 'TEXT') NOT NULL DEFAULT 'TEXT',
|
||||
`value` TEXT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `questionAnswers` (
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`ticketId` VARCHAR(19) NOT NULL,
|
||||
`questionId` VARCHAR(191) NOT NULL,
|
||||
`userId` VARCHAR(19) NOT NULL,
|
||||
`value` TEXT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `tags` (
|
||||
`content` TEXT NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`guildId` VARCHAR(19) NOT NULL,
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`regex` VARCHAR(191) NULL,
|
||||
|
||||
UNIQUE INDEX `tags_guildId_name_key`(`guildId`, `name`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `tickets` (
|
||||
`categoryId` INTEGER NULL,
|
||||
`claimedById` VARCHAR(19) NULL,
|
||||
`closedAt` DATETIME(3) NULL,
|
||||
`closedById` VARCHAR(19) NULL,
|
||||
`closedReason` TEXT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`createdById` VARCHAR(19) NOT NULL,
|
||||
`deleted` BOOLEAN NOT NULL DEFAULT false,
|
||||
`firstResponseAt` DATETIME(3) NULL,
|
||||
`guildId` VARCHAR(19) NOT NULL,
|
||||
`id` VARCHAR(19) NOT NULL,
|
||||
`lastMessageAt` DATETIME(3) NULL,
|
||||
`messageCount` INTEGER NULL,
|
||||
`number` INTEGER NOT NULL,
|
||||
`open` BOOLEAN NOT NULL DEFAULT true,
|
||||
`openingMessageId` VARCHAR(19) NOT NULL,
|
||||
`pinnedMessageIds` JSON NOT NULL,
|
||||
`priority` ENUM('LOW', 'MEDIUM', 'HIGH') NULL,
|
||||
`referencesMessageId` VARCHAR(19) NULL,
|
||||
`referencesTicketId` VARCHAR(19) NULL,
|
||||
`topic` TEXT NULL,
|
||||
|
||||
UNIQUE INDEX `tickets_guildId_number_key`(`guildId`, `number`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `users` (
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`id` VARCHAR(19) NOT NULL,
|
||||
`messageCount` INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `archivedChannels` ADD CONSTRAINT `archivedChannels_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `archivedMessages` ADD CONSTRAINT `archivedMessages_ticketId_authorId_fkey` FOREIGN KEY (`ticketId`, `authorId`) REFERENCES `archivedUsers`(`ticketId`, `userId`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `archivedMessages` ADD CONSTRAINT `archivedMessages_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `archivedRoles` ADD CONSTRAINT `archivedRoles_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `archivedUsers` ADD CONSTRAINT `archivedUsers_ticketId_roleId_fkey` FOREIGN KEY (`ticketId`, `roleId`) REFERENCES `archivedRoles`(`ticketId`, `roleId`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `archivedUsers` ADD CONSTRAINT `archivedUsers_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `categories` ADD CONSTRAINT `categories_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `feedback` ADD CONSTRAINT `feedback_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `feedback` ADD CONSTRAINT `feedback_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `feedback` ADD CONSTRAINT `feedback_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `questions` ADD CONSTRAINT `questions_categoryId_fkey` FOREIGN KEY (`categoryId`) REFERENCES `categories`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `questionAnswers` ADD CONSTRAINT `questionAnswers_ticketId_fkey` FOREIGN KEY (`ticketId`) REFERENCES `tickets`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `questionAnswers` ADD CONSTRAINT `questionAnswers_questionId_fkey` FOREIGN KEY (`questionId`) REFERENCES `questions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `questionAnswers` ADD CONSTRAINT `questionAnswers_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tags` ADD CONSTRAINT `tags_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tickets` ADD CONSTRAINT `tickets_categoryId_fkey` FOREIGN KEY (`categoryId`) REFERENCES `categories`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tickets` ADD CONSTRAINT `tickets_claimedById_fkey` FOREIGN KEY (`claimedById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tickets` ADD CONSTRAINT `tickets_closedById_fkey` FOREIGN KEY (`closedById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tickets` ADD CONSTRAINT `tickets_createdById_fkey` FOREIGN KEY (`createdById`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tickets` ADD CONSTRAINT `tickets_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `guilds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `tickets` ADD CONSTRAINT `tickets_referencesTicketId_fkey` FOREIGN KEY (`referencesTicketId`) REFERENCES `tickets`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
3
db/mysql/migrations/migration_lock.toml
Normal file
3
db/mysql/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "mysql"
|
248
db/mysql/schema.prisma
Normal file
248
db/mysql/schema.prisma
Normal file
@ -0,0 +1,248 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DB_CONNECTION_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
model ArchivedChannel {
|
||||
channelId String @db.VarChar(19)
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
|
||||
@@id([ticketId, channelId])
|
||||
@@unique([ticketId, channelId])
|
||||
@@map("archivedChannels")
|
||||
}
|
||||
|
||||
model ArchivedMessage {
|
||||
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
|
||||
authorId String @db.VarChar(19)
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
deleted Boolean @default(false)
|
||||
edited Boolean @default(false)
|
||||
external Boolean @default(false)
|
||||
id String @id @db.VarChar(19)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
|
||||
@@map("archivedMessages")
|
||||
}
|
||||
|
||||
model ArchivedRole {
|
||||
archivedUsers ArchivedUser[]
|
||||
colour String @default("5865F2") @db.Char(6) // 7289DA
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
roleId String @db.VarChar(19)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
|
||||
@@id([ticketId, roleId])
|
||||
@@unique([ticketId, roleId])
|
||||
@@map("archivedRoles")
|
||||
}
|
||||
|
||||
model ArchivedUser {
|
||||
archivedMessages ArchivedMessage[]
|
||||
avatar String?
|
||||
bot Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
discriminator String? @db.Char(4)
|
||||
displayName String? @db.Text
|
||||
role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade)
|
||||
roleId String? @db.VarChar(19)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
userId String @db.VarChar(19)
|
||||
username String? @db.Text
|
||||
|
||||
@@id([ticketId, userId])
|
||||
@@unique([ticketId, userId])
|
||||
@@map("archivedUsers")
|
||||
}
|
||||
|
||||
model Category {
|
||||
channelName String
|
||||
claiming Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
cooldown Int?
|
||||
customTopic String?
|
||||
description String
|
||||
discordCategory String @db.VarChar(19)
|
||||
emoji String
|
||||
enableFeedback Boolean @default(false)
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
id Int @id @default(autoincrement())
|
||||
image String?
|
||||
memberLimit Int @default(1)
|
||||
name String
|
||||
openingMessage String @db.Text
|
||||
pingRoles Json @default("[]")
|
||||
questions Question[]
|
||||
ratelimit Int?
|
||||
requiredRoles Json @default("[]")
|
||||
requireTopic Boolean @default(false)
|
||||
staffRoles Json
|
||||
tickets Ticket[]
|
||||
totalLimit Int @default(50)
|
||||
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
model Feedback {
|
||||
comment String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
rating Int
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @id @db.VarChar(19)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String? @db.VarChar(19)
|
||||
|
||||
@@map("feedback")
|
||||
}
|
||||
|
||||
model Guild {
|
||||
autoClose Int @default(43200000)
|
||||
autoTag Json @default("[]")
|
||||
archive Boolean @default(true)
|
||||
blocklist Json @default("[]")
|
||||
categories Category[]
|
||||
claimButton Boolean @default(false)
|
||||
closeButton Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
errorColour String @default("Red")
|
||||
feedback Feedback[]
|
||||
footer String? @default("Discord Tickets by eartharoid")
|
||||
id String @id @db.VarChar(19)
|
||||
locale String @default("en-GB")
|
||||
logChannel String? @db.VarChar(19)
|
||||
primaryColour String @default("#009999")
|
||||
staleAfter Int?
|
||||
successColour String @default("Green")
|
||||
tags Tag[]
|
||||
tickets Ticket[]
|
||||
workingHours Json @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]")
|
||||
|
||||
@@map("guilds")
|
||||
}
|
||||
|
||||
model Question {
|
||||
answers QuestionAnswer[]
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(uuid())
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
categoryId Int
|
||||
label String @db.VarChar(45)
|
||||
maxLength Int? @default(4000)
|
||||
minLength Int? @default(0)
|
||||
options Json @default("[]")
|
||||
order Int
|
||||
placeholder String? @db.VarChar(100)
|
||||
required Boolean @default(true)
|
||||
style Int @default(2)
|
||||
type QuestionType @default(TEXT)
|
||||
value String? @db.Text
|
||||
|
||||
@@map("questions")
|
||||
}
|
||||
|
||||
model QuestionAnswer {
|
||||
createdAt DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
questionId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.VarChar(19)
|
||||
value String? @db.Text
|
||||
|
||||
@@map("questionAnswers")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
regex String?
|
||||
|
||||
@@unique([guildId, name])
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model Ticket {
|
||||
archivedChannels ArchivedChannel[]
|
||||
archivedMessages ArchivedMessage[]
|
||||
archivedRoles ArchivedRole[]
|
||||
archivedUsers ArchivedUser[]
|
||||
category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
categoryId Int?
|
||||
claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
|
||||
claimedById String? @db.VarChar(19)
|
||||
closedAt DateTime?
|
||||
closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
|
||||
closedById String? @db.VarChar(19)
|
||||
closedReason String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
|
||||
createdById String @db.VarChar(19)
|
||||
deleted Boolean @default(false)
|
||||
feedback Feedback?
|
||||
firstResponseAt DateTime?
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
id String @id @db.VarChar(19)
|
||||
lastMessageAt DateTime?
|
||||
messageCount Int?
|
||||
number Int
|
||||
open Boolean @default(true)
|
||||
openingMessageId String @db.VarChar(19)
|
||||
pinnedMessageIds Json @default("[]")
|
||||
priority TicketPriority?
|
||||
referencedBy Ticket[] @relation("TicketsReferencedByTicket")
|
||||
referencesMessageId String? @db.VarChar(19)
|
||||
referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull)
|
||||
referencesTicketId String? @db.VarChar(19)
|
||||
topic String? @db.Text
|
||||
questionAnswers QuestionAnswer[]
|
||||
|
||||
@@unique([guildId, number])
|
||||
@@map("tickets")
|
||||
}
|
||||
|
||||
model User {
|
||||
createdAt DateTime @default(now())
|
||||
feedback Feedback[]
|
||||
id String @id @db.VarChar(19)
|
||||
messageCount Int @default(0)
|
||||
ticketsCreated Ticket[] @relation("TicketsCreatedByUser")
|
||||
ticketsClosed Ticket[] @relation("TicketsClosedByUser")
|
||||
ticketsClaimed Ticket[] @relation("TicketsClaimedByUser")
|
||||
questionAnswers QuestionAnswer[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
enum TicketPriority {
|
||||
LOW
|
||||
MEDIUM
|
||||
HIGH
|
||||
}
|
||||
|
||||
enum QuestionType {
|
||||
MENU
|
||||
TEXT
|
||||
}
|
273
db/postgresql/migrations/20230309132703_4_0_0/migration.sql
Normal file
273
db/postgresql/migrations/20230309132703_4_0_0/migration.sql
Normal file
@ -0,0 +1,273 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TicketPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "QuestionType" AS ENUM ('MENU', 'TEXT');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedChannels" (
|
||||
"channelId" VARCHAR(19) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" TEXT NOT NULL,
|
||||
"ticketId" VARCHAR(19) NOT NULL,
|
||||
|
||||
CONSTRAINT "archivedChannels_pkey" PRIMARY KEY ("ticketId","channelId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedMessages" (
|
||||
"authorId" VARCHAR(19) NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"edited" BOOLEAN NOT NULL DEFAULT false,
|
||||
"external" BOOLEAN NOT NULL DEFAULT false,
|
||||
"id" VARCHAR(19) NOT NULL,
|
||||
"ticketId" VARCHAR(19) NOT NULL,
|
||||
|
||||
CONSTRAINT "archivedMessages_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedRoles" (
|
||||
"colour" CHAR(6) NOT NULL DEFAULT '5865F2',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" TEXT NOT NULL,
|
||||
"roleId" VARCHAR(19) NOT NULL,
|
||||
"ticketId" VARCHAR(19) NOT NULL,
|
||||
|
||||
CONSTRAINT "archivedRoles_pkey" PRIMARY KEY ("ticketId","roleId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedUsers" (
|
||||
"avatar" TEXT,
|
||||
"bot" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"discriminator" CHAR(4),
|
||||
"displayName" TEXT,
|
||||
"roleId" VARCHAR(19),
|
||||
"ticketId" VARCHAR(19) NOT NULL,
|
||||
"userId" VARCHAR(19) NOT NULL,
|
||||
"username" TEXT,
|
||||
|
||||
CONSTRAINT "archivedUsers_pkey" PRIMARY KEY ("ticketId","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "categories" (
|
||||
"channelName" TEXT NOT NULL,
|
||||
"claiming" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cooldown" INTEGER,
|
||||
"customTopic" TEXT,
|
||||
"description" TEXT NOT NULL,
|
||||
"discordCategory" VARCHAR(19) NOT NULL,
|
||||
"emoji" TEXT NOT NULL,
|
||||
"enableFeedback" BOOLEAN NOT NULL DEFAULT false,
|
||||
"guildId" VARCHAR(19) NOT NULL,
|
||||
"id" SERIAL NOT NULL,
|
||||
"image" TEXT,
|
||||
"memberLimit" INTEGER NOT NULL DEFAULT 1,
|
||||
"name" TEXT NOT NULL,
|
||||
"openingMessage" TEXT NOT NULL,
|
||||
"pingRoles" JSONB NOT NULL DEFAULT '[]',
|
||||
"ratelimit" INTEGER,
|
||||
"requiredRoles" JSONB NOT NULL DEFAULT '[]',
|
||||
"requireTopic" BOOLEAN NOT NULL DEFAULT false,
|
||||
"staffRoles" JSONB NOT NULL,
|
||||
"totalLimit" INTEGER NOT NULL DEFAULT 50,
|
||||
|
||||
CONSTRAINT "categories_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "feedback" (
|
||||
"comment" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"guildId" VARCHAR(19) NOT NULL,
|
||||
"rating" INTEGER NOT NULL,
|
||||
"ticketId" VARCHAR(19) NOT NULL,
|
||||
"userId" VARCHAR(19),
|
||||
|
||||
CONSTRAINT "feedback_pkey" PRIMARY KEY ("ticketId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "guilds" (
|
||||
"autoClose" INTEGER NOT NULL DEFAULT 43200000,
|
||||
"autoTag" JSONB NOT NULL DEFAULT '[]',
|
||||
"archive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"blocklist" JSONB NOT NULL DEFAULT '[]',
|
||||
"claimButton" BOOLEAN NOT NULL DEFAULT false,
|
||||
"closeButton" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"errorColour" TEXT NOT NULL DEFAULT 'Red',
|
||||
"footer" TEXT DEFAULT 'Discord Tickets by eartharoid',
|
||||
"id" VARCHAR(19) NOT NULL,
|
||||
"locale" TEXT NOT NULL DEFAULT 'en-GB',
|
||||
"logChannel" VARCHAR(19),
|
||||
"primaryColour" TEXT NOT NULL DEFAULT '#009999',
|
||||
"staleAfter" INTEGER,
|
||||
"successColour" TEXT NOT NULL DEFAULT 'Green',
|
||||
"workingHours" JSONB NOT NULL DEFAULT '["UTC", ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"]]',
|
||||
|
||||
CONSTRAINT "guilds_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "questions" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" TEXT NOT NULL,
|
||||
"categoryId" INTEGER NOT NULL,
|
||||
"label" VARCHAR(45) NOT NULL,
|
||||
"maxLength" INTEGER DEFAULT 4000,
|
||||
"minLength" INTEGER DEFAULT 0,
|
||||
"options" JSONB NOT NULL DEFAULT '[]',
|
||||
"order" INTEGER NOT NULL,
|
||||
"placeholder" VARCHAR(100),
|
||||
"required" BOOLEAN NOT NULL DEFAULT true,
|
||||
"style" INTEGER NOT NULL DEFAULT 2,
|
||||
"type" "QuestionType" NOT NULL DEFAULT 'TEXT',
|
||||
"value" TEXT,
|
||||
|
||||
CONSTRAINT "questions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "questionAnswers" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" SERIAL NOT NULL,
|
||||
"ticketId" VARCHAR(19) NOT NULL,
|
||||
"questionId" TEXT NOT NULL,
|
||||
"userId" VARCHAR(19) NOT NULL,
|
||||
"value" TEXT,
|
||||
|
||||
CONSTRAINT "questionAnswers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tags" (
|
||||
"content" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"guildId" VARCHAR(19) NOT NULL,
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"regex" TEXT,
|
||||
|
||||
CONSTRAINT "tags_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tickets" (
|
||||
"categoryId" INTEGER,
|
||||
"claimedById" VARCHAR(19),
|
||||
"closedAt" TIMESTAMP(3),
|
||||
"closedById" VARCHAR(19),
|
||||
"closedReason" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdById" VARCHAR(19) NOT NULL,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"firstResponseAt" TIMESTAMP(3),
|
||||
"guildId" VARCHAR(19) NOT NULL,
|
||||
"id" VARCHAR(19) NOT NULL,
|
||||
"lastMessageAt" TIMESTAMP(3),
|
||||
"messageCount" INTEGER,
|
||||
"number" INTEGER NOT NULL,
|
||||
"open" BOOLEAN NOT NULL DEFAULT true,
|
||||
"openingMessageId" VARCHAR(19) NOT NULL,
|
||||
"pinnedMessageIds" JSONB NOT NULL DEFAULT '[]',
|
||||
"priority" "TicketPriority",
|
||||
"referencesMessageId" VARCHAR(19),
|
||||
"referencesTicketId" VARCHAR(19),
|
||||
"topic" TEXT,
|
||||
|
||||
CONSTRAINT "tickets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" VARCHAR(19) NOT NULL,
|
||||
"messageCount" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "archivedChannels_ticketId_channelId_key" ON "archivedChannels"("ticketId", "channelId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "archivedRoles_ticketId_roleId_key" ON "archivedRoles"("ticketId", "roleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "archivedUsers_ticketId_userId_key" ON "archivedUsers"("ticketId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tags_guildId_name_key" ON "tags"("guildId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tickets_guildId_number_key" ON "tickets"("guildId", "number");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "archivedChannels" ADD CONSTRAINT "archivedChannels_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "archivedMessages" ADD CONSTRAINT "archivedMessages_ticketId_authorId_fkey" FOREIGN KEY ("ticketId", "authorId") REFERENCES "archivedUsers"("ticketId", "userId") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "archivedMessages" ADD CONSTRAINT "archivedMessages_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "archivedRoles" ADD CONSTRAINT "archivedRoles_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "archivedUsers" ADD CONSTRAINT "archivedUsers_ticketId_roleId_fkey" FOREIGN KEY ("ticketId", "roleId") REFERENCES "archivedRoles"("ticketId", "roleId") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "archivedUsers" ADD CONSTRAINT "archivedUsers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "categories" ADD CONSTRAINT "categories_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "feedback" ADD CONSTRAINT "feedback_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "feedback" ADD CONSTRAINT "feedback_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "feedback" ADD CONSTRAINT "feedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "questions" ADD CONSTRAINT "questions_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "questionAnswers" ADD CONSTRAINT "questionAnswers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "questionAnswers" ADD CONSTRAINT "questionAnswers_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "questions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "questionAnswers" ADD CONSTRAINT "questionAnswers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tags" ADD CONSTRAINT "tags_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_claimedById_fkey" FOREIGN KEY ("claimedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_referencesTicketId_fkey" FOREIGN KEY ("referencesTicketId") REFERENCES "tickets"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
3
db/postgresql/migrations/migration_lock.toml
Normal file
3
db/postgresql/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
247
db/postgresql/schema.prisma
Normal file
247
db/postgresql/schema.prisma
Normal file
@ -0,0 +1,247 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DB_CONNECTION_URL")
|
||||
}
|
||||
|
||||
model ArchivedChannel {
|
||||
channelId String @db.VarChar(19)
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
|
||||
@@id([ticketId, channelId])
|
||||
@@unique([ticketId, channelId])
|
||||
@@map("archivedChannels")
|
||||
}
|
||||
|
||||
model ArchivedMessage {
|
||||
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
|
||||
authorId String @db.VarChar(19)
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
deleted Boolean @default(false)
|
||||
edited Boolean @default(false)
|
||||
external Boolean @default(false)
|
||||
id String @id @db.VarChar(19)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
|
||||
@@map("archivedMessages")
|
||||
}
|
||||
|
||||
model ArchivedRole {
|
||||
archivedUsers ArchivedUser[]
|
||||
colour String @default("5865F2") @db.Char(6) // 7289DA
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
roleId String @db.VarChar(19)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
|
||||
@@id([ticketId, roleId])
|
||||
@@unique([ticketId, roleId])
|
||||
@@map("archivedRoles")
|
||||
}
|
||||
|
||||
model ArchivedUser {
|
||||
archivedMessages ArchivedMessage[]
|
||||
avatar String?
|
||||
bot Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
discriminator String? @db.Char(4)
|
||||
displayName String? @db.Text
|
||||
role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade)
|
||||
roleId String? @db.VarChar(19)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
userId String @db.VarChar(19)
|
||||
username String? @db.Text
|
||||
|
||||
@@id([ticketId, userId])
|
||||
@@unique([ticketId, userId])
|
||||
@@map("archivedUsers")
|
||||
}
|
||||
|
||||
model Category {
|
||||
channelName String
|
||||
claiming Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
cooldown Int?
|
||||
customTopic String?
|
||||
description String
|
||||
discordCategory String @db.VarChar(19)
|
||||
emoji String
|
||||
enableFeedback Boolean @default(false)
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
id Int @id @default(autoincrement())
|
||||
image String?
|
||||
memberLimit Int @default(1)
|
||||
name String
|
||||
openingMessage String @db.Text
|
||||
pingRoles Json @default("[]")
|
||||
questions Question[]
|
||||
ratelimit Int?
|
||||
requiredRoles Json @default("[]")
|
||||
requireTopic Boolean @default(false)
|
||||
staffRoles Json
|
||||
tickets Ticket[]
|
||||
totalLimit Int @default(50)
|
||||
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
model Feedback {
|
||||
comment String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
rating Int
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @id @db.VarChar(19)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String? @db.VarChar(19)
|
||||
|
||||
@@map("feedback")
|
||||
}
|
||||
|
||||
model Guild {
|
||||
autoClose Int @default(43200000)
|
||||
autoTag Json @default("[]")
|
||||
archive Boolean @default(true)
|
||||
blocklist Json @default("[]")
|
||||
categories Category[]
|
||||
claimButton Boolean @default(false)
|
||||
closeButton Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
errorColour String @default("Red")
|
||||
feedback Feedback[]
|
||||
footer String? @default("Discord Tickets by eartharoid")
|
||||
id String @id @db.VarChar(19)
|
||||
locale String @default("en-GB")
|
||||
logChannel String? @db.VarChar(19)
|
||||
primaryColour String @default("#009999")
|
||||
staleAfter Int?
|
||||
successColour String @default("Green")
|
||||
tags Tag[]
|
||||
tickets Ticket[]
|
||||
workingHours Json @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]")
|
||||
|
||||
@@map("guilds")
|
||||
}
|
||||
|
||||
model Question {
|
||||
answers QuestionAnswer[]
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(uuid())
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
categoryId Int
|
||||
label String @db.VarChar(45)
|
||||
maxLength Int? @default(4000)
|
||||
minLength Int? @default(0)
|
||||
options Json @default("[]")
|
||||
order Int
|
||||
placeholder String? @db.VarChar(100)
|
||||
required Boolean @default(true)
|
||||
style Int @default(2)
|
||||
type QuestionType @default(TEXT)
|
||||
value String? @db.Text
|
||||
|
||||
@@map("questions")
|
||||
}
|
||||
|
||||
model QuestionAnswer {
|
||||
createdAt DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @db.VarChar(19)
|
||||
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
questionId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.VarChar(19)
|
||||
value String? @db.Text
|
||||
|
||||
@@map("questionAnswers")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
regex String?
|
||||
|
||||
@@unique([guildId, name])
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model Ticket {
|
||||
archivedChannels ArchivedChannel[]
|
||||
archivedMessages ArchivedMessage[]
|
||||
archivedRoles ArchivedRole[]
|
||||
archivedUsers ArchivedUser[]
|
||||
category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
categoryId Int?
|
||||
claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
|
||||
claimedById String? @db.VarChar(19)
|
||||
closedAt DateTime?
|
||||
closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
|
||||
closedById String? @db.VarChar(19)
|
||||
closedReason String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
|
||||
createdById String @db.VarChar(19)
|
||||
deleted Boolean @default(false)
|
||||
feedback Feedback?
|
||||
firstResponseAt DateTime?
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String @db.VarChar(19)
|
||||
id String @id @db.VarChar(19)
|
||||
lastMessageAt DateTime?
|
||||
messageCount Int?
|
||||
number Int
|
||||
open Boolean @default(true)
|
||||
openingMessageId String @db.VarChar(19)
|
||||
pinnedMessageIds Json @default("[]")
|
||||
priority TicketPriority?
|
||||
referencedBy Ticket[] @relation("TicketsReferencedByTicket")
|
||||
referencesMessageId String? @db.VarChar(19)
|
||||
referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull)
|
||||
referencesTicketId String? @db.VarChar(19)
|
||||
topic String? @db.Text
|
||||
questionAnswers QuestionAnswer[]
|
||||
|
||||
@@unique([guildId, number])
|
||||
@@map("tickets")
|
||||
}
|
||||
|
||||
model User {
|
||||
createdAt DateTime @default(now())
|
||||
feedback Feedback[]
|
||||
id String @id @db.VarChar(19)
|
||||
messageCount Int @default(0)
|
||||
ticketsCreated Ticket[] @relation("TicketsCreatedByUser")
|
||||
ticketsClosed Ticket[] @relation("TicketsClosedByUser")
|
||||
ticketsClaimed Ticket[] @relation("TicketsClaimedByUser")
|
||||
questionAnswers QuestionAnswer[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
enum TicketPriority {
|
||||
LOW
|
||||
MEDIUM
|
||||
HIGH
|
||||
}
|
||||
|
||||
enum QuestionType {
|
||||
MENU
|
||||
TEXT
|
||||
}
|
207
db/sqlite/migrations/20230309142817_4_0_0/migration.sql
Normal file
207
db/sqlite/migrations/20230309142817_4_0_0/migration.sql
Normal file
@ -0,0 +1,207 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedChannels" (
|
||||
"channelId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" TEXT NOT NULL,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("ticketId", "channelId"),
|
||||
CONSTRAINT "archivedChannels_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedMessages" (
|
||||
"authorId" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"edited" BOOLEAN NOT NULL DEFAULT false,
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"external" BOOLEAN NOT NULL DEFAULT false,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
CONSTRAINT "archivedMessages_ticketId_authorId_fkey" FOREIGN KEY ("ticketId", "authorId") REFERENCES "archivedUsers" ("ticketId", "userId") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "archivedMessages_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedRoles" (
|
||||
"colour" TEXT NOT NULL DEFAULT '5865F2',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("ticketId", "roleId"),
|
||||
CONSTRAINT "archivedRoles_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "archivedUsers" (
|
||||
"avatar" TEXT,
|
||||
"bot" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"discriminator" TEXT,
|
||||
"displayName" TEXT,
|
||||
"roleId" TEXT,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"username" TEXT,
|
||||
|
||||
PRIMARY KEY ("ticketId", "userId"),
|
||||
CONSTRAINT "archivedUsers_ticketId_roleId_fkey" FOREIGN KEY ("ticketId", "roleId") REFERENCES "archivedRoles" ("ticketId", "roleId") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "archivedUsers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "categories" (
|
||||
"channelName" TEXT NOT NULL,
|
||||
"claiming" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cooldown" INTEGER,
|
||||
"customTopic" TEXT,
|
||||
"description" TEXT NOT NULL,
|
||||
"discordCategory" TEXT NOT NULL,
|
||||
"emoji" TEXT NOT NULL,
|
||||
"enableFeedback" BOOLEAN NOT NULL DEFAULT false,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"image" TEXT,
|
||||
"memberLimit" INTEGER NOT NULL DEFAULT 1,
|
||||
"name" TEXT NOT NULL,
|
||||
"openingMessage" TEXT NOT NULL,
|
||||
"pingRoles" TEXT NOT NULL DEFAULT '[]',
|
||||
"ratelimit" INTEGER,
|
||||
"requiredRoles" TEXT NOT NULL DEFAULT '[]',
|
||||
"requireTopic" BOOLEAN NOT NULL DEFAULT false,
|
||||
"staffRoles" TEXT NOT NULL,
|
||||
"totalLimit" INTEGER NOT NULL DEFAULT 50,
|
||||
CONSTRAINT "categories_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "feedback" (
|
||||
"comment" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"rating" INTEGER NOT NULL,
|
||||
"ticketId" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT,
|
||||
CONSTRAINT "feedback_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "feedback_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "feedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "guilds" (
|
||||
"autoClose" INTEGER NOT NULL DEFAULT 43200000,
|
||||
"autoTag" TEXT NOT NULL DEFAULT '[]',
|
||||
"archive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"blocklist" TEXT NOT NULL DEFAULT '[]',
|
||||
"claimButton" BOOLEAN NOT NULL DEFAULT false,
|
||||
"closeButton" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"errorColour" TEXT NOT NULL DEFAULT 'Red',
|
||||
"footer" TEXT DEFAULT 'Discord Tickets by eartharoid',
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"locale" TEXT NOT NULL DEFAULT 'en-GB',
|
||||
"logChannel" TEXT,
|
||||
"primaryColour" TEXT NOT NULL DEFAULT '#009999',
|
||||
"staleAfter" INTEGER,
|
||||
"successColour" TEXT NOT NULL DEFAULT 'Green',
|
||||
"workingHours" TEXT NOT NULL DEFAULT '["UTC", ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"], ["00:00","23:59"]]'
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "questions" (
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"categoryId" INTEGER NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"maxLength" INTEGER DEFAULT 4000,
|
||||
"minLength" INTEGER DEFAULT 0,
|
||||
"options" TEXT NOT NULL DEFAULT '[]',
|
||||
"order" INTEGER NOT NULL,
|
||||
"placeholder" TEXT,
|
||||
"required" BOOLEAN NOT NULL DEFAULT true,
|
||||
"style" INTEGER NOT NULL DEFAULT 2,
|
||||
"type" TEXT NOT NULL DEFAULT 'TEXT',
|
||||
"value" TEXT,
|
||||
CONSTRAINT "questions_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "questionAnswers" (
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
"questionId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"value" TEXT,
|
||||
CONSTRAINT "questionAnswers_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "questionAnswers_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "questions" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "questionAnswers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tags" (
|
||||
"content" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"regex" TEXT,
|
||||
CONSTRAINT "tags_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tickets" (
|
||||
"categoryId" INTEGER,
|
||||
"claimedById" TEXT,
|
||||
"closedAt" DATETIME,
|
||||
"closedById" TEXT,
|
||||
"closedReason" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"firstResponseAt" DATETIME,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"lastMessageAt" DATETIME,
|
||||
"messageCount" INTEGER,
|
||||
"number" INTEGER NOT NULL,
|
||||
"open" BOOLEAN NOT NULL DEFAULT true,
|
||||
"openingMessageId" TEXT NOT NULL,
|
||||
"pinnedMessageIds" TEXT NOT NULL DEFAULT '[]',
|
||||
"priority" TEXT,
|
||||
"referencesMessageId" TEXT,
|
||||
"referencesTicketId" TEXT,
|
||||
"topic" TEXT,
|
||||
CONSTRAINT "tickets_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "tickets_claimedById_fkey" FOREIGN KEY ("claimedById") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "tickets_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "tickets_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "tickets_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "tickets_referencesTicketId_fkey" FOREIGN KEY ("referencesTicketId") REFERENCES "tickets" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"messageCount" INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "archivedChannels_ticketId_channelId_key" ON "archivedChannels"("ticketId", "channelId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "archivedRoles_ticketId_roleId_key" ON "archivedRoles"("ticketId", "roleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "archivedUsers_ticketId_userId_key" ON "archivedUsers"("ticketId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tags_guildId_name_key" ON "tags"("guildId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tickets_guildId_number_key" ON "tickets"("guildId", "number");
|
3
db/sqlite/migrations/migration_lock.toml
Normal file
3
db/sqlite/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
236
db/sqlite/schema.prisma
Normal file
236
db/sqlite/schema.prisma
Normal file
@ -0,0 +1,236 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:../user/database.db"
|
||||
}
|
||||
|
||||
model ArchivedChannel {
|
||||
channelId String
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String
|
||||
|
||||
@@id([ticketId, channelId])
|
||||
@@unique([ticketId, channelId])
|
||||
@@map("archivedChannels")
|
||||
}
|
||||
|
||||
model ArchivedMessage {
|
||||
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
|
||||
authorId String
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
deleted Boolean @default(false)
|
||||
edited Boolean @default(false)
|
||||
id String @id
|
||||
external Boolean @default(false)
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String
|
||||
|
||||
@@map("archivedMessages")
|
||||
}
|
||||
|
||||
model ArchivedRole {
|
||||
archivedUsers ArchivedUser[]
|
||||
colour String @default("5865F2") // 7289DA
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
roleId String
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String
|
||||
|
||||
@@id([ticketId, roleId])
|
||||
@@unique([ticketId, roleId])
|
||||
@@map("archivedRoles")
|
||||
}
|
||||
|
||||
model ArchivedUser {
|
||||
archivedMessages ArchivedMessage[]
|
||||
avatar String?
|
||||
bot Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
discriminator String?
|
||||
displayName String?
|
||||
role ArchivedRole? @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade)
|
||||
roleId String?
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String
|
||||
userId String
|
||||
username String?
|
||||
|
||||
@@id([ticketId, userId])
|
||||
@@unique([ticketId, userId])
|
||||
@@map("archivedUsers")
|
||||
}
|
||||
|
||||
model Category {
|
||||
channelName String
|
||||
claiming Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
cooldown Int?
|
||||
customTopic String?
|
||||
description String
|
||||
discordCategory String
|
||||
emoji String
|
||||
enableFeedback Boolean @default(false)
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String
|
||||
id Int @id @default(autoincrement())
|
||||
image String?
|
||||
memberLimit Int @default(1)
|
||||
name String
|
||||
openingMessage String
|
||||
pingRoles String @default("[]")
|
||||
questions Question[]
|
||||
ratelimit Int?
|
||||
requiredRoles String @default("[]")
|
||||
requireTopic Boolean @default(false)
|
||||
staffRoles String
|
||||
tickets Ticket[]
|
||||
totalLimit Int @default(50)
|
||||
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
model Feedback {
|
||||
comment String?
|
||||
createdAt DateTime @default(now())
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String
|
||||
rating Int
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String @id
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String?
|
||||
|
||||
@@map("feedback")
|
||||
}
|
||||
|
||||
model Guild {
|
||||
autoClose Int @default(43200000)
|
||||
autoTag String @default("[]")
|
||||
archive Boolean @default(true)
|
||||
blocklist String @default("[]")
|
||||
categories Category[]
|
||||
claimButton Boolean @default(false)
|
||||
closeButton Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
errorColour String @default("Red")
|
||||
feedback Feedback[]
|
||||
footer String? @default("Discord Tickets by eartharoid")
|
||||
id String @id
|
||||
locale String @default("en-GB")
|
||||
logChannel String?
|
||||
primaryColour String @default("#009999")
|
||||
staleAfter Int?
|
||||
successColour String @default("Green")
|
||||
tags Tag[]
|
||||
tickets Ticket[]
|
||||
workingHours String @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]")
|
||||
|
||||
@@map("guilds")
|
||||
}
|
||||
|
||||
model Question {
|
||||
answers QuestionAnswer[]
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(uuid())
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
categoryId Int
|
||||
label String
|
||||
maxLength Int? @default(4000)
|
||||
minLength Int? @default(0)
|
||||
options String @default("[]")
|
||||
order Int
|
||||
placeholder String?
|
||||
required Boolean @default(true)
|
||||
style Int @default(2)
|
||||
type String @default("TEXT")
|
||||
value String?
|
||||
|
||||
@@map("questions")
|
||||
}
|
||||
|
||||
model QuestionAnswer {
|
||||
createdAt DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String
|
||||
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
questionId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
value String?
|
||||
|
||||
@@map("questionAnswers")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
regex String?
|
||||
|
||||
@@unique([guildId, name])
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model Ticket {
|
||||
archivedChannels ArchivedChannel[]
|
||||
archivedMessages ArchivedMessage[]
|
||||
archivedRoles ArchivedRole[]
|
||||
archivedUsers ArchivedUser[]
|
||||
category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
categoryId Int?
|
||||
claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
|
||||
claimedById String?
|
||||
closedAt DateTime?
|
||||
closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
|
||||
closedById String?
|
||||
closedReason String?
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
|
||||
createdById String
|
||||
deleted Boolean @default(false)
|
||||
feedback Feedback?
|
||||
firstResponseAt DateTime?
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
guildId String
|
||||
id String @id
|
||||
lastMessageAt DateTime?
|
||||
messageCount Int?
|
||||
number Int
|
||||
open Boolean @default(true)
|
||||
openingMessageId String
|
||||
pinnedMessageIds String @default("[]")
|
||||
priority String?
|
||||
referencedBy Ticket[] @relation("TicketsReferencedByTicket")
|
||||
referencesMessageId String?
|
||||
referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull)
|
||||
referencesTicketId String?
|
||||
topic String?
|
||||
questionAnswers QuestionAnswer[]
|
||||
|
||||
@@unique([guildId, number])
|
||||
@@map("tickets")
|
||||
}
|
||||
|
||||
model User {
|
||||
createdAt DateTime @default(now())
|
||||
feedback Feedback[]
|
||||
id String @id
|
||||
messageCount Int @default(0)
|
||||
ticketsCreated Ticket[] @relation("TicketsCreatedByUser")
|
||||
ticketsClosed Ticket[] @relation("TicketsClosedByUser")
|
||||
ticketsClaimed Ticket[] @relation("TicketsClaimedByUser")
|
||||
questionAnswers QuestionAnswer[]
|
||||
|
||||
@@map("users")
|
||||
}
|
@ -1,38 +1,54 @@
|
||||
version: "3.8"
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
restart: unless-stopped
|
||||
hostname: mysql
|
||||
networks:
|
||||
- discord-tickets
|
||||
volumes:
|
||||
- tickets-mysql:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: tickets
|
||||
MYSQL_PASSWORD: insecure # change this to a secure password
|
||||
MYSQL_ROOT_PASSWORD: insecure # change this to a (different) secure password
|
||||
MYSQL_USER: tickets
|
||||
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose.Dockerfile
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./src:/tickets/src
|
||||
- ./user:/tickets/user
|
||||
- ./logs:/tickets/logs
|
||||
- ./.env:/tickets/.env:ro
|
||||
environment:
|
||||
- DB_TYPE=mysql
|
||||
- DB_HOST=db
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=tickets
|
||||
- DB_USER=tickets
|
||||
- DB_PASS=tickets
|
||||
- DB_TABLE_PREFIX=dsctickets_
|
||||
image: eartharoid/discord-tickets:4.0
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: mariadb:10.6
|
||||
- mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- "MYSQL_DATABASE=tickets"
|
||||
- "MYSQL_USER=tickets"
|
||||
- "MYSQL_PASSWORD=tickets"
|
||||
|
||||
- "MYSQL_RANDOM_ROOT_PASSWORD=yes"
|
||||
hostname: bot
|
||||
networks:
|
||||
- discord-tickets
|
||||
ports:
|
||||
- 8169:8169
|
||||
volumes:
|
||||
- db:/var/lib/mysql
|
||||
- tickets-bot:/usr/bot/user
|
||||
tty: true
|
||||
stdin_open: true
|
||||
# Please refer to the documentation:
|
||||
# https://discordtickets.app/self-hosting/configuration/#environment-variables
|
||||
environment:
|
||||
DB_CONNECTION_URL: mysql://tickets:insecure@mysql/tickets # change `insecure` to the MYSQL_PASSWORD you set above
|
||||
DISCORD_SECRET: # required
|
||||
DISCORD_TOKEN: # required
|
||||
ENCRYPTION_KEY: # required
|
||||
DB_PROVIDER: mysql
|
||||
HTTP_EXTERNAL: http://127.0.0.1:8169 # change this to your server's external IP (or domain)
|
||||
HTTP_HOST: 0.0.0.0
|
||||
HTTP_PORT: 8169
|
||||
HTTP_TRUST_PROXY: false # set to true if you're using a reverse proxy
|
||||
OVERRIDE_ARCHIVE: null
|
||||
PUBLIC_BOT: false
|
||||
PUBLISH_COMMANDS: false
|
||||
SUPER: 319467558166069248 # optionally add `,youruseridhere`
|
||||
|
||||
networks:
|
||||
discord-tickets:
|
||||
|
||||
volumes:
|
||||
db:
|
||||
tickets-mysql:
|
||||
tickets-bot:
|
||||
|
@ -1,9 +0,0 @@
|
||||
DISCORD_TOKEN=
|
||||
DB_ENCRYPTION_KEY=
|
||||
DB_TYPE=sqlite
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASS=
|
||||
DB_TABLE_PREFIX=dsctickets_
|
13
jsconfig.json
Normal file
13
jsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": "src",
|
||||
"resolveJsonModule": true,
|
||||
"checkJs": false,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.js"
|
||||
]
|
||||
}
|
96
package.json
96
package.json
@ -1,18 +1,25 @@
|
||||
{
|
||||
"name": "@eartharoid/discord-tickets",
|
||||
"version": "3.1.3",
|
||||
"private": true,
|
||||
"name": "discord-tickets",
|
||||
"version": "4.0.0-beta.12",
|
||||
"private": "true",
|
||||
"description": "An open-source Discord bot for ticket management",
|
||||
"main": "src/index.js",
|
||||
"main": "src/",
|
||||
"scripts": {
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
||||
"contributors:add": "all-contributors add",
|
||||
"contributors:generate": "all-contributors generate",
|
||||
"lint": "eslint src --fix",
|
||||
"start": "node src/",
|
||||
"test": "echo \"Nothing to test! Run with 'npm start'\" && exit 1"
|
||||
"keygen": "node scripts/keygen",
|
||||
"lint": "eslint src scripts --fix",
|
||||
"preinstall": "node scripts/preinstall",
|
||||
"postinstall": "node scripts/postinstall",
|
||||
"start": "node .",
|
||||
"studio": "npx prisma studio",
|
||||
"test": "echo \"There's nothing to test\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.6"
|
||||
"lint-staged": {
|
||||
"**/*.js": [
|
||||
"npm run lint --"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -29,41 +36,54 @@
|
||||
"url": "https://github.com/discord-tickets/bot/issues"
|
||||
},
|
||||
"homepage": "https://discordtickets.app",
|
||||
"funding": "https://github.com/discord-tickets/bot/?sponsor=1",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eartharoid/i18n": "^1.0.1",
|
||||
"boxen": "^5.1.2",
|
||||
"cryptr": "^6.0.2",
|
||||
"discord.js": "^13.14.0",
|
||||
"dotenv": "^8.6.0",
|
||||
"keyv": "^4.0.3",
|
||||
"leeks.js": "^0.2.4",
|
||||
"leekslazylogger": "^3.0.2",
|
||||
"@discord-tickets/settings": "^2.1.4",
|
||||
"@eartharoid/dbf": "^0.4.0",
|
||||
"@eartharoid/dtf": "^2.0.1",
|
||||
"@eartharoid/i18n": "^1.2.1",
|
||||
"@fastify/cookie": "^6.0.0",
|
||||
"@fastify/jwt": "^5.0.1",
|
||||
"@fastify/oauth2": "^5.1.0",
|
||||
"@prisma/client": "^4.14.1",
|
||||
"boxen": "^7.1.0",
|
||||
"cryptr": "^6.2.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"fastify": "^4.17.0",
|
||||
"figlet": "^1.6.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"keyv": "^4.5.2",
|
||||
"leeks.js": "^0.3.0",
|
||||
"leekslazylogger": "^5.0.1",
|
||||
"ms": "^2.1.3",
|
||||
"mustache": "^4.2.0",
|
||||
"node-fetch": "^2.6.5",
|
||||
"semver": "^7.3.5",
|
||||
"sequelize": "^6.6.5",
|
||||
"terminal-link": "^2.1.1"
|
||||
"node-dir": "^0.1.17",
|
||||
"node-emoji": "^1.11.0",
|
||||
"object-diffy": "^1.0.4",
|
||||
"prisma": "^4.14.1",
|
||||
"semver": "^7.5.1",
|
||||
"spacetime": "^7.4.4",
|
||||
"terminal-link": "^2.1.1",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"all-contributors-cli": "^6.20.0",
|
||||
"eslint": "^7.32.0",
|
||||
"mariadb": "^2.5.4",
|
||||
"mysql2": "^2.3.0",
|
||||
"nodemon": "^2.0.13",
|
||||
"pg": "^8.7.1",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"tedious": "^11.8.0"
|
||||
"@commitlint/cli": "^17.6.3",
|
||||
"@commitlint/config-conventional": "^17.6.3",
|
||||
"all-contributors-cli": "^6.26.0",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"nodemon": "^2.0.22"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sqlite3": "^5.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mariadb": "^2.5.4",
|
||||
"mysql2": "^2.3.0",
|
||||
"pg": "^8.7.1",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"tedious": "^11.4.0"
|
||||
"bufferutil": "^4.0.7",
|
||||
"erlpack": "github:discord/erlpack",
|
||||
"utf-8-validate": "^5.0.10",
|
||||
"zlib-sync": "^0.1.8"
|
||||
}
|
||||
}
|
||||
|
5127
pnpm-lock.yaml
5127
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,50 +0,0 @@
|
||||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
|
||||
"meta": {
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-05-26T22:41:44+02:00",
|
||||
"name": "Discord Tickets",
|
||||
"author": "contact@discordtickets.app",
|
||||
"description": "https:\/\/discordtickets.app",
|
||||
"features": null,
|
||||
"images": [
|
||||
"ghcr.io\/parkervcp\/yolks:nodejs_16"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": "if [[ ! -z ${VERSION} ]]; then\r\n echo -e \\\"Using version ${VERSION}\\\";\r\nelse\r\n echo -e \\\"Please set the VERSION variable \\(e.g. v3.1.1\\)\\\";\r\n exit 0;\r\nfi;\r\n\r\nif [[ -d .git ]]; then\r\n git fetch --all --tags;\r\n git checkout tags\/${VERSION};\r\nfi;\r\n\r\nif [ -f \/home\/container\/package.json ]; then\r\n \/usr\/local\/bin\/npm install --production;\r\nfi;\r\n\r\nif [[ ! -z ${PLUGINS} ]]; then\r\n \/usr\/local\/bin\/npm install ${PLUGINS} --no-save;\r\nfi;\r\n\r\n\/usr\/local\/bin\/npm start",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"Connected to Discord as \",\r\n \"userInteraction\": [\r\n \"Please set your bot's \\\"DISCORD_TOKEN\\\" in \\\".\/.env\\\".\"\r\n ]\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "^C^C"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/bash\r\n\r\napt update\r\napt install -y git curl jq file unzip make gcc g++ python python-dev libtool\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nif [[ ! -z ${VERSION} ]]; then\r\n echo -e \\\"Using version ${VERSION}\\\"\r\nelse\r\n echo -e \\\"Please set the VERSION variable \\(e.g. v3.0.0\\)\\\"\r\n exit 1\r\nfi\r\n\r\nif [ \\\"$(ls -A \/mnt\/server)\\\" ] && [[ -d .git ]]; then\r\n echo -e \\\".git directory exists\\\"\r\n if [ -f .git\\\/config ]; then\r\n echo \\\"Upating...\\\"\r\n git fetch --all --tags\r\n git checkout tags\/${VERSION}\r\n else\r\n echo -e \\\"files found with no git config\\\"\r\n echo -e \\\"closing out without touching things to not break anything\\\"\r\n exit 1\r\n fi\r\nelse\r\n echo -e \\\"Cloning...\\\"\r\n git clone https:\/\/github.com\/discord-tickets\/bot.git .\r\n\tgit checkout tags\/${VERSION}\r\nfi\r\n\r\necho \\\"Installing dependencies\\\"\r\n\r\nif [[ ! -z ${PUGINS} ]]; then\r\n \/usr\/local\/bin\/npm install ${PUGINS}\r\nfi\r\n\r\nif [ -f \/mnt\/server\/package.json ]; then\r\n \/usr\/local\/bin\/npm install --production\r\nfi\r\n\r\necho -e \\\"Installed\\\"\r\nexit 0",
|
||||
"container": "node:16-bullseye-slim",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "Version",
|
||||
"description": "The version of the bot to use.",
|
||||
"env_variable": "VERSION",
|
||||
"default_value": "v3.1.3",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20"
|
||||
},
|
||||
{
|
||||
"name": "Plugins",
|
||||
"description": "A list of extra NPM package names to install as plugins, separated by a space: \"example1 exmaple2 example3\"",
|
||||
"env_variable": "PLUGINS",
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string"
|
||||
}
|
||||
]
|
||||
}
|
9
scripts/keygen.js
Normal file
9
scripts/keygen.js
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
const { randomBytes } = require('crypto');
|
||||
const { short } = require('leeks.js');
|
||||
|
||||
console.log(short(
|
||||
'Set the "ENCRYPTION_KEY" environment variable to: \n&!b ' +
|
||||
randomBytes(24).toString('hex') +
|
||||
' &r\n\n&0&!e WARNING &r &e&lIf you lose the encryption key, most of the data in the database will become unreadable, requiring a new key and a full reset.',
|
||||
));
|
42
scripts/postinstall.js
Normal file
42
scripts/postinstall.js
Normal file
@ -0,0 +1,42 @@
|
||||
/* eslint-disable no-console */
|
||||
require('dotenv').config();
|
||||
const fs = require('fs-extra');
|
||||
const util = require('util');
|
||||
const exec = util.promisify(require('child_process').exec);
|
||||
const { short } = require('leeks.js');
|
||||
|
||||
function log(...strings) {
|
||||
console.log(short('&9[postinstall]&r'), ...strings);
|
||||
}
|
||||
|
||||
async function npx(cmd) {
|
||||
log(`> ${cmd}`);
|
||||
const {
|
||||
stderr,
|
||||
stdout,
|
||||
} = await exec('npx ' + cmd);
|
||||
if (stdout) console.log(stdout.toString());
|
||||
if (stderr) console.log(stderr.toString());
|
||||
}
|
||||
|
||||
const providers = ['mysql', 'postgresql', 'sqlite'];
|
||||
const provider = process.env.DB_PROVIDER;
|
||||
|
||||
if (!provider) {
|
||||
log('environment not set, exiting.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!providers.includes(provider)) throw new Error(`DB_PROVIDER must be one of: ${providers}`);
|
||||
|
||||
log(`provider=${provider}`);
|
||||
log(`copying ${provider} schema & migrations`);
|
||||
|
||||
if (!fs.existsSync('./prisma')) fs.mkdirSync('./prisma');
|
||||
fs.copySync(`./db/${provider}`, './prisma'); // copy schema & migrations
|
||||
|
||||
(async () => {
|
||||
await npx('prisma generate');
|
||||
await npx('prisma migrate deploy');
|
||||
})();
|
||||
|
40
scripts/preinstall.js
Normal file
40
scripts/preinstall.js
Normal file
@ -0,0 +1,40 @@
|
||||
/* eslint-disable no-console */
|
||||
const { randomBytes } = require('crypto');
|
||||
const fs = require('fs');
|
||||
const { short } = require('leeks.js');
|
||||
|
||||
function log (...strings) {
|
||||
console.log(short('&9[preinstall]&r'), ...strings);
|
||||
}
|
||||
|
||||
if (process.env.CI) {
|
||||
log('CI detected, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const env = {
|
||||
DB_CONNECTION_URL: '',
|
||||
DB_PROVIDER: '', // don't default to sqlite, postinstall checks if empty
|
||||
DISCORD_SECRET: '',
|
||||
DISCORD_TOKEN: '',
|
||||
ENCRYPTION_KEY: randomBytes(24).toString('hex'),
|
||||
HTTP_EXTERNAL: 'http://127.0.0.1:8169',
|
||||
HTTP_HOST: '0.0.0.0',
|
||||
HTTP_PORT: 8169,
|
||||
HTTP_TRUST_PROXY: false,
|
||||
NODE_ENV: 'production', // not bot-specific
|
||||
OVERRIDE_ARCHIVE: '',
|
||||
PUBLIC_BOT: false,
|
||||
PUBLISH_COMMANDS: false,
|
||||
SUPER: '319467558166069248',
|
||||
};
|
||||
|
||||
// check ENCRYPTION_KEY because we don't want to force use of the .env file
|
||||
if (!process.env.ENCRYPTION_KEY && !fs.existsSync('./.env')) {
|
||||
log('generating ENCRYPTION_KEY');
|
||||
fs.writeFileSync('./.env', Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n'));
|
||||
log('created .env file');
|
||||
log(short('&r&0&!e WARNING &r &e&lkeep your environment variables safe, don\'t lose your encryption key or you will lose data'));
|
||||
} else {
|
||||
log('nothing to do');
|
||||
}
|
10
scripts/start.sh
Executable file
10
scripts/start.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Checking environment..."
|
||||
node scripts/preinstall
|
||||
|
||||
echo "Preparing the database..."
|
||||
node scripts/postinstall
|
||||
|
||||
echo "Starting..."
|
||||
node src/
|
37
src/autocomplete/category.js
Normal file
37
src/autocomplete/category.js
Normal file
@ -0,0 +1,37 @@
|
||||
const { Autocompleter } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class CategoryCompleter extends Autocompleter {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'category',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {*} command
|
||||
* @param {import("discord.js").AutocompleteInteraction} interaction
|
||||
*/
|
||||
async run(value, command, interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
let categories = await client.prisma.category.findMany({ where: { guildId: interaction.guild.id } });
|
||||
|
||||
if (command.name === 'move') {
|
||||
const ticket = await client.prisma.ticket.findUnique({ where: { id: interaction.channel.id } });
|
||||
if (ticket) categories = categories.filter(category => ticket.categoryId !== category.id);
|
||||
}
|
||||
|
||||
const options = value ? categories.filter(category => category.name.match(new RegExp(value, 'i'))) : categories;
|
||||
await interaction.respond(
|
||||
options
|
||||
.slice(0, 25)
|
||||
.map(category => ({
|
||||
name: category.name,
|
||||
value: category.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
};
|
26
src/autocomplete/references.js
Normal file
26
src/autocomplete/references.js
Normal file
@ -0,0 +1,26 @@
|
||||
const { Autocompleter } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class ReferencesCompleter extends Autocompleter {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'references',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {*} comamnd
|
||||
* @param {import("discord.js").AutocompleteInteraction} interaction
|
||||
*/
|
||||
async run(value, comamnd, interaction) {
|
||||
await interaction.respond(
|
||||
await this.client.autocomplete.components.get('ticket').getOptions(value, {
|
||||
guildId: interaction.guild.id,
|
||||
open: false,
|
||||
userId: interaction.user.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
50
src/autocomplete/tag.js
Normal file
50
src/autocomplete/tag.js
Normal file
@ -0,0 +1,50 @@
|
||||
const { Autocompleter } = require('@eartharoid/dbf');
|
||||
const ms = require('ms');
|
||||
|
||||
module.exports = class TagCompleter extends Autocompleter {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'tag',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {*} command
|
||||
* @param {import("discord.js").AutocompleteInteraction} interaction
|
||||
*/
|
||||
async run(value, command, interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
const cacheKey = `cache/guild-tags:${interaction.guild.id}`;
|
||||
let tags = await client.keyv.get(cacheKey);
|
||||
if (!tags) {
|
||||
tags = await client.prisma.tag.findMany({
|
||||
select: {
|
||||
content: true,
|
||||
id: true,
|
||||
name: true,
|
||||
regex: true,
|
||||
},
|
||||
where: { guildId: interaction.guild.id },
|
||||
});
|
||||
client.keyv.set(cacheKey, tags, ms('1h'));
|
||||
}
|
||||
|
||||
const options = value ? tags.filter(tag =>
|
||||
tag.name.match(new RegExp(value, 'i')) ||
|
||||
tag.content.match(new RegExp(value, 'i')) ||
|
||||
tag.regex?.match(new RegExp(value, 'i')),
|
||||
) : tags;
|
||||
await interaction.respond(
|
||||
options
|
||||
.slice(0, 25)
|
||||
.map(tag => ({
|
||||
name: tag.name,
|
||||
value: tag.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
};
|
86
src/autocomplete/ticket.js
Normal file
86
src/autocomplete/ticket.js
Normal file
@ -0,0 +1,86 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const { Autocompleter } = require('@eartharoid/dbf');
|
||||
const emoji = require('node-emoji');
|
||||
const Cryptr = require('cryptr');
|
||||
const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
|
||||
const Keyv = require('keyv');
|
||||
const ms = require('ms');
|
||||
const { isStaff } = require('../lib/users');
|
||||
|
||||
module.exports = class TicketCompleter extends Autocompleter {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'ticket',
|
||||
});
|
||||
|
||||
this.cache = new Keyv();
|
||||
}
|
||||
|
||||
async getOptions(value, {
|
||||
guildId,
|
||||
open,
|
||||
userId,
|
||||
}) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
const cacheKey = [guildId, userId, open].join('/');
|
||||
|
||||
let tickets = await this.cache.get(cacheKey);
|
||||
|
||||
if (!tickets) {
|
||||
const { locale } = await client.prisma.guild.findUnique({
|
||||
select: { locale: true },
|
||||
where: { id: guildId },
|
||||
});
|
||||
tickets = await client.prisma.ticket.findMany({
|
||||
include: {
|
||||
category: {
|
||||
select: {
|
||||
emoji: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
createdById: userId,
|
||||
guildId,
|
||||
open,
|
||||
},
|
||||
});
|
||||
tickets = tickets.map(ticket => {
|
||||
const date = new Date(ticket.createdAt).toLocaleString([locale, 'en-GB'], { dateStyle: 'short' });
|
||||
const topic = ticket.topic ? '- ' + decrypt(ticket.topic).replace(/\n/g, ' ').substring(0, 50) : '';
|
||||
const category = emoji.hasEmoji(ticket.category.emoji) ? emoji.get(ticket.category.emoji) + ' ' + ticket.category.name : ticket.category.name;
|
||||
ticket._name = `${category} #${ticket.number} (${date}) ${topic}`;
|
||||
return ticket;
|
||||
});
|
||||
this.cache.set(cacheKey, tickets, ms('1m'));
|
||||
}
|
||||
|
||||
const options = value ? tickets.filter(t => t._name.match(new RegExp(value, 'i'))) : tickets;
|
||||
return options
|
||||
.slice(0, 25)
|
||||
.map(t => ({
|
||||
name: t._name,
|
||||
value: t.id,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {*} command
|
||||
* @param {import("discord.js").AutocompleteInteraction} interaction
|
||||
*/
|
||||
async run(value, command, interaction) {
|
||||
const otherMember = await isStaff(interaction.guild, interaction.user.id) && interaction.options.data[1]?.value;
|
||||
const userId = otherMember || interaction.user.id;
|
||||
await interaction.respond(
|
||||
await this.getOptions(value, {
|
||||
guildId: interaction.guild.id,
|
||||
open: ['add', 'close', 'force-close', 'remove'].includes(command.name), // false for `new`, `transcript` etc
|
||||
userId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
const link = require('terminal-link');
|
||||
const leeks = require('leeks.js');
|
||||
|
||||
const {
|
||||
version, homepage
|
||||
} = require('../package.json');
|
||||
|
||||
module.exports = () => {
|
||||
console.log(leeks.colours.cyan(`
|
||||
######## #### ###### ###### ####### ######## ########
|
||||
## ## ## ## ## ## ## ## ## ## ## ## ##
|
||||
## ## ## ## ## ## ## ## ## ## ##
|
||||
## ## ## ###### ## ## ## ######## ## ##
|
||||
## ## ## ## ## ## ## ## ## ## ##
|
||||
## ## ## ## ## ## ## ## ## ## ## ## ##
|
||||
######## #### ###### ###### ####### ## ## ########
|
||||
|
||||
######## #### ###### ## ## ######## ######## ######
|
||||
## ## ## ## ## ## ## ## ## ##
|
||||
## ## ## ## ## ## ## ##
|
||||
## ## ## ##### ###### ## ######
|
||||
## ## ## ## ## ## ## ##
|
||||
## ## ## ## ## ## ## ## ## ##
|
||||
## #### ###### ## ## ######## ## ######
|
||||
`));
|
||||
console.log(leeks.colours.cyanBright(`Discord Tickets bot v${version} by eartharoid`));
|
||||
console.log(leeks.colours.cyanBright(homepage + '\n'));
|
||||
console.log(leeks.colours.cyanBright(link('Sponsor this project', 'https://discordtickets.app/sponsor') + '\n'));
|
||||
};
|
22
src/buttons/claim.js
Normal file
22
src/buttons/claim.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { Button } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class ClaimButton extends Button {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'claim',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} id
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(id, interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
await client.tickets.claim(interaction);
|
||||
}
|
||||
};
|
81
src/buttons/close.js
Normal file
81
src/buttons/close.js
Normal file
@ -0,0 +1,81 @@
|
||||
const { Button } = require('@eartharoid/dbf');
|
||||
const ExtendedEmbedBuilder = require('../lib/embed');
|
||||
const { isStaff } = require('../lib/users');
|
||||
|
||||
module.exports = class CloseButton extends Button {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'close',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} id
|
||||
* @param {import("discord.js").ButtonInteraction} interaction
|
||||
*/
|
||||
async run(id, interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
if (id.accepted === undefined) {
|
||||
// the close button on the opening message, the same as using /close
|
||||
await client.tickets.beforeRequestClose(interaction);
|
||||
} else {
|
||||
const ticket = await client.tickets.getTicket(interaction.channel.id, true); // true to override cache and load new feedback
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
const staff = await isStaff(interaction.guild, interaction.user.id);
|
||||
|
||||
if (id.expect === 'staff' && !staff) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setDescription(getMessage('ticket.close.wait_for_staff')),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
} else if (id.expect === 'user' && interaction.user.id !== ticket.createdById) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setDescription(getMessage('ticket.close.wait_for_user')),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
} else {
|
||||
if (id.accepted) {
|
||||
if (
|
||||
ticket.createdById === interaction.user.id &&
|
||||
ticket.category.enableFeedback &&
|
||||
!ticket.feedback
|
||||
) {
|
||||
return await interaction.showModal(client.tickets.buildFeedbackModal(ticket.guild.locale, { next: 'acceptClose' }));
|
||||
} else {
|
||||
await interaction.deferReply();
|
||||
await client.tickets.acceptClose(interaction);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await interaction.update({
|
||||
components: [],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() }))
|
||||
.setFooter({ text: null }),
|
||||
],
|
||||
});
|
||||
|
||||
} finally { // this should run regardless of whatever happens above
|
||||
client.tickets.$stale.delete(ticket.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
23
src/buttons/create.js
Normal file
23
src/buttons/create.js
Normal file
@ -0,0 +1,23 @@
|
||||
const { Button } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class CreateButton extends Button {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'create',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} id
|
||||
* @param {import("discord.js").ButtonInteraction} interaction
|
||||
*/
|
||||
async run(id, interaction) {
|
||||
if (id.targetUser && id.targetUser !== interaction.user.id) return;
|
||||
await this.client.tickets.create({
|
||||
categoryId: id.target,
|
||||
interaction,
|
||||
topic: id.topic,
|
||||
});
|
||||
}
|
||||
};
|
111
src/buttons/edit.js
Normal file
111
src/buttons/edit.js
Normal file
@ -0,0 +1,111 @@
|
||||
const { Button } = require('@eartharoid/dbf');
|
||||
const {
|
||||
ActionRowBuilder,
|
||||
ModalBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
} = require('discord.js');
|
||||
const emoji = require('node-emoji');
|
||||
const Cryptr = require('cryptr');
|
||||
const cryptr = new Cryptr(process.env.ENCRYPTION_KEY);
|
||||
|
||||
module.exports = class EditButton extends Button {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'edit',
|
||||
});
|
||||
}
|
||||
|
||||
async run(id, interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
select: {
|
||||
category: { select: { name: true } },
|
||||
guild: { select: { locale: true } },
|
||||
questionAnswers: { include: { question: true } },
|
||||
topic: true,
|
||||
},
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
|
||||
if (ticket.questionAnswers.length === 0) {
|
||||
await interaction.showModal(
|
||||
new ModalBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'topic',
|
||||
edit: true,
|
||||
}))
|
||||
.setTitle(ticket.category.name)
|
||||
.setComponents(
|
||||
new ActionRowBuilder()
|
||||
.setComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('topic')
|
||||
.setLabel(getMessage('modals.topic.label'))
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setMaxLength(1000)
|
||||
.setMinLength(5)
|
||||
.setPlaceholder(getMessage('modals.topic.placeholder'))
|
||||
.setRequired(true)
|
||||
.setValue(ticket.topic ? cryptr.decrypt(ticket.topic) : ''),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await interaction.showModal(
|
||||
new ModalBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'questions',
|
||||
edit: true,
|
||||
}))
|
||||
.setTitle(ticket.category.name)
|
||||
.setComponents(
|
||||
ticket.questionAnswers
|
||||
.filter(a => a.question.type === 'TEXT') // TODO: remove this when modals support select menus
|
||||
.map(a => {
|
||||
if (a.question.type === 'TEXT') {
|
||||
return new ActionRowBuilder()
|
||||
.setComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId(String(a.id))
|
||||
.setLabel(a.question.label)
|
||||
.setStyle(a.question.style)
|
||||
.setMaxLength(Math.min(a.question.maxLength, 1000))
|
||||
.setMinLength(a.question.minLength)
|
||||
.setPlaceholder(a.question.placeholder)
|
||||
.setRequired(a.question.required)
|
||||
.setValue(a.value ? cryptr.decrypt(a.value) : a.question.value),
|
||||
);
|
||||
} else if (a.question.type === 'MENU') {
|
||||
return new ActionRowBuilder()
|
||||
.setComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId(a.question.id)
|
||||
.setPlaceholder(a.question.placeholder || a.question.label)
|
||||
.setMaxValues(a.question.maxLength)
|
||||
.setMinValues(a.question.minLength)
|
||||
.setOptions(
|
||||
a.question.options.map((o, i) => {
|
||||
const builder = new StringSelectMenuOptionBuilder()
|
||||
.setValue(String(i))
|
||||
.setLabel(o.label);
|
||||
if (o.description) builder.setDescription(o.description);
|
||||
if (o.emoji) builder.setEmoji(emoji.hasEmoji(o.emoji) ? emoji.get(o.emoji) : { id: o.emoji });
|
||||
return builder;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
22
src/buttons/unclaim.js
Normal file
22
src/buttons/unclaim.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { Button } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class UnclaimButton extends Button {
|
||||
constructor(client, options) {
|
||||
super(client, {
|
||||
...options,
|
||||
id: 'unclaim',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} id
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(id, interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
await client.tickets.release(interaction);
|
||||
}
|
||||
};
|
70
src/client.js
Normal file
70
src/client.js
Normal file
@ -0,0 +1,70 @@
|
||||
const { FrameworkClient } = require('@eartharoid/dbf');
|
||||
const {
|
||||
GatewayIntentBits,
|
||||
Partials,
|
||||
} = require('discord.js');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const Keyv = require('keyv');
|
||||
const I18n = require('@eartharoid/i18n');
|
||||
const fs = require('fs');
|
||||
const { join } = require('path');
|
||||
const YAML = require('yaml');
|
||||
const TicketManager = require('./lib/tickets/manager');
|
||||
const sqliteMiddleware = require('./lib/middleware/prisma-sqlite');
|
||||
|
||||
module.exports = class Client extends FrameworkClient {
|
||||
constructor(config, log) {
|
||||
super({
|
||||
intents: [
|
||||
GatewayIntentBits.DirectMessages,
|
||||
GatewayIntentBits.DirectMessageReactions,
|
||||
GatewayIntentBits.DirectMessageTyping,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildPresences,
|
||||
],
|
||||
partials: [
|
||||
Partials.Channel,
|
||||
Partials.Message,
|
||||
Partials.Reaction,
|
||||
],
|
||||
});
|
||||
|
||||
const locales = {};
|
||||
fs.readdirSync(join(__dirname, 'i18n'))
|
||||
.filter(file => file.endsWith('.yml'))
|
||||
.forEach(file => {
|
||||
const data = fs.readFileSync(join(__dirname, 'i18n/' + file), { encoding: 'utf8' });
|
||||
const name = file.slice(0, file.length - 4);
|
||||
locales[name] = YAML.parse(data);
|
||||
});
|
||||
|
||||
/** @type {I18n} */
|
||||
this.i18n = new I18n('en-GB', locales);
|
||||
/** @type {TicketManager} */
|
||||
this.tickets = new TicketManager(this);
|
||||
this.config = config;
|
||||
this.log = log;
|
||||
this.supers = (process.env.SUPER ?? '').split(',');
|
||||
}
|
||||
|
||||
async login(token) {
|
||||
/** @type {PrismaClient} */
|
||||
this.prisma = new PrismaClient();
|
||||
if (process.env.DB_PROVIDER === 'sqlite') {
|
||||
this.prisma.$use(sqliteMiddleware);
|
||||
// make sqlite faster (https://www.sqlite.org/wal.html),
|
||||
// and the missing parentheses are not a mistake, `$queryRaw` is a tagged template literal
|
||||
this.log.debug(await this.prisma.$queryRaw`PRAGMA journal_mode=WAL;`);
|
||||
}
|
||||
this.keyv = new Keyv();
|
||||
return super.login(token);
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
await this.prisma.$disconnect();
|
||||
return super.destroy();
|
||||
}
|
||||
};
|
@ -1,118 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class AddCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.add.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.add.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.add.options.member.description'),
|
||||
name: i18n('commands.add.options.member.name'),
|
||||
required: true,
|
||||
type: Command.option_types.USER
|
||||
},
|
||||
{
|
||||
description: i18n('commands.add.options.ticket.description'),
|
||||
name: i18n('commands.add.options.ticket.name'),
|
||||
required: false,
|
||||
type: Command.option_types.CHANNEL
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const channel = interaction.options.getChannel(default_i18n('commands.add.options.ticket.name')) ?? interaction.channel;
|
||||
const t_row = await this.client.tickets.resolve(channel.id, interaction.guild.id);
|
||||
|
||||
if (!t_row) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.add.response.not_a_ticket.title'))
|
||||
.setDescription(i18n('commands.add.response.not_a_ticket.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const member = interaction.options.getMember(default_i18n('commands.add.options.member.name'));
|
||||
|
||||
if (!member) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.add.response.no_member.title'))
|
||||
.setDescription(i18n('commands.add.response.no_member.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (t_row.creator !== interaction.member.id && !await this.client.utils.isStaff(interaction.member)) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.add.response.no_permission.title'))
|
||||
.setDescription(i18n('commands.add.response.no_permission.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setAuthor(member.user.username, member.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.add.response.added.title'))
|
||||
.setDescription(i18n('commands.add.response.added.description', member.toString(), channel.toString()))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
await channel.send({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setAuthor(member.user.username, member.user.displayAvatarURL())
|
||||
.setTitle(i18n('ticket.member_added.title'))
|
||||
.setDescription(i18n('ticket.member_added.description', member.toString(), interaction.user.toString()))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
]
|
||||
});
|
||||
|
||||
await channel.permissionOverwrites.edit(member, {
|
||||
ATTACH_FILES: true,
|
||||
READ_MESSAGE_HISTORY: true,
|
||||
SEND_MESSAGES: true,
|
||||
VIEW_CHANNEL: true
|
||||
}, `${interaction.user.tag} added ${member.user.tag} to the ticket`);
|
||||
|
||||
await this.client.tickets.archives.updateMember(channel.id, member);
|
||||
|
||||
this.client.log.info(`${interaction.user.tag} added ${member.user.tag} to ${channel.id}`);
|
||||
}
|
||||
};
|
@ -1,156 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed,
|
||||
Role
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class BlacklistCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.blacklist.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.blacklist.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.blacklist.options.add.description'),
|
||||
name: i18n('commands.blacklist.options.add.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.blacklist.options.add.options.member_or_role.description'),
|
||||
name: i18n('commands.blacklist.options.add.options.member_or_role.name'),
|
||||
required: true,
|
||||
type: Command.option_types.MENTIONABLE
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
},
|
||||
{
|
||||
description: i18n('commands.blacklist.options.remove.description'),
|
||||
name: i18n('commands.blacklist.options.remove.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.blacklist.options.remove.options.member_or_role.description'),
|
||||
name: i18n('commands.blacklist.options.remove.options.member_or_role.name'),
|
||||
required: true,
|
||||
type: Command.option_types.MENTIONABLE
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
},
|
||||
{
|
||||
description: i18n('commands.blacklist.options.show.description'),
|
||||
name: i18n('commands.blacklist.options.show.name'),
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
}
|
||||
],
|
||||
staff_only: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
const blacklist = JSON.parse(JSON.stringify(settings.blacklist)); // not the same as `const blacklist = { ...settings.blacklist };` ..?
|
||||
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case default_i18n('commands.blacklist.options.add.name'): {
|
||||
const member_or_role = interaction.options.getMentionable(default_i18n('commands.blacklist.options.add.options.member_or_role.name'));
|
||||
const type = member_or_role instanceof Role ? 'role' : 'member';
|
||||
|
||||
if (type === 'member' && await this.client.utils.isStaff(member_or_role)) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.blacklist.response.illegal_action.title'))
|
||||
.setDescription(i18n('commands.blacklist.response.illegal_action.description', member_or_role.toString()))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
blacklist[type + 's'].push(member_or_role.id);
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n(`commands.blacklist.response.${type}_added.title`))
|
||||
.setDescription(i18n(`commands.blacklist.response.${type}_added.description`, member_or_role.id))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
await settings.update({ blacklist });
|
||||
break;
|
||||
}
|
||||
case default_i18n('commands.blacklist.options.remove.name'): {
|
||||
const member_or_role = interaction.options.getMentionable(default_i18n('commands.blacklist.options.remove.options.member_or_role.name'));
|
||||
const type = member_or_role instanceof Role ? 'role' : 'member';
|
||||
const index = blacklist[type + 's'].findIndex(element => element === member_or_role.id);
|
||||
|
||||
if (index === -1) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.blacklist.response.invalid.title'))
|
||||
.setDescription(i18n('commands.blacklist.response.invalid.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
blacklist[type + 's'].splice(index, 1);
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n(`commands.blacklist.response.${type}_removed.title`))
|
||||
.setDescription(i18n(`commands.blacklist.response.${type}_removed.description`, member_or_role.id))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
await settings.update({ blacklist });
|
||||
break;
|
||||
}
|
||||
case default_i18n('commands.blacklist.options.show.name'): {
|
||||
if (blacklist.members.length === 0 && blacklist.roles.length === 0) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.blacklist.response.empty_list.title'))
|
||||
.setDescription(i18n('commands.blacklist.response.empty_list.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
} else {
|
||||
const members = blacklist.members.map(id => `**·** <@${id}>`);
|
||||
const roles = blacklist.roles.map(id => `**·** <@&${id}>`);
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.blacklist.response.list.title'))
|
||||
.addField(i18n('commands.blacklist.response.list.fields.members'), members.join('\n') || 'none')
|
||||
.addField(i18n('commands.blacklist.response.list.fields.roles'), roles.join('\n') || 'none')
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -1,322 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageActionRow,
|
||||
MessageButton,
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
const { Op } = require('sequelize');
|
||||
const ms = require('ms');
|
||||
|
||||
module.exports = class CloseCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.close.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.close.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.close.options.reason.description'),
|
||||
name: i18n('commands.close.options.reason.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.close.options.ticket.description'),
|
||||
name: i18n('commands.close.options.ticket.name'),
|
||||
required: false,
|
||||
type: Command.option_types.INTEGER
|
||||
},
|
||||
{
|
||||
description: i18n('commands.close.options.time.description'),
|
||||
name: i18n('commands.close.options.time.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const reason = interaction.options.getString(default_i18n('commands.close.options.reason.name'));
|
||||
const ticket = interaction.options.getInteger(default_i18n('commands.close.options.ticket.name'));
|
||||
const time = interaction.options.getString(default_i18n('commands.close.options.time.name'));
|
||||
|
||||
if (time) {
|
||||
if (!await this.client.utils.isStaff(interaction.member)) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.no_permission.title'))
|
||||
.setDescription(i18n('commands.close.response.no_permission.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
let period;
|
||||
try {
|
||||
period = ms(time);
|
||||
} catch {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.invalid_time.title'))
|
||||
.setDescription(i18n('commands.close.response.invalid_time.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
const tickets = await this.client.db.models.Ticket.findAndCountAll({
|
||||
where: {
|
||||
guild: interaction.guild.id,
|
||||
last_message: { [Op.lte]: new Date(Date.now() - period) },
|
||||
open: true
|
||||
}
|
||||
});
|
||||
|
||||
if (tickets.count === 0) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.no_tickets.title'))
|
||||
.setDescription(i18n('commands.close.response.no_tickets.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({
|
||||
components: [
|
||||
new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(`confirm_close_multiple:${interaction.id}`)
|
||||
.setLabel(i18n('commands.close.response.confirm_multiple.buttons.confirm', tickets.count, tickets.count))
|
||||
.setEmoji('✅')
|
||||
.setStyle('SUCCESS')
|
||||
)
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(`cancel_close_multiple:${interaction.id}`)
|
||||
.setLabel(i18n('commands.close.response.confirm_multiple.buttons.cancel'))
|
||||
.setEmoji('❌')
|
||||
.setStyle('SECONDARY')
|
||||
)
|
||||
],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.close.response.confirm_multiple.title'))
|
||||
.setDescription(i18n('commands.close.response.confirm_multiple.description', tickets.count, tickets.count))
|
||||
.setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
|
||||
const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id);
|
||||
const collector = interaction.channel.createMessageComponentCollector({
|
||||
filter,
|
||||
time: 30000
|
||||
});
|
||||
|
||||
collector.on('collect', async i => {
|
||||
await i.deferUpdate();
|
||||
|
||||
if (i.customId === `confirm_close_multiple:${interaction.id}`) {
|
||||
for (const ticket of tickets.rows) {
|
||||
await this.client.tickets.close(ticket.id, interaction.user.id, interaction.guild.id, reason);
|
||||
}
|
||||
|
||||
await i.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n('commands.close.response.closed_multiple.title', tickets.count, tickets.count))
|
||||
.setDescription(i18n('commands.close.response.closed_multiple.description', tickets.count, tickets.count))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
} else {
|
||||
await i.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.canceled.title'))
|
||||
.setDescription(i18n('commands.close.response.canceled.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
collector.stop();
|
||||
});
|
||||
|
||||
collector.on('end', async collected => {
|
||||
if (collected.size === 0) {
|
||||
await interaction.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.close.response.confirmation_timeout.title'))
|
||||
.setDescription(i18n('commands.close.response.confirmation_timeout.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let t_row;
|
||||
if (ticket) {
|
||||
t_row = await this.client.tickets.resolve(ticket, interaction.guild.id);
|
||||
if (!t_row) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.unresolvable.title'))
|
||||
.setDescription(i18n('commands.close.response.unresolvable.description', ticket))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } });
|
||||
if (!t_row) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.not_a_ticket.title'))
|
||||
.setDescription(i18n('commands.close.response.not_a_ticket.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (t_row.creator !== interaction.member.id && !await this.client.utils.isStaff(interaction.member)) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.no_permission.title'))
|
||||
.setDescription(i18n('commands.close.response.no_permission.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
components: [
|
||||
new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(`confirm_close:${interaction.id}`)
|
||||
.setLabel(i18n('commands.close.response.confirm.buttons.confirm'))
|
||||
.setEmoji('✅')
|
||||
.setStyle('SUCCESS')
|
||||
)
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(`cancel_close:${interaction.id}`)
|
||||
.setLabel(i18n('commands.close.response.confirm.buttons.cancel'))
|
||||
.setEmoji('❌')
|
||||
.setStyle('SECONDARY')
|
||||
)
|
||||
],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.close.response.confirm.title'))
|
||||
.setDescription(settings.log_messages ? i18n('commands.close.response.confirm.description_with_archive') : i18n('commands.close.response.confirm.description'))
|
||||
.setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
|
||||
const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id);
|
||||
const collector = interaction.channel.createMessageComponentCollector({
|
||||
filter,
|
||||
time: 30000
|
||||
});
|
||||
|
||||
collector.on('collect', async i => {
|
||||
await i.deferUpdate();
|
||||
|
||||
if (i.customId === `confirm_close:${interaction.id}`) {
|
||||
await this.client.tickets.close(t_row.id, interaction.user.id, interaction.guild.id, reason);
|
||||
await i.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n('commands.close.response.closed.title', t_row.number))
|
||||
.setDescription(i18n('commands.close.response.closed.description', t_row.number))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
} else {
|
||||
await i.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.close.response.canceled.title'))
|
||||
.setDescription(i18n('commands.close.response.canceled.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
collector.stop();
|
||||
});
|
||||
|
||||
collector.on('end', async collected => {
|
||||
if (collected.size === 0) {
|
||||
await interaction.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.close.response.confirmation_timeout.title'))
|
||||
.setDescription(i18n('commands.close.response.confirmation_timeout.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,67 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{survey}} Survey Responses | Discord Tickets</title>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
||||
|
||||
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css'>
|
||||
<link rel='stylesheet' href='https://jenil.github.io/bulmaswatch/darkly/bulmaswatch.min.css'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class='section'>
|
||||
<container class='container box has-text-centered'>
|
||||
<div class='content'>
|
||||
<h1>{{survey}} survey responses</h1>
|
||||
</div>
|
||||
|
||||
<div class='level'>
|
||||
<div class='level-item has-text-centered'>
|
||||
<div class='box'>
|
||||
<p class='title'>{{count.responses}}</p>
|
||||
<p class='heading'>Responses</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='level-item has-text-centered'>
|
||||
<div class='box'>
|
||||
<p class='title'>{{count.users}}</p>
|
||||
<p class='heading'>Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='table-container'>
|
||||
<table class='table is-bordered is-striped is-hoverable is-fullwidth'>
|
||||
<thead>
|
||||
<tr>
|
||||
{{#columns}}
|
||||
<th>{{.}}</th>
|
||||
{{/columns}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#responses}}
|
||||
<tr>
|
||||
{{#.}}
|
||||
<td>{{.}}</td>
|
||||
{{/.}}
|
||||
</tr>
|
||||
{{/responses}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
{{#columns}}
|
||||
<th>{{.}}</th>
|
||||
{{/columns}}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</container>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,49 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class HelpCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.help.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.help.name')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const is_staff = await this.client.utils.isStaff(interaction.member);
|
||||
const commands = this.manager.commands.filter(command => {
|
||||
if (command.permissions.length >= 1) return interaction.member.permissions.has(command.permissions);
|
||||
else if (command.staff_only) return is_staff;
|
||||
else return true;
|
||||
});
|
||||
const list = commands.map(command => {
|
||||
const description = command.description.length > 50
|
||||
? command.description.substring(0, 50) + '...'
|
||||
: command.description;
|
||||
return `**\`/${command.name}\` ·** ${description}`;
|
||||
});
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.help.response.list.title'))
|
||||
.setDescription(i18n('commands.help.response.list.description'))
|
||||
.addField(i18n('commands.help.response.list.fields.commands'), list.join('\n'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
23
src/commands/message/create.js
Normal file
23
src/commands/message/create.js
Normal file
@ -0,0 +1,23 @@
|
||||
const { MessageCommand } = require('@eartharoid/dbf');
|
||||
const { useGuild } = require('../../lib/tickets/utils');
|
||||
|
||||
module.exports = class CreateMessageCommand extends MessageCommand {
|
||||
constructor(client, options) {
|
||||
const nameLocalizations = {};
|
||||
client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.message.create.name')));
|
||||
|
||||
super(client, {
|
||||
...options,
|
||||
dmPermission: false,
|
||||
name: nameLocalizations['en-GB'],
|
||||
nameLocalizations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").MessageContextMenuCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
await useGuild(this.client, interaction, { referencesMessage: interaction.targetMessage.channelId + '/' + interaction.targetId });
|
||||
}
|
||||
};
|
75
src/commands/message/pin.js
Normal file
75
src/commands/message/pin.js
Normal file
@ -0,0 +1,75 @@
|
||||
const { MessageCommand } = require('@eartharoid/dbf');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
|
||||
module.exports = class PinMessageCommand extends MessageCommand {
|
||||
constructor(client, options) {
|
||||
const nameLocalizations = {};
|
||||
client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.message.pin.name')));
|
||||
|
||||
super(client, {
|
||||
...options,
|
||||
dmPermission: false,
|
||||
name: nameLocalizations['en-GB'],
|
||||
nameLocalizations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").MessageContextMenuCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
include: { guild: true },
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('commands.message.pin.not_ticket.title'))
|
||||
.setDescription(getMessage('commands.message.pin.not_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
|
||||
if (!interaction.targetMessage.pinnable) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('commands.message.pin.not_pinnable.title'))
|
||||
.setDescription(getMessage('commands.message.pin.not_pinnable.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.targetMessage.pin();
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.successColour)
|
||||
.setTitle(getMessage('commands.message.pin.pinned.title'))
|
||||
.setDescription(getMessage('commands.message.pin.pinned.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
@ -1,190 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars,
|
||||
MessageActionRow,
|
||||
MessageEmbed,
|
||||
MessageSelectMenu
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class NewCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.new.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.new.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.new.options.topic.description'),
|
||||
name: i18n('commands.new.options.topic.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const topic = interaction.options.getString(default_i18n('commands.new.options.topic.name'));
|
||||
|
||||
const create = async (cat_row, i) => {
|
||||
const tickets = await this.client.db.models.Ticket.findAndCountAll({
|
||||
where: {
|
||||
category: cat_row.id,
|
||||
creator: interaction.user.id,
|
||||
open: true
|
||||
}
|
||||
});
|
||||
|
||||
if (tickets.count >= cat_row.max_per_member) {
|
||||
if (cat_row.max_per_member === 1) {
|
||||
const response = {
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.has_a_ticket.title'))
|
||||
.setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
};
|
||||
await i ? i.editReply(response) : interaction.reply(response);
|
||||
} else {
|
||||
const list = tickets.rows.map(row => {
|
||||
if (row.topic) {
|
||||
const description = row.topic.substring(0, 30);
|
||||
const ellipses = row.topic.length > 30 ? '...' : '';
|
||||
return `<#${row.id}>: \`${description}${ellipses}\``;
|
||||
} else {
|
||||
return `<#${row.id}>`;
|
||||
}
|
||||
});
|
||||
const response = {
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.max_tickets.title', tickets.count))
|
||||
.setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n')))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
};
|
||||
await i ? i.editReply(response) : interaction.reply(response);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const t_row = await this.client.tickets.create(interaction.guild.id, interaction.user.id, cat_row.id, topic);
|
||||
const response = {
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.created.title'))
|
||||
.setDescription(i18n('commands.new.response.created.description', `<#${t_row.id}>`))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
};
|
||||
await i ? i.editReply(response) : interaction.reply(response);
|
||||
} catch (error) {
|
||||
const response = {
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.error.title'))
|
||||
.setDescription(error.message)
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
};
|
||||
await i ? i.editReply(response) : interaction.reply(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const categories = await this.client.db.models.Category.findAndCountAll({ where: { guild: interaction.guild.id } });
|
||||
|
||||
if (categories.count === 0) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.no_categories.title'))
|
||||
.setDescription(i18n('commands.new.response.no_categories.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
]
|
||||
});
|
||||
} else if (categories.count === 1) {
|
||||
create(categories.rows[0]); // skip the category selection
|
||||
} else {
|
||||
await interaction.reply({
|
||||
components: [
|
||||
new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageSelectMenu()
|
||||
.setCustomId(`select_category:${interaction.id}`)
|
||||
.setPlaceholder('Select a category')
|
||||
.addOptions(categories.rows.map(row => ({
|
||||
label: row.name,
|
||||
value: row.id
|
||||
})))
|
||||
)
|
||||
],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.select_category.title'))
|
||||
.setDescription(i18n('commands.new.response.select_category.description'))
|
||||
.setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id);
|
||||
const collector = interaction.channel.createMessageComponentCollector({
|
||||
filter,
|
||||
time: 30000
|
||||
});
|
||||
|
||||
collector.on('collect', async i => {
|
||||
await i.deferUpdate();
|
||||
create(categories.rows.find(row => row.id === i.values[0]), i);
|
||||
collector.stop();
|
||||
});
|
||||
|
||||
collector.on('end', async collected => {
|
||||
if (collected.size === 0) {
|
||||
await interaction.editReply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.new.response.select_category_timeout.title'))
|
||||
.setDescription(i18n('commands.new.response.select_category_timeout.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,210 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageActionRow,
|
||||
MessageButton,
|
||||
MessageEmbed,
|
||||
MessageSelectMenu
|
||||
} = require('discord.js');
|
||||
const { some } = require('../utils');
|
||||
|
||||
module.exports = class PanelCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.panel.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.panel.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.panel.options.categories.description'),
|
||||
multiple: true,
|
||||
name: i18n('commands.panel.options.categories.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.panel.options.description.description'),
|
||||
name: i18n('commands.panel.options.description.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.panel.options.image.description'),
|
||||
name: i18n('commands.panel.options.image.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.panel.options.just_type.description') + ' (false)',
|
||||
name: i18n('commands.panel.options.just_type.name'),
|
||||
required: false,
|
||||
type: Command.option_types.BOOLEAN
|
||||
},
|
||||
{
|
||||
description: i18n('commands.panel.options.title.description'),
|
||||
name: i18n('commands.panel.options.title.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.panel.options.thumbnail.description'),
|
||||
name: i18n('commands.panel.options.thumbnail.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
],
|
||||
staff_only: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const categories = interaction.options.getString(default_i18n('commands.panel.options.categories.name')).match(/\d{17,19}/g) ?? [];
|
||||
const description = interaction.options.getString(default_i18n('commands.panel.options.description.name'))?.replace(/\\n/g, '\n');
|
||||
const image = interaction.options.getString(default_i18n('commands.panel.options.image.name'));
|
||||
const just_type = interaction.options.getBoolean(default_i18n('commands.panel.options.just_type.name'));
|
||||
const title = interaction.options.getString(default_i18n('commands.panel.options.title.name'));
|
||||
const thumbnail = interaction.options.getString(default_i18n('commands.panel.options.thumbnail.name'));
|
||||
|
||||
if (just_type && categories.length > 1) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.panel.response.too_many_categories.title'))
|
||||
.setDescription(i18n('commands.panel.response.too_many_categories.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const invalid_category = await some(categories, async id => {
|
||||
const cat_row = await this.client.db.models.Category.findOne({
|
||||
where: {
|
||||
guild: interaction.guild.id,
|
||||
id
|
||||
}
|
||||
});
|
||||
return !cat_row;
|
||||
});
|
||||
|
||||
if (invalid_category) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.panel.response.invalid_category.title'))
|
||||
.setDescription(i18n('commands.panel.response.invalid_category.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
let panel_channel;
|
||||
|
||||
const embed = new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setFooter(settings.footer, interaction.guild.iconURL());
|
||||
|
||||
if (description) embed.setDescription(description);
|
||||
if (image) embed.setImage(image);
|
||||
if (title) embed.setTitle(title);
|
||||
if (thumbnail) embed.setThumbnail(thumbnail);
|
||||
|
||||
if (just_type) {
|
||||
panel_channel = await interaction.guild.channels.create('create-a-ticket', {
|
||||
permissionOverwrites: [
|
||||
{
|
||||
allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY'],
|
||||
deny: ['ATTACH_FILES', 'EMBED_LINKS', 'ADD_REACTIONS'],
|
||||
id: interaction.guild.roles.everyone
|
||||
},
|
||||
{
|
||||
allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'],
|
||||
id: this.client.user.id
|
||||
}
|
||||
],
|
||||
position: 1,
|
||||
rateLimitPerUser: 30,
|
||||
reason: `${interaction.user.tag} created a new message panel`,
|
||||
type: 'GUILD_TEXT'
|
||||
});
|
||||
await panel_channel.send({ embeds: [embed] });
|
||||
this.client.log.info(`${interaction.user.tag} has created a new message panel`);
|
||||
} else {
|
||||
panel_channel = await interaction.guild.channels.create('create-a-ticket', {
|
||||
permissionOverwrites: [
|
||||
{
|
||||
allow: ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'],
|
||||
deny: ['SEND_MESSAGES', 'ADD_REACTIONS'],
|
||||
id: interaction.guild.roles.everyone
|
||||
},
|
||||
{
|
||||
allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'],
|
||||
id: this.client.user.id
|
||||
}
|
||||
],
|
||||
position: 1,
|
||||
reason: `${interaction.user.tag} created a new panel`,
|
||||
type: 'GUILD_TEXT'
|
||||
});
|
||||
|
||||
if (categories.length === 1) {
|
||||
// single category
|
||||
await panel_channel.send({
|
||||
components: [
|
||||
new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(`panel.single:${categories[0]}`)
|
||||
.setLabel(i18n('panel.create_ticket'))
|
||||
.setStyle('PRIMARY')
|
||||
)
|
||||
],
|
||||
embeds: [embed]
|
||||
});
|
||||
this.client.log.info(`${interaction.user.tag} has created a new button panel`);
|
||||
} else {
|
||||
// multi category
|
||||
const rows = (await this.client.db.models.Category.findAll({ where: { guild: interaction.guild.id } })).filter(row => categories.includes(row.id));
|
||||
await panel_channel.send({
|
||||
components: [
|
||||
new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageSelectMenu()
|
||||
.setCustomId(`panel.multiple:${panel_channel.id}`)
|
||||
.setPlaceholder('Select a category')
|
||||
.addOptions(rows.map(row => ({
|
||||
label: row.name,
|
||||
value: row.id
|
||||
})))
|
||||
)
|
||||
],
|
||||
embeds: [embed]
|
||||
});
|
||||
this.client.log.info(`${interaction.user.tag} has created a new select panel`);
|
||||
}
|
||||
}
|
||||
|
||||
interaction.reply({
|
||||
content: `✅ ${panel_channel}`,
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
await this.client.db.models.Panel.create({
|
||||
category: categories.length === 1 ? categories[0] : null,
|
||||
channel: panel_channel.id,
|
||||
guild: interaction.guild.id
|
||||
});
|
||||
}
|
||||
};
|
@ -1,111 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class RemoveCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.remove.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.remove.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.remove.options.member.description'),
|
||||
name: i18n('commands.remove.options.member.name'),
|
||||
required: true,
|
||||
type: Command.option_types.USER
|
||||
},
|
||||
{
|
||||
description: i18n('commands.remove.options.ticket.description'),
|
||||
name: i18n('commands.remove.options.ticket.name'),
|
||||
required: false,
|
||||
type: Command.option_types.CHANNEL
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const channel = interaction.options.getChannel(default_i18n('commands.remove.options.channel.name')) ?? interaction.channel;
|
||||
const t_row = await this.client.tickets.resolve(channel.id, interaction.guild.id);
|
||||
|
||||
if (!t_row) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.remove.response.not_a_channel.title'))
|
||||
.setDescription(i18n('commands.remove.response.not_a_channel.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const member = interaction.options.getMember(default_i18n('commands.remove.options.member.name'));
|
||||
|
||||
if (!member) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.remove.response.no_member.title'))
|
||||
.setDescription(i18n('commands.remove.response.no_member.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (t_row.creator !== interaction.user.id && !await this.client.utils.isStaff(interaction.member)) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.remove.response.no_permission.title'))
|
||||
.setDescription(i18n('commands.remove.response.no_permission.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setAuthor(member.user.username, member.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.remove.response.removed.title'))
|
||||
.setDescription(i18n('commands.remove.response.removed.description', member.toString(), channel.toString()))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
await channel.send({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setAuthor(member.user.username, member.user.displayAvatarURL())
|
||||
.setTitle(i18n('ticket.member_removed.title'))
|
||||
.setDescription(i18n('ticket.member_removed.description', member.toString(), interaction.user.toString()))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
]
|
||||
});
|
||||
|
||||
await channel.permissionOverwrites.delete(member.user.id, `${interaction.user.tag} removed ${member.user.tag} from the ticket`);
|
||||
|
||||
this.client.log.info(`${interaction.user.tag} removed ${member.user.tag} from ${channel.id}`);
|
||||
}
|
||||
};
|
@ -1,358 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class SettingsCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.settings.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.settings.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.description'),
|
||||
name: i18n('commands.settings.options.categories.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.create.description'),
|
||||
name: i18n('commands.settings.options.categories.options.create.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.create.options.name.description'),
|
||||
name: i18n('commands.settings.options.categories.options.create.options.name.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.create.options.roles.description'),
|
||||
name: i18n('commands.settings.options.categories.options.create.options.roles.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.delete.description'),
|
||||
name: i18n('commands.settings.options.categories.options.delete.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.delete.options.id.description'),
|
||||
name: i18n('commands.settings.options.categories.options.delete.options.id.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.id.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.id.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.claiming.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.claiming.name'),
|
||||
required: false,
|
||||
type: Command.option_types.BOOLEAN
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.image.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.image.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.max_per_member.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.max_per_member.name'),
|
||||
required: false,
|
||||
type: Command.option_types.INTEGER
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.name.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.name.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.name_format.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.name_format.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.opening_message.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.opening_message.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.opening_questions.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.opening_questions.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.ping.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.ping.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.require_topic.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.require_topic.name'),
|
||||
required: false,
|
||||
type: Command.option_types.BOOLEAN
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.roles.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.roles.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.edit.options.survey.description'),
|
||||
name: i18n('commands.settings.options.categories.options.edit.options.survey.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.categories.options.list.description'),
|
||||
name: i18n('commands.settings.options.categories.options.list.name'),
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND_GROUP
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.description'),
|
||||
name: i18n('commands.settings.options.set.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.close_button.description'),
|
||||
name: i18n('commands.settings.options.set.options.close_button.name'),
|
||||
required: false,
|
||||
type: Command.option_types.BOOLEAN
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.colour.description'),
|
||||
name: i18n('commands.settings.options.set.options.colour.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.error_colour.description'),
|
||||
name: i18n('commands.settings.options.set.options.error_colour.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.footer.description'),
|
||||
name: i18n('commands.settings.options.set.options.footer.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.locale.description'),
|
||||
name: i18n('commands.settings.options.set.options.locale.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.log_messages.description'),
|
||||
name: i18n('commands.settings.options.set.options.log_messages.name'),
|
||||
required: false,
|
||||
type: Command.option_types.BOOLEAN
|
||||
},
|
||||
{
|
||||
description: i18n('commands.settings.options.set.options.success_colour.description'),
|
||||
name: i18n('commands.settings.options.set.options.success_colour.name'),
|
||||
required: false,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
],
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
}
|
||||
],
|
||||
permissions: ['MANAGE_GUILD']
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case default_i18n('commands.settings.options.categories.options.create.name'): {
|
||||
const name = interaction.options.getString(default_i18n('commands.settings.options.categories.options.create.options.name.name'));
|
||||
const roles = interaction.options.getString(default_i18n('commands.settings.options.categories.options.create.options.roles.name'))?.match(/\d{17,19}/g) ?? [];
|
||||
const allowed_permissions = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES'];
|
||||
const cat_channel = await interaction.guild.channels.create(name, {
|
||||
permissionOverwrites: [
|
||||
...[
|
||||
{
|
||||
deny: ['VIEW_CHANNEL'],
|
||||
id: interaction.guild.roles.everyone
|
||||
},
|
||||
{
|
||||
allow: allowed_permissions,
|
||||
id: this.client.user.id
|
||||
}
|
||||
],
|
||||
...roles.map(r => ({
|
||||
allow: allowed_permissions,
|
||||
id: r
|
||||
}))
|
||||
],
|
||||
position: 1,
|
||||
reason: `Tickets category created by ${interaction.user.tag}`,
|
||||
type: 'GUILD_CATEGORY'
|
||||
});
|
||||
await this.client.db.models.Category.create({
|
||||
guild: interaction.guild.id,
|
||||
id: cat_channel.id,
|
||||
name,
|
||||
roles
|
||||
});
|
||||
await this.client.commands.updatePermissions(interaction.guild);
|
||||
interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n('commands.settings.response.category_created', name))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
break;
|
||||
}
|
||||
case default_i18n('commands.settings.options.categories.options.delete.name'): {
|
||||
const category = await this.client.db.models.Category.findOne({ where: { id: interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')) } });
|
||||
if (category) {
|
||||
const channel = this.client.channels.cache.get(interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')));
|
||||
if (channel) channel.delete();
|
||||
await category.destroy();
|
||||
interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n('commands.settings.response.category_deleted', category.name))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
} else {
|
||||
interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.settings.response.category_does_not_exist'))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case default_i18n('commands.settings.options.categories.options.edit.name'): {
|
||||
const category = await this.client.db.models.Category.findOne({ where: { id: interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')) } });
|
||||
if (!category) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.settings.response.category_does_not_exist'))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
const claiming = interaction.options.getBoolean(default_i18n('commands.settings.options.categories.options.edit.options.claiming.name'));
|
||||
const image = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.image.name'));
|
||||
const max_per_member = interaction.options.getInteger(default_i18n('commands.settings.options.categories.options.edit.options.max_per_member.name'));
|
||||
const name = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.name.name'));
|
||||
const name_format = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.name_format.name'));
|
||||
const opening_message = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.opening_message.name'));
|
||||
const opening_questions = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.opening_questions.name'));
|
||||
const ping = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.ping.name'));
|
||||
const require_topic = interaction.options.getBoolean(default_i18n('commands.settings.options.categories.options.edit.options.require_topic.name'));
|
||||
const roles = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.roles.name'));
|
||||
const survey = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.survey.name'));
|
||||
if (claiming !== null) category.set('claiming', claiming);
|
||||
if (max_per_member !== null) category.set('max_per_member', max_per_member);
|
||||
if (image !== null) category.set('image', image);
|
||||
if (name !== null) category.set('name', name);
|
||||
if (name_format !== null) category.set('name_format', name_format);
|
||||
if (opening_message !== null) category.set('opening_message', opening_message.replace(/\\n/g, '\n'));
|
||||
if (opening_questions !== null) category.set('opening_questions', JSON.parse(opening_questions));
|
||||
if (ping !== null) category.set('ping', ping.match(/\d{17,19}/g) ?? []);
|
||||
if (require_topic !== null) category.set('require_topic', require_topic);
|
||||
if (roles !== null) category.set('roles', roles.match(/\d{17,19}/g) ?? []);
|
||||
if (survey !== null) category.set('survey', survey);
|
||||
await category.save();
|
||||
interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n('commands.settings.response.category_updated', category.name))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
break;
|
||||
}
|
||||
case default_i18n('commands.settings.options.categories.options.list.name'): {
|
||||
const categories = await this.client.db.models.Category.findAll({ where: { guild: interaction.guild.id } });
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.settings.response.category_list'))
|
||||
.setDescription(categories.map(c => `- ${c.name} (\`${c.id}\`)`).join('\n'))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
break;
|
||||
}
|
||||
case default_i18n('commands.settings.options.set.name'): {
|
||||
const close_button = interaction.options.getBoolean(default_i18n('commands.settings.options.set.options.close_button.name'));
|
||||
const colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.colour.name'));
|
||||
const error_colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.error_colour.name'));
|
||||
const footer = interaction.options.getString(default_i18n('commands.settings.options.set.options.footer.name'));
|
||||
const locale = interaction.options.getString(default_i18n('commands.settings.options.set.options.locale.name'));
|
||||
const log_messages = interaction.options.getBoolean(default_i18n('commands.settings.options.set.options.log_messages.name'));
|
||||
const success_colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.success_colour.name'));
|
||||
if (close_button !== null) settings.set('close_button', close_button);
|
||||
if (colour !== null) settings.set('colour', colour.toUpperCase());
|
||||
if (error_colour !== null) settings.set('error_colour', error_colour.toUpperCase());
|
||||
if (footer !== null) settings.set('footer', footer);
|
||||
if (locale !== null) settings.set('locale', locale);
|
||||
if (log_messages !== null) settings.set('log_messages', log_messages);
|
||||
if (success_colour !== null) settings.set('success_colour', success_colour.toUpperCase());
|
||||
await settings.save();
|
||||
interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setTitle(i18n('commands.settings.response.settings_updated'))
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
144
src/commands/slash/add.js
Normal file
144
src/commands/slash/add.js
Normal file
@ -0,0 +1,144 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
const { logTicketEvent } = require('../../lib/logging');
|
||||
|
||||
module.exports = class AddSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'add';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
name: 'member',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.User,
|
||||
},
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'ticket',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
include: { guild: true },
|
||||
where: { id: interaction.options.getString('ticket', false) || interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.invalid_ticket.title'))
|
||||
.setDescription(getMessage('misc.invalid_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
|
||||
if (
|
||||
ticket.id !== interaction.channel.id &&
|
||||
ticket.createdById !== interaction.member.id &&
|
||||
!(await isStaff(interaction.guild, interaction.member.id))
|
||||
) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('commands.slash.add.not_staff.title'))
|
||||
.setDescription(getMessage('commands.slash.add.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import("discord.js").TextChannel} */
|
||||
const ticketChannel = await interaction.guild.channels.fetch(ticket.id);
|
||||
const member = interaction.options.getMember('member', true);
|
||||
|
||||
await ticketChannel.permissionOverwrites.edit(
|
||||
member,
|
||||
{
|
||||
AttachFiles: true,
|
||||
EmbedLinks: true,
|
||||
ReadMessageHistory: true,
|
||||
SendMessages: true,
|
||||
ViewChannel: true,
|
||||
},
|
||||
`${interaction.user.tag} added ${member.user.tag} to the ticket`,
|
||||
);
|
||||
|
||||
await ticketChannel.send({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(ticket.guild.primaryColour)
|
||||
.setDescription(getMessage('commands.slash.add.added', {
|
||||
added: member.toString(),
|
||||
by: interaction.member.toString(),
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.successColour)
|
||||
.setTitle(getMessage('commands.slash.add.success.title'))
|
||||
.setDescription(getMessage('commands.slash.add.success.description', {
|
||||
member: member.toString(),
|
||||
ticket: ticketChannel.toString(),
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
logTicketEvent(this.client, {
|
||||
action: 'update',
|
||||
diff: {
|
||||
original: {},
|
||||
updated: { [getMessage('log.ticket.added')]: member.user.tag },
|
||||
},
|
||||
target: {
|
||||
id: ticket.id,
|
||||
name: `<#${ticket.id}>`,
|
||||
},
|
||||
userId: interaction.user.id,
|
||||
});
|
||||
|
||||
}
|
||||
};
|
26
src/commands/slash/claim.js
Normal file
26
src/commands/slash/claim.js
Normal file
@ -0,0 +1,26 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class ClaimSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'claim';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
await client.tickets.claim(interaction);
|
||||
}
|
||||
};
|
37
src/commands/slash/close.js
Normal file
37
src/commands/slash/close.js
Normal file
@ -0,0 +1,37 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
|
||||
module.exports = class CloseSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'close';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
name: 'reason',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
await client.tickets.beforeRequestClose(interaction);
|
||||
}
|
||||
};
|
286
src/commands/slash/force-close.js
Normal file
286
src/commands/slash/force-close.js
Normal file
@ -0,0 +1,286 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const {
|
||||
ApplicationCommandOptionType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
} = require('discord.js');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
const ms = require('ms');
|
||||
|
||||
module.exports = class ForceCloseSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'force-close';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'category',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'ticket',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
let ticket;
|
||||
|
||||
if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('commands.slash.force-close.not_staff.title'))
|
||||
.setDescription(getMessage('commands.slash.force-close.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (interaction.options.getString('ticket', false)) { // if ticket option is passed
|
||||
ticket = await client.prisma.ticket.findUnique({
|
||||
include: { category: true },
|
||||
where: { id: interaction.options.getString('ticket') },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.invalid_ticket.title'))
|
||||
.setDescription(getMessage('misc.invalid_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.successColour)
|
||||
.setTitle(getMessage('commands.slash.force-close.closed_one.title'))
|
||||
.setDescription(getMessage('commands.slash.force-close.closed_one.description', { ticket: ticket.id })),
|
||||
],
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
await client.tickets.finallyClose(ticket.id, {
|
||||
closedBy: interaction.user.id,
|
||||
reason: interaction.options.getString('reason', false),
|
||||
});
|
||||
}, ms('3s'));
|
||||
|
||||
} else if (interaction.options.getString('time', false)) { // if time option is passed
|
||||
const time = ms(interaction.options.getString('time', false));
|
||||
|
||||
if (!time) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('commands.slash.close.invalid_time.title'))
|
||||
.setDescription(getMessage('commands.slash.close.invalid_time.description', { input: interaction.options.getString('time', false) })),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const categoryId = interaction.options.getInteger('category', false);
|
||||
const tickets = await client.prisma.ticket.findMany({
|
||||
where: {
|
||||
categoryId: categoryId ?? undefined, // must be undefined not null
|
||||
lastMessageAt: { lte: new Date(Date.now() - time) },
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (tickets.length === 0) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('commands.slash.force-close.no_tickets.title'))
|
||||
.setDescription(getMessage('commands.slash.force-close.no_tickets.description', { time: ms(time, { long: true }) })),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const collectorTime = ms('15s');
|
||||
const confirmationM = await interaction.editReply({
|
||||
components: [
|
||||
new ActionRowBuilder()
|
||||
.addComponents([
|
||||
new ButtonBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'custom',
|
||||
id: 'close',
|
||||
}))
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setEmoji(getMessage('buttons.close.emoji'))
|
||||
.setLabel(getMessage('buttons.close.text')),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'custom',
|
||||
id: 'cancel',
|
||||
}))
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji(getMessage('buttons.cancel.emoji'))
|
||||
.setLabel(getMessage('buttons.cancel.text')),
|
||||
]),
|
||||
],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: getMessage('misc.expires_in', { time: ms(collectorTime, { long: true }) }),
|
||||
})
|
||||
.setColor(settings.primaryColour)
|
||||
.setTitle(getMessage('commands.slash.force-close.confirm_multiple.title'))
|
||||
.setDescription(getMessage('commands.slash.force-close.confirm_multiple.description', {
|
||||
count: tickets.length,
|
||||
tickets: tickets.map(t => `> <#${t.id}>`).join('\n'),
|
||||
time: ms(time, { long: true }),
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
confirmationM.awaitMessageComponent({
|
||||
componentType: ComponentType.Button,
|
||||
filter: i => i.user.id === interaction.user.id,
|
||||
time: collectorTime,
|
||||
})
|
||||
.then(async i => {
|
||||
if (JSON.parse(i.customId).id === 'close') {
|
||||
await i.reply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.successColour)
|
||||
.setTitle(getMessage('commands.slash.force-close.confirmed_multiple.title', tickets.length, tickets.length))
|
||||
.setDescription(getMessage('commands.slash.force-close.confirmed_multiple.description')),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
setTimeout(async () => {
|
||||
for (const ticket of tickets) {
|
||||
await client.tickets.finallyClose(ticket.id, {
|
||||
closedBy: interaction.user.id,
|
||||
reason: interaction.options.getString('reason', false),
|
||||
});
|
||||
}
|
||||
}, ms('3s'));
|
||||
} else {
|
||||
await interaction.deleteReply();
|
||||
}
|
||||
})
|
||||
.catch(async error => {
|
||||
client.log.error(error);
|
||||
await interaction.reply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.expired.title'))
|
||||
.setDescription(getMessage('misc.expired.description', { time: ms(time, { long: true }) })),
|
||||
],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ticket = await client.prisma.ticket.findUnique({
|
||||
include: { category: true },
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.not_ticket.title'))
|
||||
.setDescription(getMessage('misc.not_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.successColour)
|
||||
.setTitle(getMessage('commands.slash.force-close.closed_one.title'))
|
||||
.setDescription(getMessage('commands.slash.force-close.closed_one.description', { ticket: ticket.id })),
|
||||
],
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
await client.tickets.finallyClose(ticket.id, {
|
||||
closedBy: interaction.user.id,
|
||||
reason: interaction.options.getString('reason', false),
|
||||
});
|
||||
}, ms('3s'));
|
||||
}
|
||||
}
|
||||
};
|
79
src/commands/slash/help.js
Normal file
79
src/commands/slash/help.js
Normal file
@ -0,0 +1,79 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const { version } = require('../../../package.json');
|
||||
|
||||
module.exports = class ClaimSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'help';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const staff = await isStaff(interaction.guild, interaction.member.id);
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
const commands = client.application.commands.cache
|
||||
.filter(c => c.type === 1)
|
||||
.map(c => `> </${c.name}:${c.id}>: ${c.description}`)
|
||||
.join('\n');
|
||||
const newCommand = client.application.commands.cache.find(c => c.name === 'new');
|
||||
const fields = [
|
||||
{
|
||||
name: getMessage('commands.slash.help.response.commands'),
|
||||
value: commands,
|
||||
},
|
||||
];
|
||||
|
||||
if (staff) {
|
||||
fields.unshift(
|
||||
{
|
||||
inline: true,
|
||||
name: getMessage('commands.slash.help.response.links.links'),
|
||||
value: [
|
||||
['commands', 'https://discordtickets.app/features/commands'],
|
||||
['docs', 'https://discordtickets.app'],
|
||||
['feedback', 'https://lnk.earth/dsctickets-feedback'],
|
||||
['support', 'https://lnk.earth/discord'],
|
||||
]
|
||||
.map(([l, url]) => `> [${getMessage('commands.slash.help.response.links.' + l)}](${url})`)
|
||||
.join('\n'),
|
||||
},
|
||||
{
|
||||
inline: true,
|
||||
name: getMessage('commands.slash.help.response.settings'),
|
||||
value: '> ' + process.env.HTTP_EXTERNAL + '/settings',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.primaryColour)
|
||||
.setTitle(getMessage('commands.slash.help.title'))
|
||||
.setDescription(staff
|
||||
? `**Discord Tickets v${version} by eartharoid.**`
|
||||
: getMessage('commands.slash.help.response.description', { command: `</${newCommand.name}:${newCommand.id}>` }))
|
||||
.setFields(fields),
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
170
src/commands/slash/move.js
Normal file
170
src/commands/slash/move.js
Normal file
@ -0,0 +1,170 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
|
||||
module.exports = class MoveSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'move';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'category',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
include: {
|
||||
category: true,
|
||||
guild: true,
|
||||
},
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
const { locale } = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(locale);
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('misc.not_ticket.title'))
|
||||
.setDescription(getMessage('misc.not_ticket.description')),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
|
||||
if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('commands.slash.move.not_staff.title'))
|
||||
.setDescription(getMessage('commands.slash.move.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const creator = await interaction.guild.members.fetch(ticket.createdById);
|
||||
const newCategory = await client.prisma.category.findUnique({ where: { id: interaction.options.getInteger('category', true) } });
|
||||
const discordCategory = await interaction.guild.channels.fetch(newCategory.discordCategory);
|
||||
|
||||
if (discordCategory.children.cache.size === 50) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('misc.category_full.title'))
|
||||
.setDescription(getMessage('misc.category_full.description')),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
} else {
|
||||
// don't reassign `ticket`, the previous value is used below
|
||||
await client.prisma.ticket.update({
|
||||
data: { category: { connect: { id: newCategory.id } } },
|
||||
where: { id: ticket.id },
|
||||
});
|
||||
|
||||
const $oldCategory = client.tickets.$count.categories[ticket.categoryId];
|
||||
const $newCategory = client.tickets.$count.categories[newCategory.id];
|
||||
|
||||
$oldCategory.total--;
|
||||
$oldCategory[ticket.createdById]--;
|
||||
|
||||
$newCategory.total ||= 0;
|
||||
$newCategory.total++;
|
||||
|
||||
$newCategory[ticket.createdById] ||= 0;
|
||||
$newCategory[ticket.createdById]++;
|
||||
|
||||
// these 3 could be done separately,
|
||||
// but using `setParent`, `setName` etc instead of a single `edit` call increases the number of API requests
|
||||
if (
|
||||
newCategory.staffRoles !== ticket.category.staffRoles ||
|
||||
newCategory.channelName !== ticket.category.channelName ||
|
||||
newCategory.discordCategory !== ticket.category.discordCategory
|
||||
) {
|
||||
const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles'];
|
||||
const channelName = newCategory.channelName
|
||||
.replace(/{+\s?(user)?name\s?}+/gi, creator.user.username)
|
||||
.replace(/{+\s?(nick|display)(name)?\s?}+/gi, creator.displayName)
|
||||
.replace(/{+\s?num(ber)?\s?}+/gi, ticket.number === 1488 ? '1487b' : ticket.number);
|
||||
await interaction.channel.edit({
|
||||
lockPermissions: false,
|
||||
name: channelName,
|
||||
parent: discordCategory,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
deny: ['ViewChannel'],
|
||||
id: interaction.guild.roles.everyone,
|
||||
},
|
||||
{
|
||||
allow,
|
||||
id: this.client.user.id,
|
||||
},
|
||||
{
|
||||
allow,
|
||||
id: creator.id,
|
||||
},
|
||||
...newCategory.staffRoles.map(id => ({
|
||||
allow,
|
||||
id,
|
||||
})),
|
||||
],
|
||||
reason: `Moved by ${interaction.user.tag}`,
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(ticket.guild.primaryColour)
|
||||
.setDescription(getMessage('commands.slash.move.moved', {
|
||||
by: interaction.user.toString(),
|
||||
from: ticket.category.name,
|
||||
to: newCategory.name,
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
};
|
38
src/commands/slash/new.js
Normal file
38
src/commands/slash/new.js
Normal file
@ -0,0 +1,38 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const { useGuild } = require('../../lib/tickets/utils');
|
||||
|
||||
module.exports = class NewSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'new';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'references',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
await useGuild(this.client, interaction, { referencesTicketId: interaction.options.getString('references', false) });
|
||||
}
|
||||
};
|
142
src/commands/slash/priority.js
Normal file
142
src/commands/slash/priority.js
Normal file
@ -0,0 +1,142 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const { logTicketEvent } = require('../../lib/logging');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
|
||||
module.exports = class PrioritySlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'priority';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
choices: ['HIGH', 'MEDIUM', 'LOW'],
|
||||
name: 'priority',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
if (option.choices) {
|
||||
option.choices = option.choices.map(choice => ({
|
||||
name: client.i18n.getMessage(null, `commands.slash.priority.options.${option.name}.choices.${choice}`),
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.priority.options.${option.name}.choices.${choice}`),
|
||||
value: choice,
|
||||
}));
|
||||
}
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
getEmoji(priority) {
|
||||
let emoji;
|
||||
switch (priority) {
|
||||
case 'HIGH': {
|
||||
emoji = '🔴';
|
||||
break;
|
||||
}
|
||||
case 'MEDIUM': {
|
||||
emoji = '🟠';
|
||||
break;
|
||||
}
|
||||
case 'LOW': {
|
||||
emoji = '🟢';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return emoji;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
include: { category: { select: { channelName: true } } },
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.not_ticket.title'))
|
||||
.setDescription(getMessage('misc.not_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('commands.slash.move.not_staff.title'))
|
||||
.setDescription(getMessage('commands.slash.move.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const priority = interaction.options.getString('priority', true);
|
||||
let name = interaction.channel.name;
|
||||
if (ticket.priority) name = name.replace(this.getEmoji(ticket.priority), this.getEmoji(priority));
|
||||
else name = this.getEmoji(priority) + name;
|
||||
await interaction.channel.setName(name);
|
||||
|
||||
// don't reassign ticket because the original is used below
|
||||
await client.prisma.ticket.update({
|
||||
data: { priority },
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
logTicketEvent(this.client, {
|
||||
action: 'update',
|
||||
diff: {
|
||||
original: { priority: ticket.priority },
|
||||
updated: { priority: priority },
|
||||
},
|
||||
target: {
|
||||
id: ticket.id,
|
||||
name: `<#${ticket.id}>`,
|
||||
},
|
||||
userId: interaction.user.id,
|
||||
});
|
||||
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.successColour)
|
||||
.setTitle(getMessage('commands.slash.priority.success.title'))
|
||||
.setDescription(getMessage('commands.slash.priority.success.description', { priority: getMessage(`commands.slash.priority.options.priority.choices.${priority}`) })),
|
||||
],
|
||||
});
|
||||
|
||||
}
|
||||
};
|
26
src/commands/slash/release.js
Normal file
26
src/commands/slash/release.js
Normal file
@ -0,0 +1,26 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
|
||||
module.exports = class ReleaseSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'release';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
await client.tickets.release(interaction);
|
||||
}
|
||||
};
|
143
src/commands/slash/remove.js
Normal file
143
src/commands/slash/remove.js
Normal file
@ -0,0 +1,143 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
const { logTicketEvent } = require('../../lib/logging');
|
||||
|
||||
module.exports = class RemoveSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'remove';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
name: 'member',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.User,
|
||||
},
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'ticket',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
include: { guild: true },
|
||||
where: { id: interaction.options.getString('ticket', false) || interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.invalid_ticket.title'))
|
||||
.setDescription(getMessage('misc.invalid_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
|
||||
if (
|
||||
ticket.id !== interaction.channel.id &&
|
||||
ticket.createdById !== interaction.member.id &&
|
||||
!(await isStaff(interaction.guild, interaction.member.id))
|
||||
) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle(getMessage('commands.slash.remove.not_staff.title'))
|
||||
.setDescription(getMessage('commands.slash.remove.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import("discord.js").TextChannel} */
|
||||
const ticketChannel = await interaction.guild.channels.fetch(ticket.id);
|
||||
const member = interaction.options.getMember('member', true);
|
||||
|
||||
if (member.id === client.user.id || member.id === ticket.createdById) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(ticket.guild.errorColour)
|
||||
.setTitle('❌'),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await ticketChannel.permissionOverwrites.delete(member, `${interaction.user.tag} removed ${member.user.tag} from the ticket`);
|
||||
|
||||
await ticketChannel.send({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(ticket.guild.primaryColour)
|
||||
.setDescription(getMessage('commands.slash.remove.removed', {
|
||||
by: interaction.member.toString(),
|
||||
removed: member.toString(),
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: ticket.guild.footer,
|
||||
})
|
||||
.setColor(ticket.guild.successColour)
|
||||
.setTitle(getMessage('commands.slash.remove.success.title'))
|
||||
.setDescription(getMessage('commands.slash.remove.success.description', {
|
||||
member: member.toString(),
|
||||
ticket: ticketChannel.toString(),
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
logTicketEvent(this.client, {
|
||||
action: 'update',
|
||||
diff: {
|
||||
original: { [getMessage('log.ticket.removed')]: member.user.tag },
|
||||
updated: {},
|
||||
},
|
||||
target: {
|
||||
id: ticket.id,
|
||||
name: `<#${ticket.id}>`,
|
||||
},
|
||||
userId: interaction.user.id,
|
||||
});
|
||||
}
|
||||
};
|
60
src/commands/slash/tag.js
Normal file
60
src/commands/slash/tag.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
|
||||
module.exports = class TagSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'tag';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'tag',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
},
|
||||
{
|
||||
name: 'for',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.User,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
const user = interaction.options.getUser('for', false);
|
||||
await interaction.deferReply({ ephemeral: !user });
|
||||
const tag = await client.prisma.tag.findUnique({
|
||||
include: { guild: true },
|
||||
where: { id: interaction.options.getInteger('tag', true) },
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
allowedMentions: { users: user ? [user.id]: [] },
|
||||
content: user?.toString(),
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(tag.guild.primaryColour)
|
||||
.setDescription(tag.content),
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
133
src/commands/slash/tickets.js
Normal file
133
src/commands/slash/tickets.js
Normal file
@ -0,0 +1,133 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const Cryptr = require('cryptr');
|
||||
const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
|
||||
|
||||
module.exports = class TicketsSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'tickets';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
name: 'member',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.User,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
await client.application.commands.fetch();
|
||||
|
||||
const member = interaction.options.getMember('member', false) ?? interaction.member;
|
||||
const ownOrOther = member.id === interaction.member.id ? 'own' : 'other';
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
|
||||
if (member.id !== interaction.member.id && !(await isStaff(interaction.guild, interaction.member.id))) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('commands.slash.tickets.not_staff.title'))
|
||||
.setDescription(getMessage('commands.slash.tickets.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const fields = [];
|
||||
|
||||
const open = await client.prisma.ticket.findMany({
|
||||
include: { category: true },
|
||||
where: {
|
||||
createdById: member.id,
|
||||
guildId: interaction.guild.id,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
const closed = await client.prisma.ticket.findMany({
|
||||
include: { category: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10, // max 10 rows
|
||||
where: {
|
||||
createdById: member.id,
|
||||
guildId: interaction.guild.id,
|
||||
open: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (open.length >= 1) {
|
||||
fields.push({
|
||||
name: getMessage('commands.slash.tickets.response.fields.open.name'),
|
||||
value: open.map(ticket =>{
|
||||
const topic = ticket.topic ? `- \`${decrypt(ticket.topic).replace(/\n/g, ' ').slice(0, 30) }\`` : '';
|
||||
return `> <#${ticket.id}> ${topic}`;
|
||||
}).join('\n'),
|
||||
});
|
||||
}
|
||||
|
||||
if (closed.length === 0) {
|
||||
const newCommand = client.application.commands.cache.find(c => c.name === 'new');
|
||||
fields.push({
|
||||
name: getMessage('commands.slash.tickets.response.fields.closed.name'),
|
||||
value: getMessage(`commands.slash.tickets.response.fields.closed.none.${ownOrOther}`, {
|
||||
new: `</${newCommand.name}:${newCommand.id}>`,
|
||||
user: member.user.toString(),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
fields.push({
|
||||
name: getMessage('commands.slash.tickets.response.fields.closed.name'),
|
||||
value: closed.map(ticket => {
|
||||
const topic = ticket.topic ? `- \`${decrypt(ticket.topic).replace(/\n/g, ' ').slice(0, 30)}\`` : '';
|
||||
return `> ${ticket.category.name} #${ticket.number} ${topic}`;
|
||||
}).join('\n'),
|
||||
});
|
||||
}
|
||||
// TODO: add portal URL to view all (this list is limited to the last 10)
|
||||
|
||||
const embed = new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.primaryColour)
|
||||
.setAuthor({
|
||||
iconURL: member.displayAvatarURL(),
|
||||
name: member.displayName,
|
||||
})
|
||||
.setTitle(getMessage(`commands.slash.tickets.response.title.${ownOrOther}`, { displayName: member.displayName }))
|
||||
.setFields(fields);
|
||||
|
||||
if (settings.archive && process.env.OVERRIDE_ARCHIVE !== 'false') {
|
||||
const transcriptCommand = client.application.commands.cache.find(c => c.name === 'transcript');
|
||||
embed.setDescription(getMessage('commands.slash.tickets.response.description', { transcript: `</${transcriptCommand.name}:${transcriptCommand.id}>` }));
|
||||
}
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
};
|
84
src/commands/slash/topic.js
Normal file
84
src/commands/slash/topic.js
Normal file
@ -0,0 +1,84 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const {
|
||||
ActionRowBuilder,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
} = require('discord.js');
|
||||
const Cryptr = require('cryptr');
|
||||
const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
|
||||
module.exports = class TopicSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'topic';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
select: {
|
||||
category: { select: { name: true } },
|
||||
guild: { select: { locale: true } },
|
||||
questionAnswers: { include: { question: true } },
|
||||
topic: true,
|
||||
},
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.not_ticket.title'))
|
||||
.setDescription(getMessage('misc.not_ticket.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const getMessage = client.i18n.getLocale(ticket.guild.locale);
|
||||
|
||||
const field = new TextInputBuilder()
|
||||
.setCustomId('topic')
|
||||
.setLabel(getMessage('modals.topic.label'))
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setMaxLength(1000)
|
||||
.setMinLength(5)
|
||||
.setPlaceholder(getMessage('modals.topic.placeholder'))
|
||||
.setRequired(true);
|
||||
|
||||
if (ticket.topic) field.setValue(decrypt(ticket.topic)); // why can't discord.js accept null or undefined :(
|
||||
|
||||
await interaction.showModal(
|
||||
new ModalBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'topic',
|
||||
edit: true,
|
||||
}))
|
||||
.setTitle(ticket.category.name)
|
||||
.setComponents(
|
||||
new ActionRowBuilder()
|
||||
.setComponents(field),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
151
src/commands/slash/transcript.js
Normal file
151
src/commands/slash/transcript.js
Normal file
@ -0,0 +1,151 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const { ApplicationCommandOptionType } = require('discord.js');
|
||||
const fs = require('fs');
|
||||
const { join } = require('path');
|
||||
const Mustache = require('mustache');
|
||||
const { AttachmentBuilder } = require('discord.js');
|
||||
const Cryptr = require('cryptr');
|
||||
const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
|
||||
|
||||
module.exports = class TranscriptSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'transcript';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
autocomplete: true,
|
||||
name: 'ticket',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.User,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
|
||||
Mustache.escape = text => text; // don't HTML-escape
|
||||
this.template = fs.readFileSync(
|
||||
join('./user/templates/', this.client.config.templates.transcript + '.mustache'),
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
}
|
||||
|
||||
async fillTemplate(ticketId) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
const ticket = await client.prisma.ticket.findUnique({
|
||||
include: {
|
||||
archivedChannels: true,
|
||||
archivedMessages: {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
where: { external: false },
|
||||
},
|
||||
archivedRoles: true,
|
||||
archivedUsers: true,
|
||||
category: true,
|
||||
claimedBy: true,
|
||||
closedBy: true,
|
||||
createdBy: true,
|
||||
feedback: true,
|
||||
guild: true,
|
||||
questionAnswers: true,
|
||||
},
|
||||
where: { id: ticketId },
|
||||
});
|
||||
if (!ticket) throw new Error(`Ticket ${ticketId} does not exist`);
|
||||
|
||||
ticket.claimedBy = ticket.archivedUsers.find(u => u.userId === ticket.claimedById);
|
||||
ticket.closedBy = ticket.archivedUsers.find(u => u.userId === ticket.closedById);
|
||||
ticket.createdBy = ticket.archivedUsers.find(u => u.userId === ticket.createdById);
|
||||
|
||||
if (ticket.closedReason) ticket.closedReason = decrypt(ticket.closedReason);
|
||||
if (ticket.feedback?.comment) ticket.feedback.comment = decrypt(ticket.feedback.comment);
|
||||
if (ticket.topic) ticket.topic = decrypt(ticket.topic).replace(/\n/g, '\n\t');
|
||||
|
||||
ticket.archivedUsers.forEach((user, i) => {
|
||||
if (user.displayName) user.displayName = decrypt(user.displayName);
|
||||
user.username = decrypt(user.username);
|
||||
ticket.archivedUsers[i] = user;
|
||||
});
|
||||
|
||||
ticket.archivedMessages.forEach((message, i) => {
|
||||
message.author = ticket.archivedUsers.find(u => u.userId === message.authorId);
|
||||
message.content = JSON.parse(decrypt(message.content));
|
||||
message.text = message.content.content?.replace(/\n/g, '\n\t') ?? '';
|
||||
message.content.attachments?.forEach(a => (message.text += '\n\t' + a.url));
|
||||
message.content.embeds?.forEach(() => (message.text += '\n\t[embedded content]'));
|
||||
message.number = 'M' + String(i + 1).padStart(ticket.archivedMessages.length.toString().length, '0');
|
||||
ticket.archivedMessages[i] = message;
|
||||
});
|
||||
|
||||
ticket.pinnedMessageIds = ticket.pinnedMessageIds.map(id => ticket.archivedMessages.find(message => message.id === id)?.number);
|
||||
|
||||
const channelName = ticket.category.channelName
|
||||
.replace(/{+\s?(user)?name\s?}+/gi, ticket.createdBy?.username)
|
||||
.replace(/{+\s?(nick|display)(name)?\s?}+/gi, ticket.createdBy?.displayName)
|
||||
.replace(/{+\s?num(ber)?\s?}+/gi, ticket.number);
|
||||
const fileName = `${channelName}.${this.client.config.templates.transcript.split('.').slice(-1)[0]}`;
|
||||
const transcript = Mustache.render(this.template, {
|
||||
channelName,
|
||||
closedAtFull: function () {
|
||||
return new Intl.DateTimeFormat([ticket.guild.locale, 'en-GB'], {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'Etc/UTC',
|
||||
}).format(this.closedAt);
|
||||
},
|
||||
createdAtFull: function () {
|
||||
return new Intl.DateTimeFormat([ticket.guild.locale, 'en-GB'], {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'Etc/UTC',
|
||||
}).format(this.createdAt);
|
||||
},
|
||||
createdAtTimestamp: function () {
|
||||
return new Intl.DateTimeFormat([ticket.guild.locale, 'en-GB'], {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'Etc/UTC',
|
||||
}).format(this.createdAt);
|
||||
},
|
||||
guildName: client.guilds.cache.get(ticket.guildId)?.name,
|
||||
pinned: ticket.pinnedMessageIds.join(', '),
|
||||
ticket,
|
||||
});
|
||||
|
||||
return {
|
||||
fileName,
|
||||
transcript,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const {
|
||||
fileName,
|
||||
transcript,
|
||||
} = await this.fillTemplate(interaction.options.getString('ticket', true));
|
||||
const attachment = new AttachmentBuilder()
|
||||
.setFile(Buffer.from(transcript))
|
||||
.setName(fileName);
|
||||
await interaction.editReply({ files: [attachment] });
|
||||
// TODO: add portal link
|
||||
}
|
||||
};
|
87
src/commands/slash/transfer.js
Normal file
87
src/commands/slash/transfer.js
Normal file
@ -0,0 +1,87 @@
|
||||
const { SlashCommand } = require('@eartharoid/dbf');
|
||||
const {
|
||||
ApplicationCommandOptionType,
|
||||
EmbedBuilder,
|
||||
} = require('discord.js');
|
||||
const Cryptr = require('cryptr');
|
||||
const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
|
||||
|
||||
module.exports = class TransferSlashCommand extends SlashCommand {
|
||||
constructor(client, options) {
|
||||
const name = 'transfer';
|
||||
super(client, {
|
||||
...options,
|
||||
description: client.i18n.getMessage(null, `commands.slash.${name}.description`),
|
||||
descriptionLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.description`),
|
||||
dmPermission: false,
|
||||
name,
|
||||
nameLocalizations: client.i18n.getAllMessages(`commands.slash.${name}.name`),
|
||||
options: [
|
||||
{
|
||||
name: 'member',
|
||||
required: true,
|
||||
type: ApplicationCommandOptionType.User,
|
||||
},
|
||||
].map(option => {
|
||||
option.descriptionLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.description`);
|
||||
option.description = option.descriptionLocalizations['en-GB'];
|
||||
option.nameLocalizations = client.i18n.getAllMessages(`commands.slash.${name}.options.${option.name}.name`);
|
||||
return option;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").ChatInputCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
|
||||
const member = interaction.options.getMember('member', true);
|
||||
|
||||
let ticket = await client.prisma.ticket.findUnique({ where: { id: interaction.channel.id } });
|
||||
const from = ticket.createdById;
|
||||
|
||||
ticket = await client.prisma.ticket.update({
|
||||
data: {
|
||||
createdBy: {
|
||||
connectOrCreate: {
|
||||
create: { id: member.id },
|
||||
where: { id: member.id },
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { guild: true },
|
||||
where: { id: interaction.channel.id },
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(ticket.guild.primaryColour)
|
||||
.setDescription(client.i18n.getMessage(ticket.guild.locale, `commands.slash.transfer.transferred${interaction.member.id !== from ? '_from' : ''}`, {
|
||||
from: `<@${from}>`,
|
||||
to: member.toString(),
|
||||
user: interaction.user.toString(),
|
||||
})),
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
await interaction.channel.setTopic(`${member.toString()}${ticket.topic?.length > 0 ? ` | ${decrypt(ticket.topic)}` : ''}`);
|
||||
|
||||
await interaction.channel.permissionOverwrites.edit(
|
||||
member,
|
||||
{
|
||||
AttachFiles: true,
|
||||
EmbedLinks: true,
|
||||
ReadMessageHistory: true,
|
||||
SendMessages: true,
|
||||
ViewChannel: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
@ -1,95 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const Keyv = require('keyv');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class StatsCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.stats.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.stats.name'),
|
||||
staff_only: true
|
||||
});
|
||||
|
||||
this.cache = new Keyv({ namespace: 'cache.commands.stats' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const messages = await this.client.db.models.Message.findAndCountAll();
|
||||
|
||||
let stats = await this.cache.get(interaction.guild.id);
|
||||
|
||||
if (!stats) {
|
||||
const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { guild: interaction.guild.id } });
|
||||
stats = { // maths
|
||||
messages: settings.log_messages
|
||||
? await messages.rows
|
||||
.reduce(async (acc, row) => (await this.client.db.models.Ticket.findOne({ where: { id: row.ticket } }))
|
||||
.guild === interaction.guild.id
|
||||
? await acc + 1
|
||||
: await acc, 0)
|
||||
: null,
|
||||
response_time: Math.floor(tickets.rows.reduce((acc, row) => row.first_response
|
||||
? acc + ((Math.abs(new Date(row.createdAt) - new Date(row.first_response)) / 1000) / 60)
|
||||
: acc, 0) / tickets.count),
|
||||
tickets: tickets.count
|
||||
};
|
||||
await this.cache.set(interaction.guild.id, stats, 60 * 60 * 1000); // cache for an hour
|
||||
}
|
||||
|
||||
const guild_embed = new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.stats.response.guild.title'))
|
||||
.setDescription(i18n('commands.stats.response.guild.description'))
|
||||
.addField(i18n('commands.stats.fields.tickets'), String(stats.tickets), true)
|
||||
.addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', stats.response_time), true)
|
||||
.setFooter(settings.footer, interaction.guild.iconURL());
|
||||
|
||||
if (stats.messages) guild_embed.addField(i18n('commands.stats.fields.messages'), String(stats.messages), true);
|
||||
|
||||
const embeds = [guild_embed];
|
||||
|
||||
if (this.client.guilds.cache.size > 1) {
|
||||
let global = await this.cache.get('global');
|
||||
|
||||
if (!global) {
|
||||
const tickets = await this.client.db.models.Ticket.findAndCountAll();
|
||||
global = { // maths
|
||||
messages: settings.log_messages
|
||||
? await messages.count
|
||||
: null,
|
||||
response_time: Math.floor(tickets.rows.reduce((acc, row) => row.first_response
|
||||
? acc + ((Math.abs(new Date(row.createdAt) - new Date(row.first_response)) / 1000) / 60)
|
||||
: acc, 0) / tickets.count),
|
||||
tickets: tickets.count
|
||||
};
|
||||
await this.cache.set('global', global, 60 * 60 * 1000); // cache for an hour
|
||||
}
|
||||
|
||||
const global_embed = new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.stats.response.global.title'))
|
||||
.setDescription(i18n('commands.stats.response.global.description'))
|
||||
.addField(i18n('commands.stats.fields.tickets'), String(global.tickets), true)
|
||||
.addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', global.response_time), true)
|
||||
.setFooter(settings.footer, interaction.guild.iconURL());
|
||||
|
||||
if (stats.messages) global_embed.addField(i18n('commands.stats.fields.messages'), String(global.messages), true);
|
||||
|
||||
embeds.push(global_embed);
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds });
|
||||
}
|
||||
};
|
@ -1,113 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {
|
||||
Message, // eslint-disable-line no-unused-vars
|
||||
MessageAttachment,
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
const fsp = require('fs').promises;
|
||||
const { path } = require('../utils/fs');
|
||||
const mustache = require('mustache');
|
||||
|
||||
module.exports = class SurveyCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.survey.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.survey.name'),
|
||||
options: async guild => {
|
||||
const surveys = await this.client.db.models.Survey.findAll({ where: { guild: guild.id } });
|
||||
return [
|
||||
{
|
||||
choices: surveys.map(survey => ({
|
||||
name: survey.name,
|
||||
value: survey.name
|
||||
})),
|
||||
description: i18n('commands.survey.options.survey.description'),
|
||||
name: i18n('commands.survey.options.survey.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
];
|
||||
},
|
||||
staff_only: true
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const name = interaction.options.getString(default_i18n('commands.survey.options.survey.name'));
|
||||
|
||||
const survey = await this.client.db.models.Survey.findOne({
|
||||
where: {
|
||||
guild: interaction.guild.id,
|
||||
name
|
||||
}
|
||||
});
|
||||
|
||||
if (survey) {
|
||||
const {
|
||||
rows: responses, count
|
||||
} = await this.client.db.models.SurveyResponse.findAndCountAll({ where: { survey: survey.id } });
|
||||
|
||||
const users = new Set();
|
||||
|
||||
for (const i in responses) {
|
||||
const ticket = await this.client.db.models.Ticket.findOne({ where: { id: responses[i].ticket } });
|
||||
users.add(ticket.creator);
|
||||
const answers = responses[i].answers.map(a => this.client.cryptr.decrypt(a));
|
||||
answers.unshift(ticket.number);
|
||||
responses[i] = answers;
|
||||
}
|
||||
|
||||
let template = await fsp.readFile(path('./src/commands/extra/survey.template.html'), { encoding: 'utf8' });
|
||||
|
||||
template = template.replace(/[\r\n\t]/g, '');
|
||||
|
||||
survey.questions.unshift('Ticket #');
|
||||
|
||||
const html = mustache.render(template, {
|
||||
columns: survey.questions,
|
||||
count: {
|
||||
responses: count,
|
||||
users: users.size
|
||||
},
|
||||
responses,
|
||||
survey: survey.name.charAt(0).toUpperCase() + survey.name.slice(1)
|
||||
});
|
||||
|
||||
const attachment = new MessageAttachment(
|
||||
Buffer.from(html),
|
||||
`${survey.name}.html`
|
||||
);
|
||||
|
||||
return await interaction.reply({
|
||||
ephemeral: true,
|
||||
files: [attachment]
|
||||
});
|
||||
} else {
|
||||
const surveys = await this.client.db.models.Survey.findAll({ where: { guild: interaction.guild.id } });
|
||||
|
||||
const list = surveys.map(s => `❯ **\`${s.name}\`**`);
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.survey.response.list.title'))
|
||||
.setDescription(list.join('\n'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,71 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class TagCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.tag.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.tag.name'),
|
||||
options: async guild => {
|
||||
const settings = await client.utils.getSettings(guild.id);
|
||||
return Object.keys(settings.tags).map(tag => ({
|
||||
description: settings.tags[tag].substring(0, 100),
|
||||
name: tag,
|
||||
options: [...settings.tags[tag].matchAll(/(?<!\\){{1,2}\s?([A-Za-z0-9._:]+)\s?(?<!\\)}{1,2}/gi)]
|
||||
.map(match => ({
|
||||
description: match[1],
|
||||
name: match[1],
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
})),
|
||||
type: Command.option_types.SUB_COMMAND
|
||||
}));
|
||||
},
|
||||
staff_only: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
try {
|
||||
const tag_name = interaction.options.getSubcommand();
|
||||
const tag = settings.tags[tag_name];
|
||||
const args = interaction.options.data[0]?.options;
|
||||
const text = tag.replace(/(?<!\\){{1,2}\s?([A-Za-z0-9._:]+)\s?(?<!\\)}{1,2}/gi, ($, $1) => {
|
||||
const arg = args.find(arg => arg.name === $1);
|
||||
return arg ? arg.value : $;
|
||||
});
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setDescription(text)
|
||||
],
|
||||
ephemeral: false
|
||||
});
|
||||
} catch {
|
||||
const list = Object.keys(settings.tags).map(t => `❯ **\`${t}\`**`);
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setTitle(i18n('commands.tag.response.list.title'))
|
||||
.setDescription(list.join('\n'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,87 +0,0 @@
|
||||
const Command = require('../modules/commands/command');
|
||||
const {
|
||||
Interaction, // eslint-disable-line no-unused-vars
|
||||
MessageEmbed
|
||||
} = require('discord.js');
|
||||
|
||||
module.exports = class TopicCommand extends Command {
|
||||
constructor(client) {
|
||||
const i18n = client.i18n.getLocale(client.config.locale);
|
||||
super(client, {
|
||||
description: i18n('commands.topic.description'),
|
||||
internal: true,
|
||||
name: i18n('commands.topic.name'),
|
||||
options: [
|
||||
{
|
||||
description: i18n('commands.topic.options.new_topic.description'),
|
||||
name: i18n('commands.topic.options.new_topic.name'),
|
||||
required: true,
|
||||
type: Command.option_types.STRING
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Interaction} interaction
|
||||
* @returns {Promise<void|any>}
|
||||
*/
|
||||
async execute(interaction) {
|
||||
const settings = await this.client.utils.getSettings(interaction.guild.id);
|
||||
const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale
|
||||
const i18n = this.client.i18n.getLocale(settings.locale);
|
||||
|
||||
const topic = interaction.options.getString(default_i18n('commands.topic.options.new_topic.name'));
|
||||
|
||||
const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } });
|
||||
|
||||
if (!t_row) {
|
||||
return await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.error_colour)
|
||||
.setTitle(i18n('commands.topic.response.not_a_ticket.title'))
|
||||
.setDescription(i18n('commands.topic.response.not_a_ticket.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
await t_row.update({ topic: this.client.cryptr.encrypt(topic) });
|
||||
|
||||
const member = await interaction.guild.members.fetch(t_row.creator);
|
||||
interaction.channel.setTopic(`${member} | ${topic}`, { reason: 'User updated ticket topic' });
|
||||
|
||||
const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } });
|
||||
const description = cat_row.opening_message
|
||||
.replace(/{+\s?(user)?name\s?}+/gi, member.displayName)
|
||||
.replace(/{+\s?(tag|ping|mention)?\s?}+/gi, member.user.toString());
|
||||
const opening_message = await interaction.channel.messages.fetch(t_row.opening_message);
|
||||
|
||||
await opening_message.edit({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.colour)
|
||||
.setAuthor(member.user.username, member.user.displayAvatarURL())
|
||||
.setDescription(description)
|
||||
.addField(i18n('ticket.opening_message.fields.topic'), topic)
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
]
|
||||
});
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setColor(settings.success_colour)
|
||||
.setAuthor(interaction.user.username, interaction.user.displayAvatarURL())
|
||||
.setTitle(i18n('commands.topic.response.changed.title'))
|
||||
.setDescription(i18n('commands.topic.response.changed.description'))
|
||||
.setFooter(settings.footer, interaction.guild.iconURL())
|
||||
],
|
||||
ephemeral: false
|
||||
});
|
||||
|
||||
this.client.log.info(`${interaction.user.tag} changed the topic of #${interaction.channel.name}`);
|
||||
}
|
||||
};
|
168
src/commands/user/create.js
Normal file
168
src/commands/user/create.js
Normal file
@ -0,0 +1,168 @@
|
||||
const { UserCommand } = require('@eartharoid/dbf');
|
||||
const { isStaff } = require('../../lib/users');
|
||||
const ExtendedEmbedBuilder = require('../../lib/embed');
|
||||
const ms = require('ms');
|
||||
const {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
} = require('discord.js');
|
||||
const emoji = require('node-emoji');
|
||||
|
||||
module.exports = class CreateUserCommand extends UserCommand {
|
||||
constructor(client, options) {
|
||||
const nameLocalizations = {};
|
||||
client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.user.create.name')));
|
||||
|
||||
super(client, {
|
||||
...options,
|
||||
dmPermission: false,
|
||||
name: nameLocalizations['en-GB'],
|
||||
nameLocalizations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").UserContextMenuCommandInteraction} interaction
|
||||
*/
|
||||
async run(interaction) {
|
||||
/** @type {import("client")} */
|
||||
const client = this.client;
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const settings = await client.prisma.guild.findUnique({
|
||||
include: { categories:true },
|
||||
where: { id: interaction.guild.id },
|
||||
});
|
||||
const getMessage = client.i18n.getLocale(settings.locale);
|
||||
|
||||
if (!await isStaff(interaction.guild, interaction.user.id)) {
|
||||
return await interaction.editReply({
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('commands.user.create.not_staff.title'))
|
||||
.setDescription(getMessage('commands.user.create.not_staff.description')),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const prompt = async categoryId => {
|
||||
interaction.followUp({
|
||||
components: [
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'create',
|
||||
target: categoryId,
|
||||
targetUser: interaction.targetId,
|
||||
}))
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji(getMessage('buttons.create.emoji')) // emoji.get('ticket')
|
||||
.setLabel(getMessage('buttons.create.text')),
|
||||
),
|
||||
],
|
||||
content: interaction.targetUser.toString(),
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(settings.primaryColour)
|
||||
.setAuthor({
|
||||
iconURL: interaction.member.displayAvatarURL(),
|
||||
name: interaction.member.displayName,
|
||||
})
|
||||
.setTitle(getMessage('commands.user.create.prompt.title'))
|
||||
.setDescription(getMessage('commands.user.create.prompt.description')),
|
||||
],
|
||||
ephemeral: false,
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.categories.length === 0) {
|
||||
interaction.reply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder()
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.no_categories.title'))
|
||||
.setDescription(getMessage('misc.no_categories.description')),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
} else if (settings.categories.length === 1) {
|
||||
await prompt(settings.categories[0].id);
|
||||
} else {
|
||||
const collectorTime = ms('15s');
|
||||
const confirmationM = await interaction.editReply({
|
||||
components: [
|
||||
new ActionRowBuilder()
|
||||
.setComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId(JSON.stringify({
|
||||
action: 'promptCreate',
|
||||
user: interaction.targetId,
|
||||
}))
|
||||
.setPlaceholder(getMessage('menus.category.placeholder'))
|
||||
.setOptions(
|
||||
settings.categories.map(category =>
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setValue(String(category.id))
|
||||
.setLabel(category.name)
|
||||
.setDescription(category.description)
|
||||
.setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
confirmationM.awaitMessageComponent({
|
||||
componentType: ComponentType.StringSelect,
|
||||
filter: i => i.user.id === interaction.user.id,
|
||||
time: collectorTime,
|
||||
})
|
||||
.then(async i => {
|
||||
const category = settings.categories.find(c => c.id === Number(i.values[0]));
|
||||
await i.update({
|
||||
components: [],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.successColour)
|
||||
.setTitle(getMessage('commands.user.create.sent.title'))
|
||||
.setDescription(getMessage('commands.user.create.sent.description', {
|
||||
category: category.name,
|
||||
user: interaction.targetUser.toString(),
|
||||
})),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
await prompt(category.id);
|
||||
})
|
||||
.catch(async error => {
|
||||
client.log.error(error);
|
||||
await interaction.reply({
|
||||
components: [],
|
||||
embeds: [
|
||||
new ExtendedEmbedBuilder({
|
||||
iconURL: interaction.guild.iconURL(),
|
||||
text: settings.footer,
|
||||
})
|
||||
.setColor(settings.errorColour)
|
||||
.setTitle(getMessage('misc.expired.title'))
|
||||
.setDescription(getMessage('misc.expired.description')),
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
module.exports = {
|
||||
maria: {
|
||||
dialect: 'mariadb',
|
||||
name: 'MariaDB',
|
||||
packages: ['mariadb']
|
||||
},
|
||||
mariadb: {
|
||||
dialect: 'mariadb',
|
||||
name: 'MariaDB',
|
||||
packages: ['mariadb']
|
||||
},
|
||||
microsoft: {
|
||||
dialect: 'mssql',
|
||||
name: 'Microsoft SQL',
|
||||
packages: ['tedious']
|
||||
},
|
||||
mysql: {
|
||||
dialect: 'mysql',
|
||||
name: 'MySQL',
|
||||
packages: ['mysql2']
|
||||
},
|
||||
postgre: { // this is wrong
|
||||
dialect: 'postgres',
|
||||
name: 'PostgreSQL',
|
||||
packages: ['pg', 'pg-hstore']
|
||||
},
|
||||
postgres: {
|
||||
dialect: 'postgres',
|
||||
name: 'PostgreSQL',
|
||||
packages: ['pg', 'pg-hstore']
|
||||
},
|
||||
postgresql: {
|
||||
dialect: 'postgres',
|
||||
name: 'PostgreSQL',
|
||||
packages: ['pg', 'pg-hstore']
|
||||
},
|
||||
sqlite: {
|
||||
dialect: 'sqlite',
|
||||
name: 'SQLite',
|
||||
packages: ['sqlite3']
|
||||
}
|
||||
};
|
@ -1,73 +0,0 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
const fs = require('fs');
|
||||
const { path } = require('../utils/fs');
|
||||
const types = require('./dialects');
|
||||
|
||||
module.exports = async client => {
|
||||
|
||||
const {
|
||||
DB_TYPE,
|
||||
DB_HOST,
|
||||
DB_PORT,
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
DB_NAME
|
||||
} = process.env;
|
||||
|
||||
const type = (DB_TYPE || 'sqlite').toLowerCase();
|
||||
|
||||
const supported = Object.keys(types);
|
||||
if (!supported.includes(type)) {
|
||||
client.log.error(new Error(`DB_TYPE (${type}) is not a valid type`));
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
try {
|
||||
types[type].packages.forEach(pkg => require(pkg));
|
||||
} catch {
|
||||
const required = types[type].packages.map(i => `"${i}"`).join(' and ');
|
||||
client.log.error(new Error(`Please install the package(s) for your selected database type: ${required}`));
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
let sequelize;
|
||||
|
||||
if (type === 'sqlite') {
|
||||
client.log.info('Using SQLite storage');
|
||||
sequelize = new Sequelize({
|
||||
dialect: types[type].dialect,
|
||||
logging: text => client.log.debug(text),
|
||||
storage: path('./user/database.sqlite')
|
||||
});
|
||||
client.config.defaults.log_messages = false;
|
||||
client.log.warn('Message logging is disabled due to insufficient database');
|
||||
} else {
|
||||
client.log.info(`Connecting to ${types[type].name} database...`);
|
||||
sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, {
|
||||
dialect: types[type].dialect,
|
||||
host: DB_HOST,
|
||||
logging: text => client.log.debug(text),
|
||||
port: DB_PORT
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
client.log.success('Connected to database successfully');
|
||||
} catch (error) {
|
||||
client.log.warn('Failed to connect to database');
|
||||
client.log.error(error);
|
||||
return process.exit();
|
||||
}
|
||||
|
||||
const models = fs.readdirSync(path('./src/database/models'))
|
||||
.filter(filename => filename.endsWith('.model.js'));
|
||||
|
||||
for (const model of models) {
|
||||
require(`./models/${model}`)(client, sequelize);
|
||||
}
|
||||
|
||||
await sequelize.sync({ alter: false });
|
||||
|
||||
return sequelize;
|
||||
};
|
@ -1,93 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = ({ config }, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('Category', {
|
||||
claiming: {
|
||||
defaultValue: false,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
guild: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'guilds'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'name-guild'
|
||||
},
|
||||
id: {
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
image: {
|
||||
allowNull: true,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
max_per_member: {
|
||||
defaultValue: 1,
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
name: {
|
||||
allowNull: false,
|
||||
type: DataTypes.STRING,
|
||||
unique: 'name-guild'
|
||||
},
|
||||
name_format: {
|
||||
allowNull: false,
|
||||
defaultValue: config.defaults.name_format,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
opening_message: {
|
||||
defaultValue: config.defaults.opening_message,
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
opening_questions: {
|
||||
allowNull: true,
|
||||
get() {
|
||||
const raw_value = this.getDataValue('opening_questions');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
},
|
||||
ping: {
|
||||
defaultValue: [],
|
||||
get() {
|
||||
const raw_value = this.getDataValue('ping');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
},
|
||||
require_topic: {
|
||||
defaultValue: false,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
roles: {
|
||||
allowNull: false,
|
||||
get() {
|
||||
const raw_value = this.getDataValue('roles');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
},
|
||||
survey: {
|
||||
allowNull: true,
|
||||
type: DataTypes.STRING
|
||||
}
|
||||
}, {
|
||||
paranoid: true,
|
||||
tableName: DB_TABLE_PREFIX + 'categories'
|
||||
});
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (_client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('ChannelEntity', {
|
||||
channel: {
|
||||
allowNull: false,
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'channel-ticket'
|
||||
},
|
||||
name: DataTypes.TEXT,
|
||||
ticket: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'tickets'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'channel-ticket'
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'channel_entities' });
|
||||
};
|
@ -1,66 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = ({ config }, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('Guild', {
|
||||
blacklist: {
|
||||
defaultValue: {
|
||||
members: [],
|
||||
roles: []
|
||||
},
|
||||
get() {
|
||||
const raw_value = this.getDataValue('blacklist');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
},
|
||||
close_button: {
|
||||
defaultValue: false,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
colour: {
|
||||
defaultValue: config.defaults.colour,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
error_colour: {
|
||||
defaultValue: 'RED',
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
footer: {
|
||||
defaultValue: 'Discord Tickets by eartharoid',
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
id: {
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
locale: {
|
||||
defaultValue: config.locale,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
log_messages: {
|
||||
defaultValue: config.defaults.log_messages,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
success_colour: {
|
||||
defaultValue: 'GREEN',
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
tags: {
|
||||
defaultValue: {},
|
||||
get() {
|
||||
const raw_value = this.getDataValue('tags');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'guilds' });
|
||||
};
|
@ -1,35 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (_client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('Message', {
|
||||
author: {
|
||||
allowNull: false,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
data: {
|
||||
allowNull: false,
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
deleted: {
|
||||
defaultValue: false,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
edited: {
|
||||
defaultValue: false,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
id: {
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
ticket: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'tickets'
|
||||
},
|
||||
type: DataTypes.CHAR(19)
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'messages' });
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('Panel', {
|
||||
category: {
|
||||
allowNull: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
channel: {
|
||||
allowNull: false,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
guild: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'guilds'
|
||||
},
|
||||
type: DataTypes.CHAR(19)
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'panels' });
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('RoleEntity', {
|
||||
colour: {
|
||||
defaultValue: '7289DA',
|
||||
type: DataTypes.CHAR(6)
|
||||
},
|
||||
name: DataTypes.TEXT,
|
||||
role: {
|
||||
allowNull: false,
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'role-ticket'
|
||||
},
|
||||
ticket: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'tickets'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'role-ticket'
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'role_entities' });
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('Survey', {
|
||||
guild: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'guilds'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'name-guild'
|
||||
},
|
||||
name: {
|
||||
allowNull: false,
|
||||
type: DataTypes.STRING,
|
||||
unique: 'name-guild'
|
||||
},
|
||||
questions: {
|
||||
allowNull: true,
|
||||
get() {
|
||||
const raw_value = this.getDataValue('questions');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'surveys' });
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('SurveyResponse', {
|
||||
answers: {
|
||||
allowNull: true,
|
||||
get() {
|
||||
const raw_value = this.getDataValue('answers');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
},
|
||||
survey: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'surveys'
|
||||
},
|
||||
type: DataTypes.INTEGER,
|
||||
unique: 'survey-ticket'
|
||||
},
|
||||
ticket: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'tickets'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'survey-ticket'
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'survey_responses' });
|
||||
};
|
@ -1,81 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (_client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('Ticket', {
|
||||
category: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'categories'
|
||||
},
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
claimed_by: {
|
||||
allowNull: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
closed_by: {
|
||||
allowNull: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
closed_reason: {
|
||||
allowNull: true,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
creator: {
|
||||
allowNull: false,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
first_response: {
|
||||
allowNull: true,
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
guild: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'guilds'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'number-guild'
|
||||
},
|
||||
id: {
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
last_message: {
|
||||
allowNull: true,
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
number: {
|
||||
allowNull: false,
|
||||
type: DataTypes.INTEGER,
|
||||
unique: 'number-guild'
|
||||
},
|
||||
open: {
|
||||
defaultValue: true,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
opening_message: {
|
||||
allowNull: true,
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
pinned_messages: {
|
||||
defaultValue: [],
|
||||
get() {
|
||||
const raw_value = this.getDataValue('pinned_messages');
|
||||
return raw_value
|
||||
? typeof raw_value === 'string'
|
||||
? JSON.parse(raw_value)
|
||||
: raw_value
|
||||
: null;
|
||||
},
|
||||
type: DataTypes.JSON
|
||||
},
|
||||
topic: {
|
||||
allowNull: true,
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
}, { tableName: DB_TABLE_PREFIX + 'tickets' });
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (client, sequelize) => {
|
||||
const { DB_TABLE_PREFIX } = process.env;
|
||||
sequelize.define('UserEntity', {
|
||||
avatar: DataTypes.STRING,
|
||||
bot: DataTypes.BOOLEAN,
|
||||
discriminator: DataTypes.STRING,
|
||||
display_name: DataTypes.TEXT,
|
||||
role: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'role',
|
||||
model: DB_TABLE_PREFIX + 'role_entities'
|
||||
},
|
||||
type: DataTypes.CHAR(19)
|
||||
},
|
||||
ticket: {
|
||||
allowNull: false,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: DB_TABLE_PREFIX + 'tickets'
|
||||
},
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'user-ticket'
|
||||
},
|
||||
user: {
|
||||
allowNull: false,
|
||||
type: DataTypes.CHAR(19),
|
||||
unique: 'user-ticket'
|
||||
},
|
||||
username: DataTypes.TEXT
|
||||
|
||||
}, { tableName: DB_TABLE_PREFIX + 'user_entities' });
|
||||
};
|
57
src/env.js
Normal file
57
src/env.js
Normal file
@ -0,0 +1,57 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const dotenv = require('dotenv');
|
||||
const { colours } = require('leeks.js');
|
||||
|
||||
const providers = ['mysql', 'postgresql', 'sqlite'];
|
||||
|
||||
// ideally the defaults would be set here too, but the pre-install script may run when `src/` is not available
|
||||
const env = {
|
||||
DB_CONNECTION_URL: v =>
|
||||
!!v ||
|
||||
(process.env.DB_PROVIDER === 'sqlite') ||
|
||||
new Error('must be set when "DB_PROVIDER" is not "sqlite"'),
|
||||
DB_PROVIDER: v =>
|
||||
(!!v && providers.includes(v)) ||
|
||||
new Error(`must be one of: ${providers.map(v => `"${v}"`).join(', ')}`),
|
||||
DISCORD_SECRET: v =>
|
||||
!!v ||
|
||||
new Error('is required'),
|
||||
DISCORD_TOKEN: v =>
|
||||
!!v ||
|
||||
new Error('is required'),
|
||||
ENCRYPTION_KEY: v =>
|
||||
(!!v && v.length >= 48) ||
|
||||
new Error('is required and must be at least 48 characters long; run "npm run keygen" to generate a key'),
|
||||
HTTP_EXTERNAL: v =>
|
||||
(!!v && v.startsWith('http') && !v.endsWith('/')) ||
|
||||
new Error('must be a valid URL without a trailing slash'),
|
||||
HTTP_HOST: v =>
|
||||
(!!v && !v.startsWith('http')) ||
|
||||
new Error('is required and must be an address, not a URL'),
|
||||
HTTP_PORT: v =>
|
||||
!!v ||
|
||||
new Error('is required'),
|
||||
HTTP_TRUST_PROXY: () => true, // optional
|
||||
INVALIDATE_TOKENS: () => true, // optional
|
||||
OVERRIDE_ARCHIVE: () => true, // optional
|
||||
PUBLIC_BOT: () => true, // optional
|
||||
PUBLISH_COMMANDS: () => true, // optional
|
||||
SUPER: () => true, // optional
|
||||
};
|
||||
|
||||
const load = options => {
|
||||
dotenv.config(options);
|
||||
Object.entries(env).forEach(([name, validate]) => {
|
||||
const result = validate(process.env[name]); // `true` for pass, or `Error` for fail
|
||||
if (result instanceof Error) {
|
||||
console.log('\x07' + colours.redBright(`Error: The "${name}" environment variable ${result.message}.`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
env,
|
||||
load,
|
||||
};
|
176
src/http.js
Normal file
176
src/http.js
Normal file
@ -0,0 +1,176 @@
|
||||
const fastify = require('fastify')({ trustProxy: process.env.HTTP_TRUST_PROXY === 'true' });
|
||||
const oauth = require('@fastify/oauth2');
|
||||
const { randomBytes } = require('crypto');
|
||||
const { short } = require('leeks.js');
|
||||
const { join } = require('path');
|
||||
const { files } = require('node-dir');
|
||||
const { PermissionsBitField } = require('discord.js');
|
||||
|
||||
process.env.ORIGIN = process.env.HTTP_EXTERNAL;
|
||||
|
||||
module.exports = async client => {
|
||||
// oauth2 plugin
|
||||
fastify.states = new Map();
|
||||
fastify.register(oauth, {
|
||||
callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`,
|
||||
checkStateFunction: (state, callback) => {
|
||||
if (fastify.states.has(state)) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback(new Error('Invalid state'));
|
||||
},
|
||||
credentials: {
|
||||
auth: oauth.DISCORD_CONFIGURATION,
|
||||
client: {
|
||||
id: client.user.id,
|
||||
secret: process.env.DISCORD_SECRET,
|
||||
},
|
||||
},
|
||||
generateStateFunction: req => {
|
||||
const state = randomBytes(12).toString('hex');
|
||||
fastify.states.set(state, req.query.r);
|
||||
return state;
|
||||
},
|
||||
name: 'discord',
|
||||
scope: ['applications.commands.permissions.update', 'guilds', 'identify'],
|
||||
startRedirectPath: '/auth/login',
|
||||
});
|
||||
|
||||
// cookies plugin
|
||||
fastify.register(require('@fastify/cookie'));
|
||||
|
||||
// jwt plugin
|
||||
fastify.register(require('@fastify/jwt'), {
|
||||
cookie: {
|
||||
cookieName: 'token',
|
||||
signed: false,
|
||||
},
|
||||
secret: process.env.ENCRYPTION_KEY,
|
||||
});
|
||||
|
||||
// auth
|
||||
fastify.decorate('authenticate', async (req, res) => {
|
||||
try {
|
||||
const data = await req.jwtVerify();
|
||||
if (data.expiresAt < Date.now()) throw 'expired';
|
||||
if (data.createdAt < new Date(process.env.INVALIDATE_TOKENS).getTime()) throw 'expired';
|
||||
} catch (error) {
|
||||
return res.code(401).send({
|
||||
error: 'Unauthorised',
|
||||
message: error === 'expired' ? 'Your token has expired; please re-authenticate.' : 'You are not authenticated.',
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.decorate('isAdmin', async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const guildId = req.params.guild;
|
||||
const guild = client.guilds.cache.get(guildId);
|
||||
if (!guild) {
|
||||
return res.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: 'The requested resource could not be found.',
|
||||
statusCode: 404,
|
||||
|
||||
});
|
||||
}
|
||||
const guildMember = await guild.members.fetch(userId);
|
||||
const isAdmin = guildMember?.permissions.has(PermissionsBitField.Flags.ManageGuild) || client.supers.includes(userId);
|
||||
if (!isAdmin) {
|
||||
return res.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: 'You are not permitted for this action.',
|
||||
statusCode: 403,
|
||||
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
res.send(err);
|
||||
}
|
||||
});
|
||||
|
||||
// body processing
|
||||
fastify.addHook('preHandler', (req, res, done) => {
|
||||
if (req.body && typeof req.body === 'object') {
|
||||
for (const prop in req.body) {
|
||||
if (typeof req.body[prop] === 'string') {
|
||||
req.body[prop] = req.body[prop].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
// logging
|
||||
fastify.addHook('onResponse', (req, res, done) => {
|
||||
done();
|
||||
const status = (res.statusCode >= 500
|
||||
? '&4'
|
||||
: res.statusCode >= 400
|
||||
? '&6'
|
||||
: res.statusCode >= 300
|
||||
? '&3'
|
||||
: res.statusCode >= 200
|
||||
? '&2'
|
||||
: '&f') + res.statusCode;
|
||||
let responseTime = res.getResponseTime().toFixed(2);
|
||||
responseTime = (responseTime >= 100
|
||||
? '&c'
|
||||
: responseTime >= 10
|
||||
? '&e'
|
||||
: '&a') + responseTime + 'ms';
|
||||
const level = req.routerPath === '/*' ? 'verbose' : 'info';
|
||||
client.log[level].http(short(`${req.id} ${req.ip} ${req.method} ${req.routerPath ?? '*'} &m-+>&r ${status}&b in ${responseTime}`));
|
||||
if (!req.routerPath) client.log.verbose.http(`${req.id} ${req.method} ${req.url}`);
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.addHook('onError', async (req, res, err) => client.log.error.http(req.id, err));
|
||||
|
||||
// route loading
|
||||
const dir = join(__dirname, '/routes');
|
||||
files(dir, {
|
||||
exclude: /^\./,
|
||||
match: /.js$/,
|
||||
sync: true,
|
||||
}).forEach(file => {
|
||||
const path = file
|
||||
.substring(0, file.length - 3) // remove `.js`
|
||||
.substring(dir.length) // remove higher directories
|
||||
.replace(/\\/g, '/') // replace `\` with `/` because Windows is stupid
|
||||
.replace(/\[(\w+)\]/gi, ':$1') // convert [] to :
|
||||
.replace('/index', '') || '/'; // remove index
|
||||
const route = require(file);
|
||||
|
||||
Object.keys(route).forEach(method => fastify.route({
|
||||
config: { client },
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
...route[method](fastify),
|
||||
})); // register route
|
||||
});
|
||||
|
||||
const { handler } = await import('@discord-tickets/settings/build/handler.js');
|
||||
|
||||
// https://stackoverflow.com/questions/72317071/how-to-set-up-fastify-correctly-so-that-sveltekit-works-fine
|
||||
fastify.all('/*', {}, (req, res) => handler(req.raw, res.raw, () => { }));
|
||||
|
||||
// start the fastify server
|
||||
fastify.listen({
|
||||
host: process.env.HTTP_HOST,
|
||||
port: process.env.HTTP_PORT,
|
||||
}, (err, addr) => {
|
||||
if (err) client.log.error.http(err);
|
||||
else client.log.success.http(`Listening at ${addr}`);
|
||||
});
|
||||
|
||||
process.on('sveltekit:error', ({
|
||||
error,
|
||||
errorId,
|
||||
}) => {
|
||||
client.log.error.http(`SvelteKit ${errorId} ${error}`);
|
||||
});
|
||||
};
|
231
src/i18n/cs.yml
Normal file
231
src/i18n/cs.yml
Normal file
@ -0,0 +1,231 @@
|
||||
commands:
|
||||
message:
|
||||
pin:
|
||||
name: Připnout zprávu
|
||||
pinned:
|
||||
description: Zpráva byla připnuta.
|
||||
title: ✅ Zpráva připnuta
|
||||
not_pinnable:
|
||||
description: "Tato zpráva nemůže být připnuta.\nProsím zeptejte se admininistrátora,\
|
||||
\ jestli má robot dostatečné oprávnění.\n"
|
||||
title: ❌ Chyba
|
||||
not_ticket:
|
||||
description: Zprávy můžeš připnout jen v ticketech.
|
||||
title: ❌ Tento kanál není ticket
|
||||
create:
|
||||
name: Vytvořit ticket ze zprávy
|
||||
slash:
|
||||
add:
|
||||
name: přidat
|
||||
options:
|
||||
ticket:
|
||||
name: tiket
|
||||
description: Ticket na přidání člena
|
||||
member:
|
||||
name: člen
|
||||
description: Člen, kterého chcete přidat do ticketu
|
||||
added: ➡️{by} přidal {added}.
|
||||
description: Přidat člena do ticketu
|
||||
not_staff:
|
||||
description: Pouze členové staff teamu můžou přidávat členy do ticketů ostatních.
|
||||
title: ❌ Chyba
|
||||
success:
|
||||
description: '{member} byl přidán do {ticket}.'
|
||||
title: ✅ Přidán
|
||||
claim:
|
||||
name: převzít
|
||||
description: Převzít ticket
|
||||
close:
|
||||
invalid_time:
|
||||
title: ❌ Neplatný
|
||||
description: '`{input}` není platný formát času.'
|
||||
description: Zažádat o uzavření ticketu
|
||||
name: uzavřít
|
||||
options:
|
||||
reason:
|
||||
description: Důvod pro uzavření ticketu
|
||||
name: důvod
|
||||
remove:
|
||||
options:
|
||||
ticket:
|
||||
name: tiket
|
||||
description: Tiket pro odebrání člena
|
||||
member:
|
||||
description: Člen, který má být odstraněn z tiketu
|
||||
name: člen
|
||||
not_staff:
|
||||
description: Pouze členové staff teamu mohou odstranit členy z ostatních tiketů.
|
||||
title: ❌ Chyba
|
||||
success:
|
||||
description: '{member} byl odstraněn z {ticket}.'
|
||||
title: ✅ Odstraněn
|
||||
description: Odstranit člena z tiketu
|
||||
name: odstranit
|
||||
removed: ⬅️ {removed} byl odstraněn uživatelem {by}.
|
||||
help:
|
||||
response:
|
||||
links:
|
||||
feedback: Zpětná vazba
|
||||
commands: Celý list příkazů
|
||||
docs: Dokumentace
|
||||
links: Užitečné odkazy
|
||||
support: Podpora
|
||||
commands: Příkazy
|
||||
settings: Nastavení bota
|
||||
description: '**Použijte {command} pro vytvoření ticketu a získání podpory.**'
|
||||
description: Zobrazit pomocné menu
|
||||
name: pomoc
|
||||
title: Pomoc
|
||||
force-close:
|
||||
options:
|
||||
category:
|
||||
name: kategorie
|
||||
description: Uzavřít všechny tickety v zadané kategorii (lze použít s `time`).
|
||||
reason:
|
||||
description: Důvod pro uzavření ticketu(ů)
|
||||
name: důvod
|
||||
ticket:
|
||||
description: Ticket k uzavření
|
||||
name: tiket
|
||||
time:
|
||||
name: čas
|
||||
description: Zavřít všechny tickety, které byly po zadanou dobu neaktivní
|
||||
(lze použít s `category`).
|
||||
confirm_multiple:
|
||||
title: ❓Jste si jist?
|
||||
description: "Chystáte se uzavřít **{count}** ticketů, které jsou neaktivní\
|
||||
\ déle než `{time}`:\n{tickets}\n"
|
||||
name: uzavřít-silou
|
||||
description: Násilně uzavřít tiketu
|
||||
no_tickets:
|
||||
title: ❌Žádné tikety
|
||||
description: Neexistují žádné otevřené tickety, které by byly neaktivní déle
|
||||
než `{time}`.
|
||||
not_staff:
|
||||
title: ❌Chyba
|
||||
description: Tickety mohou násilně uzavírat pouze zaměstnanci.
|
||||
move:
|
||||
description: Přesunout ticket do jiné kategorie
|
||||
name: přesunout
|
||||
options:
|
||||
category:
|
||||
description: Kategorie, do které se má ticket přesunout
|
||||
name: kategorie
|
||||
moved: 🗃️ {by} přesunul tento ticket z **{from}** do **{to}**.
|
||||
new:
|
||||
description: Vytvořit nový ticket
|
||||
name: nový
|
||||
options:
|
||||
references:
|
||||
description: Číslo souvisejícího tiketu
|
||||
name: reference
|
||||
priority:
|
||||
description: Nastavit důležitost ticketu
|
||||
name: důležitost
|
||||
options:
|
||||
priority:
|
||||
choices:
|
||||
HIGH: 🔴 Vysoká
|
||||
LOW: 🟢 Malá
|
||||
MEDIUM: 🟠 Střední
|
||||
description: Priorita tiketu
|
||||
name: priorita
|
||||
success:
|
||||
description: Priorita tiketu byla nastavena na `{priority}`.
|
||||
title: ✅ Priorita nastavena
|
||||
release:
|
||||
name: uvolnit
|
||||
description: Uvolnit (vrátit) ticket
|
||||
tag:
|
||||
description: Použijte tag
|
||||
name: tag
|
||||
options:
|
||||
for:
|
||||
description: Uživatel, na kterého má cílit tag
|
||||
name: pro
|
||||
tag:
|
||||
description: Jméno tagu pro použití
|
||||
name: tag
|
||||
tickets:
|
||||
name: tickety
|
||||
response:
|
||||
fields:
|
||||
closed:
|
||||
name: Uzavřené tickety
|
||||
open:
|
||||
name: Otevřít tickety
|
||||
title:
|
||||
other: Tickety uživatele {displayName}
|
||||
not_staff:
|
||||
title: ❌ Chyba
|
||||
topic:
|
||||
name: téma
|
||||
buttons:
|
||||
accept_close_request:
|
||||
emoji: ✅
|
||||
text: Přijmout
|
||||
create:
|
||||
text: Vytvořit ticket
|
||||
emoji: 🎫
|
||||
cancel:
|
||||
emoji: ✖️
|
||||
text: Zrušit
|
||||
claim:
|
||||
emoji: 🙌
|
||||
text: Převzít
|
||||
edit:
|
||||
emoji: ✏️
|
||||
text: Upravit
|
||||
close:
|
||||
emoji: ✖️
|
||||
text: Zavřít
|
||||
confirm_open:
|
||||
emoji: ✅
|
||||
text: Vytvořit ticket
|
||||
reject_close_request:
|
||||
emoji: ✖️
|
||||
text: Odmítnut
|
||||
unclaim:
|
||||
emoji: ♻️
|
||||
text: Vydat
|
||||
dm:
|
||||
closed:
|
||||
archived: Chcete-li zobrazit archivované zprávy, zadejte `/transcript` do **{guild}**.
|
||||
title: Váš ticket byl uzavřen
|
||||
fields:
|
||||
closed_by: Zavřeno uživatelem
|
||||
ticket: Ticket
|
||||
closed:
|
||||
name: Zavřeno v
|
||||
value: '{timestamp} (po {duration})'
|
||||
created: Vytvořeno v
|
||||
feedback: Vaše zpětná vazba
|
||||
reason: Zavřeno, protože
|
||||
response: Doba odezvy
|
||||
topic: Téma
|
||||
confirm_open:
|
||||
title: Chcete otevřít ticket s následujícím tématem?
|
||||
log:
|
||||
admin:
|
||||
description:
|
||||
target:
|
||||
tag: značka
|
||||
settings: nastavení
|
||||
category: kategorie
|
||||
panel: panel
|
||||
question: otázka
|
||||
joined: '{user} {verb} {targetType}'
|
||||
verb:
|
||||
create: vytvořeno
|
||||
title:
|
||||
target:
|
||||
panel: Panel
|
||||
category: Kategorie
|
||||
question: Otázka
|
||||
settings: Nastavení
|
||||
tag: Štítek
|
||||
joined: '{targetType} {verb}'
|
||||
changes: Změny
|
||||
misc:
|
||||
error:
|
||||
title: ⚠️ Něco se pokazilo
|
405
src/i18n/de.yml
Normal file
405
src/i18n/de.yml
Normal file
@ -0,0 +1,405 @@
|
||||
buttons:
|
||||
cancel:
|
||||
emoji: ✖️
|
||||
text: Abbrechen
|
||||
close:
|
||||
emoji: ✖️
|
||||
text: Schließen
|
||||
create:
|
||||
text: Ticket erstellen
|
||||
emoji: 🎫
|
||||
accept_close_request:
|
||||
emoji: ✅
|
||||
text: Akzeptieren
|
||||
claim:
|
||||
emoji: 🙌
|
||||
text: Übernehmen
|
||||
confirm_open:
|
||||
emoji: ✅
|
||||
text: Ticket erstellen
|
||||
reject_close_request:
|
||||
emoji: ✖️
|
||||
text: Ablehnen
|
||||
edit:
|
||||
emoji: ✏️
|
||||
text: Bearbeiten
|
||||
unclaim:
|
||||
text: Freigeben
|
||||
emoji: ♻️
|
||||
commands:
|
||||
message:
|
||||
pin:
|
||||
name: Nachricht anheften
|
||||
not_pinnable:
|
||||
description: "Diese Nachricht kann nicht angeheftet werden.\nBitte einen Administrator,\
|
||||
\ die Berechtigungen des Bots zu überprüfen.\n"
|
||||
title: ❌ Fehler
|
||||
not_ticket:
|
||||
description: Du kannst Nachrichten nur in Tickets anheften.
|
||||
title: ❌ Dies ist kein Ticketkanal
|
||||
pinned:
|
||||
description: Die Nachricht wurde angeheftet.
|
||||
title: ✅ Nachricht angeheftet
|
||||
create:
|
||||
name: Ticket aus Nachricht erstellen
|
||||
slash:
|
||||
add:
|
||||
added: ➡️ {added} wurde von {by} hinzugefügt.
|
||||
not_staff:
|
||||
title: ❌ Fehler
|
||||
description: Nur Teammitglieder können Benutzer zu Tickets anderer hinzufügen.
|
||||
options:
|
||||
member:
|
||||
name: mitglied
|
||||
description: Das Mitglied, das dem Ticket hinzugefügt werden soll
|
||||
ticket:
|
||||
name: ticket
|
||||
description: Das Ticket, zu dem das Mitglied hinzugefügt werden soll
|
||||
success:
|
||||
description: '{member} wurde zu {ticket} hinzugefügt.'
|
||||
title: ✅ Hinzugefügt
|
||||
description: Benutzer zu einem Ticket hinzufügen
|
||||
name: hinzufügen
|
||||
help:
|
||||
response:
|
||||
links:
|
||||
support: Unterstützung
|
||||
links: Nützliche Links
|
||||
docs: Dokumentation
|
||||
commands: Vollständige Befehlsliste
|
||||
feedback: Feedback
|
||||
commands: Befehle
|
||||
description: '**Verwende {command} um ein Ticket zu erstellen und Hilfe zu
|
||||
erhalten.**'
|
||||
settings: Bot-Einstellungen
|
||||
name: hilfe
|
||||
title: hilfe
|
||||
description: Zeigt das Hilfemenü an
|
||||
force-close:
|
||||
options:
|
||||
reason:
|
||||
name: grund
|
||||
description: Der Grund für das Schließen des/der Tickets
|
||||
category:
|
||||
name: kategorie
|
||||
description: Schließe alle Tickets einer Kategorie (kann mit `time` benutzt
|
||||
werden)
|
||||
time:
|
||||
name: zeit
|
||||
description: Alle Tickets schließen, die für die angegebene Zeit inaktiv
|
||||
waren (mit `Kategorie` verwendbar)
|
||||
ticket:
|
||||
description: Das Ticket welches geschlossen werden soll
|
||||
name: ticket
|
||||
no_tickets:
|
||||
description: Es gibt keine offenen Tickets, die länger als `{time}` inaktiv
|
||||
waren.
|
||||
title: ❌ Keine Tickets
|
||||
not_staff:
|
||||
title: ❌ Fehler
|
||||
description: Nur Teammitglieder können Tickets zwangsweise abschliessen.
|
||||
confirm_multiple:
|
||||
description: "Du bist dabei **{count}** Tickets zu schließen die länger als\
|
||||
\ `{time}`:\n{tickets} inaktiv waren.\n"
|
||||
title: ❓Bist du dir sicher?
|
||||
description: Ticket zwangsweise schließen
|
||||
name: force-close
|
||||
close:
|
||||
invalid_time:
|
||||
title: ❌ Ungültig
|
||||
description: '`{input}` ist kein gültiges Zeitformat.'
|
||||
options:
|
||||
reason:
|
||||
name: grund
|
||||
description: Der Grund für das Schließen des Tickets
|
||||
description: Beantragen, dass ein Ticket geschlossen wird
|
||||
name: schließen
|
||||
move:
|
||||
name: verschieben
|
||||
description: Verschiebe ein Ticket in eine andere Kategorie
|
||||
moved: 🗃️ {by} hat dieses Ticket von **{from}** nach **{to}** verschoben.
|
||||
options:
|
||||
category:
|
||||
description: Die Kategorie in welche das Ticket verschoben werden soll
|
||||
name: kategorie
|
||||
not_staff:
|
||||
description: Nur Mitarbeiter können Tickets verschieben.
|
||||
title: ❌ Fehler
|
||||
new:
|
||||
name: neu
|
||||
description: Neues Ticket erstellen
|
||||
options:
|
||||
references:
|
||||
name: verweise
|
||||
description: Die Nummer eines zugehörigen Tickets
|
||||
claim:
|
||||
description: Ticket übernehmen
|
||||
name: übernehmen
|
||||
not_staff:
|
||||
title: ❌ Fehler
|
||||
priority:
|
||||
options:
|
||||
priority:
|
||||
description: Die Priorität des Tickets
|
||||
name: priorität
|
||||
choices:
|
||||
HIGH: 🔴 Hoch
|
||||
LOW: 🟢 Niedrig
|
||||
MEDIUM: 🟠 Mittel
|
||||
success:
|
||||
title: ✅ Priorität gesetzt
|
||||
description: Die Priorität dieses Tickets wurde auf `{priority}` gesetzt.
|
||||
name: priorität
|
||||
description: Legt die Priorität eines Tickets fest
|
||||
not_staff:
|
||||
title: ❌ Fehler
|
||||
tickets:
|
||||
not_staff:
|
||||
title: ❌ Fehler
|
||||
description: Nur Teammitglieder können Tickets anderer einsehen.
|
||||
options:
|
||||
member:
|
||||
description: Das Mitglied, dessen Tickets aufgelistet werden sollen
|
||||
name: mitglied
|
||||
name: tickets
|
||||
response:
|
||||
title:
|
||||
own: Deine Tickets
|
||||
other: '{displayName}s Tickets'
|
||||
fields:
|
||||
closed:
|
||||
none:
|
||||
own: "Du hast noch keine Tickets erstellt\nBenutze {new} um ein Ticket\
|
||||
\ zu öffnen.\n"
|
||||
other: '{user} hat noch keine Tickets erstellt.'
|
||||
name: Geschlossene Tickets
|
||||
open:
|
||||
name: Offene Tickets
|
||||
description: Verwende {transcript}, um die Abschrift eines Tickets herunterzuladen.
|
||||
description: Eigene oder fremde Tickets auflisten
|
||||
remove:
|
||||
not_staff:
|
||||
description: Nur Mitarbeiter können Mitglieder aus den Tickets anderer entfernen.
|
||||
title: ❌ Fehler
|
||||
options:
|
||||
member:
|
||||
name: mitglied
|
||||
description: Das Mitglied, das aus dem Ticket entfernt werden soll
|
||||
ticket:
|
||||
name: ticket
|
||||
description: Das Ticket, aus dem das Mitglied entfernt werden soll
|
||||
success:
|
||||
title: ✅ Entfernt
|
||||
description: '{member} wurde von {ticket} entfernt.'
|
||||
description: Entfernt ein Mitglied aus einem Ticket
|
||||
name: entfernen
|
||||
removed: ⬅️ {removed} wurde von {by} entfernt.
|
||||
topic:
|
||||
description: Thema eines Tickets ändern
|
||||
name: thema
|
||||
transcript:
|
||||
name: abschrift
|
||||
options:
|
||||
ticket:
|
||||
name: ticket
|
||||
description: Die Nummer des Tickets von welchem die Abschrift erstellt werden
|
||||
soll
|
||||
description: Abschrift eines Tickets erhalten
|
||||
transfer:
|
||||
name: übertragen
|
||||
transferred_from: 📨 {user} hat dieses Ticket von {from} auf {to} übertragen.
|
||||
options:
|
||||
member:
|
||||
name: mitglied
|
||||
description: Das Mitglied, an das die Eigentümerschaft übertragen werden
|
||||
soll
|
||||
transferred: 📨 {user} hat dieses Ticket an {to} übertragen.
|
||||
description: Ein Ticket an ein anderes Mitglied übertragen
|
||||
tag:
|
||||
options:
|
||||
for:
|
||||
name: für
|
||||
description: Der Benutzer, an den das Tag gerichtet werden soll
|
||||
tag:
|
||||
name: tag
|
||||
description: Der Name des zu verwendenden Tags
|
||||
name: tag
|
||||
description: Verwende einen Tag
|
||||
release:
|
||||
description: Ein Ticket freigeben (unclaim)
|
||||
name: freigeben
|
||||
user:
|
||||
create:
|
||||
name: Erstelle Ticket für Benutzer
|
||||
log:
|
||||
admin:
|
||||
changes: Änderungen
|
||||
description:
|
||||
target:
|
||||
question: eine Frage
|
||||
settings: Einstellungen
|
||||
category: eine Kategorie
|
||||
panel: ein Panel
|
||||
tag: ein Tag
|
||||
joined: '{user} {verb} {targetType}'
|
||||
title:
|
||||
target:
|
||||
category: Kategorie
|
||||
panel: Panel
|
||||
settings: Einstellungen
|
||||
question: Frage
|
||||
tag: Tag
|
||||
joined: '{targetType} {verb}'
|
||||
verb:
|
||||
delete: gelöscht
|
||||
create: erstellt
|
||||
update: Aktualisiert
|
||||
message:
|
||||
description: '{user} {verb} eine Nachricht'
|
||||
verb:
|
||||
delete: gelöscht
|
||||
update: Aktualisiert
|
||||
message: Nachricht
|
||||
title: Nachricht {verb}
|
||||
ticket:
|
||||
title: Ticket {verb}
|
||||
description: '{user} {verb} ein Ticket'
|
||||
ticket: Ticket
|
||||
verb:
|
||||
claim: übernommen
|
||||
close: Geschlossen
|
||||
update: Aktualisiert
|
||||
create: erstellt
|
||||
unclaim: freigegeben
|
||||
added: Mitglieder hinzugefügt
|
||||
removed: Entfernte Mitglieder
|
||||
menus:
|
||||
category:
|
||||
placeholder: Wähle eine Ticketkategorie
|
||||
guild:
|
||||
placeholder: Wähle einen Server
|
||||
misc:
|
||||
blocked:
|
||||
title: ❌ Gesperrt
|
||||
description: Du darfst keine Tickets erstellen.
|
||||
cooldown:
|
||||
description: Bitte warte {time}, bevor du ein weiteres Ticket in dieser Kategorie
|
||||
erstellst.
|
||||
title: ❌Bitte warten
|
||||
error:
|
||||
description: "Entschuldigung, ein unerwarteter Fehler ist aufgetreten.\nBitte\
|
||||
\ gib diese Informationen an einen Administrator weiter.\n"
|
||||
fields:
|
||||
code: Fehlercode
|
||||
identifier: Kennung
|
||||
title: ⚠️ Etwas ist schief gelaufen
|
||||
expired:
|
||||
description: Du hast nicht rechtzeitig geantwortet. Bitte versuche es erneut.
|
||||
title: ⏰ Abgelaufen
|
||||
expires_in: Läuft in {time} ab
|
||||
invalid_ticket:
|
||||
description: Bitte gib ein gültiges Ticket an.
|
||||
title: ❌ Ungültiges Ticket
|
||||
member_limit:
|
||||
description:
|
||||
- Bitte verwende dein vorhandenes Ticket oder schließe es ab, bevor du ein neues
|
||||
erstellst.
|
||||
- "Bitte schließe ein Ticket, bevor du ein neues erstellst.\nVerwende `/Tickets`,\
|
||||
\ um deine bestehenden Tickets anzuzeigen.\n"
|
||||
title:
|
||||
- ❌ Du hast bereits ein Ticket
|
||||
- ❌ Du hast bereits %d offene Tickets
|
||||
not_ticket:
|
||||
title: ❌ Dies ist kein Ticketkanal
|
||||
description: Du kannst diesen Befehl nur in Tickets verwenden.
|
||||
ratelimited:
|
||||
description: Versuche es in ein paar Sekunden erneut.
|
||||
title: 🐢 Bitte langsamer
|
||||
unknown_category:
|
||||
description: Bitte versuche es mit einer anderen Kategorie.
|
||||
title: ❌ Diese Ticketkategorie existiert nicht
|
||||
no_categories:
|
||||
title: ❌ Es gibt keine Ticketkategorien
|
||||
description: Es wurden keine Ticketkategorien konfiguriert.
|
||||
missing_roles:
|
||||
description: Du hast nicht die erforderlichen Rollen, um ein Ticket in dieser
|
||||
Kategorie erstellen zu können.
|
||||
title: ❌ Unzureichende Rollen
|
||||
category_full:
|
||||
description: "Die Kategorie hat ihre maximale Kapazität erreicht.\nBitte versuche\
|
||||
\ es später erneut.\n"
|
||||
title: ❌ Kategorie voll
|
||||
modals:
|
||||
feedback:
|
||||
comment:
|
||||
placeholder: Hast du zusätzliches Feedback?
|
||||
label: Kommentar
|
||||
rating:
|
||||
label: Bewertung
|
||||
placeholder: 1-5
|
||||
title: Wie haben wir das gemacht?
|
||||
topic:
|
||||
label: Thema
|
||||
placeholder: Worum geht es in diesem Ticket?
|
||||
ticket:
|
||||
answers:
|
||||
no_value: '*Keine Antwort*'
|
||||
close:
|
||||
closed:
|
||||
description: Dieser Kanal wird in wenigen Sekunden gelöscht…
|
||||
title: ✅ Ticket geschlossen
|
||||
forbidden:
|
||||
title: ❌ Fehler
|
||||
description: Du bist nicht berechtigt, dieses Ticket zu schließen.
|
||||
rejected: ✋ {user} hat eine Anfrage zum Schließen dieses Tickets abgelehnt.
|
||||
staff_request:
|
||||
title: ❓ Kann dieses Ticket geschlossen werden?
|
||||
archived: "\nDie Nachrichten in diesem Kanal werden zum späteren Nachschlagen\
|
||||
\ archiviert.\n"
|
||||
description: "{requestedBy} möchte dieses Ticket schließen.\nKlicke auf \"Akzeptieren\"\
|
||||
, um es jetzt zu schließen, oder auf \"Ablehnen\", wenn du noch Hilfe benötigst.\n"
|
||||
user_request:
|
||||
title: ❓ {requestedBy} möchte dieses Ticket schließen
|
||||
wait_for_user: ✋ Bitte warte bis der Benutzer antwortet.
|
||||
wait_for_staff: ✋ Bitte wartee, bis das Personal dieses Ticket schließt.
|
||||
created:
|
||||
title: ✅ Ticket erstellt
|
||||
description: 'Dein Ticketkanal wurde erstellt: {channel}.'
|
||||
edited:
|
||||
description: Deine Änderungen wurden gespeichert.
|
||||
title: ✅ Ticket aktualisiert
|
||||
feedback: Danke für deine Rückmeldung.
|
||||
opening_message:
|
||||
content: "{staff}\n{creator} hat ein neues Ticket erstellt\n"
|
||||
fields:
|
||||
topic: Thema
|
||||
references_message:
|
||||
title: ℹ️ Referenz
|
||||
description: Verweist auf [eine Nachricht]({url}), die {timestamp} von {author}
|
||||
gesendet wurde.
|
||||
references_ticket:
|
||||
description: 'Dieses Ticket bezieht sich auf ein vorheriges Ticket:'
|
||||
fields:
|
||||
date: Erstellt am
|
||||
number: Nummer
|
||||
topic: Thema
|
||||
title: ℹ️ Referenz
|
||||
claimed: 🙌 {user} hat dieses Ticket übernommen.
|
||||
released: ♻️ {user} hat das ticket freigegeben.
|
||||
dm:
|
||||
closed:
|
||||
title: Dein Ticket wurde geschlossen
|
||||
fields:
|
||||
closed:
|
||||
name: Geschlossen um
|
||||
value: '{timestamp} (nach {duration})'
|
||||
closed_by: Geschlossen durch
|
||||
created: Erstellt um
|
||||
feedback: Deine Rückmeldung
|
||||
reason: Grund des Schließens
|
||||
response: Reaktionszeit
|
||||
ticket: Ticket
|
||||
topic: Thema
|
||||
archived: Benutze `/transcript` in **{guild}** um archivierte Nachrichten einzusehen.
|
||||
confirm_open:
|
||||
title: Möchtest du ein Ticket mit dem folgendem Thema eröffnen?
|
1
src/i18n/el.yml
Normal file
1
src/i18n/el.yml
Normal file
@ -0,0 +1 @@
|
||||
{}
|
467
src/i18n/en-GB.yml
Normal file
467
src/i18n/en-GB.yml
Normal file
@ -0,0 +1,467 @@
|
||||
buttons:
|
||||
accept_close_request:
|
||||
emoji: ✅
|
||||
text: Accept
|
||||
cancel:
|
||||
emoji: ➖
|
||||
text: Cancel
|
||||
claim:
|
||||
emoji: 🙌
|
||||
text: Claim
|
||||
close:
|
||||
emoji: ✖️
|
||||
text: Close
|
||||
confirm_open:
|
||||
emoji: ✅
|
||||
text: Create ticket
|
||||
create:
|
||||
emoji: 🎫
|
||||
text: Create a ticket
|
||||
edit:
|
||||
emoji: ✏️
|
||||
text: Edit
|
||||
reject_close_request:
|
||||
emoji: ✖️
|
||||
text: Reject
|
||||
unclaim:
|
||||
emoji: ♻️
|
||||
text: Release
|
||||
commands:
|
||||
message:
|
||||
create:
|
||||
name: Create ticket from message
|
||||
pin:
|
||||
name: Pin message
|
||||
not_pinnable:
|
||||
description: |
|
||||
This message can't be pinned.
|
||||
Please ask an administrator to check the bot's permissions.
|
||||
title: ❌ Error
|
||||
not_ticket:
|
||||
description: You can only pin messages in tickets.
|
||||
title: ❌ This isn't a ticket channel
|
||||
pinned:
|
||||
description: The message has been pinned.
|
||||
title: ✅ Pinned message
|
||||
slash:
|
||||
add:
|
||||
added: ➡️ {added} has been added by {by}.
|
||||
description: Add a member to a ticket
|
||||
name: add
|
||||
not_staff:
|
||||
description: Only staff members can add members to others' tickets.
|
||||
title: ❌ Error
|
||||
options:
|
||||
member:
|
||||
description: The member to add to the ticket
|
||||
name: member
|
||||
ticket:
|
||||
description: The ticket to add the member to
|
||||
name: ticket
|
||||
success:
|
||||
description: "{member} has been added to {ticket}."
|
||||
title: ✅ Added
|
||||
claim:
|
||||
description: Claim a ticket
|
||||
name: claim
|
||||
not_staff:
|
||||
description: Only staff members can claim tickets.
|
||||
title: ❌ Error
|
||||
close:
|
||||
description: Request a ticket to be closed
|
||||
invalid_time:
|
||||
description: "`{input}` is not a valid time format."
|
||||
title: ❌ Invalid
|
||||
name: close
|
||||
options:
|
||||
reason:
|
||||
description: The reason for closing the ticket
|
||||
name: reason
|
||||
force-close:
|
||||
closed_one:
|
||||
description: The channel will be deleted in a few seconds.
|
||||
title: ✅ Ticket closed
|
||||
confirm_multiple:
|
||||
description: >
|
||||
You are about to close **{count}** tickets that have been inactive
|
||||
for more than `{time}`:
|
||||
|
||||
{tickets}
|
||||
title: ❓ Are you sure?
|
||||
confirmed_multiple:
|
||||
description: The channels will be deleted in a few seconds.
|
||||
title:
|
||||
- ✅ Closing %d ticket
|
||||
- ✅ Closing %d tickets
|
||||
description: Forcibly close a ticket
|
||||
name: force-close
|
||||
no_tickets:
|
||||
description: >-
|
||||
There are no open tickets that have been inactive for more than
|
||||
`{time}`.
|
||||
title: ❌ No tickets
|
||||
not_staff:
|
||||
description: Only staff members can force-close tickets.
|
||||
title: ❌ Error
|
||||
options:
|
||||
category:
|
||||
description:
|
||||
Close all tickets in the specified category (must be used with
|
||||
`time`)
|
||||
name: category
|
||||
reason:
|
||||
description: The reason for closing the ticket(s)
|
||||
name: reason
|
||||
ticket:
|
||||
description: The ticket to close
|
||||
name: ticket
|
||||
time:
|
||||
description: Close all tickets that have been inactive for the specified time
|
||||
name: time
|
||||
help:
|
||||
description: Show the help menu
|
||||
name: help
|
||||
response:
|
||||
commands: Commands
|
||||
description: "**Use {command} to create a ticket and get support.**"
|
||||
links:
|
||||
commands: Full command list
|
||||
docs: Documentation
|
||||
feedback: Feedback
|
||||
links: Useful links
|
||||
support: Support
|
||||
settings: Bot settings
|
||||
title: Help
|
||||
move:
|
||||
description: Move a ticket to another category
|
||||
moved: 🗃️ {by} has moved this ticket from **{from}** to **{to}**.
|
||||
name: move
|
||||
not_staff:
|
||||
description: Only staff members can move tickets.
|
||||
title: ❌ Error
|
||||
options:
|
||||
category:
|
||||
description: The category to move the ticket to
|
||||
name: category
|
||||
new:
|
||||
description: Create a new ticket
|
||||
name: new
|
||||
options:
|
||||
references:
|
||||
description: The number of a related ticket
|
||||
name: references
|
||||
priority:
|
||||
description: Set the priority of a ticket
|
||||
name: priority
|
||||
not_staff:
|
||||
description: Only staff members can change the priority of tickets.
|
||||
title: ❌ Error
|
||||
options:
|
||||
priority:
|
||||
choices:
|
||||
HIGH: 🔴 High
|
||||
LOW: 🟢 Low
|
||||
MEDIUM: 🟠 Medium
|
||||
description: The priority of the ticket
|
||||
name: priority
|
||||
success:
|
||||
description: This ticket's priority has been set to `{priority}`.
|
||||
title: ✅ Priority set
|
||||
release:
|
||||
description: Release (unclaim) a ticket
|
||||
name: release
|
||||
remove:
|
||||
description: Remove a member from a ticket
|
||||
name: remove
|
||||
not_staff:
|
||||
description: Only staff members can remove members from others' tickets.
|
||||
title: ❌ Error
|
||||
options:
|
||||
member:
|
||||
description: The member to remove from the ticket
|
||||
name: member
|
||||
ticket:
|
||||
description: The ticket to remove the member from
|
||||
name: ticket
|
||||
removed: ⬅️ {removed} has been removed by {by}.
|
||||
success:
|
||||
description: "{member} has been removed from {ticket}."
|
||||
title: ✅ Removed
|
||||
tag:
|
||||
description: Use a tag
|
||||
name: tag
|
||||
options:
|
||||
for:
|
||||
description: The user to target the tag to
|
||||
name: for
|
||||
tag:
|
||||
description: The name of the tag to use
|
||||
name: tag
|
||||
tickets:
|
||||
description: List your own or someone else's tickets
|
||||
name: tickets
|
||||
not_staff:
|
||||
description: Only staff members can view others' tickets.
|
||||
title: ❌ Error
|
||||
options:
|
||||
member:
|
||||
description: The member to list the tickets of
|
||||
name: member
|
||||
response:
|
||||
description: Use {transcript} to download the transcript of a ticket.
|
||||
fields:
|
||||
closed:
|
||||
name: Closed tickets
|
||||
none:
|
||||
other: "{user} hasn't made any tickets."
|
||||
own: |
|
||||
You haven't made any tickets.
|
||||
Use {new} to open a ticket.
|
||||
open:
|
||||
name: Open tickets
|
||||
title:
|
||||
other: "{displayName}'s tickets"
|
||||
own: Your tickets
|
||||
topic:
|
||||
description: Change the topic of a ticket
|
||||
name: topic
|
||||
transcript:
|
||||
description: Get the transcript of a ticket
|
||||
name: transcript
|
||||
options:
|
||||
member:
|
||||
description: The member to search for tickets of
|
||||
name: member
|
||||
ticket:
|
||||
description: The ticket to get the transcript of
|
||||
name: ticket
|
||||
transfer:
|
||||
description: Transfer ownership of a ticket to another member
|
||||
name: transfer
|
||||
options:
|
||||
member:
|
||||
description: The member to transfer ownership to
|
||||
name: member
|
||||
transferred: 📨 {user} has transferred this ticket to {to}.
|
||||
transferred_from: 📨 {user} has transferred this ticket from {from} to {to}.
|
||||
user:
|
||||
create:
|
||||
name: Create ticket for user
|
||||
not_staff:
|
||||
description: Only staff members can open tickets for other members.
|
||||
title: ❌ Error
|
||||
prompt:
|
||||
description: Click the button below to create a ticket.
|
||||
title: Please create a ticket
|
||||
sent:
|
||||
description: "{user} has been invited to create a ticket in **{category}**."
|
||||
title: ✅ Prompt sent
|
||||
dm:
|
||||
closed:
|
||||
archived: Use the `/transcript` command in **{guild}** to view the archived messages.
|
||||
fields:
|
||||
closed:
|
||||
name: Closed at
|
||||
value: "{timestamp} (after {duration})"
|
||||
closed_by: Closed by
|
||||
created: Created at
|
||||
feedback: Your feedback
|
||||
reason: Closed because
|
||||
response: Response time
|
||||
ticket: Ticket
|
||||
topic: Topic
|
||||
title: Your ticket has been closed
|
||||
confirm_open:
|
||||
title: Do you want to open a ticket with the following topic?
|
||||
log:
|
||||
admin:
|
||||
changes: Changes
|
||||
description:
|
||||
joined: "{user} {verb} {targetType}"
|
||||
target:
|
||||
category: a category
|
||||
panel: a panel
|
||||
question: a question
|
||||
settings: the settings
|
||||
tag: a tag
|
||||
title:
|
||||
joined: "{targetType} {verb}"
|
||||
target:
|
||||
category: Category
|
||||
panel: Panel
|
||||
question: Question
|
||||
settings: Settings
|
||||
tag: Tag
|
||||
verb:
|
||||
create: created
|
||||
delete: deleted
|
||||
update: updated
|
||||
message:
|
||||
description: "{user} {verb} a message"
|
||||
message: Message
|
||||
title: Message {verb}
|
||||
verb:
|
||||
delete: deleted
|
||||
update: updated
|
||||
ticket:
|
||||
added: Added members
|
||||
description: "{user} {verb} a ticket"
|
||||
removed: Removed members
|
||||
ticket: Ticket
|
||||
title: Ticket {verb}
|
||||
verb:
|
||||
claim: claimed
|
||||
close: closed
|
||||
create: created
|
||||
unclaim: released
|
||||
update: updated
|
||||
menus:
|
||||
category:
|
||||
placeholder: Select a ticket category
|
||||
guild:
|
||||
placeholder: Select a server
|
||||
misc:
|
||||
blocked:
|
||||
description: You are not allowed to create tickets.
|
||||
title: ❌ Blocked
|
||||
category_full:
|
||||
description: |
|
||||
The category has reached its maximum capacity.
|
||||
Please try again later.
|
||||
title: ❌ Category full
|
||||
cooldown:
|
||||
description: Please wait {time} before creating another ticket in this category.
|
||||
title: ❌ Please wait
|
||||
error:
|
||||
description: |
|
||||
Sorry, an unexpected error occurred.
|
||||
Please give this information to an administrator.
|
||||
fields:
|
||||
code: Error code
|
||||
identifier: Identifier
|
||||
title: ⚠️ Something went wrong
|
||||
expired:
|
||||
description: You didn't respond in time. Please try again.
|
||||
title: ⏰ Expired
|
||||
expires_in: Expires in {time}
|
||||
invalid_ticket:
|
||||
description: Please specify a valid ticket.
|
||||
title: ❌ Invalid ticket
|
||||
member_limit:
|
||||
description:
|
||||
- Please use your existing ticket or close it before creating another.
|
||||
- |
|
||||
Please close a ticket before creating another.
|
||||
Use `/tickets` to view your existing tickets.
|
||||
title:
|
||||
- ❌ You already have a ticket
|
||||
- ❌ You already have %d open tickets
|
||||
missing_roles:
|
||||
description: >-
|
||||
You do not have the roles required to be able to create a ticket in
|
||||
this category.
|
||||
title: ❌ Insufficient roles
|
||||
no_categories:
|
||||
description: No ticket categories have been configured.
|
||||
title: ❌ There are no ticket categories
|
||||
not_ticket:
|
||||
description: You can only use this command in tickets.
|
||||
title: ❌ This isn't a ticket channel
|
||||
ratelimited:
|
||||
description: Try again in a few seconds.
|
||||
title: 🐢 Please slow down
|
||||
unknown_category:
|
||||
description: Please try a different category.
|
||||
title: ❌ That ticket category doesn't exist
|
||||
update:
|
||||
description: |
|
||||
> [View `{version}` on GitHub]({github})
|
||||
> [Changelog]({changelog})
|
||||
> [Update guide]({guide})
|
||||
title: An update is available
|
||||
modals:
|
||||
feedback:
|
||||
comment:
|
||||
label: Comment
|
||||
placeholder: Do you have any additional feedback?
|
||||
rating:
|
||||
label: Rating
|
||||
placeholder: 1-5
|
||||
title: How did we do?
|
||||
topic:
|
||||
label: Topic
|
||||
placeholder: What is this ticket about?
|
||||
ticket:
|
||||
answers:
|
||||
no_value: "*No response*"
|
||||
claimed: 🙌 {user} has claimed this ticket.
|
||||
close:
|
||||
closed:
|
||||
description: This channel will be deleted in a few seconds…
|
||||
title: ✅ Ticket closed
|
||||
forbidden:
|
||||
description: You don't have permission to close this ticket.
|
||||
title: ❌ Error
|
||||
rejected: ✋ {user} rejected a request to close this ticket.
|
||||
staff_request:
|
||||
archived: |
|
||||
|
||||
The messages in this channel will be archived for future reference.
|
||||
description: |
|
||||
{requestedBy} wants to close this ticket.
|
||||
Click "Accept" to close it now, or "Reject" if you still need help.
|
||||
title: ❓ Can this ticket be closed?
|
||||
user_request:
|
||||
title: ❓ {requestedBy} wants to close this ticket
|
||||
wait_for_staff: ✋ Please wait for staff to close this ticket.
|
||||
wait_for_user: ✋ Please wait for the user to respond.
|
||||
closing_soon:
|
||||
description: |
|
||||
This ticket will be closed due to inactivity <t:{timestamp}:R>.
|
||||
Send a message to cancel this automation.
|
||||
title: ⌛ This ticket will be closed soon
|
||||
created:
|
||||
description: "Your ticket channel has been created: {channel}."
|
||||
title: ✅ Ticket created
|
||||
edited:
|
||||
description: Your changes have been saved.
|
||||
title: ✅ Ticket updated
|
||||
feedback: Thank you for your feedback.
|
||||
inactive:
|
||||
description: |
|
||||
There hasn't been any activity in this channel since <t:{timestamp}:R>.
|
||||
Please continue the conversation or {close} the ticket.
|
||||
title: ⏰ This ticket is inactive
|
||||
offline:
|
||||
description:
|
||||
There aren't any staff members available at the moment, so it may
|
||||
take longer than usual to get a response.
|
||||
title: 😴 We're not online
|
||||
opening_message:
|
||||
content: |
|
||||
{staff}
|
||||
{creator} has created a new ticket
|
||||
fields:
|
||||
topic: Topic
|
||||
references_message:
|
||||
description: References [a message]({url}) sent {timestamp} by {author}.
|
||||
title: ℹ️ Reference
|
||||
references_ticket:
|
||||
description: "This ticket is related to a previous ticket:"
|
||||
fields:
|
||||
date: Created at
|
||||
number: Number
|
||||
topic: Topic
|
||||
title: ℹ️ Reference
|
||||
released: ♻️ {user} has released this ticket.
|
||||
working_hours:
|
||||
next:
|
||||
description:
|
||||
We'll be back at <t:{timestamp}:F> (<t:{timestamp}:R>), although
|
||||
you may receive a response before then.
|
||||
title: 🕗 We're not working at the moment
|
||||
today:
|
||||
description:
|
||||
You may receive a response before, but we don't start working until
|
||||
<t:{timestamp}:t> today (<t:{timestamp}:R>).
|
||||
title: 🕗 We're not working at the moment
|
164
src/i18n/fi.yml
Normal file
164
src/i18n/fi.yml
Normal file
@ -0,0 +1,164 @@
|
||||
commands:
|
||||
message:
|
||||
pin:
|
||||
not_ticket:
|
||||
title: ❌ Tämä ei ole tiketti kanava
|
||||
description: Voit kiinnittää viestejä vain tiketeissä.
|
||||
name: Kiinnitä viesti
|
||||
not_pinnable:
|
||||
title: ❌ Virhe
|
||||
pinned:
|
||||
description: Viesti on kiinnitetty.
|
||||
title: ✅ Kiinnitetty viesti
|
||||
create:
|
||||
name: Luo tiketti viestistä
|
||||
slash:
|
||||
add:
|
||||
description: Lisää jäsen tikettiin
|
||||
name: lisää
|
||||
options:
|
||||
ticket:
|
||||
name: tiketti
|
||||
description: Tiketti johon jäsen lisätään
|
||||
member:
|
||||
name: jäsen
|
||||
description: Jäsen joka lisätään tikettiin
|
||||
not_staff:
|
||||
description: Vain henkilökunta voi lisätä toisia tiketteihin.
|
||||
title: ❌ Virhe
|
||||
success:
|
||||
title: ✅ Lisätty
|
||||
description: '{member} lisättiin {ticket}'
|
||||
force-close:
|
||||
not_staff:
|
||||
title: ❌ Virhe
|
||||
no_tickets:
|
||||
title: ❌ Ei tikettejä
|
||||
options:
|
||||
category:
|
||||
name: kategoria
|
||||
reason:
|
||||
name: syy
|
||||
ticket:
|
||||
name: tiketti
|
||||
time:
|
||||
name: aika
|
||||
confirm_multiple:
|
||||
title: ❓ Oletko varma?
|
||||
help:
|
||||
description: Näyttää apua valikon
|
||||
response:
|
||||
links:
|
||||
feedback: Palaute
|
||||
links: Hyödyllisiä linkkejä
|
||||
support: Tuki
|
||||
commands: Koko komento lista
|
||||
settings: Botin asetukset
|
||||
commands: Komennot
|
||||
title: Apua
|
||||
name: apua
|
||||
move:
|
||||
name: siirrä
|
||||
options:
|
||||
category:
|
||||
name: kategoria
|
||||
description: Siirrä tiketti toiseen kategoriaan
|
||||
remove:
|
||||
options:
|
||||
member:
|
||||
description: Jäsen joka poistetaan tiketistä
|
||||
name: jäsen
|
||||
ticket:
|
||||
name: tiketti
|
||||
name: poista
|
||||
not_staff:
|
||||
title: ❌ Virhe
|
||||
success:
|
||||
title: ✅ Poistettu
|
||||
description: Poista jäsen tiketistä
|
||||
close:
|
||||
invalid_time:
|
||||
title: ❌ Virheellinen
|
||||
name: sulje
|
||||
options:
|
||||
reason:
|
||||
name: syy
|
||||
description: Syy tiketin(t) sulkemiseen
|
||||
description: Pyydä tiketin sulkemista
|
||||
claim:
|
||||
description: Varaa tiketti
|
||||
name: varaa
|
||||
new:
|
||||
description: Luo uusi tiketti
|
||||
name: uusi
|
||||
priority:
|
||||
options:
|
||||
priority:
|
||||
choices:
|
||||
LOW: 🟢 Hidas
|
||||
HIGH: 🔴 Kiireellinen
|
||||
MEDIUM: 🟠 Normaali
|
||||
topic:
|
||||
name: aihe
|
||||
tickets:
|
||||
not_staff:
|
||||
title: ❌ Virhe
|
||||
response:
|
||||
title:
|
||||
other: '{displayName} tiketit'
|
||||
own: Sinun tiketit
|
||||
fields:
|
||||
closed:
|
||||
name: Suljetut tiketit
|
||||
name: tiketit
|
||||
options:
|
||||
member:
|
||||
name: jäsen
|
||||
transfer:
|
||||
options:
|
||||
member:
|
||||
name: jäsen
|
||||
name: siirrä
|
||||
transcript:
|
||||
options:
|
||||
ticket:
|
||||
name: tiketti
|
||||
release:
|
||||
name: julkaise
|
||||
buttons:
|
||||
close:
|
||||
text: Sulje
|
||||
emoji: ✖️
|
||||
edit:
|
||||
text: Muokkaa
|
||||
emoji: ✏️
|
||||
reject_close_request:
|
||||
text: Hylkää
|
||||
emoji: ✖️
|
||||
unclaim:
|
||||
text: Julkaise
|
||||
emoji: ♻️
|
||||
accept_close_request:
|
||||
text: Hyväksy
|
||||
emoji: ✅
|
||||
cancel:
|
||||
text: Peruuta
|
||||
emoji: ✖️
|
||||
confirm_open:
|
||||
text: Luo tiketti
|
||||
emoji: ✅
|
||||
create:
|
||||
emoji: 🎫
|
||||
text: Luo tiketti
|
||||
claim:
|
||||
text: Varaa
|
||||
emoji: 🙌
|
||||
ticket:
|
||||
opening_message:
|
||||
fields:
|
||||
topic: Aihe
|
||||
references_ticket:
|
||||
fields:
|
||||
topic: Aihe
|
||||
number: Numero
|
||||
feedback: Kiitos palautteestasi.
|
410
src/i18n/fr.yml
Normal file
410
src/i18n/fr.yml
Normal file
@ -0,0 +1,410 @@
|
||||
buttons:
|
||||
accept_close_request:
|
||||
text: Accepter
|
||||
emoji: ✅
|
||||
cancel:
|
||||
emoji: ✖️
|
||||
text: Annuler
|
||||
claim:
|
||||
emoji: 🙌
|
||||
text: S'approprier
|
||||
close:
|
||||
text: Fermer
|
||||
emoji: ✖️
|
||||
reject_close_request:
|
||||
emoji: ✖️
|
||||
text: Rejeter
|
||||
confirm_open:
|
||||
emoji: ✅
|
||||
text: Créer un ticket
|
||||
create:
|
||||
text: Créer un ticket
|
||||
emoji: 🎫
|
||||
edit:
|
||||
emoji: ✏️
|
||||
text: Modifier
|
||||
unclaim:
|
||||
emoji: ♻️
|
||||
text: Relâcher
|
||||
commands:
|
||||
message:
|
||||
pin:
|
||||
name: Épingler le message
|
||||
not_pinnable:
|
||||
title: ❌ Erreur
|
||||
description: "Ce message ne peut être épinglé.\nVeuillez demander à un administrateur\
|
||||
\ de vérifier les permissions du bot\n"
|
||||
pinned:
|
||||
title: ✅ Message épinglé
|
||||
description: Les message à été épinglé.
|
||||
not_ticket:
|
||||
title: ❌ Ceci n'est pas un salon de ticket
|
||||
description: Vous pouvez seulement épingler des messages dans les tickets.
|
||||
create:
|
||||
name: Transformer ce message en ticket
|
||||
slash:
|
||||
add:
|
||||
not_staff:
|
||||
title: ❌ Erreur
|
||||
description: Seul les membres de l'équipe peuvent ajouter des membres aux
|
||||
tickets des autres.
|
||||
success:
|
||||
title: ✅ Ajouté
|
||||
description: '{member} à été ajouté au ticket {ticket}.'
|
||||
added: '{added} à été ajouté par {by}.'
|
||||
description: Ajouter un membre à un ticket
|
||||
name: ajouter
|
||||
options:
|
||||
member:
|
||||
description: Le membre à ajouter au ticket
|
||||
name: membre
|
||||
ticket:
|
||||
description: Le ticket auquel ajouter le membre
|
||||
name: ticket
|
||||
close:
|
||||
invalid_time:
|
||||
title: ❌ Invalide
|
||||
description: "`{input}` n'est pas un format de date valide."
|
||||
options:
|
||||
reason:
|
||||
description: La raison de la clôture du ticket
|
||||
name: raison
|
||||
description: Demander la fermeture d'un ticket
|
||||
name: fermer
|
||||
force-close:
|
||||
options:
|
||||
ticket:
|
||||
name: ticket
|
||||
description: le ticket à clôturer
|
||||
category:
|
||||
description: Fermer tout les tickets dans la catégorie spécifiée (peut être
|
||||
utilisée avec `time`)
|
||||
name: catégorie
|
||||
time:
|
||||
description: Clôturer tout les tickets inactifs depuis le temps spécifié
|
||||
(peut être utilisé avec `category`)
|
||||
name: temps
|
||||
reason:
|
||||
name: raison
|
||||
description: La raison de la fermeture du/des ticket(s)
|
||||
description: Forcer la clôture d'un ticket
|
||||
name: fermeture-forcée
|
||||
no_tickets:
|
||||
description: Il n'y as pas de tickets ouverts inactifs depuis plus de `{time}`.
|
||||
title: ❌ Aucun ticket
|
||||
not_staff:
|
||||
description: Seuls les membres de l'équipe peuvent forcer la fermeture de
|
||||
tickets.
|
||||
title: ❌ Erreur
|
||||
confirm_multiple:
|
||||
description: "Vous êtes sur le point de clôturer **{count}** tickets inactifs\
|
||||
\ depuis plus de `{time}` :\n{tickets}\n"
|
||||
title: ❓Êtes vous sûr ?
|
||||
claim:
|
||||
description: S'approprier un ticket
|
||||
name: claim
|
||||
not_staff:
|
||||
description: Seul les membres de l'équipe peuvent s'approprier un ticket.
|
||||
title: ❌ Erreur
|
||||
help:
|
||||
name: aide
|
||||
response:
|
||||
links:
|
||||
commands: Liste complète des commandes
|
||||
docs: Documentation
|
||||
support: Support
|
||||
links: Liens utiles
|
||||
feedback: Retour
|
||||
commands: Commandes
|
||||
settings: Paramètres du bot
|
||||
description: '**Utilise {command} pour créer un ticket et obtenir un support.**'
|
||||
title: Aide
|
||||
description: Afficher le menu d'aide
|
||||
move:
|
||||
description: Déplacer un ticket dans une autre catégorie
|
||||
options:
|
||||
category:
|
||||
name: catégorie
|
||||
description: La catégorie dans laquelle déplacer le ticket
|
||||
moved: 🗃️ {by} as déplacé ce ticket de **{from}** vers **{to}**.
|
||||
name: déplacer
|
||||
not_staff:
|
||||
description: Seuls les membres de l'équipe peuvent déplacer des tickets.
|
||||
title: ❌ Erreur
|
||||
new:
|
||||
name: nouveau
|
||||
description: Créer un nouveau ticket
|
||||
options:
|
||||
references:
|
||||
description: Le numéro d'un ticket associé
|
||||
name: références
|
||||
priority:
|
||||
name: priorité
|
||||
options:
|
||||
priority:
|
||||
choices:
|
||||
MEDIUM: 🟠 Moyenne
|
||||
HIGH: 🔴 Haute
|
||||
LOW: 🟢 Basse
|
||||
description: La priorité du ticket
|
||||
name: priorité
|
||||
description: Définir la priorité d'un ticket
|
||||
success:
|
||||
title: ✅ Priorité définie
|
||||
description: La priorité du ticket à été définie sur `{priority}`.
|
||||
not_staff:
|
||||
title: ❌ Erreur
|
||||
description: Seuls les membres de l'équipe peuvent changer la priorité d'un
|
||||
ticket.
|
||||
release:
|
||||
name: relâcher
|
||||
description: Relâcher (ne plus s'approprier) un ticket
|
||||
remove:
|
||||
description: Retirer un membre d'un ticket
|
||||
name: retirer
|
||||
success:
|
||||
title: ✅ Retiré
|
||||
description: '{member} as été retiré de {ticket}.'
|
||||
options:
|
||||
member:
|
||||
description: Le membre à retirer du ticket
|
||||
name: membre
|
||||
ticket:
|
||||
description: Le ticket dont il faut retirer le membre
|
||||
name: ticket
|
||||
removed: ⬅️ {removed} as été retiré par {by}.
|
||||
not_staff:
|
||||
description: Seuls les membres de l'équipe peuvent retirer des membres des
|
||||
tickets des autres.
|
||||
title: ❌ Erreur
|
||||
tag:
|
||||
name: tag
|
||||
description: Utiliser un tag
|
||||
options:
|
||||
for:
|
||||
description: L'utilisateur à qui attribuer le tag
|
||||
name: pour
|
||||
tag:
|
||||
description: Le nom à utiliser pour le tag
|
||||
name: tag
|
||||
tickets:
|
||||
options:
|
||||
member:
|
||||
name: membre
|
||||
description: Le membre dont il faut lister les tickets
|
||||
description: Lister votre tickets ou ceux de quelqu'un d'autre
|
||||
not_staff:
|
||||
description: Seuls les membres de l'équipe peuvent voir les tickets des autres
|
||||
membres.
|
||||
title: ❌ Erreur
|
||||
response:
|
||||
fields:
|
||||
closed:
|
||||
none:
|
||||
other: "{user} n'as ouvert aucun ticket."
|
||||
own: "Vous n'avez ouvert aucun ticket\nUtilisez {new} pour ouvrir un\
|
||||
\ ticket.\n"
|
||||
name: Tickets clôturés
|
||||
open:
|
||||
name: Tickets ouverts
|
||||
description: Utilisez {transcript} pour télécharger la transcription du ticket.
|
||||
title:
|
||||
other: Tickets de {displayName}
|
||||
own: Vos tickets
|
||||
name: tickets
|
||||
transcript:
|
||||
name: transcription
|
||||
description: Obtenir la transcription d'un ticket
|
||||
options:
|
||||
ticket:
|
||||
name: ticket
|
||||
description: Le nombre de ticket dont vous souhaitez obtenir la transcription
|
||||
transfer:
|
||||
description: Transférer la propriété d'un ticket à un autre membre
|
||||
name: transférer
|
||||
transferred_from: '{user} as transféré ce ticket de {from} à {to}.'
|
||||
options:
|
||||
member:
|
||||
description: Le membre à qui transférer la propriété
|
||||
name: membre
|
||||
transferred: '{user} as transféré ce ticket à {to}.'
|
||||
topic:
|
||||
name: sujet
|
||||
description: Changer le sujet d'un ticket
|
||||
user:
|
||||
create:
|
||||
name: Créer un ticket pour un membre
|
||||
misc:
|
||||
update:
|
||||
description: "> [Voir`{version}` sur GitHub]({github})\n> [Notice de changement]({changelog})\n\
|
||||
> [Guide de mise à jour]({guide})\n"
|
||||
title: Une mise à jour est disponible
|
||||
category_full:
|
||||
description: "Cette catégorie as atteint sa capacité maximum.\nVeuillez réessayer\
|
||||
\ ultérieurement.\n"
|
||||
title: ❌ Catégorie pleine
|
||||
expires_in: Expire dans {time}
|
||||
not_ticket:
|
||||
title: ❌ Ce salon n'est pas un ticket
|
||||
description: Vous ne pouvez utiliser cette commande que dans un ticket.
|
||||
ratelimited:
|
||||
description: Réessayez dans quelques secondes.
|
||||
title: 🐢 Veuillez ralentir
|
||||
blocked:
|
||||
description: Vous n'êtes pas autorisé à créer un ticket.
|
||||
title: ❌ Bloqué
|
||||
cooldown:
|
||||
title: ❌ Veuillez patienter
|
||||
description: Veuillez patienter {time} avant de créer un autre ticket dans cette
|
||||
catégorie.
|
||||
error:
|
||||
fields:
|
||||
code: Code d'erreur
|
||||
identifier: Identifiant
|
||||
description: "Désolé, une erreur inattendue s'est produite.\nVeuillez transmettre\
|
||||
\ cette information à un administrateur.\n"
|
||||
title: ⚠️ Quelque chose s'est mal passé
|
||||
missing_roles:
|
||||
description: Vous n'avez pas les rôles requis pour créer un ticket dans cette
|
||||
catégorie.
|
||||
title: ❌ Rôles manquants
|
||||
no_categories:
|
||||
description: Aucune catégorie de ticket n'est configurée.
|
||||
title: ❌ Il n'existe pas de catégorie de ticket
|
||||
expired:
|
||||
description: Vous n'avez pas répondu à temps. Veuillez réessayer.
|
||||
title: ⏰ Expiré
|
||||
invalid_ticket:
|
||||
description: Veuillez spécifier un ticket valide.
|
||||
title: ❌ Ticket invalide
|
||||
member_limit:
|
||||
title:
|
||||
- ❌ Vous avez déjà un ticket
|
||||
- Vous avez déjà %d tickets ouverts
|
||||
description:
|
||||
- Veuillez utiliser votre ticket ouvert ou le clôturer avant d'en créer un nouveau.
|
||||
- "Veuillez clôturer votre ticket avant d'en créer un autre.\nUtilisez `/tickets`\
|
||||
\ afin de voir vos tickets existants.\n"
|
||||
unknown_category:
|
||||
description: Veuillez tenter une autre catégorie.
|
||||
title: ❌ Cette catégorie de ticket n'existe pas
|
||||
modals:
|
||||
feedback:
|
||||
rating:
|
||||
label: Notation
|
||||
placeholder: 1-5
|
||||
title: Comment avons-nous fait ?
|
||||
comment:
|
||||
label: Commentaire
|
||||
placeholder: Avez vous un retour supplémentaire ?
|
||||
topic:
|
||||
label: Sujet
|
||||
placeholder: Quelle est la raison de ce ticket ?
|
||||
ticket:
|
||||
answers:
|
||||
no_value: '*Pas de réponse*'
|
||||
claimed: 🙌 {user} s'est approprié ce ticket.
|
||||
close:
|
||||
closed:
|
||||
description: Ce salon sera supprimé dans quelques secondes…
|
||||
title: ✅ Ticket clôturé
|
||||
staff_request:
|
||||
title: ❓ Ce ticket peut il être clôturé ?
|
||||
description: "{requestedBy} souhaite clôturer ce ticket.\nCliquez sur \"Accepter\"\
|
||||
\ pour le fermer, or \"Refuser\" si vous avez toujours besoin d'aide.\n"
|
||||
archived: "\nLes messages de ce salon seront archivés pour une référence future.\n"
|
||||
forbidden:
|
||||
title: ❌ Erreur
|
||||
description: Vous n'avez pas la permission de clôturer ce ticket.
|
||||
rejected: ✋ {user} as rejeté une demande de clôture de ce ticket.
|
||||
user_request:
|
||||
title: ❓ {requestedBy} souhaite clôturer le ticket
|
||||
wait_for_staff: ✋ Veuillez attendre que l'équipe ferme ce ticket.
|
||||
wait_for_user: ✋ Veuillez attendre une réponse de l'utilisateur.
|
||||
released: ♻️ {user} as relâché ce ticket.
|
||||
references_message:
|
||||
title: ℹ️ Référence
|
||||
description: Fait référence à [a message]({url}) envoyé {timestamp} par {author}.
|
||||
references_ticket:
|
||||
title: ℹ️ Référence
|
||||
fields:
|
||||
number: Nombre
|
||||
topic: Sujet
|
||||
date: Crée à
|
||||
description: 'Ce ticket est lié à un ticket précédent :'
|
||||
opening_message:
|
||||
content: "{staff}\n{creator} as ouvert un nouveau ticket\n"
|
||||
fields:
|
||||
topic: Sujet
|
||||
edited:
|
||||
title: ✅ Ticket mis à jour
|
||||
description: Vos modifications ont été enregistrées.
|
||||
created:
|
||||
description: 'Votre ticket as été crée : {channel}.'
|
||||
title: ✅ Ticket crée
|
||||
feedback: Merci pour votre retour.
|
||||
dm:
|
||||
closed:
|
||||
title: Votre ticket as été clôturé
|
||||
fields:
|
||||
created: Crée à
|
||||
closed:
|
||||
name: Clôturé à
|
||||
value: '{timestamp} (après {duration})'
|
||||
closed_by: Clôturé par
|
||||
feedback: Vos retours
|
||||
ticket: Ticket
|
||||
topic: Sujet
|
||||
reason: Raison de la clôture
|
||||
response: Temps de réponse
|
||||
archived: Utilisez la commande `/transcript` sur **{guild}** pour voir les messages
|
||||
archivés.
|
||||
confirm_open:
|
||||
title: Souhaitez vous ouvrir un ticket avec le sujet suivant ?
|
||||
log:
|
||||
admin:
|
||||
changes: Modifications
|
||||
description:
|
||||
joined: '{user} {verb} {targetType}'
|
||||
target:
|
||||
tag: un tag
|
||||
question: une question
|
||||
settings: les paramètres
|
||||
panel: un panneau
|
||||
category: une catégorie
|
||||
title:
|
||||
target:
|
||||
tag: Tag
|
||||
category: Catégorie
|
||||
settings: Paramètres
|
||||
panel: Panneau
|
||||
question: Question
|
||||
joined: '{targetType} {verb}'
|
||||
verb:
|
||||
update: mis à jour
|
||||
delete: supprimé
|
||||
create: crée
|
||||
ticket:
|
||||
description: '{user} {verb} un ticket'
|
||||
verb:
|
||||
unclaim: Relâché
|
||||
update: mis à jour
|
||||
close: Fermé
|
||||
create: Crée
|
||||
claim: Approprié
|
||||
added: Membres ajoutés
|
||||
removed: Membres supprimés
|
||||
title: Ticket {verb}
|
||||
ticket: Ticket
|
||||
message:
|
||||
title: Message {verb}
|
||||
description: '{user} {verb} un message'
|
||||
verb:
|
||||
update: mis à jour
|
||||
delete: supprimé
|
||||
message: Message
|
||||
menus:
|
||||
category:
|
||||
placeholder: Sélectionnez une catégorie de ticket
|
||||
guild:
|
||||
placeholder: Sélectionnez un serveur
|
414
src/i18n/hu.yml
Normal file
414
src/i18n/hu.yml
Normal file
@ -0,0 +1,414 @@
|
||||
log:
|
||||
admin:
|
||||
description:
|
||||
target:
|
||||
panel: panel
|
||||
question: kérdés
|
||||
category: kategória
|
||||
settings: beállítás
|
||||
tag: címke
|
||||
joined: '{user} {verb} {targetType}'
|
||||
title:
|
||||
target:
|
||||
category: Kategória
|
||||
settings: Beállítások
|
||||
panel: Panel
|
||||
question: Kérdés
|
||||
tag: Címke
|
||||
joined: '{targetType} {verb}'
|
||||
verb:
|
||||
create: létrehozva
|
||||
delete: törölve
|
||||
update: frissítve
|
||||
changes: Változások
|
||||
ticket:
|
||||
added: Hozzáadott felhasználók
|
||||
description: '{user} {verb} hibajegy'
|
||||
removed: Eltávolított felhasználók
|
||||
title: Hibjaegy {verb}
|
||||
verb:
|
||||
create: létrehozva
|
||||
claim: begyűjtve
|
||||
close: bezárva
|
||||
unclaim: felszabadítva
|
||||
update: frissítve
|
||||
ticket: Hibajegy
|
||||
message:
|
||||
message: Üzenet
|
||||
verb:
|
||||
update: frissítve
|
||||
delete: törölve
|
||||
description: '{user} {verb} a(z) üzenet'
|
||||
title: Üzenet {verb}
|
||||
buttons:
|
||||
accept_close_request:
|
||||
emoji: ✅
|
||||
text: Elfogadás
|
||||
cancel:
|
||||
emoji: ✖️
|
||||
text: Mégse
|
||||
claim:
|
||||
emoji: 🙌
|
||||
text: Begyűjtés
|
||||
close:
|
||||
emoji: ✖️
|
||||
text: Bezárás
|
||||
confirm_open:
|
||||
text: Hibajegy létrehozása
|
||||
emoji: ✅
|
||||
edit:
|
||||
text: Szerkesztés
|
||||
emoji: ✏️
|
||||
reject_close_request:
|
||||
text: Elutasítás
|
||||
emoji: ✖️
|
||||
create:
|
||||
emoji: 🎫
|
||||
text: Hibajegy létrehozása
|
||||
unclaim:
|
||||
emoji: ♻️
|
||||
text: Felszabadítás
|
||||
commands:
|
||||
message:
|
||||
pin:
|
||||
not_ticket:
|
||||
title: ❌ Nem hibajegy csatorna
|
||||
description: Csak a hibajegyekben tudod kitűzni az üzeneteket.
|
||||
name: Üzenet kitűzése
|
||||
not_pinnable:
|
||||
title: ❌ Hiba
|
||||
description: "Ezt az üzenetet nem tudod kitűzni.\nKérlek vedd fel a kapcsolatot\
|
||||
\ egy adminsztrátorral, aki le tudja ellenőrizni a bot jogosultságait.\n"
|
||||
pinned:
|
||||
description: Az üzenet kitűzésre került.
|
||||
title: ✅ Üzenet kitűzve
|
||||
create:
|
||||
name: Hibajegy létrehozása üzenetből
|
||||
slash:
|
||||
force-close:
|
||||
options:
|
||||
time:
|
||||
name: idő
|
||||
description: Összes hibajegy bezására, amely inaktív volt a megadott ideig
|
||||
(használható a(z) `kategóriával` is)
|
||||
category:
|
||||
description: Összes hibajegy bezására a megadott kategóriában (használható
|
||||
a(z) `idővel` is)
|
||||
name: kategória
|
||||
reason:
|
||||
description: Indok a hibajegy(ek) bezárására
|
||||
name: indok
|
||||
ticket:
|
||||
description: A hibajegy, amit be szeretnél zárni
|
||||
name: hibajegy
|
||||
no_tickets:
|
||||
description: Nincs olyan nyitott hibajegy, mely inaktív lenne `{time}` ideje.
|
||||
title: ❌ Nincs hibajegy
|
||||
confirm_multiple:
|
||||
description: "Bezására kerül **{count}** darab hibajegy, mely inaktív `{time}`\
|
||||
\ ideje:\n{tickets}\n"
|
||||
title: ❓ Biztos vagy benne?
|
||||
description: Hibajegy erőltetett bezárása
|
||||
name: erőltetett-bezárás
|
||||
not_staff:
|
||||
description: Csak a személyzeti tagok erőltethetik a hibajegy bezárását.
|
||||
title: ❌ Hiba
|
||||
close:
|
||||
invalid_time:
|
||||
description: '`{input}` nem megfelelő formátumú.'
|
||||
title: ❌ Érvénytelen
|
||||
description: Kérelmezd a hibajegy bezárását
|
||||
options:
|
||||
reason:
|
||||
description: Indok a hibajegy(ek) bezárásához
|
||||
name: indok
|
||||
name: bezárás
|
||||
help:
|
||||
name: segítség
|
||||
response:
|
||||
commands: Parancsok
|
||||
links:
|
||||
support: Segítség
|
||||
commands: Teljes parancs lista
|
||||
docs: Dokumentáció
|
||||
feedback: Visszajelzés
|
||||
links: Hasznos linkek
|
||||
settings: Bot konfiguráció
|
||||
description: '**Használd a(z) {command} parancsot a hibajegy létrehozásához..**'
|
||||
description: Segítség menü megjelenítése
|
||||
title: Segítség
|
||||
tickets:
|
||||
response:
|
||||
title:
|
||||
own: Hibajegyeid
|
||||
other: '{displayName} hibajegyei'
|
||||
description: Használd a(z) {transcript}, hogy letölthesd a hibajegy átiratát.
|
||||
fields:
|
||||
closed:
|
||||
name: Bezárt hibajegyek
|
||||
none:
|
||||
other: '{user} még nem hozott létre hibajegyet.'
|
||||
own: "Nem hoztál még létre hibajegyet.\nHasználd a(z) {new} a létrehozáshoz.\n"
|
||||
open:
|
||||
name: Nyitott hibajegyek
|
||||
name: hibajegyek
|
||||
not_staff:
|
||||
description: Csak a személyzet tagjai tekinthetik meg mások hibajegyeit.
|
||||
title: ❌ Hiba
|
||||
description: Sorolja fel saját vagy valaki más hibajegyeit
|
||||
options:
|
||||
member:
|
||||
description: A felhasználó, akinek a hibajegyeit szeretnéd listázni
|
||||
name: felhasználó
|
||||
new:
|
||||
options:
|
||||
references:
|
||||
description: Kapcsolódó hibajegyek száma
|
||||
name: hivatkozások
|
||||
name: új
|
||||
description: Hibajegy létrehozása
|
||||
priority:
|
||||
options:
|
||||
priority:
|
||||
description: A hibajegy prioritása
|
||||
choices:
|
||||
HIGH: 🔴 Magas
|
||||
LOW: 🟢 Alacsony
|
||||
MEDIUM: 🟠 Közepes
|
||||
name: prioritás
|
||||
name: prioritás
|
||||
success:
|
||||
title: ✅ Prioritás beállítva
|
||||
description: 'A hibajegy prioritása beállításra került: `{priority}`.'
|
||||
description: Hibajegy prioritásának beállítása
|
||||
not_staff:
|
||||
description: Csak a személyzet tagjai tudják megváltoztatni a hibajegy prioritását.
|
||||
title: ❌ Hiba
|
||||
move:
|
||||
description: Hibajegy áthelyezése másik kategóriába
|
||||
moved: '🗃️ {by} áthelyezte ezt a jegyet a következőről: **{from}** a következőre:
|
||||
**{to}**.'
|
||||
options:
|
||||
category:
|
||||
description: Kategória, amibe át szeretnéd helyezni a hibajegyet
|
||||
name: kategória
|
||||
name: mozgat
|
||||
not_staff:
|
||||
title: ❌ Hiba
|
||||
description: Csak a személyzet tagjai mozgathatják a jegyeket.
|
||||
remove:
|
||||
options:
|
||||
ticket:
|
||||
name: hibajegy
|
||||
description: A hibajegy, amiből el szeretnéd távolítani a felhasználót
|
||||
member:
|
||||
name: felhasználó
|
||||
description: A felhasználó, akit el szeretnél távolítani a hibajegyből
|
||||
name: eltávolítás
|
||||
not_staff:
|
||||
description: Csak személyzeti tagok tudnak eltávolítani felhasználókat a felhasználók
|
||||
hibajegyéből.
|
||||
title: ❌ Hiba
|
||||
removed: ⬅️ {removed} eltávolításra került {by} által.
|
||||
success:
|
||||
description: '{member} eltávolításra került a(z) {ticket} hibajegyből.'
|
||||
title: ✅ Eltávolítva
|
||||
description: Felhasználó eltávolítása a hibajegyből
|
||||
release:
|
||||
name: felszabadítás
|
||||
description: Hibajegy felszabadítása
|
||||
tag:
|
||||
options:
|
||||
tag:
|
||||
name: címke
|
||||
description: A használandó címke neve
|
||||
for:
|
||||
name: részére
|
||||
description: A felhasználó, akihez a címkét szeretnéd kötni
|
||||
description: Címke használata
|
||||
name: címke
|
||||
transfer:
|
||||
name: átruházás
|
||||
description: A hibajegy tulajdonjogának átruházása egy másik felhasználóra
|
||||
options:
|
||||
member:
|
||||
description: A felhasználó, akire át szeretnéd ruházni a tulajdonjogot
|
||||
name: felhasználó
|
||||
transferred: 📨 {user} átruházta a hibajegy tulajdonjogát {to} számára.
|
||||
transferred_from: '📨 {user} áthelyezte ezt a hibahegyet innen: {from} ide: {to}.'
|
||||
add:
|
||||
description: Felhasználó hozzáadása a hibajegyhez
|
||||
success:
|
||||
title: ✅ Hozzáadva
|
||||
description: '{member} hozzáadásra került a(z) {ticket} hibajegyhez.'
|
||||
name: hozzáadás
|
||||
added: ➡️ {added} hozzáadásra került {by} által.
|
||||
options:
|
||||
member:
|
||||
description: Felhasználó, akit hozzá szeretnél adni a hibajegyhez
|
||||
name: felhasználó
|
||||
ticket:
|
||||
description: A hibajegy, amihez hozzá szeretnéd adni a felhasználót
|
||||
name: hibajegy
|
||||
not_staff:
|
||||
description: Csak a személyzet tagiai tudnak hozzáadni felhasználót más felhasználó
|
||||
hibajegyéhez.
|
||||
title: ❌ Hiba
|
||||
claim:
|
||||
description: Hibajegy begyűjtése
|
||||
name: begyűjtés
|
||||
not_staff:
|
||||
description: Csak a személyzet tagjai tudják begyűjteni a hibajegyet.
|
||||
title: ❌ Hiba
|
||||
topic:
|
||||
description: Hibajegy témájának megváltoztatása
|
||||
name: téma
|
||||
transcript:
|
||||
options:
|
||||
ticket:
|
||||
description: A hibajegy száma, melynek az átiratát szeretnéd lekérni
|
||||
name: hibajegy
|
||||
description: Hibajegy átiratának lekérése
|
||||
name: hibajegy
|
||||
user:
|
||||
create:
|
||||
name: Create ticket for user
|
||||
dm:
|
||||
closed:
|
||||
title: A hibajegyed bezárásra került
|
||||
archived: Használd a(z) `/transcript` a(z)**{guild}** szerveren, hogy megtekintsd
|
||||
az archivált üzeneteket.
|
||||
fields:
|
||||
closed:
|
||||
name: Bezárva ekkor
|
||||
value: '{timestamp} ({duration} alatt)'
|
||||
closed_by: Bezárva általa
|
||||
created: Létrehozva ekkor
|
||||
feedback: Visszajelzésed
|
||||
reason: Bezárva ezért
|
||||
response: Válaszidő
|
||||
ticket: Hibajegy
|
||||
topic: Téma
|
||||
confirm_open:
|
||||
title: Létre szeretnél hozni egy hibajegyet a megadott témával?
|
||||
misc:
|
||||
blocked:
|
||||
title: ❌ Letiltva
|
||||
description: Nem tudsz hibajegyet létrehozni.
|
||||
category_full:
|
||||
description: "A kategória elérte a maximális kapacitást.\nPróbáld újra később.\n"
|
||||
title: ❌ Kategória megtelt
|
||||
cooldown:
|
||||
description: Kérlek várj {time}, mielőtt új hibajegyet hoznál létre a kategóriában.
|
||||
title: ❌ Kérlek várj
|
||||
ratelimited:
|
||||
description: Próbáld újra néhány másodperc múlva.
|
||||
title: 🐢 Kérlek lassíts
|
||||
expires_in: Lejár {time}
|
||||
error:
|
||||
description: "Sajnálom váratlan hiba lépett fel.\nKérlek vedd fel a kapcsolatot\
|
||||
\ az adminisztrátorral.\n"
|
||||
fields:
|
||||
code: Hibakód
|
||||
identifier: Azonosító
|
||||
title: ⚠️ Hiba lépett fel
|
||||
invalid_ticket:
|
||||
description: Kérlek add meg egy létező hibajegyet.
|
||||
title: ❌ Érvénytelen hibajegy
|
||||
expired:
|
||||
description: Nem válaszoltál időben. Kérlek próbáld újra.
|
||||
title: ⏰ Lejárt
|
||||
member_limit:
|
||||
description:
|
||||
- Kérlek használd a meglévő hibajegyed, vagy zárd be mielőtt újat hoznál létre.
|
||||
- "Kérlek zárd be a hibajegyet, mielőtt másikat hoznál létre.\nHasználd a(z) `/tickets`\
|
||||
\ parancsot a meglévő hibajegyeid megtekintéséhez.\n"
|
||||
title:
|
||||
- ❌ Már van hibajegyed
|
||||
- ❌ Már van %d megnyitott hibajegyed
|
||||
missing_roles:
|
||||
title: ❌ Nem megfelelő rangok
|
||||
description: Nincs megfelelő rangod ahhoz, hogy létrehozhass hibajegyet ebbe a
|
||||
kategóriába.
|
||||
no_categories:
|
||||
description: Nem került konfigurálásra egyetlen hibajegy kategória sem.
|
||||
title: ❌ Nincsenek hibajegy kategóriák
|
||||
not_ticket:
|
||||
description: Csak hibajegyekben használhatod ezt a parancsot.
|
||||
title: ❌ Nem hibajegy csatorna
|
||||
unknown_category:
|
||||
description: Kérlek próbálj egy másik kategóriát.
|
||||
title: ❌ Nem létezik ilyen hibajegy kategória
|
||||
update:
|
||||
description: "> [`{version}` megtekintése a GitHubon]({github})\n> [Változásnapló]({changelog})\n\
|
||||
> [Frissítési útmutató]({guide})\n"
|
||||
title: Frissítés elérhető
|
||||
menus:
|
||||
category:
|
||||
placeholder: Kategória választása
|
||||
guild:
|
||||
placeholder: Szerver választása
|
||||
ticket:
|
||||
edited:
|
||||
description: A változtatások mentésre kerültek.
|
||||
title: ✅ Hibajegy frissítve
|
||||
references_ticket:
|
||||
title: ℹ️ Referencia
|
||||
fields:
|
||||
topic: Téma
|
||||
number: Szám
|
||||
date: Létrehozva
|
||||
description: 'Ez a hibajegy egy korábbi hibajegyhez kapcsolódik:'
|
||||
close:
|
||||
wait_for_staff: ✋ Kérlek várj a személyzetre, ahhoz hogy bezárhasd ezt a hibajegyet.
|
||||
closed:
|
||||
description: A csatorna törlésre kerül néhány másodperc múlva…
|
||||
title: ✅ Hibajegy bezárva
|
||||
forbidden:
|
||||
description: Nincs jogosultságod a hibajegy bezárásához.
|
||||
title: ❌ Hiba
|
||||
rejected: ✋ {user} elutasította a kérést a hibajegy bezárásáról.
|
||||
staff_request:
|
||||
archived: "\nAz üzenetek ebben a csatornában archiválásra kerülnek a későbbiekre.\n"
|
||||
description: "{requestedBy} be szeretné zárni ezt a hibajegyet.\nKattints \"\
|
||||
Elfogadás\" a bezáráshoz, vagy \"Elutasíts\" ha további segítsére van szükséged.\n"
|
||||
title: ❓ Bezárásra kerülhet a hibajegy?
|
||||
user_request:
|
||||
title: ❓ {requestedBy} be szeretné zárni ezt a hibajegyet
|
||||
wait_for_user: ✋ Kérlek várj a felhasználó válaszára.
|
||||
released: ♻️ {user} felszabadította a hibajegyet.
|
||||
created:
|
||||
description: 'A hibajegyed létrehozásra került: {channel}.'
|
||||
title: ✅ Hibajegy létrehozva
|
||||
claimed: 🙌 {user} begyűjtötte a hibajegyet.
|
||||
answers:
|
||||
no_value: '*Nem válaszolt*'
|
||||
feedback: Köszönjük a visszajelzést.
|
||||
opening_message:
|
||||
content: "{staff}\n{creator} létrehozott egy új hibajegyet\n"
|
||||
fields:
|
||||
topic: Téma
|
||||
references_message:
|
||||
title: ℹ️ Referencia
|
||||
description: Referencia [üzenet]({url}) elküldve {timestamp} {author} által.
|
||||
working_hours:
|
||||
next:
|
||||
description: Visszatérünk a <t:{timestamp}:F> (<t:{timestamp}:R>) időpontra,
|
||||
bár előfordulhat, hogy ez előtt választ kap.
|
||||
title: 🕗 Jelenleg nem dolgozunk
|
||||
today:
|
||||
description: 'Előfordulhat, hogy korábban is kap választ, azonban nem dolgozunk
|
||||
<t:{timestamp}:t> -ig (ma: <t:{timestamp}:R> )'
|
||||
title: 🕗 Jelenleg nem dolgozunk
|
||||
modals:
|
||||
feedback:
|
||||
rating:
|
||||
placeholder: 1-5
|
||||
label: Értékelés
|
||||
comment:
|
||||
label: Megjegyzés
|
||||
placeholder: Van további visszajelzése?
|
||||
title: Mit gondolsz, hogy teljesítettünk?
|
||||
topic:
|
||||
label: Téma
|
||||
placeholder: Miről szól a hibajegy?
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user