mirror of
https://github.com/PreMiD/PreMiD.git
synced 2026-04-06 04:41:58 +02:00
Compare commits
19 Commits
api-worker
...
pd-v1.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0785b49ced | ||
|
|
1e1e4f88b4 | ||
|
|
9a5dbd3428 | ||
|
|
596b7fc759 | ||
|
|
de305d76e6 | ||
|
|
7f520d775d | ||
|
|
3df13b6147 | ||
|
|
1106c77649 | ||
|
|
f121006db0 | ||
|
|
459d567a4b | ||
|
|
10278567fd | ||
|
|
038b71326c | ||
|
|
4ecd1b6c0b | ||
|
|
d37ca76318 | ||
|
|
4367f3f679 | ||
|
|
9920bdf83e | ||
|
|
e4c9719b44 | ||
|
|
19e249ce9a | ||
|
|
3338915f4e |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@premid/discord-bot",
|
||||
"type": "module",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.8",
|
||||
"private": true,
|
||||
"description": "PreMiD's discord bot",
|
||||
"license": "MPL-2.0",
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { AutocompleteInteraction, ChatInputCommandInteraction } from "discord.js";
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder } from "discord.js";
|
||||
import { Presence } from "@premid/db";
|
||||
import { Presence as Activity } from "@premid/db";
|
||||
import { createStandardEmbed } from "../util/createStandardEmbed.js";
|
||||
import type { Command } from "../util/loadCommands.js";
|
||||
import { getPresenceList } from "../util/presenceList.js";
|
||||
import { getActivityList } from "../util/activityList.js";
|
||||
import { client } from "../constants.js";
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("presence")
|
||||
.setDescription("Search for a presence")
|
||||
.setName("activity")
|
||||
.setDescription("Search for an activity")
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName("query")
|
||||
.setDescription("The presence to search for")
|
||||
.setDescription("The activity to search for")
|
||||
.setAutocomplete(true)
|
||||
.setRequired(true),
|
||||
),
|
||||
autocomplete: async (interaction: AutocompleteInteraction) => {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const presenceList = getPresenceList();
|
||||
const filtered = presenceList.filter(({ service }) => service.toLowerCase().includes(focusedValue.toLowerCase()));
|
||||
const activityList = getActivityList();
|
||||
const filtered = activityList.filter(({ service }) => service.toLowerCase().includes(focusedValue.toLowerCase()));
|
||||
|
||||
return interaction.respond(filtered.slice(0, 25).map(({ service }) => ({ name: service, value: service })));
|
||||
},
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
if (!query)
|
||||
return interaction.reply({ content: "Please provide a query to search for", ephemeral: true });
|
||||
|
||||
const presence = await Presence.findOne({ name: query }, {
|
||||
const activity = await Activity.findOne({ name: query }, {
|
||||
_id: false,
|
||||
metadata: {
|
||||
service: true,
|
||||
@@ -45,25 +45,25 @@ export default {
|
||||
},
|
||||
});
|
||||
|
||||
if (!presence)
|
||||
return interaction.reply({ content: "Presence not found", ephemeral: true });
|
||||
if (!activity)
|
||||
return interaction.reply({ content: "Activity not found", ephemeral: true });
|
||||
|
||||
const embed = createStandardEmbed({
|
||||
title: presence.metadata.service,
|
||||
description: presence.metadata.description.en,
|
||||
color: presence.metadata.color,
|
||||
fields: presence.metadata.contributors?.length
|
||||
title: activity.metadata.service,
|
||||
description: activity.metadata.description.en,
|
||||
color: activity.metadata.color,
|
||||
fields: activity.metadata.contributors?.length
|
||||
? [{
|
||||
name: "Contributors",
|
||||
value: presence.metadata.contributors.map(contributor => `<@${contributor.id}>`).join(", "),
|
||||
value: activity.metadata.contributors.map(contributor => `<@${contributor.id}>`).join(", "),
|
||||
}]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
embed.setURL(`https://${Array.isArray(presence.metadata.url) ? presence.metadata.url[0] : presence.metadata.url}`);
|
||||
embed.setThumbnail(presence.metadata.logo);
|
||||
embed.setURL(`https://${Array.isArray(activity.metadata.url) ? activity.metadata.url[0] : activity.metadata.url}`);
|
||||
embed.setThumbnail(activity.metadata.logo);
|
||||
|
||||
const author = await client.users.fetch(presence.metadata.author.id).catch(() => null);
|
||||
const author = await client.users.fetch(activity.metadata.author.id).catch(() => null);
|
||||
if (author) {
|
||||
embed.setAuthor({
|
||||
name: author.username,
|
||||
@@ -76,22 +76,22 @@ export default {
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel("Open in Store")
|
||||
.setURL(`https://premid.app/store/presences/${encodeURI(presence.metadata.service)}`)
|
||||
.setURL(`https://premid.app/store/presences/${encodeURI(activity.metadata.service)}`)
|
||||
.setStyle(ButtonStyle.Link),
|
||||
),
|
||||
] });
|
||||
},
|
||||
help: {
|
||||
name: "presence",
|
||||
value: "presence",
|
||||
command: "/presence <query>",
|
||||
commandDescription: "Search for a presence",
|
||||
name: "activity",
|
||||
value: "activity",
|
||||
command: "/activity <query>",
|
||||
commandDescription: "Search for an activity",
|
||||
embed: createStandardEmbed({
|
||||
title: "Command: /presence",
|
||||
description: "Search for a presence",
|
||||
title: "Command: /activity",
|
||||
description: "Search for an activity",
|
||||
fields: [
|
||||
{ name: "Usage", value: "`/presence <query>`", inline: true },
|
||||
{ name: "Example", value: "`/presence YouTube`", inline: true },
|
||||
{ name: "Usage", value: "`/activity <query>`", inline: true },
|
||||
{ name: "Example", value: "`/activity YouTube`", inline: true },
|
||||
],
|
||||
}),
|
||||
},
|
||||
@@ -36,6 +36,7 @@ export const roles = {
|
||||
SUPPORT_AGENT: "566417964820070421",
|
||||
MARKETING_DIRECTOR: "673681900476432387",
|
||||
LOCALIZATION_MANAGER: "811262682408943616",
|
||||
SOCIALS_MANAGER: "1027665964684300358",
|
||||
REPRESENTATIVE: "691384256672563332",
|
||||
CONTRIBUTOR: "1032759805732978708",
|
||||
PATRON: "515874214750715904",
|
||||
@@ -58,6 +59,7 @@ export const roleColors = {
|
||||
SUPPORT_AGENT: "#D67118",
|
||||
MARKETING_DIRECTOR: "#3BA576",
|
||||
LOCALIZATION_MANAGER: "#3BA576",
|
||||
SOCIALS_MANAGER: "#1abc9c",
|
||||
REPRESENTATIVE: "#3BA576",
|
||||
CONTRIBUTOR: "#EB459E",
|
||||
PATRON: "#E5472F",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getActivity } from "./util/getActivity.js";
|
||||
import loadCommands, { commands } from "./util/loadCommands.js";
|
||||
import loadEvents from "./util/loadEvents.js";
|
||||
import { logger } from "./util/logger.js";
|
||||
import { updatePresenceList } from "./util/presenceList.js";
|
||||
import { updateActivityList } from "./util/activityList.js";
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
@@ -39,7 +39,7 @@ catch (error) {
|
||||
try {
|
||||
await connect(processEnv.DATABASE_URL, { appName: "PreMiD Discord Bot" });
|
||||
logger.info("Successfully connected to database");
|
||||
await updatePresenceList();
|
||||
await updateActivityList();
|
||||
logger.info("Successfully updated presence list");
|
||||
}
|
||||
catch (error) {
|
||||
@@ -85,5 +85,5 @@ setInterval(async () => {
|
||||
}, 60000);
|
||||
|
||||
setInterval(() => {
|
||||
updatePresenceList();
|
||||
updateActivityList();
|
||||
}, 1000 * 60 * 5);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { logger } from "./logger.js";
|
||||
|
||||
let presenceList: { service: string; category: PresenceMetadataCategory }[] = [];
|
||||
|
||||
export async function updatePresenceList() {
|
||||
export async function updateActivityList() {
|
||||
presenceList = (await Presence.find({}, { metadata: { category: true, service: true } })).map(presence => ({
|
||||
service: presence.metadata.service,
|
||||
category: presence.metadata.category,
|
||||
@@ -12,6 +12,6 @@ export async function updatePresenceList() {
|
||||
logger.debug(`Updated presence list with ${presenceList.length} presences`);
|
||||
}
|
||||
|
||||
export function getPresenceList() {
|
||||
export function getActivityList() {
|
||||
return presenceList;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ActivitiesOptions } from "discord.js";
|
||||
import { ActivityType } from "discord.js";
|
||||
import { getPresenceList } from "./presenceList.js";
|
||||
import { getActivityList } from "./activityList.js";
|
||||
|
||||
export function getActivity({ previous }: {
|
||||
previous?: string;
|
||||
}): ActivitiesOptions {
|
||||
const presenceList = getPresenceList();
|
||||
const statuses = presenceList.filter(status => status.service !== previous);
|
||||
const activityList = getActivityList();
|
||||
const statuses = activityList.filter(status => status.service !== previous);
|
||||
const selectedStatus = statuses[Math.floor(Math.random() * statuses.length)]!;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@premid/pd",
|
||||
"type": "module",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.2",
|
||||
"private": true,
|
||||
"description": "A small service to shorten image urls to get around Discord's 256 character limit",
|
||||
"license": "MPL-2.0",
|
||||
|
||||
@@ -33,7 +33,6 @@ const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
const existingUrl = await keyv.get(hash);
|
||||
|
||||
if (existingUrl) {
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + existingUrl);
|
||||
}
|
||||
|
||||
@@ -42,7 +41,6 @@ const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
await keyv.set(hash, uniqueId, 30 * 60 * 1000);
|
||||
await keyv.set(uniqueId, body, 30 * 60 * 1000);
|
||||
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + uniqueId);
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
const existingUrl = await keyv.get(hash);
|
||||
|
||||
if (existingUrl) {
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + existingUrl);
|
||||
}
|
||||
|
||||
@@ -48,7 +47,6 @@ const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
keyv.set(uniqueId, body, 30 * 60 * 1000),
|
||||
]);
|
||||
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + uniqueId);
|
||||
};
|
||||
|
||||
|
||||
@@ -61,4 +61,92 @@ describe.concurrent("/create", async () => {
|
||||
expect(result2.statusCode).toBe(200);
|
||||
expect(result2.body).toStrictEqual(body);
|
||||
});
|
||||
|
||||
it("should preserve file extension when URL has a valid image extension", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/image.png`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).toMatch(/\.png$/);
|
||||
});
|
||||
|
||||
it("should preserve file extension when URL has .jpg extension", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/photo.jpg`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).toMatch(/\.jpg$/);
|
||||
});
|
||||
|
||||
it("should preserve file extension when URL has .webp extension", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/image.webp`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).toMatch(/\.webp$/);
|
||||
});
|
||||
|
||||
it("should preserve file extension when URL has .png with query parameters", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/image.png?ref=example`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).toMatch(/\.png$/);
|
||||
});
|
||||
|
||||
it("should preserve file extension when URL has .gif with complex query parameters", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/animated.gif?size=large&quality=high`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).toMatch(/\.gif$/);
|
||||
});
|
||||
|
||||
it("should not preserve file extension when URL has invalid extension", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/document.pdf`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).not.toMatch(/\.pdf$/);
|
||||
});
|
||||
|
||||
it("should work normally when URL has no file extension", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/page`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).not.toMatch(/\.\w+$/);
|
||||
});
|
||||
|
||||
it("should handle case-insensitive extensions", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.exampl${"e".repeat(256)}.com/image.PNG`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
expect(result.body).toMatch(/\.png$/); //* Should be lowercase in result
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,17 +19,25 @@ const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
if (!["http:", "https:"].includes(urlObject.protocol))
|
||||
return reply.status(400).send("Invalid URL");
|
||||
|
||||
//* Extract file extension from URL pathname
|
||||
const pathname = urlObject.pathname;
|
||||
const extensionMatch = pathname.match(/\.([^./]+)$/);
|
||||
const extension = extensionMatch?.[1]?.toLowerCase() ?? null;
|
||||
|
||||
//* Check if extension is in allowed list
|
||||
const allowedExtensions = ["png", "jpeg", "jpg", "gif", "webp"];
|
||||
const hasValidExtension = extension && allowedExtensions.includes(extension);
|
||||
|
||||
const hash = crypto.createHash("sha256").update(url).digest("hex");
|
||||
const existingShortenedUrl = await keyv.get(hash);
|
||||
|
||||
void reply.header("Cache-control", "public, max-age=1800");
|
||||
|
||||
if (existingShortenedUrl) {
|
||||
await Promise.all([keyv.set(hash, existingShortenedUrl, 1800), keyv.set(existingShortenedUrl, url, 1800)]);
|
||||
return reply.send(process.env.BASE_URL! + existingShortenedUrl);
|
||||
}
|
||||
|
||||
const uniqueId = nanoid(10);
|
||||
//* Create unique ID with extension if valid, otherwise without extension
|
||||
const uniqueId = hasValidExtension ? `${nanoid(10)}.${extension}` : nanoid(10);
|
||||
|
||||
await Promise.all([keyv.set(hash, uniqueId, 1800), keyv.set(uniqueId, url, 1800)]);
|
||||
|
||||
|
||||
@@ -98,4 +98,31 @@ describe("getFullLink", async () => {
|
||||
expect(result.headers.get("content-type")).toBe("image/png");
|
||||
expect(Buffer.from(await result.arrayBuffer())).toStrictEqual(imageBuffer);
|
||||
});
|
||||
|
||||
it("should fetch and return PNG image instead of redirecting for URLs with .png extension", async () => {
|
||||
const testUrl = `https://cdn.rcd.gg/PreMiD/resources/reading.png?v=${"a".repeat(250)}`;
|
||||
|
||||
const { body } = await server.inject({
|
||||
url: `/create/${testUrl}`,
|
||||
});
|
||||
|
||||
expect(body).toStrictEqual(expect.any(String));
|
||||
|
||||
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
|
||||
|
||||
const result = await fetch(`${url}${body}`, {
|
||||
headers: {
|
||||
"cf-connecting-ip": "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.headers.get("content-type")).toMatch(/^image\//);
|
||||
//* Should return image data, not redirect
|
||||
expect(result.headers.get("location")).toBeNull();
|
||||
|
||||
//* Verify we got actual image data
|
||||
const imageBuffer = await result.arrayBuffer();
|
||||
expect(imageBuffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,20 +32,50 @@ const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
const hash = crypto.createHash("sha256").update(url).digest("hex");
|
||||
|
||||
await Promise.all([keyv.set(hash, id, 30 * 60 * 1000), keyv.set(id, url, 30 * 60 * 1000)]);
|
||||
void reply.header("Cache-control", "public, max-age=1800");
|
||||
|
||||
//* If it is not a base64 string, redirect to it
|
||||
if (!url.startsWith("data:image"))
|
||||
return reply.redirect(url);
|
||||
//* If it is a base64 string, decode and return the image
|
||||
if (url.startsWith("data:image")) {
|
||||
const image = Buffer.from(
|
||||
url.replace(/^data:image\/\w+;base64,/, ""),
|
||||
"base64",
|
||||
);
|
||||
|
||||
const image = Buffer.from(
|
||||
url.replace(/^data:image\/\w+;base64,/, ""),
|
||||
"base64",
|
||||
);
|
||||
const mime = url.split(";")[0]!.split(":")[1]!;
|
||||
|
||||
const mime = url.split(";")[0]!.split(":")[1]!;
|
||||
return reply.type(mime).send(image);
|
||||
}
|
||||
|
||||
return reply.type(mime).send(image);
|
||||
//* Check if URL has a valid image extension
|
||||
const urlObject = new URL(url);
|
||||
const pathname = urlObject.pathname;
|
||||
const extensionMatch = pathname.match(/\.([^./]+)$/);
|
||||
const extension = extensionMatch?.[1]?.toLowerCase() ?? null;
|
||||
|
||||
const allowedExtensions = ["png", "jpeg", "jpg", "gif", "webp"];
|
||||
const hasValidImageExtension = extension && allowedExtensions.includes(extension);
|
||||
|
||||
//* If URL has valid image extension, fetch and return the image
|
||||
if (hasValidImageExtension) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
return reply.code(404).send("Image not found");
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || `image/${extension}`;
|
||||
const imageBuffer = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
return reply.type(contentType).send(imageBuffer);
|
||||
}
|
||||
catch {
|
||||
//* If fetch fails, fall back to redirect
|
||||
return reply.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
//* For all other URLs, redirect to them
|
||||
return reply.redirect(url);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@premid/schema-server",
|
||||
"type": "module",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.10",
|
||||
"private": true,
|
||||
"description": "A small service to serve the JSON schemas for PreMiD",
|
||||
"license": "MPL-2.0",
|
||||
|
||||
248
apps/schema-server/schemas/metadata/1.13.json
Normal file
248
apps/schema-server/schemas/metadata/1.13.json
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://schemas.premid.app/metadata/1.13",
|
||||
"title": "Metadata",
|
||||
"type": "object",
|
||||
"description": "Metadata that describes a activity.",
|
||||
"definitions": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "User information.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Discord snowflake of the user.",
|
||||
"pattern": "^\\d+$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
|
||||
"type": "string",
|
||||
"description": "The metadata schema URL."
|
||||
},
|
||||
"author": {
|
||||
"$ref": "#/definitions/user",
|
||||
"description": "The author of this activity."
|
||||
},
|
||||
"contributors": {
|
||||
"type": "array",
|
||||
"description": "Any extra contributors to this activity.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/user"
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The service this activity is for."
|
||||
},
|
||||
"altnames": {
|
||||
"type": "array",
|
||||
"description": "Alternative names for the service.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "An alternative name."
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the activity in multiple languages.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The language key. The key must be languagecode(-regioncode).",
|
||||
"pattern": "^[a-z]{2,3}(?:-(?:[a-z]{2}|[0-9]{1,3}))?$"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^[a-z]{2,3}(?:-(?:[a-z]{2}|[0-9]{1,3}))?$": {
|
||||
"type": "string",
|
||||
"description": "The description of the activity in the key's language."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "One of the service's website URLs.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
|
||||
},
|
||||
"minItems": 2
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The SemVer version of the activity. Must just be major.minor.patch.",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "integer",
|
||||
"description": "The Activity System version this activity supports.",
|
||||
"minimum": 1,
|
||||
"maximum": 2
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"description": "The logo of the service this activity is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string",
|
||||
"description": "A thumbnail of the service this activity is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "The theme color of the service this activity is for. Must be either a 6 digit or a 3 digit hex code.",
|
||||
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"description": "The tags for the activity.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A tag.",
|
||||
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "The category the activity falls under.",
|
||||
"enum": [
|
||||
"anime",
|
||||
"games",
|
||||
"music",
|
||||
"socials",
|
||||
"videos",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"iframe": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity should run in IFrames."
|
||||
},
|
||||
"readLogs": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the extension should be reading logs."
|
||||
},
|
||||
"regExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match URLs for the activity to inject into."
|
||||
},
|
||||
"iFrameRegExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match IFrames for the activity to inject into."
|
||||
},
|
||||
"settings": {
|
||||
"type": "array",
|
||||
"description": "An array of settings the user can change in the activity.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A setting.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
|
||||
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
|
||||
},
|
||||
"if": {
|
||||
"type": "object",
|
||||
"description": "Restrict showing this setting if another setting is the defined value.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"patternProperties": {
|
||||
"": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string",
|
||||
"description": "The placeholder for settings that require input. Shown when the input is empty."
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The default value of the setting. Not compatible with `values`."
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"description": "The default values of the setting. Not compatible with `value`.",
|
||||
"items": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"multiLanguage": {
|
||||
"type": "boolean",
|
||||
"description": "When true, strings from the `general.json` file are available for use, plus the <service>.json file. False is not allowed."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"mobile": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity has support for mobile devices."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"author",
|
||||
"service",
|
||||
"description",
|
||||
"url",
|
||||
"version",
|
||||
"apiVersion",
|
||||
"logo",
|
||||
"thumbnail",
|
||||
"color",
|
||||
"tags",
|
||||
"category"
|
||||
]
|
||||
}
|
||||
252
apps/schema-server/schemas/metadata/1.14.json
Normal file
252
apps/schema-server/schemas/metadata/1.14.json
Normal file
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://schemas.premid.app/metadata/1.14",
|
||||
"title": "Metadata",
|
||||
"type": "object",
|
||||
"description": "Metadata that describes a activity.",
|
||||
"definitions": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "User information.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Discord snowflake of the user.",
|
||||
"pattern": "^\\d+$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
|
||||
"type": "string",
|
||||
"description": "The metadata schema URL."
|
||||
},
|
||||
"author": {
|
||||
"$ref": "#/definitions/user",
|
||||
"description": "The author of this activity."
|
||||
},
|
||||
"contributors": {
|
||||
"type": "array",
|
||||
"description": "Any extra contributors to this activity.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/user"
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The service this activity is for."
|
||||
},
|
||||
"altnames": {
|
||||
"type": "array",
|
||||
"description": "Alternative names for the service.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "An alternative name."
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the activity in multiple languages.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The language key. The key must be languagecode(-regioncode).",
|
||||
"pattern": "^[a-z]{2,3}(?:-(?:[a-z]{2}|[0-9]{1,3}))?$"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^[a-z]{2,3}(?:-(?:[a-z]{2}|[0-9]{1,3}))?$": {
|
||||
"type": "string",
|
||||
"description": "The description of the activity in the key's language."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "One of the service's website URLs.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
|
||||
},
|
||||
"minItems": 2
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The SemVer version of the activity. Must just be major.minor.patch.",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "integer",
|
||||
"description": "The Activity System version this activity supports.",
|
||||
"minimum": 1,
|
||||
"maximum": 2
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"description": "The logo of the service this activity is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string",
|
||||
"description": "A thumbnail of the service this activity is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "The theme color of the service this activity is for. Must be either a 6 digit or a 3 digit hex code.",
|
||||
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"description": "The tags for the activity.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A tag.",
|
||||
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "The category the activity falls under.",
|
||||
"enum": [
|
||||
"anime",
|
||||
"games",
|
||||
"music",
|
||||
"socials",
|
||||
"videos",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"iframe": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity should run in IFrames."
|
||||
},
|
||||
"readLogs": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the extension should be reading logs."
|
||||
},
|
||||
"regExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match URLs for the activity to inject into."
|
||||
},
|
||||
"iFrameRegExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match IFrames for the activity to inject into."
|
||||
},
|
||||
"settings": {
|
||||
"type": "array",
|
||||
"description": "An array of settings the user can change in the activity.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A setting.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The description of the setting. Only applicable if `multiLanguage` is disabled."
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
|
||||
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
|
||||
},
|
||||
"if": {
|
||||
"type": "object",
|
||||
"description": "Restrict showing this setting if another setting is the defined value.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"patternProperties": {
|
||||
"": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string",
|
||||
"description": "The placeholder for settings that require input. Shown when the input is empty."
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The default value of the setting. Not compatible with `values`."
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"description": "The default values of the setting. Not compatible with `value`.",
|
||||
"items": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"multiLanguage": {
|
||||
"type": "boolean",
|
||||
"description": "When true, strings from the `general.json` file are available for use, plus the <service>.json file. False is not allowed."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"mobile": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity has support for mobile devices."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"author",
|
||||
"service",
|
||||
"description",
|
||||
"url",
|
||||
"version",
|
||||
"apiVersion",
|
||||
"logo",
|
||||
"thumbnail",
|
||||
"color",
|
||||
"tags",
|
||||
"category"
|
||||
]
|
||||
}
|
||||
256
apps/schema-server/schemas/metadata/1.15.json
Normal file
256
apps/schema-server/schemas/metadata/1.15.json
Normal file
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://schemas.premid.app/metadata/1.15",
|
||||
"title": "Metadata",
|
||||
"type": "object",
|
||||
"description": "Metadata that describes a activity.",
|
||||
"definitions": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "User information.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Discord snowflake of the user.",
|
||||
"pattern": "^\\d+$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
|
||||
"type": "string",
|
||||
"description": "The metadata schema URL."
|
||||
},
|
||||
"author": {
|
||||
"$ref": "#/definitions/user",
|
||||
"description": "The author of this activity."
|
||||
},
|
||||
"contributors": {
|
||||
"type": "array",
|
||||
"description": "Any extra contributors to this activity.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/user"
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The service this activity is for."
|
||||
},
|
||||
"altnames": {
|
||||
"type": "array",
|
||||
"description": "Alternative names for the service.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "An alternative name."
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the activity in multiple languages.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The language key. The key must be languagecode(-regioncode).",
|
||||
"pattern": "^[a-z]{2,3}(?:-(?:[a-z]{2}|[0-9]{1,3}))?$"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^[a-z]{2,3}(?:-(?:[a-z]{2}|[0-9]{1,3}))?$": {
|
||||
"type": "string",
|
||||
"description": "The description of the activity in the key's language."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "One of the service's website URLs.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
|
||||
},
|
||||
"minItems": 2
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The SemVer version of the activity. Must just be major.minor.patch.",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "integer",
|
||||
"description": "The Activity System version this activity supports.",
|
||||
"minimum": 1,
|
||||
"maximum": 2
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"description": "The logo of the service this activity is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string",
|
||||
"description": "A thumbnail of the service this activity is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "The theme color of the service this activity is for. Must be either a 6 digit or a 3 digit hex code.",
|
||||
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"description": "The tags for the activity.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A tag.",
|
||||
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "The category the activity falls under.",
|
||||
"enum": [
|
||||
"anime",
|
||||
"games",
|
||||
"music",
|
||||
"socials",
|
||||
"videos",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"iframe": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity should run in IFrames."
|
||||
},
|
||||
"readLogs": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the extension should be reading logs."
|
||||
},
|
||||
"regExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match URLs for the activity to inject into."
|
||||
},
|
||||
"iFrameRegExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match IFrames for the activity to inject into."
|
||||
},
|
||||
"allowURLOverrides": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity should allow URL overrides."
|
||||
},
|
||||
"settings": {
|
||||
"type": "array",
|
||||
"description": "An array of settings the user can change in the activity.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A setting.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The description of the setting. Only applicable if `multiLanguage` is disabled."
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
|
||||
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
|
||||
},
|
||||
"if": {
|
||||
"type": "object",
|
||||
"description": "Restrict showing this setting if another setting is the defined value.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"patternProperties": {
|
||||
"": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string",
|
||||
"description": "The placeholder for settings that require input. Shown when the input is empty."
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The default value of the setting. Not compatible with `values`."
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"description": "The default values of the setting. Not compatible with `value`.",
|
||||
"items": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"multiLanguage": {
|
||||
"type": "boolean",
|
||||
"description": "When true, strings from the `general.json` file are available for use, plus the <service>.json file. False is not allowed."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"mobile": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the activity has support for mobile devices."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"author",
|
||||
"service",
|
||||
"description",
|
||||
"url",
|
||||
"version",
|
||||
"apiVersion",
|
||||
"logo",
|
||||
"thumbnail",
|
||||
"color",
|
||||
"tags",
|
||||
"category"
|
||||
]
|
||||
}
|
||||
@@ -15,6 +15,8 @@ const staff = computed(() => {
|
||||
"514546359865442304",
|
||||
//* Support
|
||||
"566417964820070421",
|
||||
//* Social Media Manager
|
||||
"1027665964684300358",
|
||||
].includes(item?.user?.roleId || ""),
|
||||
)
|
||||
.sort(sortContributors) || []
|
||||
|
||||
Reference in New Issue
Block a user