mirror of
https://github.com/stoatchat/service-admin-panel.git
synced 2026-06-30 21:47:56 -04:00
chore: general house keeping
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
antispam
|
||||
**/*.json
|
||||
**/*.json
|
||||
_disabled
|
||||
@@ -27,3 +27,13 @@ NTFY_PASSWORD=
|
||||
|
||||
# Platform Account ID
|
||||
PLATFORM_ACCOUNT_ID=01FC17E1WTM2BGE4F3ARN3FDAF
|
||||
|
||||
# Integrations
|
||||
INTEGRATION_ZAMMAD_ENDPOINT=https://help.revolt.chat/api/v1
|
||||
INTEGRATION_ZAMMAD_ACCESS_TOKEN=
|
||||
INTEGRATION_AUTHENTIK_ENDPOINT=https://sso.revolt.chat/api/v3
|
||||
INTEGRATION_AUTHENTIK_API_TOKEN=
|
||||
INTEGRATION_PLANE_ENDPOINT=https://plane.revolt.chat/api/v1
|
||||
INTEGRATION_PLANE_API_TOKEN=
|
||||
INTEGRATION_AFFINE_ENDPOINT=https://affine.revolt.wtf/api/demon-core
|
||||
INTEGRATION_AFFINE_TOKEN=
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
tempGdpr
|
||||
@@ -1,26 +0,0 @@
|
||||
import { PageTitle } from "@/components/common/navigation/PageTitle";
|
||||
import { Metadata } from "next";
|
||||
|
||||
import { Text } from "@radix-ui/themes";
|
||||
|
||||
import pkg from "../../../package.json";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "About",
|
||||
description:
|
||||
"Version information and other useful tidbits about this software.",
|
||||
};
|
||||
|
||||
export default async function About() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle metadata={metadata} />
|
||||
<Text>
|
||||
Version {pkg.version} ·{" "}
|
||||
<a href="https://git.is.horse/revolt/research-development/swiss-army-knife">
|
||||
Source code
|
||||
</a>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Badge, Flex, Text } from "@radix-ui/themes";
|
||||
|
||||
export function Cat() {
|
||||
return (
|
||||
<>
|
||||
<Text color="iris" size="1">
|
||||
Problems with the case
|
||||
</Text>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<Badge color="gray">
|
||||
<b>Invalid</b> Report(s) don't make sense
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>False</b> Report(s) are clearly false
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Spam</b> Report(s) are clearly spam
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Not Enough Evidence</b> Not enough evidence to act on report
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Clarification Needed</b> More information is needed
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Acknowledge</b> Report(s) will be acknowledged but no action taken
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Ignore</b>
|
||||
</Badge>
|
||||
</Flex>
|
||||
|
||||
<Text color="iris" size="1">
|
||||
Reasons for acting on case
|
||||
</Text>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<Badge color="gray">
|
||||
<b>Extremism</b> Violent extremism
|
||||
</Badge>{" "}
|
||||
<Badge color="gray">
|
||||
<b>Misinformation</b> Misinformation & conspiracy theories
|
||||
</Badge>{" "}
|
||||
<Badge color="gray">
|
||||
<b>Hate Conduct</b> Hate speech or hateful conduct
|
||||
</Badge>{" "}
|
||||
<Badge color="gray">
|
||||
<b>Self Harm</b> Promoting, encouraging or glorifying self-harm or
|
||||
suicide
|
||||
</Badge>{" "}
|
||||
<Badge color="gray">
|
||||
<b>Illegal Behaviour</b> Promoting, organising or engaging in illegal
|
||||
behaviour
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Off Platform</b> Promoting off-platform content in violation of
|
||||
usage policy
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Illegal Content</b> Content deemed illegal
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Harassment</b> Content designed to harass or degrade someone (incl.
|
||||
death threats, doxing, ignoring someoneone's privacy)
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Raiding</b> Participating in a raid
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Rights Infringement</b> Content violating intellectual property or
|
||||
other rights
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Gore</b> Real-life violence, gore, or animal cruelty
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Malware</b>
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Impersonation</b>
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Underage Sexual Content</b> Soliciting or providing sexual content
|
||||
to minors
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Underage Sexual Conduct</b> Engaging in sexual conduct with minors
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Underage Unsafe Conduct</b> Minors engaging in any conduct that may
|
||||
put their safety at risk
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Explicit Content</b> Sharing explicit content in places where it
|
||||
can't be age restricted
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Unsolicited Advertising</b>
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Fraud</b> Financial scams or other types of fraud
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Account Trade</b> Buying or selling of Revolt accounts
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Artificial Growth</b> Providing or using means to artificially grow
|
||||
servers
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Evasion</b> Evasion of permanent platform-level moderation actions
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Hacking</b> Engaging in hacking such as phishing, malware, DoS, etc
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Unauthorised Access</b> Attempts to hack or otherwise gain
|
||||
unauthorised access to our service
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Abuse</b> Abusing our server resources
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>User Bot</b> Automated activity from user not initiated by the user
|
||||
</Badge>
|
||||
<Badge color="gray">
|
||||
<b>Misleading Team</b> Providing false or deceptive reports to our
|
||||
team
|
||||
</Badge>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
type AuthorisedUser = {
|
||||
@@ -41,3 +42,18 @@ export function useAuthorisedUser(allowNull = false): AuthorisedUser {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the currently authorised user has a given scope and return them if such
|
||||
* @param scope Required scope
|
||||
* @returns User email
|
||||
*/
|
||||
export async function useScopedUser(scope: string) {
|
||||
const session = await getServerSession();
|
||||
if (!session?.user?.email) throw "Unauthenticated!";
|
||||
|
||||
// TODO: RBAC code
|
||||
console.debug(`Check ${session.user.email} against scope ${scope}`);
|
||||
|
||||
return session.user.email;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Filter } from "mongodb";
|
||||
import { Message, User } from "revolt-api";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
import {
|
||||
ChangeLogDocument,
|
||||
changelog,
|
||||
channels,
|
||||
messages,
|
||||
users,
|
||||
} from "../db/types";
|
||||
import { publish, publishPrivate } from "../events";
|
||||
|
||||
/**
|
||||
* Send a message
|
||||
* @param message Message
|
||||
*/
|
||||
export async function sendMessage(message: Omit<Message, "_id">) {
|
||||
const doc: Message = {
|
||||
_id: ulid(),
|
||||
...message,
|
||||
};
|
||||
|
||||
await publish(message.channel, {
|
||||
type: "Message",
|
||||
...doc,
|
||||
});
|
||||
|
||||
await messages().insertOne(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or find existing DM between users
|
||||
* @param userA User A
|
||||
* @param userB User B
|
||||
* @returns DM Channel
|
||||
*/
|
||||
export async function createOrFindDM(userA: string, userB: string) {
|
||||
let dm = await channels().findOne({
|
||||
channel_type: "DirectMessage",
|
||||
recipients: { $all: [userA, userB] },
|
||||
});
|
||||
|
||||
if (!dm) {
|
||||
dm = {
|
||||
_id: ulid(),
|
||||
channel_type: "DirectMessage",
|
||||
active: true,
|
||||
recipients: [userA, userB],
|
||||
};
|
||||
|
||||
await channels().insertOne(dm);
|
||||
|
||||
for (const user of [userA, userB])
|
||||
await publishPrivate(user, {
|
||||
type: "ChannelCreate",
|
||||
...dm,
|
||||
});
|
||||
}
|
||||
|
||||
return dm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a platform alert from moderation user
|
||||
* @param userId Target user
|
||||
* @param content Content to send
|
||||
*/
|
||||
export async function sendPlatformAlert(userId: string, content: string) {
|
||||
const dm = await createOrFindDM(userId, process.env.PLATFORM_ACCOUNT_ID!);
|
||||
|
||||
await sendMessage({
|
||||
channel: dm._id,
|
||||
author: process.env.PLATFORM_ACCOUNT_ID!,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a changelog entry
|
||||
* @param userEmail User email
|
||||
* @param change Change
|
||||
*/
|
||||
export async function createChangelog(
|
||||
userEmail: string,
|
||||
change: Omit<ChangeLogDocument, "_id" | "userEmail">,
|
||||
) {
|
||||
const document = {
|
||||
_id: ulid(),
|
||||
userEmail,
|
||||
...change,
|
||||
} as ChangeLogDocument;
|
||||
await changelog().insertOne(document);
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch messages from channel with authors
|
||||
*/
|
||||
export async function fetchMessages(query: Filter<Message>): Promise<{
|
||||
messages: Message[];
|
||||
authors: Record<string, User>;
|
||||
}> {
|
||||
const recentMessages = await messages()
|
||||
.find(query, {
|
||||
sort: {
|
||||
_id: -1,
|
||||
},
|
||||
limit: 200,
|
||||
})
|
||||
.toArray();
|
||||
|
||||
const authors = await users()
|
||||
.find(
|
||||
{
|
||||
_id: {
|
||||
$in: [...new Set(recentMessages.map((message) => message.author))],
|
||||
},
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
discriminator: 1,
|
||||
avatar: 1,
|
||||
},
|
||||
},
|
||||
)
|
||||
.toArray()
|
||||
.then((arr) =>
|
||||
arr.reduce((d, v) => ({ ...d, [v._id]: v }), {} as Record<string, User>),
|
||||
);
|
||||
|
||||
return {
|
||||
messages: recentMessages,
|
||||
authors,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { CaseDocument } from "./types";
|
||||
|
||||
export const TYPES_PROBLEM_WITH_CASE = {
|
||||
Invalid: "Report(s) don't make sense or there isn't a rule violation",
|
||||
False: "Clearly false",
|
||||
"Report Spam": "Clearly spam",
|
||||
"Not Enough Evidence": "Not enough evidence to act on report",
|
||||
"Clarification Needed": "More information is needed",
|
||||
"Bridged Content": "Content came from a remote service",
|
||||
Acknowledge: "Report(s) will be acknowledged but no action taken",
|
||||
Duplicate: "Reported content has already been dealt with",
|
||||
Ignore: "",
|
||||
};
|
||||
|
||||
export const TYPES_PROBLEM_WITH_CASE_KEYS = Object.keys(
|
||||
TYPES_PROBLEM_WITH_CASE,
|
||||
).toSorted() as (keyof typeof TYPES_PROBLEM_WITH_CASE)[];
|
||||
|
||||
export const TYPES_VALID_CATEGORY = {
|
||||
Extremism: "Violent extremism",
|
||||
Misinformation: "Misinformation & conspiracy theories",
|
||||
"Hate Conduct": "Hate speech or hateful conduct",
|
||||
"Self Harm": "Promoting, encouraging or glorifying self-harm or suicide",
|
||||
"Illegal Behaviour": "Promoting, organising or engaging in illegal behaviour",
|
||||
"Off Platform": "Promoting off-platform content in violation of usage policy",
|
||||
"Illegal Content": "Content deemed illegal",
|
||||
Harassment:
|
||||
"Content designed to harass or degrade someone (incl. death threats, doxing, ignoring someone's privacy)",
|
||||
Raiding: "Participating in a raid",
|
||||
"Rights Infringement":
|
||||
"Content violating intellectual property or other rights",
|
||||
Gore: "Real-life violence, gore, or animal cruelty",
|
||||
Malware: "",
|
||||
Impersonation: "",
|
||||
Underage: "User is not within platform age requirements",
|
||||
"Underage Sexual Content": "Soliciting or providing sexual content to minors",
|
||||
"Underage Sexual Conduct": "Engaging in sexual conduct with minors",
|
||||
"Underage Unsafe Conduct":
|
||||
"Minors engaging in any conduct that may put their safety at risk",
|
||||
"Explicit Content":
|
||||
"Sharing explicit content in places where it can't be age restricted",
|
||||
"Unsolicited Advertising": "",
|
||||
Fraud: "Financial scams or other types of fraud",
|
||||
"Account Trade": "Buying or selling of Revolt accounts",
|
||||
"Artificial Growth": "Providing or using means to artificially grow servers",
|
||||
Evasion: "Evasion of permanent platform-level moderation actions",
|
||||
Hacking: "Engaging in hacking such as phishing, malware, DoS, etc",
|
||||
"Unauthorised Access":
|
||||
"Attempts to hack or otherwise gain unauthorised access to our service",
|
||||
Abuse: "Abusing our server resources",
|
||||
Spam: "Content or users with only intent to spam",
|
||||
"User Bot": "Automated activity from user not initiated by the user",
|
||||
"Misleading Team": "Providing false or deceptive reports to our team",
|
||||
};
|
||||
|
||||
export const TYPES_VALID_CATEGORY_KEYS = Object.keys(
|
||||
TYPES_VALID_CATEGORY,
|
||||
).toSorted() as (keyof typeof TYPES_VALID_CATEGORY)[];
|
||||
|
||||
export function reportCategoryTemplateResolutionResponse(
|
||||
key: CaseDocument["category"],
|
||||
) {
|
||||
switch (key[0]) {
|
||||
case "Invalid":
|
||||
return "These report(s) are invalid and no further action can be taken at this time.\nThis may be because:\n- No platform rules are being broken\n- The report doesn't make any sense\n- The reason provided doesn't apply to the content reported";
|
||||
case "False":
|
||||
return "These report(s) are clearly false and no action will be taken.\nRepeated false reports may lead to additional action against your account.";
|
||||
case "Report Spam":
|
||||
return "Report spam may lead to additional action against your account.";
|
||||
case "Not Enough Evidence":
|
||||
return "These report(s) have not been actioned at this time due to a lack of supporting evidence, if you have additional information to support your report, please either report individual relevant messages or send an email to contact@revolt.chat.";
|
||||
case "Clarification Needed":
|
||||
return "These report(s) need clarification and no further action can be taken at this time.\nIf you have additional information to support your report, please either report individual relevant messages or send an email to contact@revolt.chat.";
|
||||
case "Bridged Content":
|
||||
return "These report(s) concern bridged content and we cannot take any further action.\nIf appropriate, the content has been deleted.\nPlease notify the server's moderation team to deal with the matter on their end.\nIf you find the moderation team to be unresponsive, please follow up by reporting the server.";
|
||||
case "Acknowledge":
|
||||
return "These report(s) have been acknowledged, however no action will be taken at this time.";
|
||||
case "Duplicate":
|
||||
return "Appropriate action has already been taken in regards to the reported content, no further action will be taken.";
|
||||
case "Ignore":
|
||||
return "-no valid response-";
|
||||
default:
|
||||
if (key.length === 0) return "-no category selected-";
|
||||
|
||||
return "Report(s) have been actioned and appropriate action has been taken.";
|
||||
}
|
||||
}
|
||||
+147
-6
@@ -1,12 +1,97 @@
|
||||
import {
|
||||
Bot,
|
||||
Channel,
|
||||
Invite,
|
||||
Member,
|
||||
Message,
|
||||
ReportedContent,
|
||||
Server,
|
||||
User,
|
||||
} from "revolt-api";
|
||||
import { col } from ".";
|
||||
|
||||
import { col, db } from ".";
|
||||
import { TYPES_PROBLEM_WITH_CASE, TYPES_VALID_CATEGORY } from "./enums";
|
||||
|
||||
export type ChangeLogDocument = {
|
||||
_id: string;
|
||||
userEmail: string;
|
||||
} & (
|
||||
| ({
|
||||
object: {
|
||||
type: "Server";
|
||||
id: string;
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
type: "comment";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "server/discover/approve";
|
||||
}
|
||||
| {
|
||||
type: "server/discover/reject" | "server/discover/delist";
|
||||
reason: string;
|
||||
}
|
||||
))
|
||||
| ({
|
||||
object: {
|
||||
type: "Bot";
|
||||
id: string;
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
type: "comment";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "bot/discover/approve";
|
||||
}
|
||||
| {
|
||||
type: "bot/discover/reject" | "bot/discover/delist";
|
||||
reason: string;
|
||||
}
|
||||
))
|
||||
| ({
|
||||
object: {
|
||||
type: "Case";
|
||||
id: string;
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
type: "comment";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "case/categorise";
|
||||
category: CaseDocument["category"];
|
||||
}
|
||||
| {
|
||||
type: "case/status";
|
||||
status: CaseDocument["status"];
|
||||
}
|
||||
| {
|
||||
type: "case/title";
|
||||
title: string;
|
||||
}
|
||||
| {
|
||||
type: "case/add_report";
|
||||
reportId: string;
|
||||
}
|
||||
| {
|
||||
type: "case/notify";
|
||||
userIds: string;
|
||||
content: string;
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
/**
|
||||
* Use `revolt_admin/changelog` collection
|
||||
*/
|
||||
export function changelog() {
|
||||
return db("revolt_admin").collection<ChangeLogDocument>("changelog");
|
||||
}
|
||||
|
||||
export type CaseDocument = {
|
||||
_id: string;
|
||||
@@ -14,7 +99,11 @@ export type CaseDocument = {
|
||||
notes?: string;
|
||||
author: string;
|
||||
status: "Open" | "Closed";
|
||||
closed_at?: string;
|
||||
category: (
|
||||
| keyof typeof TYPES_PROBLEM_WITH_CASE
|
||||
| keyof typeof TYPES_VALID_CATEGORY
|
||||
)[];
|
||||
closed_at?: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,14 +119,15 @@ export type ReportDocument = {
|
||||
content: ReportedContent;
|
||||
additional_context: string;
|
||||
case_id?: string;
|
||||
_temp_escalated?: boolean;
|
||||
} & (
|
||||
| { status: "Created" }
|
||||
| ({
|
||||
type: "Rejected" | "Resolved";
|
||||
status: "Rejected" | "Resolved";
|
||||
closed_at?: string;
|
||||
} & (
|
||||
| { type: "Rejected"; rejection_reason?: string }
|
||||
| { type: "Resolved" }
|
||||
| { status: "Rejected"; rejection_reason?: string }
|
||||
| { status: "Resolved" }
|
||||
))
|
||||
);
|
||||
|
||||
@@ -77,6 +167,57 @@ export function servers() {
|
||||
return col<Server>("servers");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `invites` collection
|
||||
*/
|
||||
export function invites() {
|
||||
return col<Invite>("channel_invites");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `analytics/servers` collection
|
||||
*/
|
||||
export function serverAnalytics() {
|
||||
return db("analytics").collection<{
|
||||
_id: string;
|
||||
members: number;
|
||||
volume: number;
|
||||
discoverable: boolean;
|
||||
}>("servers");
|
||||
}
|
||||
|
||||
export type DiscoverRequestDocument = {
|
||||
_id: string;
|
||||
} & ({ type: "Server"; serverId: string } | { type: "Bot"; botId: string });
|
||||
|
||||
/**
|
||||
* Use `revolt_admin/discover_requests` collection
|
||||
*/
|
||||
export function adminDiscoverRequests() {
|
||||
return db("revolt_admin").collection<DiscoverRequestDocument>(
|
||||
"discover_requests",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `bots` collection
|
||||
*/
|
||||
export function bots() {
|
||||
return col<Bot>("bots");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `analytics/bots` collection
|
||||
*/
|
||||
export function botAnalytics() {
|
||||
return db("analytics").collection<{
|
||||
_id: string;
|
||||
servers: number;
|
||||
usage: number;
|
||||
discoverable: boolean;
|
||||
}>("bots");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `channels` collection
|
||||
*/
|
||||
@@ -110,7 +251,7 @@ export function messages() {
|
||||
*/
|
||||
export function accounts() {
|
||||
return col<{ _id: string; email: string; disabled: boolean; spam: boolean }>(
|
||||
"accounts"
|
||||
"accounts",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -1,4 +1,4 @@
|
||||
import { createClient, RedisClientType } from "@redis/client";
|
||||
import { RedisClientType, createClient } from "@redis/client";
|
||||
import type { ProtocolV1 } from "revolt.js/lib/events/v1";
|
||||
|
||||
export { RedisEventListener } from "./eventListener";
|
||||
@@ -39,3 +39,16 @@ export async function newRedis() {
|
||||
export async function publish(topic: string, message: ProtocolV1["server"]) {
|
||||
(await redis()).publish(topic, JSON.stringify(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish to private topic
|
||||
* @param topic Topic
|
||||
* @param message Message
|
||||
*/
|
||||
export async function publishPrivate(
|
||||
topic: string,
|
||||
message: ProtocolV1["server"],
|
||||
) {
|
||||
const privateTopic = `${topic}!`;
|
||||
await publish(privateTopic, message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const queryClient = new QueryClient();
|
||||
|
||||
export async function ClientQueryProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function WindowTrigger({
|
||||
url,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
url: string;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
className="!inline-flex h-fit"
|
||||
style={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
new window.WinBox(title, {
|
||||
url,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import "winbox";
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
export { default } from "next-auth/middleware";
|
||||
|
||||
export const config = {
|
||||
matcher: ["/panel"],
|
||||
matcher: ["/panel", "/panel/:path*"],
|
||||
pages: {
|
||||
signIn: "/",
|
||||
},
|
||||
|
||||
+5
-1
@@ -14,6 +14,7 @@
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/themes": "^2.0.3",
|
||||
"@tanstack/react-query": "^5.24.2",
|
||||
"@tensorflow/tfjs-node": "^4.17.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -28,7 +29,8 @@
|
||||
"revolt.js": "^7.0.0-beta.11",
|
||||
"scikitjs": "^1.24.0",
|
||||
"string-comparison": "^1.3.0",
|
||||
"ulid": "^2.3.0"
|
||||
"ulid": "^2.3.0",
|
||||
"winbox": "^0.2.82"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
@@ -36,6 +38,8 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/winbox": "^0.2.5",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.4",
|
||||
|
||||
Reference in New Issue
Block a user