diff --git a/app/panel/inspect/user/[id]/page.tsx b/app/panel/inspect/user/[id]/page.tsx
index 5d3ccdb..52a3f6a 100644
--- a/app/panel/inspect/user/[id]/page.tsx
+++ b/app/panel/inspect/user/[id]/page.tsx
@@ -8,13 +8,14 @@ import { RelevantReports } from "@/components/pages/inspector/RelevantReports";
import { UserActions } from "@/components/pages/inspector/UserActions";
import { Card, CardHeader } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
-import { PLATFORM_MOD_ID } from "@/lib/constants";
import {
fetchBotById,
fetchBotsByUser,
fetchMembershipsByUser,
- fetchMessages,
+ fetchNoticesByUser,
fetchReports,
+ fetchReportsAgainstUser,
+ fetchReportsByUser,
fetchServers,
fetchSnapshots,
fetchStrikesByUser,
@@ -37,17 +38,14 @@ export default async function User({
const bot = user.bot ? await fetchBotById(user._id) : undefined;
const botOwner = user.bot ? await fetchUserById(user.bot.owner) : undefined;
- // Fetch strikes
+ // Fetch strikes and moderation alerts
const strikes = await fetchStrikesByUser(user._id);
-
- // Fetch moderation alerts
- const dm = await findDM(PLATFORM_MOD_ID, user._id);
- const notices = dm ? await fetchMessages({ channel: dm._id }) : [];
+ const notices = await fetchNoticesByUser(user._id);
// Fetch friends and bots
- const botIds = await fetchBotsByUser(user._id).then((bots) =>
- bots.map((bot) => bot._id)
- );
+ const botIds = await fetchBotsByUser(user._id)
+ .then((bots) => bots.map((bot) => bot._id))
+ .catch(() => []);
const relevantUsers = await fetchUsersById([
...botIds,
@@ -59,30 +57,18 @@ export default async function User({
relevantUsers.sort((a) => (a.bot ? -1 : 0));
// Fetch server memberships
- const serverMemberships = await fetchMembershipsByUser(user._id);
+ const serverMemberships = await fetchMembershipsByUser(user._id).catch(
+ () => []
+ );
const servers = await fetchServers({
_id: {
$in: serverMemberships.map((member) => member._id.server),
},
- });
+ }).catch(() => []);
// Fetch reports
- const reportsByUser = await fetchReports({
- author_id: user._id,
- });
-
- const reportIdsInvolvingUser = await fetchSnapshots({
- // TODO: slow query
- $or: [{ "content._id": user._id }, { "content.author": user._id }],
- }).then((snapshots) => [
- ...new Set(snapshots.map((snapshot) => snapshot.report_id)),
- ]);
-
- const reportsAgainstUser = await fetchReports({
- _id: {
- $in: reportIdsInvolvingUser,
- },
- });
+ const reportsByUser = await fetchReportsByUser(user._id);
+ const reportsAgainstUser = await fetchReportsAgainstUser(user._id);
return (
@@ -123,7 +109,7 @@ export default async function User({
-
+
diff --git a/app/panel/reports/[id]/page.tsx b/app/panel/reports/[id]/page.tsx
index 3aeae2e..da81b1a 100644
--- a/app/panel/reports/[id]/page.tsx
+++ b/app/panel/reports/[id]/page.tsx
@@ -8,7 +8,7 @@ import { NavigationToolbar } from "@/components/common/NavigationToolbar";
import { ReportActions } from "@/components/pages/inspector/ReportActions";
import { Separator } from "@/components/ui/separator";
import {
- fetchRelatedReports,
+ fetchRelatedReportsByContent,
fetchReportById,
fetchSnapshotsByReport,
fetchUserById,
@@ -28,7 +28,7 @@ export default async function Reports({ params }: { params: { id: string } }) {
snapshots
.map((snapshot) => snapshot._id)
.map((contentId) =>
- fetchRelatedReports(contentId).then((reports) =>
+ fetchRelatedReportsByContent(contentId).then((reports) =>
reports.filter((entry) => entry._id !== report._id)
)
)
diff --git a/components/common/ListCompactor.tsx b/components/common/ListCompactor.tsx
index 286dccb..224108f 100644
--- a/components/common/ListCompactor.tsx
+++ b/components/common/ListCompactor.tsx
@@ -15,7 +15,12 @@ export function ListCompactor
({
return (
<>
{data.length === 0 && (
- Empty List
+ <>
+ Empty List
+
+ You may be lacking permissions.
+
+ >
)}
{data.slice(0, limit).map((item, index) => (
diff --git a/components/pages/inspector/RecentMessages.tsx b/components/pages/inspector/RecentMessages.tsx
index 08053b5..a8fc505 100644
--- a/components/pages/inspector/RecentMessages.tsx
+++ b/components/pages/inspector/RecentMessages.tsx
@@ -7,23 +7,32 @@ import {
CardHeader,
CardTitle,
} from "../../ui/card";
-import { fetchMessages, fetchUsersById } from "@/lib/db";
+import { fetchMessages, fetchMessagesByUser, fetchUsersById } from "@/lib/db";
import { CompactMessage } from "../../cards/CompactMessage";
-export async function RecentMessages({
- query,
- users,
-}: {
- query: Filter;
- users?: boolean | User[];
-}) {
- const recentMessages = (await fetchMessages(query)).reverse();
+type Props = { users?: boolean | User[] } & (
+ | {
+ query: Filter;
+ }
+ | {
+ userId: string;
+ }
+);
+
+export async function RecentMessages(props: Props) {
+ const recentMessages = (
+ (props as { query: {} }).query
+ ? await fetchMessages((props as { query: Filter }).query)
+ : await fetchMessagesByUser((props as { userId: string }).userId).catch(
+ () => []
+ )
+ ).reverse();
const userList = (
- users === true
+ props.users === true
? await fetchUsersById([...new Set(recentMessages.map((x) => x.author))])
- : Array.isArray(users)
- ? users
+ : Array.isArray(props.users)
+ ? props.users
: []
).reduce((prev, next) => {
prev[next._id] = next;
@@ -38,6 +47,16 @@ export async function RecentMessages({
{/* enter reason for fetching */}
+ {!recentMessages.length && (
+ <>
+
+ No messages found
+
+
+ You may be lacking permissions.
+
+ >
+ )}
{recentMessages.map((message) => (
(
{strike.reason}
-
- {dayjs(decodeTime(strike._id))?.fromNow()}
-
+ {dayjs(decodeTime(strike._id)).fromNow()}
))}
diff --git a/lib/accessPermissions.ts b/lib/accessPermissions.ts
index c783e07..dc4c9ed 100644
--- a/lib/accessPermissions.ts
+++ b/lib/accessPermissions.ts
@@ -13,14 +13,14 @@ type Permission =
| `/fetch${"" | "/by-id" | "/by-user"}`
| `/update${"" | "/discoverability"}`}`
| `channels${"" | `/fetch${"" | "/by-id" | "/dm"}` | `/create${"" | "/dm"}`}`
- | `messages${"" | `/fetch${"" | "/by-id"}`}`
+ | `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}`
| `reports${
| ""
| `/fetch${
| ""
| "/by-id"
| "/open"
- | `/related${"" | "/by-content" | "/by-user"}`
+ | `/related${"" | "/by-content" | "/by-user" | "/against-user"}`
| `/snapshots${"" | "/by-report" | "/by-user"}`}`
| `/update${
| ""
@@ -36,24 +36,55 @@ type Permission =
| `/update${"" | "/flags" | "/discoverability"}`}`
| `users${
| ""
- | `/fetch${"" | "/by-id" | "/memberships"}`
+ | `/fetch${
+ | ""
+ | "/by-id"
+ | "/memberships"
+ | "/strikes"
+ | "/notices"
+ | "/relations"}`
| `/create${"" | "/alert" | "/strike"}`
| `/update${"" | "/badges"}`
| `/action${"" | "/unsuspend" | "/suspend" | "/wipe" | "/ban"}`}`;
const PermissionSets = {
+ // View open reports
"view-open-reports": [
- // Required for viewing open reports
"users/fetch/by-id",
"reports/fetch/open",
"reports/fetch/by-id",
"reports/fetch/related",
"reports/fetch/snapshots/by-report",
] as Permission[],
+
+ // Edit reports
+ "edit-reports": [
+ "reports/update/notes",
+ "reports/update/resolve",
+ "reports/update/reject",
+ "reports/update/reopen",
+ ] as Permission[],
+
+ // Moderate users
+ "moderate-users": [
+ "users/fetch/by-id",
+ "users/fetch/strikes",
+ "users/fetch/notices",
+ // "bots/fetch/by-user",
+ // "messages/fetch/by-user",
+ // "users/fetch/memberships",
+ // "servers/fetch",
+ "reports/fetch/related/by-user",
+ "reports/fetch/related/against-user",
+ ] as Permission[],
};
const Roles = {
- moderator: [...PermissionSets["view-open-reports"]],
+ moderator: [
+ ...PermissionSets["view-open-reports"],
+ ...PermissionSets["edit-reports"],
+ ...PermissionSets["moderate-users"],
+ ],
};
const ACL: Record> = {
diff --git a/lib/db.ts b/lib/db.ts
index 6f201ec..c062186 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -14,7 +14,8 @@ import type {
} from "revolt-api";
import { ulid } from "ulid";
import { publishMessage } from "./redis";
-import { checkPermission } from "./accessPermissions";
+import { checkPermission, hasPermissionFromSession } from "./accessPermissions";
+import { PLATFORM_MOD_ID } from "./constants";
let client: MongoClient;
@@ -121,7 +122,16 @@ export async function fetchUserById(id: string) {
return await mongo()
.db("revolt")
.collection("users")
- .findOne({ _id: id });
+ .findOne(
+ { _id: id },
+ {
+ projection: {
+ relations: (await hasPermissionFromSession("users/fetch/relations"))
+ ? 1
+ : 0,
+ },
+ }
+ );
}
export async function fetchUsersById(ids: string[]) {
@@ -130,7 +140,16 @@ export async function fetchUsersById(ids: string[]) {
return await mongo()
.db("revolt")
.collection("users")
- .find({ _id: { $in: ids } })
+ .find(
+ { _id: { $in: ids } },
+ {
+ projection: {
+ relations: (await hasPermissionFromSession("users/fetch/relations"))
+ ? 1
+ : 0,
+ },
+ }
+ )
.toArray();
}
@@ -227,6 +246,26 @@ export async function fetchMessageById(id: string) {
.findOne({ _id: id });
}
+export async function fetchMessagesByUser(userId: string) {
+ await checkPermission("messages/fetch/by-user");
+
+ return await mongo()
+ .db("revolt")
+ .collection("messages")
+ .find({ author: userId }, { sort: { _id: -1 }, limit: 50 })
+ .toArray();
+}
+
+export async function fetchMessagesByChannel(channelId: string) {
+ await checkPermission("messages/fetch/by-user");
+
+ return await mongo()
+ .db("revolt")
+ .collection("messages")
+ .find({ channel: channelId }, { sort: { _id: -1 }, limit: 50 })
+ .toArray();
+}
+
export async function fetchMessages(query: Filter, limit = 50) {
await checkPermission("messages/fetch");
@@ -254,7 +293,7 @@ export async function fetchOpenReports() {
.toArray();
}
-export async function fetchRelatedReports(contentId: string) {
+export async function fetchRelatedReportsByContent(contentId: string) {
await checkPermission("reports/fetch/related/by-content");
return await mongo()
@@ -271,6 +310,57 @@ export async function fetchRelatedReports(contentId: string) {
.toArray();
}
+export async function fetchReportsByUser(userId: string) {
+ await checkPermission("reports/fetch/related/by-user");
+
+ return await mongo()
+ .db("revolt")
+ .collection("safety_reports")
+ .find(
+ { status: "Created", authorId: userId },
+ {
+ sort: {
+ _id: -1,
+ },
+ }
+ )
+ .toArray();
+}
+
+export async function fetchReportsAgainstUser(userId: string) {
+ await checkPermission("reports/fetch/related/against-user");
+
+ const reportIdsInvolvingUser = await mongo()
+ .db("revolt")
+ .collection<{ _id: string; report_id: string; content: SnapshotContent }>(
+ "safety_snapshots"
+ )
+ .find({
+ $or: [{ "content._id": userId }, { "content.author": userId }],
+ })
+ .toArray()
+ .then((snapshots) => [
+ ...new Set(snapshots.map((snapshot) => snapshot.report_id)),
+ ]);
+
+ return await mongo()
+ .db("revolt")
+ .collection("safety_reports")
+ .find(
+ {
+ _id: {
+ $in: reportIdsInvolvingUser,
+ },
+ },
+ {
+ sort: {
+ _id: -1,
+ },
+ }
+ )
+ .toArray();
+}
+
export async function fetchReports(
query: Filter = { status: "Created" }
) {
@@ -332,7 +422,7 @@ export async function fetchSnapshotsByReport(reportId: string) {
}
export async function fetchStrikesByUser(userId: string) {
- await checkPermission("reports/fetch/snapshots/by-user");
+ await checkPermission("users/fetch/strikes");
return await mongo()
.db("revolt")
@@ -348,6 +438,28 @@ export async function fetchStrikesByUser(userId: string) {
.toArray();
}
+export async function fetchNoticesByUser(userId: string) {
+ await checkPermission("users/fetch/notices");
+
+ const dm = await mongo()
+ .db("revolt")
+ .collection("channels")
+ .findOne({
+ channel_type: "DirectMessage",
+ recipients: {
+ $all: [userId, PLATFORM_MOD_ID],
+ },
+ });
+
+ if (!dm) return [];
+
+ return await mongo()
+ .db("revolt")
+ .collection("messages")
+ .find({ channel: dm!._id }, { sort: { _id: -1 } })
+ .toArray();
+}
+
export async function fetchBotsByUser(userId: string) {
await checkPermission("bots/fetch/by-user");