26.2-pre-2
This commit is contained in:
1 parent
ea2a8c97ae
commit
762fcf45c3
26 files changed
+1252
-2963
No files matched your search
@@ -0,0 +1,199 @@
|
||||
// SupportBot | Emerald Services
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { fullPermissions, normalizePermissions } = require("./DashboardPermissions.js");
|
||||
|
||||
const GROUPS_FILE = path.join(__dirname, "../Data/dashboard-groups.json");
|
||||
|
||||
const DEFAULT_GROUPS = [
|
||||
{
|
||||
id: "owner",
|
||||
name: "Owner",
|
||||
description: "Full system owner access to all features and configs",
|
||||
color: "#10B981", // Emerald Green
|
||||
discordRoleId: null,
|
||||
isSystem: true,
|
||||
permissions: fullPermissions(),
|
||||
},
|
||||
{
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
description: "Full administrative access including user and group management",
|
||||
color: "#3B82F6", // Blue
|
||||
discordRoleId: null,
|
||||
isSystem: true,
|
||||
permissions: fullPermissions(),
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
name: "Support Staff",
|
||||
description: "Access to Overview, Live Tickets, and Transcripts",
|
||||
color: "#8B5CF6", // Purple
|
||||
discordRoleId: null,
|
||||
isSystem: false,
|
||||
permissions: {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: false,
|
||||
transcripts: true,
|
||||
settings: { view: false, update: false },
|
||||
configs: {
|
||||
supportbot: { view: false, edit: false },
|
||||
"ticket-panel": { view: false, edit: false },
|
||||
commands: { view: false, edit: false },
|
||||
messages: { view: false, edit: false },
|
||||
"supportbot-ai": { view: false, edit: false },
|
||||
api: { view: false, edit: false },
|
||||
},
|
||||
users: { view: false, manage: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "viewer",
|
||||
name: "Viewer",
|
||||
description: "Read-only access to dashboard and logs",
|
||||
color: "#6B7280", // Gray
|
||||
discordRoleId: null,
|
||||
isSystem: false,
|
||||
permissions: {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: false },
|
||||
configs: {
|
||||
supportbot: { view: true, edit: false },
|
||||
"ticket-panel": { view: true, edit: false },
|
||||
commands: { view: true, edit: false },
|
||||
messages: { view: true, edit: false },
|
||||
"supportbot-ai": { view: true, edit: false },
|
||||
api: { view: true, edit: false },
|
||||
},
|
||||
users: { view: false, manage: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function ensureStorage() {
|
||||
const dir = path.dirname(GROUPS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(GROUPS_FILE)) {
|
||||
const initialData = {
|
||||
version: 1,
|
||||
groups: DEFAULT_GROUPS,
|
||||
};
|
||||
fs.writeFileSync(GROUPS_FILE, JSON.stringify(initialData, null, 2), "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
function loadStore() {
|
||||
ensureStorage();
|
||||
try {
|
||||
const raw = fs.readFileSync(GROUPS_FILE, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.groups)) {
|
||||
return parsed.groups;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[GroupStore] Error loading groups file:", err.message);
|
||||
}
|
||||
return DEFAULT_GROUPS;
|
||||
}
|
||||
|
||||
function saveStore(groups) {
|
||||
ensureStorage();
|
||||
const payload = {
|
||||
version: 1,
|
||||
groups,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
fs.writeFileSync(GROUPS_FILE, JSON.stringify(payload, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function listGroups() {
|
||||
return loadStore();
|
||||
}
|
||||
|
||||
function getGroup(id) {
|
||||
const groups = loadStore();
|
||||
return groups.find((g) => g.id === id) || null;
|
||||
}
|
||||
|
||||
function createGroup(payload = {}) {
|
||||
const groups = loadStore();
|
||||
const name = (payload.name || "New Group").trim();
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
|
||||
let newId = slug || `group_${Date.now()}`;
|
||||
|
||||
let counter = 1;
|
||||
while (groups.some((g) => g.id === newId)) {
|
||||
newId = `${slug}_${counter++}`;
|
||||
}
|
||||
|
||||
const newGroup = {
|
||||
id: newId,
|
||||
name,
|
||||
description: (payload.description || "").trim(),
|
||||
color: payload.color || "#10B981",
|
||||
discordRoleId: payload.discordRoleId ? String(payload.discordRoleId).trim() : null,
|
||||
isSystem: false,
|
||||
permissions: normalizePermissions(payload.permissions),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
groups.push(newGroup);
|
||||
saveStore(groups);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
function updateGroup(id, payload = {}) {
|
||||
const groups = loadStore();
|
||||
const index = groups.findIndex((g) => g.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error(`Group with ID "${id}" not found.`);
|
||||
}
|
||||
|
||||
const existing = groups[index];
|
||||
const updatedGroup = {
|
||||
...existing,
|
||||
name: payload.name !== undefined ? payload.name.trim() : existing.name,
|
||||
description: payload.description !== undefined ? payload.description.trim() : existing.description,
|
||||
color: payload.color || existing.color,
|
||||
discordRoleId: payload.discordRoleId !== undefined ? (payload.discordRoleId ? String(payload.discordRoleId).trim() : null) : existing.discordRoleId,
|
||||
permissions: payload.permissions ? normalizePermissions(payload.permissions) : existing.permissions,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
groups[index] = updatedGroup;
|
||||
saveStore(groups);
|
||||
return updatedGroup;
|
||||
}
|
||||
|
||||
function deleteGroup(id) {
|
||||
const groups = loadStore();
|
||||
const group = groups.find((g) => g.id === id);
|
||||
if (!group) {
|
||||
throw new Error(`Group with ID "${id}" not found.`);
|
||||
}
|
||||
|
||||
if (group.isSystem) {
|
||||
throw new Error(`System group "${group.name}" cannot be deleted.`);
|
||||
}
|
||||
|
||||
const filtered = groups.filter((g) => g.id !== id);
|
||||
saveStore(filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listGroups,
|
||||
getGroup,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
DEFAULT_GROUPS,
|
||||
};
|
||||
@@ -20,6 +20,7 @@ function configMap(view, edit) {
|
||||
function fullPermissions() {
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: true },
|
||||
@@ -36,6 +37,7 @@ function permissionsFromRole(role, customPermissions) {
|
||||
case "moderator":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: false },
|
||||
@@ -45,6 +47,7 @@ function permissionsFromRole(role, customPermissions) {
|
||||
case "editor":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: false,
|
||||
settings: { view: true, update: false },
|
||||
@@ -54,6 +57,7 @@ function permissionsFromRole(role, customPermissions) {
|
||||
case "viewer":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: false },
|
||||
@@ -73,6 +77,7 @@ function normalizePermissions(input) {
|
||||
|
||||
return {
|
||||
overview: input.overview ?? base.overview,
|
||||
tickets: input.tickets ?? input.overview ?? base.tickets,
|
||||
logs: input.logs ?? base.logs,
|
||||
transcripts: input.transcripts ?? base.transcripts,
|
||||
settings: {
|
||||
@@ -97,6 +102,7 @@ function hasPermission(permissions, check) {
|
||||
if (!permissions) return false;
|
||||
|
||||
if (check === "overview") return Boolean(permissions.overview);
|
||||
if (check === "tickets") return Boolean(permissions.tickets ?? permissions.overview);
|
||||
if (check === "logs") return Boolean(permissions.logs);
|
||||
if (check === "transcripts") return Boolean(permissions.transcripts);
|
||||
if (check === "settings.view") return Boolean(permissions.settings?.view);
|
||||
|
||||
@@ -89,19 +89,23 @@ function syncDiscordProfile(userId, profile) {
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
const { getGroup } = require("./DashboardGroupStore.js");
|
||||
|
||||
function resolvePermissions(entry, oauth, userId) {
|
||||
if (isYamlOwner(userId, oauth)) {
|
||||
return { permissions: fullPermissions(), role: "owner", isOwner: true };
|
||||
return { permissions: fullPermissions(), role: "owner", isOwner: true, groupId: "owner" };
|
||||
}
|
||||
if (!entry?.enabled) return null;
|
||||
|
||||
const role = entry.role || "viewer";
|
||||
const permissions =
|
||||
role === "custom"
|
||||
? permissionsFromRole("custom", entry.permissions)
|
||||
: permissionsFromRole(role);
|
||||
const groupId = entry.groupId || entry.role || "support";
|
||||
const group = getGroup(groupId);
|
||||
|
||||
return { permissions, role, isOwner: false };
|
||||
if (entry.role === "custom" && entry.permissions) {
|
||||
return { permissions: normalizePermissions(entry.permissions), role: "custom", isOwner: false, groupId };
|
||||
}
|
||||
|
||||
const permissions = group ? group.permissions : permissionsFromRole(entry.role || "viewer");
|
||||
return { permissions, role: entry.role || groupId, isOwner: false, groupId };
|
||||
}
|
||||
|
||||
function getAccess(userId, oauth) {
|
||||
@@ -175,7 +179,8 @@ function listUsers(oauth) {
|
||||
avatar: entry.avatar
|
||||
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
|
||||
: discordAvatarUrl({ id: entry.id, avatar: null }),
|
||||
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role,
|
||||
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role || entry.groupId || "support",
|
||||
groupId: entry.groupId || entry.role || "support",
|
||||
permissions:
|
||||
entry.role === "custom"
|
||||
? normalizePermissions(entry.permissions)
|
||||
@@ -195,11 +200,8 @@ function addUser(payload, actorId, oauth) {
|
||||
throw new Error("Invalid Discord user ID");
|
||||
}
|
||||
|
||||
const role = payload.role || "viewer";
|
||||
if (!ROLES.includes(role)) throw new Error("Invalid role");
|
||||
if (role === "owner" && !isYamlOwner(actorId, oauth)) {
|
||||
throw new Error("Only api.yml owners can assign the owner role");
|
||||
}
|
||||
const role = payload.role || payload.groupId || "support";
|
||||
const groupId = payload.groupId || payload.role || "support";
|
||||
|
||||
const store = readStore();
|
||||
if (store.users[id]) throw new Error("User already exists");
|
||||
@@ -210,6 +212,7 @@ function addUser(payload, actorId, oauth) {
|
||||
globalName: payload.globalName || null,
|
||||
avatar: null,
|
||||
role,
|
||||
groupId,
|
||||
permissions:
|
||||
role === "custom" ? normalizePermissions(payload.permissions) : null,
|
||||
enabled: payload.enabled !== false,
|
||||
@@ -231,15 +234,14 @@ function updateUser(id, payload, actorId, oauth) {
|
||||
throw new Error("Cannot disable an owner defined in api.yml");
|
||||
}
|
||||
|
||||
if (payload.groupId) {
|
||||
entry.groupId = payload.groupId;
|
||||
if (!payload.role) entry.role = payload.groupId;
|
||||
}
|
||||
|
||||
if (payload.role) {
|
||||
if (!ROLES.includes(payload.role)) throw new Error("Invalid role");
|
||||
if (payload.role === "owner" && !isYamlOwner(actorId, oauth)) {
|
||||
throw new Error("Only api.yml owners can assign the owner role");
|
||||
}
|
||||
if (isYamlOwner(id, oauth) && payload.role !== "owner") {
|
||||
throw new Error("Cannot change role of an api.yml owner");
|
||||
}
|
||||
entry.role = payload.role;
|
||||
if (!payload.groupId) entry.groupId = payload.role;
|
||||
}
|
||||
|
||||
if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled);
|
||||
|
||||
@@ -194,6 +194,18 @@ module.exports = {
|
||||
ticketName: ticket.name || null,
|
||||
ticketNumber: ticket.number || null,
|
||||
});
|
||||
|
||||
if (global.apiServer) {
|
||||
global.apiServer.broadcast("ticket_created", {
|
||||
ticket_id: ticket.id,
|
||||
user_id: ticket.user,
|
||||
subject: ticket.reason || "",
|
||||
description: ticket.description || "",
|
||||
department: ticket.department || "general",
|
||||
priority: ticket.priority || "medium",
|
||||
created_at: now
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTicketQuestionAnswers(ticketId, questionAnswers = []) {
|
||||
@@ -236,6 +248,10 @@ module.exports = {
|
||||
updatedAt: Date.now(),
|
||||
ticketId,
|
||||
});
|
||||
|
||||
if (global.apiServer && status === "closed") {
|
||||
global.apiServer.broadcast("ticket_closed", { ticket_id: ticketId });
|
||||
}
|
||||
},
|
||||
|
||||
updateTicketDepartment(ticketId, department) {
|
||||
|
||||
+53
-10
@@ -1,4 +1,4 @@
|
||||
const fs = require("fs");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
@@ -135,7 +135,14 @@ function renderEmbeds(embeds = []) {
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function createTranscript(interaction, ticket, reason) {
|
||||
async function createTranscript(interaction, ticketInput, reasonInput) {
|
||||
let ticketRecord = typeof ticketInput === "object" && ticketInput !== null ? ticketInput : null;
|
||||
let closeReason = typeof ticketInput === "string" ? ticketInput : reasonInput;
|
||||
|
||||
if (!ticketRecord && typeof db.getTicket === "function") {
|
||||
ticketRecord = db.getTicket(interaction.channel.id) || {};
|
||||
}
|
||||
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
const transcriptChannel = await getChannel(
|
||||
@@ -188,12 +195,27 @@ async function createTranscript(interaction, ticket, reason) {
|
||||
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||
}
|
||||
|
||||
const ticketUserId = ticket.user_id || ticket.user;
|
||||
let ticketUserId = ticketRecord?.user_id || ticketRecord?.user || ticketRecord?.author_id;
|
||||
|
||||
if (!ticketUserId && interaction.channel.permissionOverwrites) {
|
||||
const memberOverwrite = interaction.channel.permissionOverwrites.cache.find(
|
||||
(pow) => pow.type === 1 && pow.id !== interaction.client.user.id
|
||||
);
|
||||
if (memberOverwrite) {
|
||||
ticketUserId = memberOverwrite.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ticketUserId && allMessages.length > 0) {
|
||||
const firstNonBotMsg = allMessages.find((m) => !m.author.bot);
|
||||
if (firstNonBotMsg) {
|
||||
ticketUserId = firstNonBotMsg.author.id;
|
||||
}
|
||||
}
|
||||
|
||||
const tUser =
|
||||
interaction.client.users.cache.get(ticketUserId) ||
|
||||
(ticketUserId
|
||||
? await interaction.client.users.fetch(ticketUserId).catch(() => null)
|
||||
: null);
|
||||
(ticketUserId ? interaction.client.users.cache.get(ticketUserId) : null) ||
|
||||
(ticketUserId ? await interaction.client.users.fetch(ticketUserId).catch(() => null) : null);
|
||||
|
||||
const transcriptEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketLog.Title)
|
||||
@@ -204,16 +226,37 @@ async function createTranscript(interaction, ticket, reason) {
|
||||
})
|
||||
.setDescription(
|
||||
`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
|
||||
`> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticketUserId || "Unknown"}\`)\n` +
|
||||
`> **User:** ${tUser ? `<@${tUser.id}> (\`${tUser.tag}\`)` : "Unknown User"}\n` +
|
||||
`> **Closed by:** <@${interaction.user.id}>\n` +
|
||||
`> **Message Count:** ${transcriptData.length}`,
|
||||
)
|
||||
.addFields({
|
||||
name: "Reason",
|
||||
value: `\`\`\`${reason || "No Reason Provided."}\`\`\``,
|
||||
value: `\`\`\`${closeReason || "No Reason Provided."}\`\`\``,
|
||||
inline: false,
|
||||
});
|
||||
|
||||
try {
|
||||
const configStore = require("./ConfigStore");
|
||||
const transcriptStore = require("./TranscriptStore");
|
||||
const apiConfig = configStore.api?.API;
|
||||
const tSettings = transcriptStore.getSettings();
|
||||
|
||||
if (apiConfig?.Enabled !== false) {
|
||||
const port = apiConfig?.Port || 3000;
|
||||
const isPublic = Boolean(tSettings?.publicAccess?.enabled);
|
||||
const transcriptUrl = `http://localhost:${port}/transcripts/${interaction.channel.id}`;
|
||||
|
||||
transcriptEmbed.addFields({
|
||||
name: isPublic ? "Public Web Transcript" : "Dashboard Transcript",
|
||||
value: `[${isPublic ? "Click here to view public transcript" : "Click here to view in Dashboard"}](${transcriptUrl})`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[TicketManager] Could not add web transcript URL:", err.message);
|
||||
}
|
||||
|
||||
const transcriptDir = path.join(process.cwd(), "./Data/Transcripts");
|
||||
if (!fs.existsSync(transcriptDir)) {
|
||||
fs.mkdirSync(transcriptDir, { recursive: true });
|
||||
@@ -228,7 +271,7 @@ async function createTranscript(interaction, ticket, reason) {
|
||||
name: interaction.channel.name,
|
||||
messages: transcriptData,
|
||||
},
|
||||
reason,
|
||||
closeReason,
|
||||
);
|
||||
|
||||
fs.writeFileSync(htmlPath, html, "utf8");
|
||||
|
||||
@@ -23,9 +23,14 @@ function parseMarkdown(content = "") {
|
||||
|
||||
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
safe = safe.replace(/\|\|(.*?)\|\|/g, '<span class="spoiler">$1</span>');
|
||||
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||
safe = safe.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||
safe = safe.replace(/__(.*?)__/g, "<u>$1</u>");
|
||||
safe = safe.replace(/<@!?(\d+)>/g, '<span class="mention">@User</span>');
|
||||
safe = safe.replace(/<@&(\d+)>/g, '<span class="mention">@Role</span>');
|
||||
safe = safe.replace(
|
||||
/\[(.*?)\]\((.*?)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
@@ -112,10 +117,11 @@ function renderAttachment(att) {
|
||||
function renderEmbeds(embeds = []) {
|
||||
return embeds
|
||||
.map((embed) => {
|
||||
const color = embed.color || "#5865F2";
|
||||
const fields = (embed.fields || [])
|
||||
.map(
|
||||
(field) => `
|
||||
<div class="embed-field">
|
||||
<div class="embed-field ${field.inline ? 'inline-field' : ''}">
|
||||
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||
</div>
|
||||
@@ -123,11 +129,37 @@ function renderEmbeds(embeds = []) {
|
||||
)
|
||||
.join("");
|
||||
|
||||
const author = embed.author ? `
|
||||
<div class="embed-author">
|
||||
${embed.author.iconURL ? `<img src="${escapeHtml(embed.author.iconURL)}" class="embed-author-icon" />` : ''}
|
||||
<span>${escapeHtml(embed.author.name || '')}</span>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const thumbnail = embed.thumbnail?.url ? `
|
||||
<img src="${escapeHtml(embed.thumbnail.url)}" class="embed-thumbnail" />
|
||||
` : '';
|
||||
|
||||
const image = embed.image?.url ? `
|
||||
<div class="embed-image"><img src="${escapeHtml(embed.image.url)}" /></div>
|
||||
` : '';
|
||||
|
||||
const footer = (embed.footer || embed.timestamp) ? `
|
||||
<div class="embed-footer">
|
||||
${embed.footer?.iconURL ? `<img src="${escapeHtml(embed.footer.iconURL)}" class="embed-footer-icon" />` : ''}
|
||||
${embed.footer?.text ? `<span>${escapeHtml(embed.footer.text)}</span>` : ''}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="discord-embed">
|
||||
${embed.title ? `<div class="discord-embed-title">${escapeHtml(embed.title)}</div>` : ""}
|
||||
<div class="discord-embed" style="border-left-color: ${escapeHtml(color)}; border-left-width: 4px; border-left-style: solid; position: relative;">
|
||||
${author}
|
||||
${thumbnail}
|
||||
${embed.title ? `<div class="discord-embed-title">${parseMarkdown(embed.title)}</div>` : ""}
|
||||
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||
${fields}
|
||||
${fields ? `<div class="embed-fields-grid">${fields}</div>` : ""}
|
||||
${image}
|
||||
${footer}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
@@ -451,13 +483,22 @@ function buildStyles(template) {
|
||||
.discord-embed-title { font-weight: 800; margin-bottom: 6px; }
|
||||
.discord-embed-description { color: var(--text); line-height: 1.5; }
|
||||
|
||||
.embed-author { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 0.9em; margin-bottom: 6px; }
|
||||
.embed-author-icon { width: 18px; height: 18px; border-radius: 50%; }
|
||||
.embed-fields-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); }
|
||||
.embed-thumbnail { position: absolute; top: 12px; right: 12px; width: 44px; height: 44px; border-radius: 6px; object-fit: cover; }
|
||||
.embed-image img { width: 100%; max-height: 300px; object-fit: cover; border-radius: 8px; margin-top: 10px; }
|
||||
.embed-footer { display: flex; align-items: center; gap: 6px; font-size: 0.8em; color: var(--muted); margin-top: 8px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.04); }
|
||||
.embed-footer-icon { width: 14px; height: 14px; border-radius: 50%; }
|
||||
.mention { background: rgba(88, 101, 242, 0.2); color: #5865F2; padding: 2px 6px; border-radius: 4px; font-weight: 600; font-size: 0.9em; }
|
||||
.spoiler { background: #202225; color: transparent; border-radius: 4px; padding: 0 4px; cursor: pointer; }
|
||||
.spoiler:hover { color: inherit; background: rgba(255,255,255,0.1); }
|
||||
|
||||
.embed-field {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.embed-field-name { font-weight: 700; margin-bottom: 4px; }
|
||||
.embed-field-name { font-weight: 700; margin-bottom: 4px; font-size: 0.85em; text-transform: uppercase; color: var(--muted); }
|
||||
.embed-field-value { color: var(--text); line-height: 1.5; }
|
||||
|
||||
.attachment {
|
||||
|
||||
Reference in new issue
Block a user