(v4.2.0) Large README overhaul (Part 1)

This commit is contained in:
DJj123dj
2026-05-20 21:55:18 +02:00
parent a0b3aff661
commit a28bcd4f8b
19 changed files with 379 additions and 432 deletions
+90
View File
@@ -0,0 +1,90 @@
{
"_INFO":"This document contains all Open Ticket sponsors.",
"SPONSOR_TEMPLATE":{
"name":"INSERT_NAME",
"pictureUrl":"INSERT_PICTURE_URL",
"profileUrl":"INSERT_PROFILE_URL",
"sectionId":"INSERT_ID"
},
"SECTION_TEMPLATE":{
"name":"INSERT_NAME",
"id":"INSERT_ID",
"pfpSize":50,
"pfpColumns":5,
"withNames":true
},
"sections":[
{
"name":"💎 Platinum Sponsors",
"id":"platinum",
"pfpSize":120,
"pfpColumns":4,
"withNames":true
},
{
"name":"🥇 Gold Sponsors",
"id":"gold",
"pfpSize":60,
"pfpColumns":8,
"withNames":true
},
{
"name":"🥈 Silver Sponsors",
"id":"silver",
"pfpSize":40,
"pfpColumns":16,
"withNames":false
},
{
"name":"❤️ Past Sponsors",
"id":"past",
"pfpSize":30,
"pfpColumns":20,
"withNames":false
}
],
"sponsors":[
{
"name":"Guillee3",
"pictureUrl":"https://github.com/guillee3.png",
"profileUrl":"https://github.com/guillee3",
"sectionId":"platinum"
},
{
"name":"Jacob Humston",
"pictureUrl":"https://github.com/jacobhumston.png",
"profileUrl":"https://github.com/jacobhumston",
"sectionId":"platinum"
},
{
"name":"SpyEye2",
"pictureUrl":"https://github.com/SpyEye2.png",
"profileUrl":"https://github.com/SpyEye2",
"sectionId":"gold"
},
{
"name":"DOSEV5",
"pictureUrl":"https://github.com/DOSEV5.png",
"profileUrl":"https://github.com/DOSEV5",
"sectionId":"gold"
},
{
"name":"YeeetSK",
"pictureUrl":"https://github.com/YeeetSK.png",
"profileUrl":"https://github.com/YeeetSK",
"sectionId":"silver"
},
{
"name":"BENZORICH",
"pictureUrl":"https://github.com/BENZORICH.png",
"profileUrl":"https://github.com/BENZORICH",
"sectionId":"silver"
},
{
"name":"Mods HD",
"pictureUrl":"https://github.com/mods-hd.png",
"profileUrl":"https://github.com/mods-hd",
"sectionId":"silver"
}
]
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

+2 -4
View File
@@ -19,9 +19,7 @@ otdebug.txt
.backup/*
.tools/*
!.tools/createDocs.js
!.tools/mergeTranslations.js
!.tools/typedoc-config.json
!.tools/pterodactyl-eggs/
!.tools/createSponsors.ts
!.tools/mergeTranslations.ts
!.tools/docker-compose.yml
!.tools/dockerfile
-328
View File
@@ -1,328 +0,0 @@
//@ts-check
const fs = require("fs")
/*////// OT DOCS GENERATOR ////////
What does this file do?:
Well, this file will parse the result returned from typedoc (the documentation generator).
The resulting file will then be used in the OT Docs markdown generator to generate the Open Ticket API Reference.
In short:
This file generates the documentation for Open Ticket :)
Contributing:
If you want to contribute to the Open Ticket Docs, please run the following command:
> npm run docs
This will give you a "result.json" file which can then be used in the OT Docs.
Further instructions can be found there.
/////////////////////////////////*/
if (!fs.existsSync(".docs/typedoc-result.json")){
console.log("Unable to generate documentation! Run typedoc first!")
process.exit(1)
}
const result = JSON.parse(fs.readFileSync(".docs/typedoc-result.json").toString())
const availableElements = []
const skipElementNames = [
"#opendiscord-types"
]
const handleFunction = (memberType) => {
if (!memberType || !memberType.signatures || !Array.isArray(memberType.signatures)) return {type:"unknown"}
//try to get signature
let signature = memberType.signatures.find((s) => !s.typeParameters)
if (!signature) signature = memberType.signatures[0]
if (!signature) return {type:"unknown"}
//try to get comment
let comment = signature.comment
if (!comment) comment = memberType.signatures.find((s) => s.comment)?.comment
if (!comment) comment = null
const signatureInherited = signature.flags.isInherited ? true : false
const signatureComment = comment ? comment.summary?.map((c) => c.text).join("") : null
const signatureParameters = signature.parameters?.map((p) => {
return {name:p.name,details:handleType(p.type)}
}) ?? []
const signatureReturns = handleType(signature.type)
return {type:"function",inherited:signatureInherited,comment:signatureComment,parameters:signatureParameters,returns:signatureReturns}
}
const handleObject = (memberType) => {
if (!memberType || !memberType.children || !Array.isArray(memberType.children)) return {type:"unknown"}
const propertyIds = memberType.groups?.find((g) => g.title == "Properties")?.children ?? []
const methodIds = memberType.groups?.find((g) => g.title == "Methods")?.children ?? []
const objectChildren = []
for (const member of (memberType.children ?? [])){
const memberName = member.name
const memberType = propertyIds.includes(member.id) ? "property" : methodIds.includes(member.id) ? "method" : "other"
let memberComment = member.comment?.summary?.map((c) => c.text).join("") ?? null
const rawMemberSource = member.sources ? (member.sources[0] ?? null) : null
let memberSource = rawMemberSource ? rawMemberSource.fileName+":"+rawMemberSource.line+":"+rawMemberSource.character : null
let memberDetails = null
if (memberType == "property"){
//INTERFACE => PROPERTY
memberDetails = handleType(member.type)
}else if (memberType == "method"){
//INTERFACE => METHOD
memberDetails = handleFunction(member)
memberComment = memberDetails.comment
}
objectChildren.push({
type:memberType,
name:memberName,
comment:memberComment,
source:memberSource,
details:memberDetails
})
}
return {type:"object",children:objectChildren}
}
const handleType = (memberType) => {
if (!memberType || !memberType.type) return {type:"unknown"}
else if (memberType.type == "intrinsic") return {type:"primitive",name:memberType.name}
else if (memberType.type == "array") return {type:"array",child:handleType(memberType.elementType)}
else if (memberType.type == "union") return {type:"union",children:memberType.types.map((t) => handleType(t))}
else if (memberType.type == "intersection") return {type:"intersection",children:memberType.types.map((t) => handleType(t))}
else if (memberType.type == "reference"){
const referenceTypeArguments = (memberType.typeArguments) ? memberType.typeArguments.map((ta) => handleType(ta)) : null
let referenceType = availableElements.find((el) => el.name == memberType.name)?.type ?? null
if (referenceType){
//member is known type (from Open Ticket)
return {type:"reference",name:memberType.name,target:referenceType,typeArguments:referenceTypeArguments}
}else if (memberType.package){
//member is type from node.js or another package
if (memberType.package == "typescript") return {type:"internal",name:memberType.name,typeArguments:referenceTypeArguments}
if (memberType.package == "open-ticket" && memberType.refersToTypeParameter) return {type:"typeParam",name:memberType.name,typeArguments:referenceTypeArguments}
else return {type:"external",package:memberType.package,name:memberType.name,typeArguments:referenceTypeArguments}
}else return {type:"unknown"}
}
else if (memberType.type == "literal") return {type:"literal",name:(typeof memberType.value == "string") ? JSON.stringify(memberType.value) : String(memberType.value)}
else if (memberType.type == "typeOperator"){
if (memberType.operator == "keyof") return {type:"keyof",child:handleType(memberType.target)}
else if (memberType.operator == "readonly") return {type:"readonly",child:handleType(memberType.target)}
else if (memberType.operator == "unique") return {type:"unique",child:handleType(memberType.target)}
}
else if (memberType.type == "conditional") return {type:"conditional",checker:handleType(memberType.checkType),extends:handleType(memberType.extendsType),trueValue:handleType(memberType.trueType),falseValue:handleType(memberType.falseType)}
else if (memberType.type == "indexedAccess") return {type:"index",index:handleType(memberType.indexType),object:handleType(memberType.objectType)}
else if (memberType.type == "mapped") return {type:"mapped",parameterName:memberType.parameter,parameter:handleType(memberType.parameterType),template:handleType(memberType.templateType)}
else if (memberType.type == "optional") return {type:"optional",child:handleType(memberType.elementType)}
else if (memberType.type == "predicate") return {type:"predicate",name:memberType.name,target:handleType(memberType.targetType)}
else if (memberType.type == "query") return {type:"query",target:handleType(memberType.queryType)}
else if (memberType.type == "rest") return {type:"rest",child:handleType(memberType.elementType)}
else if (memberType.type == "tuple") return {type:"tuple",children:memberType.elements.map((t) => handleType(t))}
else if (memberType.type == "templateLiteral") return {type:"template",head:memberType.head,tails:memberType.tail.map((t) => {
return {element:handleType(t[0]),text:t[1]}
})}
else if (memberType.type == "reflection" && memberType.declaration && memberType.declaration.signatures && memberType.declaration.signatures[0]){
return handleFunction(memberType.declaration)
}
else if (memberType.type == "reflection"){
return handleObject(memberType.declaration)
}
else return {type:"unknown"}
}
for (const file of result.children){
const classIds = file.groups?.find((g) => g.title == "Classes")?.children ?? []
const interfaceIds = file.groups?.find((g) => g.title == "Interfaces")?.children ?? []
const typeIds = file.groups?.find((g) => g.title == "Type Aliases")?.children ?? []
const enumIds = file.groups?.find((g) => g.title == "Enumerations")?.children ?? []
const varIds = file.groups?.find((g) => g.title == "Variables")?.children ?? []
const funcIds = file.groups?.find((g) => g.title == "Functions")?.children ?? []
for (const declaration of file.children){
const declarationName = declaration.name
const declarationType = classIds.includes(declaration.id) ? "class" : interfaceIds.includes(declaration.id) ? "interface" : typeIds.includes(declaration.id) ? "type" : enumIds.includes(declaration.id) ? "enum" : varIds.includes(declaration.id) ? "variable" : funcIds.includes(declaration.id) ? "function" : "other"
availableElements.push({name:declarationName,type:declarationType})
}
}
const exported = []
for (const file of result.children){
const classIds = file.groups?.find((g) => g.title == "Classes")?.children ?? []
const interfaceIds = file.groups?.find((g) => g.title == "Interfaces")?.children ?? []
const typeIds = file.groups?.find((g) => g.title == "Type Aliases")?.children ?? []
const enumIds = file.groups?.find((g) => g.title == "Enumerations")?.children ?? []
const varIds = file.groups?.find((g) => g.title == "Variables")?.children ?? []
const funcIds = file.groups?.find((g) => g.title == "Functions")?.children ?? []
for (const declaration of (file.children ?? [])){
const declarationName = declaration.name
if (skipElementNames.includes(declarationName)) continue
const declarationType = classIds.includes(declaration.id) ? "class" : interfaceIds.includes(declaration.id) ? "interface" : typeIds.includes(declaration.id) ? "type" : enumIds.includes(declaration.id) ? "enum" : varIds.includes(declaration.id) ? "variable" : funcIds.includes(declaration.id) ? "function" : "other"
const declarationTypeParams = (declaration.typeParameters) ? declaration.typeParameters.map((tp) => {
return {name:tp.name,type:handleType(tp.type)}
}) : null
const declarationComment = declaration.comment?.summary?.map((c) => c.text).join("") ?? null
const declarationConstant = declaration.flags.isConst ? true : false
const rawDeclarationSource = declaration.sources ? (declaration.sources[0] ?? null) : null
const declarationSource = rawDeclarationSource ? rawDeclarationSource.fileName+":"+rawDeclarationSource.line+":"+rawDeclarationSource.character : null
const declarationChildren = []
if (declarationType == "class"){
//CLASS
const constructorIds = declaration.groups?.find((g) => g.title == "Constructors")?.children ?? []
const propertyIds = declaration.groups?.find((g) => g.title == "Properties")?.children ?? []
const methodIds = declaration.groups?.find((g) => g.title == "Methods")?.children ?? []
for (const member of (declaration.children ?? [])){
const memberName = member.name
const memberType = constructorIds.includes(member.id) ? "constructor" : propertyIds.includes(member.id) ? "property" : methodIds.includes(member.id) ? "method" : "other"
let memberComment = member.comment?.summary?.map((c) => c.text).join("") ?? null
const rawMemberSource = member.sources ? (member.sources[0] ?? null) : null
let memberSource = rawMemberSource ? rawMemberSource.fileName+":"+rawMemberSource.line+":"+rawMemberSource.character : null
const memberInherited = member.flags.isInherited ? true : false
const memberStatic = member.flags.isStatic ? true : false
const memberProtected = member.flags.isProtected ? true : false
const memberOptional = member.flags.isOptional ? true : false
const memberReadonly = member.flags.isReadonly ? true : false
let memberDetails = null
if (memberType == "property"){
//CLASS => PROPERTY
memberDetails = handleType(member.type)
}else if (memberType == "method"){
//CLASS => METHOD
memberDetails = handleFunction(member)
memberComment = memberDetails.comment
}else if (memberType == "constructor"){
//CLASS => CONSTRUCTOR
memberDetails = handleFunction(member)
memberComment = memberDetails.comment
}
declarationChildren.push({
type:memberType,
name:memberName,
comment:memberComment,
source:memberSource,
details:memberDetails,
inherited:memberInherited,
static:memberStatic,
protected:memberProtected,
optional:memberOptional,
readonly:memberReadonly
})
}
}else if (declarationType == "interface"){
//INTERFACE
const propertyIds = declaration.groups?.find((g) => g.title == "Properties")?.children ?? []
const methodIds = declaration.groups?.find((g) => g.title == "Methods")?.children ?? []
for (const member of (declaration.children ?? [])){
const memberName = member.name
const memberType = propertyIds.includes(member.id) ? "property" : methodIds.includes(member.id) ? "method" : "other"
let memberComment = member.comment?.summary?.map((c) => c.text).join("") ?? null
const rawMemberSource = member.sources ? (member.sources[0] ?? null) : null
let memberSource = rawMemberSource ? rawMemberSource.fileName+":"+rawMemberSource.line+":"+rawMemberSource.character : null
const memberInherited = member.flags.isInherited ? true : false
const memberStatic = member.flags.isStatic ? true : false
const memberProtected = member.flags.isProtected ? true : false
const memberOptional = member.flags.isOptional ? true : false
const memberReadonly = member.flags.isReadonly ? true : false
let memberDetails = null
if (memberType == "property"){
//INTERFACE => PROPERTY
memberDetails = handleType(member.type)
}else if (memberType == "method"){
//INTERFACE => METHOD
memberDetails = handleFunction(member)
memberComment = memberDetails.comment
}
declarationChildren.push({
type:memberType,
name:memberName,
comment:memberComment,
source:memberSource,
details:memberDetails,
inherited:memberInherited,
static:memberStatic,
protected:memberProtected,
optional:memberOptional,
readonly:memberReadonly
})
}
}else if (declarationType == "type"){
//TYPE
declarationChildren.push(handleType(declaration.type))
}else if (declarationType == "enum"){
//ENUM
const enumerableIds = declaration.groups?.find((g) => g.title == "Enumeration Members")?.children ?? []
for (const member of (declaration.children ?? [])){
const memberName = member.name
const memberType = enumerableIds.includes(member.id) ? "enumerable" : "other"
const memberComment = member.comment?.summary?.map((c) => c.text).join("") ?? null
const rawMemberSource = member.sources ? (member.sources[0] ?? null) : null
let memberSource = rawMemberSource ? rawMemberSource.fileName+":"+rawMemberSource.line+":"+rawMemberSource.character : null
let memberDetails = null
if (memberType == "enumerable"){
//ENUM => ENUMERABLE
memberDetails = handleType(member.type)
}
const memberInherited = member.flags.isInherited ? true : false
const memberStatic = member.flags.isStatic ? true : false
const memberProtected = member.flags.isProtected ? true : false
const memberOptional = member.flags.isOptional ? true : false
const memberReadonly = member.flags.isReadonly ? true : false
declarationChildren.push({
type:memberType,
name:memberName,
comment:memberComment,
source:memberSource,
details:memberDetails,
inherited:memberInherited,
static:memberStatic,
protected:memberProtected,
optional:memberOptional,
readonly:memberReadonly
})
}
}else if (declarationType == "variable"){
//VARIABLE
declarationChildren.push(handleType(declaration.type))
}else if (declarationType == "function"){
//FUNCTION
declarationChildren.push(handleFunction(declaration))
}
exported.push({
type:declarationType,
name:declarationName,
comment:declarationComment,
constant:declarationConstant,
source:declarationSource,
children:declarationChildren,
typeParams:declarationTypeParams
})
}
}
fs.writeFileSync(".docs/result.json",JSON.stringify(exported,null,"\t"))
fs.rmSync(".docs/typedoc-result.json")
+131
View File
@@ -0,0 +1,131 @@
import fs from "fs"
import path from "path"
import crypto from "crypto"
const sponsorData: {sponsors:Sponsor[],sections:Section[]} = JSON.parse(fs.readFileSync(path.join(process.cwd(),"./.github/SPONSORS.json")).toString())
//CONSTANTS
const CORNER_RADIUS = 10
const SPACE_MULTIPLIER = 1.2
const SVG_WIDTH = 1000
//TYPES
interface Sponsor {
name:string,
pictureUrl:string,
profileUrl:string,
sectionId:string
}
interface Section {
name:string,
id:string,
pfpSize:number,
pfpColumns:number,
withNames:boolean
}
//FUNCTIONS
async function downloadPfpToBase64URL(url:string){
const res = await fetch(url,{method:"GET"})
if (!res.ok) return null
const buffer = Buffer.from(await res.arrayBuffer())
console.log("Downloaded picture URL:",url)
return "data:image/png;base64,"+buffer.toString("base64")
}
function createTitle(yPos:number,name:string){
return `<text x="${Math.round(SVG_WIDTH/2)}" y="${yPos+20}" text-anchor="middle" class="sponsor-tier-title">${name}</text>`
}
async function createPfp(yPos:number,xPos:number,size:number,sponsor:Sponsor,withNames:boolean){
const randomId = crypto.randomBytes(8).toString("hex")
const nameElement = (withNames) ? `<text x="${Math.round(xPos+(size/2))}" y="${yPos+size+20}" text-anchor="middle" fill="currentColor">${sponsor.name}</text>` : ""
return (`<a href="${sponsor.profileUrl}" class="sponsor-link" target="_blank">
<clipPath id="clipPath-${randomId}">
<rect x="${xPos}" y="${yPos}" width="${size}" height="${size}" rx="${CORNER_RADIUS}" ry="${CORNER_RADIUS}"/>
</clipPath>
<image x="${xPos}" y="${yPos}" width="${size}" height="${size}" href="${await downloadPfpToBase64URL(sponsor.pictureUrl)}" clip-path="url(#clipPath-${randomId})"/>
${nameElement}
</a>`)
}
async function generateSection(yPos:number,section:Section,sponsors:Sponsor[]){
let sectionHtml: string = ""
sectionHtml += createTitle(yPos,section.name)
const nameOffset = (section.withNames) ? 20 : 0
//divide sponsors in rows
const groupedSponsors: Sponsor[][] = []
let currentGroup: Sponsor[] = []
for (const sponsor of sponsors){
currentGroup.push(sponsor)
if (currentGroup.length == section.pfpColumns){
groupedSponsors.push(currentGroup)
currentGroup = []
}
}
if (currentGroup.length > 0) groupedSponsors.push(currentGroup)
let y = 0
for (const sponsorGroup of groupedSponsors){
const xOffset = Math.round((SVG_WIDTH - (sponsorGroup.length * section.pfpSize * SPACE_MULTIPLIER))/2)
let x = 0
for (const sponsor of sponsorGroup){
const pfpYPos = 40 + yPos + (y * ((section.pfpSize * SPACE_MULTIPLIER) + nameOffset))
const pfpXPos = xOffset + (x * section.pfpSize * SPACE_MULTIPLIER)
sectionHtml += await createPfp(pfpYPos,pfpXPos,section.pfpSize,sponsor,section.withNames)
x++
}
y++
}
let sectionHeight: number = 40 + (y * ((section.pfpSize * SPACE_MULTIPLIER) + nameOffset))
return {sectionHtml,sectionHeight}
}
async function generateSections(sections:Section[],sponsors:Sponsor[]){
let finalHeight: number = 10
let finalHtml: string = ""
for (const section of sections){
const sectionSponsors = sponsors.filter((s) => s.sectionId === section.id)
if (sectionSponsors.length < 1) continue
const {sectionHtml,sectionHeight} = await generateSection(finalHeight,section,sectionSponsors)
finalHeight += sectionHeight
finalHtml += sectionHtml
}
finalHeight += 10
return {finalHeight,finalHtml}
}
function generateFinalHtml(sectionsHtml:string,sectionHeight:number){
return (`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${SVG_WIDTH} ${sectionHeight}" width="${SVG_WIDTH}" height="${sectionHeight}">
<style>
text {
font-weight: 300;
font-size: 14px;
fill: #777777;
font-family: 'Open Sans', 'Helvetica Neue', sans-serif;
}
.sponsor-link {
cursor: pointer;
}
.sponsor-tier-title {
font-weight: 500;
font-size: 20px;
}
</style>
<rect x="2" y="2" width="${SVG_WIDTH-4}" height="${sectionHeight-4}" rx="20" ry="20" style="fill:transparent;stroke:#f8ba00;stroke-width:3"></rect>
${sectionsHtml}
</svg>`)
}
//GENERATE SPONSORS SVG
async function main(){
const {finalHeight,finalHtml} = await generateSections(sponsorData.sections,sponsorData.sponsors)
fs.writeFileSync(path.join(process.cwd(),"./.github/SPONSORS.svg"),generateFinalHtml(finalHtml,finalHeight))
}
main()
@@ -1,6 +1,7 @@
//@ts-check
const fjs = require("formatted-json-stringify")
const fs = require("fs")
import fjs from "formatted-json-stringify"
import fs from "fs"
const formatter = new fjs.ObjectFormatter(null,true,[
new fjs.ObjectFormatter("_TRANSLATION",true,[
new fjs.PropertyFormatter("otversion"),
-13
View File
@@ -1,13 +0,0 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": [
"../src/core/api/modules/",
"../src/core/api/defaults/",
"../src/core/api/openticket/",
"../src/core/api/main.ts",
"../src/core/startup/init.ts",
],
"entryPointStrategy": "expand",
"json": "./typedoc-result.json",
"basePath": "../"
}
+89 -82
View File
@@ -8,104 +8,110 @@
<a href="https://otdocs.dj-dj.be"><img alt="Open Ticket Documentation" src="https://img.shields.io/badge/discord.js-v14-CB3837.svg?style=flat-square&logo=npm"></img></a>
<a href="https://github.com/open-discord-bots/open-ticket/blob/main/LICENSE"><img alt="Open Ticket License" src="https://img.shields.io/badge/license-GPL%203.0-important.svg?style=flat-square"></img></a>
<a href="https://otdocs.dj-dj.be"><img alt="Open Ticket Stars" src="https://img.shields.io/github/stars/djj123dj/open-ticket?color=yellow&label=stars&logo=github&style=flat-square"></img></a>
<br>
<a href="https://github.com/sponsors/DJj123dj"><img alt="Sponsor DJj123dj" src="https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors"></img></a>
<a href="https://hub.docker.com/repository/docker/djj123dj/open-ticket"><img alt="Open Ticket supports Docker!" src="https://img.shields.io/badge/docker-supported-2496ED?style=flat-square&logo=docker"></img></a>
<a href=".eggs/README.md"><img alt="Open Ticket supports Pterodactyl Eggs!" src="https://img.shields.io/badge/pterodactyl-supported-10539F?style=flat-square&logo=pterodactyl"></img></a>
<a href=".github/pterodactyl-eggs/README.md"><img alt="Open Ticket supports Pterodactyl Eggs!" src="https://img.shields.io/badge/pterodactyl-supported-10539F?style=flat-square&logo=pterodactyl"></img></a>
</p>
<p align="center">
Open Ticket is the most <b>advanced and customizable</b> Discord ticket bot available. With <b>350+ configurable settings</b>, you have full control over every aspect of your ticket system!
From <code>HTML transcripts</code> and <code>Advanced Plugins</code> to <code>Claiming & Pinning</code>, <code>Questions & Modals</code>, <code>Detailed Statistics</code>, and much more.<br><br>
The bot is fully translated into <b>37+ languages</b> and has been battle-tested in large Discord servers.<br>
Open Ticket is the most <b>advanced and customizable</b> Discord ticket bot available right now. With <b>350+ configurable settings</b>, you have full control over almost every aspect of your ticket system!
From <code>HTML transcripts</code> and <code>Advanced Plugins</code> to <code>Claiming & Pinning</code>, <code>Modal Questions & Limits</code>, <code>Detailed Statistics</code>, and much more.<br><br>
The bot is fully translated into <b>38+ languages</b> and has been battle-tested in large Discord servers.<br>
Need help or want to get involved? Feel free to join our <a href="https://discord.dj-dj.be"><b>Discord server</b></a>.
</p>
<h3 align="center"><b>⭐️ Support Open Ticket’s growth by starring this repo! ⭐️</b></h3>
<p align="center"><sup>❤️ Love Open Ticket? <a href="https://github.com/sponsors/DJj123dj">Sponsorships</a> help fuel our HTML transcript servers and future features! ❤️</sup></p>
<p align="center"><sup>❤️ Love Open Ticket? <a href="https://github.com/sponsors/DJj123dj">Sponsorships</a> help fuel our HTML transcript servers and future features! ❤️</sup><br>
<img align="center" src=".github/SPONSORS.svg" alt="Open Ticket" width="800px">
</p>
---
> **[-> Navigate to (⏱️ Quick Setup)](#️-quick-start-using-interactive-cli-tool)**
> **[-> Navigate to (📚 Documentation)](https://otdocs.dj-dj.be)**
> **[-> Navigate to (📞 Support Server)](https://discord.dj-dj.be)**
### 📌 Features
- **⏳ Quick Setup** - Using the interactive Quick Setup CLI, you can **configure Open Ticket in less than 5min!**
- **🦇 Pterodactyl Support** - Open Ticket works perfect on Pterodactyl based panels. [(Download official eggs)](.eggs/README.md)
- **💩 No Credits** - Your bot won't contain any form of bloat or credits. It's all yours!
- **🔒 Private & Secure** - It has been battletested by thousands of servers and **respects security & privacy.**
- **📈 Scalable** - Made to handle huge servers and has already been **tested in servers with 100k members.**
- **📄 HTML Transcripts** - The **built-in HTML Transcripts Service** provides beautiful & easy-to-use transcripts.
- **✅ Ticket Status** - Close, reopen, delete, claim, pin, rename or move tickets in your server.
- **🇬🇧 Translation** - Every message has been translated in more than **37 languages** by our community.
- **🎨 Customisation** - More than **200+ settings** are related to customisation & advanced features.
- **🖥️ Interactions** - The bot has full support for buttons, dropdowns, slash/text commands & modals.
- **∞ Unlimited Possibilities** - Create an infinite amount of tickets, questions & panels.
- **📝 Advanced Plugins** - Create advanced plugins or use [**pre-made plugins**](#-plugins) by our community.
- **👥 Participants** - Add or remove participants & transfer ownership from one user to another.
- **📊 Detailed Statistics** - With more than **50+ statistics** for tickets, users & the server.
- **🚫 Blacklist** - Blacklist users to prevent them from creating new tickets.
- **🚨 Priorities** - Assign different **priority levels** to tickets to mark them as important.
- **❓ Modal Questions** - Give users the ability to **answer questions** in a modal before their ticket is created.
- **✨ Commands** - Manage all your tickets with more than 28+ commands.
- **🤖 Automation** - Automate ticket handling with **autoclose, autodelete** & slow mode.
- **😎 Additional Features** - For some weird reason, the bot also supports Reaction Role & URL Buttons.
#### And even more using [pre-made community plugins](#-plugins)!
- **💬 Reviews** - Create & manage a support review system.
- **📢 Feedback** - Collect feedback & create forms for users to answer.
- **⏰ Reminders** - Create & manage customisable reminders.
- **🏷️ Tags** - Create tags & answer questions automatically using keywords.
- **📝 Forms** - Create advanced forms and automatically ask for repetitive questions.
- **🔄 Channel Display** - Create a voice channel with realtime statistics from the ticket system.
- **💾 SQLite Database** - Use an `SQLite` database for increased performance.
- **🎉 Custom Embeds** - Create your own embeds and send them using a command.
- **🎨 Customisation** - Yep, you heard it right. Even more customisation!
- **😁 And so much more...**
### ⏱️ Quick Start (Using Interactive CLI Tool)
> 1. Download the latest version of Open Ticket on [Github](https://github.com/open-discord-bots/open-ticket).
> 2. Make sure Node.js & Npm are installed using `node -v` (minimum `v20`).
> 3. Install any required dependencies using `npm install`.
> 4. Start the **Quick Setup CLI** using `npm run setup`.
> 5. Click on `> ⏱️ Quick Setup` and follow the instructions.
> 6. Start the bot using `npm start` or `node index.js`
> - If required, the bot will give a report of errors that must be solved.
> - Follow the instructions and restart the bot.
> 7. Enjoy using Open Ticket!
>
> #### 🚦 Navigation
> **[-> Navigate to (⏱️ Quick Setup)](#️-quick-start)**
> **[-> Navigate to (📚 Documentation)](https://otdocs.dj-dj.be)**
> **[-> Navigate to (📞 Support Server)](https://discord.dj-dj.be)**
> **[-> Navigate to (🧩 Download Plugins)](https://odplugins.dj-dj.be)**
> **[-> Navigate to (🧩 Plugins/Addons)](https://odplugins.dj-dj.be)**
> **[-> Navigate to (🦇 Pterodactyl Eggs)](.github/pterodactyl-eggs/README.md)**
### 📌 Features
#### Core Features
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-management.svg"></img> **Ticket Management** - Close, reopen, delete, claim or pin tickets with ease.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-commands.svg"></img> **Powerful Commands** - Manage your support system with **30+ commands** for staff & users.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-questions.svg"></img> **Modal Questions** - Ask users **custom questions** before a ticket is created.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-priorities.svg"></img> **Priorities** - Assign **priority levels** to tickets to highlight urgent requests.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-participants.svg"></img> **Participants** - Add or remove participants & transfer ownership from one user to another.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-adjustments.svg"></img> **Adjustments** - Rename tickets, change ticket types or transfer ownership.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-blacklist.svg"></img> **Blacklist & Limits** - Prevent users from creating tickets and set per-user or global ticket limits.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-customisable.svg"></img> **Highly Customisable** - Configure **350+ settings** covering appearance and behaviour.
#### Ticket Automation & Workflows
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/automation-unlimited.svg"></img> **Unlimited Possibilities** - Create unlimited tickets, panels & question flows.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/automation-autoclose.svg"></img> **Autoclose Tickets** - Automatically **close tickets** after predefined conditions.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/automation-autodelete.svg"></img> **Autodelete Tickets** - Automatically **delete closed tickets** to keep channels clean.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/automation-categories.svg"></img> **Category Routing** - Move tickets between categories based on claim or close state.
#### Transcripts & Insights
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/insights-transcripts.svg"></img> **HTML Transcripts** - Generate beautiful, easy-to-read **HTML transcripts** for every ticket.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/insights-stats.svg"></img> **Detailed Statistics** - Track **50+ statistics** for tickets, users and server activity.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/insights-logs.svg"></img> **Ticket Logs** - Track **all ticket events** such as creation, closures, and staff actions.
#### User Experience
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ux-translated.svg"></img> **Fully Translated** - Available in **38+ languages**, translated and maintained by the community.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ux-interactions.svg"></img> **Modern Interactions** - Full support for buttons, dropdowns, slash/text commands & modals.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ux-panels.svg"></img> **Panels** - Create messages with buttons or a dropdown for users to open tickets.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ux-sub-panels.svg"></img> **Sub-Panels** - One panel not enough? Use multiple panels to offer more choices.
#### Plugins & Ecosystem
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ecosystem-plugins.svg"></img> **Plugin System** - Use custom plugins to **add new features** or **modify existing behavior** of the bot.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ecosystem-community.svg"></img> **Community Plugins** - Use and share plugins built by the community.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ecosystem-api.svg"></img> **Advanced API** - Build advanced plugins with access to ticket events and internal systems.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ecosystem-integrations.svg"></img> **Integrations** - Connect Open Ticket with external services to automate workflows across platforms.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/ecosystem-bonus.svg"></img> **Bonus Features** - Somehow, we included Reaction Roles and URL Button support as well.
#### Deployment
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/deployment-quick.svg"></img> **Quick Setup** - Easy **5-minute configuration** using the Interactive Setup CLI.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/deployment-scale.svg"></img> **Scalable & Reliable** - Battle-tested in servers with **100k+ members**.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/deployment-secure.svg"></img> **Private & Secure** - Used by thousands of servers with respect for security & privacy.
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/deployment-pterodactyl.svg"></img> **Pterodactyl Support** - 100% compatible with Pterodactyl panels. [(Download official eggs)](.eggs/README.md)
- <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/deployment-docker.svg"></img> **Docker Support** - Deploy Open Ticket in minutes with Docker containers.
#### Extend functionality even more with our [pre-made community plugins](#-plugins)!
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-reviews.svg"></img> **Reviews** - Create and manage a support review system for tickets.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-tags.svg"></img> **Tags** - Define keywords that automatically trigger predefined responses.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-reminders.svg"></img> **Reminders** - Create and manage custom reminders for users or staff.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-ai.svg"></img> **AI Integrations** - Connect to AI providers such as ChatGPT, Claude, or Gemini.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-display.svg"></img> **Channel Display** - Create voice channels that display real-time ticket system statistics.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-forms.svg"></img> **Forms** - Build advanced forms for collecting structured information from users.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-embeds.svg"></img> **Custom Embeds** - Create and send custom embeds via commands.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/core-customisable.svg"></img> **Customization Tools** - Additional configuration options for advanced behavior and styling.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-dashboard.svg"></img> **Web Dashboard** - Configure and manage the bot through a remote web dashboard.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-feedback.svg"></img> **Feedback** - Collect user feedback after ticket deletion using forms.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-sqlite.svg"></img> **SQLite Database** - Use an SQLite backend for improved performance and lightweight storage.
> - <img align="center" src="https://apis.dj-dj.be/cdn/openticket/readme-icons/plugin-more.svg"></img> **And more** - Additional community plugins are available and actively expanding.
## ⏱️ Quick Start
> 1. Download the [latest version of Open Ticket](https://github.com/open-discord-bots/open-ticket/releases/latest).
> 2. Make sure you have installed Node.js on your system (check using `node -v`, minimum `v20`).
> 3. Install any required dependencies using `npm install`.
> 4. Configure the bot in one of the following ways:
> - Method 1 (Easy): Start the **Quick Setup CLI** using `npm run setup`.
> - Method 2 (Hard): Manual **JSON configuration** in `./config/...`
> 5. If using the **Quick Setup CLI**, click on `> ⏱️ Quick Setup` and follow the instructions.
> 6. Start the bot using `npm start` or `node index.js`
> - If any config errors occur, the bot will give you a report of how to solve them.
> - Follow the instructions and restart the bot.
> 7. Enjoy using Open Ticket!
> 8. Install plugins from the [**Official Plugin Repository**](https://github.com/open-discord-bots/plugins)
>
> #### 🚦 Next Steps
> **[-> Navigate to (📚 Documentation)](https://otdocs.dj-dj.be)**
> **[-> Navigate to (📞 Support Server)](https://discord.dj-dj.be)**
> **[-> Navigate to (🧩 Plugins/Addons)](https://odplugins.dj-dj.be)**
> **[-> Navigate to (🦇 Pterodactyl Eggs)](.github/pterodactyl-eggs/README.md)**
>
> #### 🖥️ Recommended Hosting
> - **A VPS (Virtual Private Server)** - Extra customisation & more stability. Recommended for most servers.
> - **Any Pterodactyl-Based Panel** - Easy installation & configuration.
### ❤️ Sponsors
Huge thanks to our sponsors for making this project possible. Your support means everything to us.
<table>
<tr>
<td><img src="https://github.com/guillee3.png" alt="Profile Picture" width="100px"></td>
<td><img src="https://github.com/yeeetSK.png" alt="Profile Picture" width="100px"></td>
<td><img src="https://github.com/jacobhumston.png" alt="Profile Picture" width="100px"></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/guillee3"><b>guillee3</b></a></td>
<td align="center"><a href="https://github.com/yeeetSK"><b>yeeetSK</b></a></td>
<td align="center"><a href="https://github.com/jacobhumston"><b>jacobhumston</b></a></td>
</tr>
</table>
**Past Sponsors:**<br>
<a href="https://github.com/sponsors/DJj123dj">
<img src="https://github.com/SpyEye2.png" alt="SpyEye" width="40px">
<img src="https://github.com/mods-hd.png" alt="Mods HD" width="40px">
<img src="https://github.com/DOSEV5.png" alt="DOSEV5" width="40px">
<img src="https://github.com/BENZORICH.png" alt="BENZORICH" width="40px">
</a>
## 📸 Preview
<img alt="An example of a panel." src="https://apis.dj-dj.be/cdn/openticket/preview-v4/panel-examples.png">
<img alt="An example of a ticket message." src="https://apis.dj-dj.be/cdn/openticket/preview-v4/ticket-example.png">
@@ -134,14 +140,14 @@ A list of amazing people who have contributed or provided supported for **Open T
</table>
### 💬 Translators
With the amazing support of our translators, we've been able to translate Open Ticket in more than **37 languages**!
With the amazing support of our translators, we've been able to translate Open Ticket in more than **38 languages**!
#### Categories:
- **🟢 Available**
- **🤖 Partially Made Using AI**
- **🟠 Incomplete**
- **🔴 Unavailable/Outdated**
|🔍 |Languages (37) |Maintainer (Github/Discord) |
|🔍 |Languages (38) |Maintainer (Github/Discord) |
|----|---------------------|--------------------------------|
|🟢 |🇬🇧 English |djj123dj |
|🟢 |🇳🇱 Dutch |djj123dj |
@@ -168,6 +174,7 @@ With the amazing support of our translators, we've been able to translate Open T
|🟢 |🇧🇩 Bengali |HanumeshGupta |
|🟢 |❓ Catalan |guillee3 |
|🟢 |🇨🇳 Traditional Chinese|me.october |
|🟢 |🇰🇭 Khmer (Cambodia) |yuuslokrobjakkroval |
|🤖 |🇪🇪 Estonian |iamnotmega |
|🤖 |🇫🇮 Finnish |iamnotmega |
|🤖 |🇯🇵 Japanese |HanumeshGupta |
+2 -2
View File
@@ -19,8 +19,8 @@
"test": "node index.js --dev-config --dev-database --soft-plugins",
"testsetup": "node index.js --cli --dev-config --dev-database --soft-plugins",
"testnc": "node index.js --no-compile --dev-config --dev-database --soft-plugins",
"docs": "npx typedoc --options .tools/typedoc-config.json && node .tools/createDocs.js",
"mergelang": "node .tools/mergeTranslations.js"
"tools:mergelang": "bun run .tools/mergeTranslations.js",
"tools:sponsors": "bun run .tools/createSponsors.ts"
},
"type": "module",
"license": "GPL-3.0-only",