diff --git a/src/util/entities/Application.ts b/src/util/entities/Application.ts
index e00b4a988..014361bca 100644
--- a/src/util/entities/Application.ts
+++ b/src/util/entities/Application.ts
@@ -21,6 +21,22 @@ import { BaseClass } from "./BaseClass";
import { Team } from "./Team";
import { User } from "./User";
import { Guild } from "./Guild";
+import {
+ ApplicationApprovableConsoleType,
+ ApplicationCompanySchema,
+ ApplicationDiscoverabilityState,
+ ApplicationEventWebhooksStatus,
+ ApplicationEventWebhooksType,
+ ApplicationExecutableSchema,
+ ApplicationGuildRestriction,
+ ApplicationIntegrationTypeConfigurationSchema,
+ ApplicationInteractionsVersion,
+ ApplicationMonetizationState,
+ ApplicationSKUSchema,
+ ApplicationVerificationState,
+ RPCApplicationState,
+ StoreApplicationState,
+} from "@spacebar/schemas";
@Entity({
name: "applications",
@@ -45,9 +61,11 @@ export class Application extends BaseClass {
hook: boolean = true;
@Column()
+ // deprecated
bot_public?: boolean = true;
@Column()
+ // deprecated
bot_require_code_grant?: boolean = false;
@Column()
@@ -57,21 +75,21 @@ export class Application extends BaseClass {
@ManyToOne(() => User, { onDelete: "CASCADE" })
owner: User;
- // TODO: enum this? https://discord.com/developers/docs/resources/application#application-object-application-flags
@Column()
+ // ApplicationFlags bitfield
flags: number = 0;
@Column({ type: "simple-array", nullable: true })
redirect_uris: string[] = [];
- @Column({ nullable: true })
- rpc_application_state: number = 0;
+ @Column()
+ rpc_application_state: RPCApplicationState = RPCApplicationState.DISABLED;
- @Column({ nullable: true })
- store_application_state: number = 1;
+ @Column()
+ store_application_state: StoreApplicationState = StoreApplicationState.NONE;
- @Column({ nullable: true })
- verification_state: number = 1;
+ @Column()
+ verification_state: ApplicationVerificationState = ApplicationVerificationState.INELIGIBLE;
@Column({ nullable: true })
interactions_endpoint_url?: string;
@@ -83,10 +101,10 @@ export class Application extends BaseClass {
integration_require_code_grant: boolean = false;
@Column({ nullable: true })
- discoverability_state: number = 1;
+ discoverability_state: ApplicationDiscoverabilityState = ApplicationDiscoverabilityState.INELIGIBLE;
@Column({ nullable: true })
- discovery_eligibility_flags: number = 2240;
+ discovery_eligibility_flags: number = 0; // ApplicationDiscoveryEligibilityFlags bitfield
@JoinColumn({ name: "bot_user_id" })
@OneToOne(() => User, { onDelete: "CASCADE" })
@@ -118,16 +136,131 @@ export class Application extends BaseClass {
@Column({ nullable: true })
custom_install_url?: string;
- //just for us
+ @Column({ nullable: true })
+ splash?: string;
+
+ @Column({ nullable: true })
+ primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
+
+ @Column({ nullable: true })
+ eula_id?: string;
+
+ @Column({ nullable: true })
+ slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
+
+ @Column({ type: "simple-array", nullable: true })
+ aliases?: string[];
+
+ @Column({ type: "simple-json", nullable: true })
+ executables?: ApplicationExecutableSchema[];
+
+ @Column({ type: "simple-json", nullable: true })
+ third_party_skus?: ApplicationSKUSchema[];
+
+ @Column({ nullable: true })
+ overlay?: boolean = false;
+
+ @Column({ nullable: true })
+ overlay_methods?: number; // only one right now is 1 << 0 OUT_OF_PROCESS
+
+ @Column({ nullable: true })
+ overlay_warn?: boolean = false;
+
+ @Column({ nullable: true })
+ overlay_compatibility_hook?: boolean = false;
+
+ @Column({ type: "simple-json", nullable: true })
+ developers?: ApplicationCompanySchema[];
+
+ @Column({ type: "simple-json", nullable: true })
+ publishers?: ApplicationCompanySchema[];
+
+ @Column({ type: "simple-array", nullable: true })
+ rpc_origins?: string[];
+
+ @Column({ nullable: true })
+ deeplink_uri?: string;
+
+ @Column({ nullable: true })
+ bot_disabled?: boolean = false;
+
+ @Column({ nullable: true })
+ bot_quarantined?: boolean = false;
+
+ @Column({ nullable: true })
+ bot_approximate_guild_count?: number;
+
+ @Column({ nullable: true })
+ approximate_guild_count?: number;
+
+ @Column({ nullable: true })
+ approximate_user_install_count?: number;
+
+ @Column({ nullable: true })
+ approximate_user_authorization_count?: number;
+
+ @Column({ nullable: true })
+ internal_guild_restriction?: ApplicationGuildRestriction;
+
+ @Column({ nullable: true, type: "text" })
+ role_connections_verification_url: string | null = null;
+
+ @Column()
+ interactions_version: ApplicationInteractionsVersion = ApplicationInteractionsVersion.VERSION_1;
- //@Column({ type: "simple-array", nullable: true })
- //rpc_origins?: string[];
+ @Column({ type: "simple-array" })
+ interactions_event_types: ApplicationEventWebhooksType[] = [];
+
+ @Column({ nullable: true })
+ event_webhooks_status?: ApplicationEventWebhooksStatus;
+
+ @Column({ nullable: true })
+ event_webhooks_url?: string;
+
+ @Column({ type: "simple-array", nullable: true })
+ event_webhooks_types?: ApplicationEventWebhooksType[];
+
+ @Column()
+ explicit_content_filter: number = 0; // Inherits the guild's explicit content filter
+
+ @Column({ type: "simple-json", nullable: true })
+ integration_types_config?: Record<number, ApplicationIntegrationTypeConfigurationSchema>;
+
+ @Column({ nullable: true })
+ connection_entrypoint_url?: string;
+
+ @Column()
+ is_verified: boolean = false;
+
+ @Column()
+ creator_monetization_state: number = 0; // unknown values
+
+ @Column()
+ is_discoverable: boolean = false;
+
+ @Column()
+ is_monetized: boolean = false;
+
+ @Column()
+ storefront_available: boolean = false;
+
+ @Column()
+ monetization_state: ApplicationMonetizationState = ApplicationMonetizationState.NONE;
+
+ @Column({ nullable: true })
+ monetization_eligibility_flags?: number = 0; // ApplicationMonetizationEligibilityFlags
+
+ @Column({ nullable: true })
+ max_participants?: number; // max participants for embedded applications, -1 for unlimited
+
+ @Column({ type: "simple-json", nullable: true })
+ embedded_activity_config?: object; // dont feel like making types for this rn
- //@Column({ nullable: true })
- //primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
+ @Column({ type: "simple-array" })
+ approved_consoles: ApplicationApprovableConsoleType[] = [];
- //@Column({ nullable: true })
- //slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
+ // @Column()
+ // pricing_localization_strategy: string;
@JoinColumn({ name: "team_id" })
@ManyToOne(() => Team, {
diff --git a/src/util/entities/SKU.ts b/src/util/entities/SKU.ts
new file mode 100644
index 000000000..e615ed088
--- /dev/null
+++ b/src/util/entities/SKU.ts
@@ -0,0 +1,225 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, OneToMany, Unique } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+import { Application } from "./Application";
+import {
+ BillingPaymentGateway,
+ CreateSKUSchema,
+ LocalizedStringSchema,
+ SKUAccessType,
+ SKUContentRatingAgency,
+ SKUContentRatingSchema,
+ SKUExternalSKUStrategySchema,
+ SKUFeature,
+ SKUGenre,
+ SKUOperatingSystem,
+ SKUPriceSchema,
+ SKUProductLine,
+ SKUSystemRequirementsSchema,
+ SKUTenantMetadataSchema,
+ SKUType,
+} from "@spacebar/schemas";
+import { SKUGuildPowerupMetadataSchema } from "../../schemas/api/store/SKUGuildPowerupMetadataSchema";
+import { slugify } from "../util";
+
+@Entity({
+ name: "skus",
+})
+export class SKU extends BaseClass {
+ @Column()
+ type: SKUType;
+
+ @Column()
+ application_id: string;
+
+ @JoinColumn({ name: "application_id" })
+ @ManyToOne(() => Application, { onDelete: "CASCADE" })
+ application: Application;
+
+ @Column()
+ product_line: SKUProductLine;
+
+ @Column()
+ product_id?: string;
+
+ @Column({ type: "bigint" })
+ flags: number = 0; // SKUFlags
+
+ @Column({ type: "simple-json" })
+ name: LocalizedStringSchema;
+
+ @Column({ nullable: true, type: "simple-json" })
+ summary?: LocalizedStringSchema;
+
+ @Column({ nullable: true, type: "simple-json" })
+ description?: LocalizedStringSchema;
+
+ @Column({ nullable: true, type: "simple-json" })
+ legal_notice?: LocalizedStringSchema;
+
+ @Column()
+ slug: string;
+
+ @Column({ nullable: true })
+ thumbnail_asset_id?: string;
+
+ @Column({ nullable: true, type: "text" })
+ dependent_sku_id?: string | null;
+
+ @JoinColumn({ name: "bundled_sku_ids" })
+ @OneToMany(() => SKU, (sku: SKU) => sku.id, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ onDelete: "CASCADE",
+ })
+ bundled_skus?: SKU[];
+
+ @Column({ nullable: true, type: "simple-array" })
+ bundled_sku_ids?: string[];
+
+ @Column()
+ access_type: SKUAccessType;
+
+ @Column({ nullable: true, type: "simple-array" })
+ manifest_labels?: string[] | null;
+
+ @Column({ type: "simple-array" })
+ features: SKUFeature[];
+
+ @Column({ nullable: true, type: "simple-array" })
+ locales?: string[];
+
+ @Column({ nullable: true, type: "simple-array" })
+ genres?: SKUGenre[];
+
+ @Column({ nullable: true, type: "simple-array" })
+ available_regions?: string[];
+
+ @Column({ nullable: true, type: "simple-json" })
+ // only for localized skus
+ content_rating?: SKUContentRatingSchema;
+
+ @Column({ nullable: true, type: "simple-json" })
+ // only for localized skus
+ content_rating_agency?: SKUContentRatingAgency;
+
+ @Column({ nullable: true, type: "simple-json" })
+ // only for unlocalized skus
+ content_ratings?: Record<SKUContentRatingAgency, SKUContentRatingSchema>;
+
+ @Column({ nullable: true, type: "simple-json" })
+ system_requirements?: Record<SKUOperatingSystem, SKUSystemRequirementsSchema>;
+
+ @Column({ nullable: true, type: "simple-json" })
+ // used for localized skus
+ price?: SKUPriceSchema | Record<string, unknown>; // discord.food says map[string, integer] but i dont think thats true, its probably SKUPriceSchema
+
+ @Column({ nullable: true })
+ // used for unlocalized skus
+ price_tier?: number;
+
+ @Column({ nullable: true })
+ // used for unlocalized skus
+ sale_price_tier?: number;
+
+ @Column({ nullable: true, type: "simple-json" })
+ // used for localized skus
+ sale_price?: Record<string, unknown>; // same here, discord.food says map[string, integer] which i dont believe
+
+ @Column()
+ created_at: Date;
+
+ @Column()
+ updated_at: Date;
+
+ @Column({ nullable: true, type: Date })
+ release_date?: Date | null;
+
+ @Column({ nullable: true })
+ preorder_approximate_release_date?: string;
+
+ @Column({ nullable: true })
+ preorder_released_at?: string;
+
+ @Column({ nullable: true })
+ external_purchase_url?: string;
+
+ @Column({ nullable: true, type: "simple-json" })
+ external_sku_strategies?: Record<BillingPaymentGateway, SKUExternalSKUStrategySchema>;
+
+ @Column({ nullable: true, type: "simple-array" })
+ eligible_payment_gateways?: BillingPaymentGateway[];
+
+ @Column()
+ premium: boolean;
+
+ @Column()
+ show_age_gate: boolean;
+
+ @Column({ nullable: true })
+ // only for localized skus
+ restricted?: boolean;
+
+ @Column({ nullable: true })
+ exclusive?: boolean;
+
+ @Column({ nullable: true, default: false })
+ deleted?: boolean = false;
+
+ @Column({ nullable: true, type: "simple-json" })
+ tenant_metadata?: SKUTenantMetadataSchema;
+
+ @Column({ nullable: true, type: "simple-json" })
+ powerup_metadata?: SKUGuildPowerupMetadataSchema;
+
+ static async createSku(data: CreateSKUSchema): Promise<SKU> {
+ const n = {
+ ...data,
+ bundled_skus: undefined,
+ name: undefined,
+ slug: undefined,
+ };
+
+ // convert string to localized string or use as is
+ const name = typeof data.name === "string" ? { default: data.name } : data.name;
+
+ const sku = await SKU.create({
+ product_line: SKUProductLine.APPLICATION,
+ dependent_sku_id: null,
+ manifest_labels: null,
+ access_type: SKUAccessType.FULL,
+ features: [],
+ release_date: null,
+ premium: false,
+ flags: 0,
+ show_age_gate: false,
+ tenant_metadata: {},
+ created_at: new Date(),
+ updated_at: new Date(),
+ ...n,
+ bundled_sku_ids: data.bundled_skus,
+ name,
+ slug: slugify(name.default),
+ });
+
+ return sku;
+ }
+}
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
index 0e67675a8..dcd91ba7f 100644
--- a/src/util/entities/index.ts
+++ b/src/util/entities/index.ts
@@ -62,3 +62,4 @@ export * from "./UserSettingsProtos";
export * from "./ValidRegistrationTokens";
export * from "./VoiceState";
export * from "./Webhook";
+export * from "./SKU";
diff --git a/src/util/util/ApplicationDiscoveryEligibilityFlags.ts b/src/util/util/ApplicationDiscoveryEligibilityFlags.ts
new file mode 100644
index 000000000..acdf64401
--- /dev/null
+++ b/src/util/util/ApplicationDiscoveryEligibilityFlags.ts
@@ -0,0 +1,40 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { BitField } from "./BitField";
+
+export class ApplicationDiscoveryEligibilityFlags extends BitField {
+ static FLAGS = {
+ VERIFIED: BigInt(1) << BigInt(0),
+ TAG: BigInt(1) << BigInt(1),
+ DESCRIPTION: BigInt(1) << BigInt(2),
+ TERMS_OF_SERVICE: BigInt(1) << BigInt(3),
+ PRIVACY_POLICY: BigInt(1) << BigInt(4),
+ INSTALL_PARAMS: BigInt(1) << BigInt(5),
+ SAFE_NAME: BigInt(1) << BigInt(6),
+ SAFE_DESCRIPTION: BigInt(1) << BigInt(7),
+ APPROVED_COMMANDS: BigInt(1) << BigInt(8),
+ SUPPORT_GUILD: BigInt(1) << BigInt(9),
+ SAFE_COMMANDS: BigInt(1) << BigInt(10),
+ MFA: BigInt(1) << BigInt(11),
+ SAFE_DIRECTORY_OVERVIEW: BigInt(1) << BigInt(12),
+ SUPPORTED_LOCALES: BigInt(1) << BigInt(13),
+ SAFE_SHORT_DESCRIPTION: BigInt(1) << BigInt(14),
+ SAFE_ROLE_CONNECTIONS: BigInt(1) << BigInt(15),
+ };
+}
diff --git a/src/util/util/ApplicationFlags.ts b/src/util/util/ApplicationFlags.ts
new file mode 100644
index 000000000..a158cf786
--- /dev/null
+++ b/src/util/util/ApplicationFlags.ts
@@ -0,0 +1,54 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { BitField } from "./BitField";
+
+export class ApplicationFlags extends BitField {
+ static FLAGS = {
+ EMBEDDED_RELEASED: BigInt(1) << BigInt(1),
+ MANAGED_EMOJI: BigInt(1) << BigInt(2),
+ EMBEDDED_IAP: BigInt(1) << BigInt(3),
+ GROUP_DM_CREATE: BigInt(1) << BigInt(4),
+ RPC_PRIVATE_BETA: BigInt(1) << BigInt(5),
+ AUTO_MODERATION_RULE_CREATE_BADGE: BigInt(1) << BigInt(6),
+ GAME_PROFILE_DISABLED: BigInt(1) << BigInt(7),
+ PUBLIC_OAUTH2_CLIENT: BigInt(1) << BigInt(8),
+ CONTEXTLESS_ACTIVITY: BigInt(1) << BigInt(9),
+ SOCIAL_LAYER_INTEGRATION_LIMITED: BigInt(1) << BigInt(10),
+ CLOUD_GAMING_DEMO: BigInt(1) << BigInt(11),
+ GATEWAY_PRESENCE: BigInt(1) << BigInt(12),
+ GATEWAY_PRESENCE_LIMITED: BigInt(1) << BigInt(13),
+ GATEWAY_GUILD_MEMBERS: BigInt(1) << BigInt(14),
+ GATEWAY_GUILD_MEMBERS_LIMITED: BigInt(1) << BigInt(15),
+ VERIFICATION_PENDING_GUILD_LIMIT: BigInt(1) << BigInt(16),
+ EMBEDDED: BigInt(1) << BigInt(17),
+ GATEWAY_MESSAGE_CONTENT: BigInt(1) << BigInt(18),
+ GATEWAY_MESSAGE_CONTENT_LIMITED: BigInt(1) << BigInt(19),
+ EMBEDDED_FIRST_PARTY: BigInt(1) << BigInt(20),
+ APPLICATION_COMMAND_MIGRATED: BigInt(1) << BigInt(21),
+ APPLICATION_COMMAND_BADGE: BigInt(1) << BigInt(23),
+ ACTIVE: BigInt(1) << BigInt(24),
+ ACTIVE_GRACE_PERIOD: BigInt(1) << BigInt(25),
+ IFRAME_MODAL: BigInt(1) << BigInt(26),
+ SOCIAL_LAYER_INTEGRATION: BigInt(1) << BigInt(27),
+ PROMOTED: BigInt(1) << BigInt(29),
+ PARTNER: BigInt(1) << BigInt(30),
+ PARENT: BigInt(1) << BigInt(33),
+ DISABLE_RELATIONSHIP_ACCESS: BigInt(1) << BigInt(34),
+ };
+}
diff --git a/src/util/util/ApplicationMonetizationEligibilityFlags.ts b/src/util/util/ApplicationMonetizationEligibilityFlags.ts
new file mode 100644
index 000000000..1fb19bd29
--- /dev/null
+++ b/src/util/util/ApplicationMonetizationEligibilityFlags.ts
@@ -0,0 +1,43 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { BitField } from "./BitField";
+
+export class ApplicationMonetizationEligibilityFlags extends BitField {
+ static FLAGS = {
+ VERIFIED: BigInt(1) << BigInt(0),
+ HAS_TEAM: BigInt(1) << BigInt(1),
+ APPROVED_COMMANDS: BigInt(1) << BigInt(2),
+ TERMS_OF_SERVICE: BigInt(1) << BigInt(3),
+ PRIVACY_POLICY: BigInt(1) << BigInt(4),
+ SAFE_NAME: BigInt(1) << BigInt(5),
+ SAFE_DESCRIPTION: BigInt(1) << BigInt(6),
+ SAFE_ROLE_CONNECTIONS: BigInt(1) << BigInt(7),
+ USER_IS_TEAM_OWNER: BigInt(1) << BigInt(8),
+ NOT_QUARANTINED: BigInt(1) << BigInt(9),
+ USER_LOCALE_SUPPORTED: BigInt(1) << BigInt(10),
+ USER_AGE_SUPPORTED: BigInt(1) << BigInt(11),
+ USER_DATE_OF_BIRTH_DEFINED: BigInt(1) << BigInt(12),
+ USER_MFA_ENABLED: BigInt(1) << BigInt(13),
+ USER_EMAIL_VERIFIED: BigInt(1) << BigInt(14),
+ TEAM_MEMBERS_EMAIL_VERIFIED: BigInt(1) << BigInt(15),
+ TEAM_MEMBERS_MFA_ENABLED: BigInt(1) << BigInt(16),
+ NO_BLOCKING_ISSUES: BigInt(1) << BigInt(17),
+ VALID_PAYOUT_STATUS: BigInt(1) << BigInt(18),
+ };
+}
diff --git a/src/util/util/index.ts b/src/util/util/index.ts
index 18f279fed..0563888a8 100644
--- a/src/util/util/index.ts
+++ b/src/util/util/index.ts
@@ -56,3 +56,7 @@ export * from "./Random";
export * from "./Url";
export * from "./SKUFlags";
export * from "./EntitlementGiftCodeFlags";
+export * from "./slugify";
+export * from "./ApplicationDiscoveryEligibilityFlags";
+export * from "./ApplicationFlags";
+export * from "./ApplicationMonetizationEligibilityFlags";
diff --git a/src/util/util/slugify.ts b/src/util/util/slugify.ts
new file mode 100644
index 000000000..03f772fae
--- /dev/null
+++ b/src/util/util/slugify.ts
@@ -0,0 +1,10 @@
+export function slugify(text: string): string {
+ // return as url friendly slug
+ return text
+ .toString()
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, "-")
+ .replace(/[^\w\\-]+/g, "")
+ .replace(/\\-\\-+/g, "-");
+}
|