Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ export class Client extends AsyncEventEmitter<Events> {
#setConnectionFailureCount: Setter<number>;
#reconnectTimeout: number | undefined;

/**
* Server and roles to calculate permissions as, for previewing a role
*/
readonly viewAs: Accessor<{ server: string; roles: string[] } | undefined>;
#setViewAs: Setter<{ server: string; roles: string[] } | undefined>;

/**
* Create Stoat.js Client
* @param configuration Deprecated - Please use `Client.initConfig` if you need to override config.
Expand Down Expand Up @@ -256,6 +262,8 @@ export class Client extends AsyncEventEmitter<Events> {
baseURL: this.options.baseURL,
});

this.#guardAPI();

const [configured, setConfigured] = createSignal(
configuration !== undefined,
);
Expand All @@ -270,6 +278,12 @@ export class Client extends AsyncEventEmitter<Events> {
this.connectionFailureCount = connectionFailureCount;
this.#setConnectionFailureCount = setConnectionFailureCount;

const [viewAs, setViewAs] = createSignal<
{ server: string; roles: string[] } | undefined
>();
this.viewAs = viewAs;
this.#setViewAs = setViewAs;

this.account = new AccountCollection(this);
this.bots = new BotCollection(this);
this.channels = new ChannelCollection(this);
Expand Down Expand Up @@ -334,6 +348,48 @@ export class Client extends AsyncEventEmitter<Events> {
: ["X-Session-Token", this.#session?.token as string];
}

/**
* Calculate permissions in a server as if we only had the given roles
* @param server Server ID, or nothing to stop previewing
* @param roles Role IDs
*/
setViewAs(server?: string, roles: string[] = []): void {
this.#setViewAs(server ? { server, roles } : undefined);
}

/**
* Whether a request would make changes in the server we are previewing
* @param path Request path
*/
#blockedWhilePreviewing(
method: string,
path: string,
data?: unknown,
): boolean {
const server = this.viewAs()?.server;
if (!server || /\/(ack|search)(\/|$)/.test(path)) return false;

const [, type, id, sub] = path.split("/");
if (type === "servers") return id === server;
if (type === "channels") return this.channels.get(id)?.serverId === server;
if (type === "custom" && id === "emoji") {
if (method === "put") {
return (data as { parent?: { id?: string } })?.parent?.id === server;
}
const emoji = this.emojis.get(sub);
return emoji
? emoji.parent.type === "Server" && emoji.parent.id === server
: true;
}
if (type === "webhooks") {
const webhook = this.channelWebhooks.get(id);
return webhook ? webhook.channel?.serverId === server : true;
}
if (type === "invites") return method === "delete";

return false;
}

/**
* Connect to Revolt
*/
Expand Down Expand Up @@ -381,6 +437,23 @@ export class Client extends AsyncEventEmitter<Events> {
revolt: this.#session,
},
});

this.#guardAPI();
}

/**
* Block changes to a server while previewing it as a role
*/
#guardAPI(): void {
const req = this.api.req.bind(this.api);
this.api.req = ((method: string, path: string, ...args: unknown[]) =>
method !== "get" && this.#blockedWhilePreviewing(method, path, args[0])
? Promise.reject({ type: "ViewingAsRole" })
: (req as (...args: unknown[]) => Promise<unknown>)(
method,
path,
...args,
)) as typeof this.api.req;
}

/**
Expand Down
35 changes: 30 additions & 5 deletions src/classes/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,9 @@ export class Server {
* Default channel for this server
*/
get defaultChannel(): Channel | undefined {
return this.orderedChannels.find((cat) => cat.channels.length)?.channels[0];
return this.orderedChannels
.flatMap((cat) => cat.channels)
.find((channel) => this.#previewCanView(channel));
}

/**
Expand All @@ -294,17 +296,19 @@ export class Server {
* @returns Whether the server is unread
*/
get unread(): boolean {
return !!this.channels.find((channel) => channel.unread);
return !!this.channels.find(
(channel) => channel.unread && this.#previewCanView(channel),
);
}

/**
* Find all message IDs of unread messages
* @returns Array of message IDs which are unread
*/
get mentions(): string[] {
const arr = this.channels.map((channel) =>
Array.from(channel.mentions?.values() ?? []),
);
const arr = this.channels
.filter((channel) => this.#previewCanView(channel))
.map((channel) => Array.from(channel.mentions?.values() ?? []));

return ([] as string[]).concat(...arr);
}
Expand Down Expand Up @@ -340,6 +344,27 @@ export class Server {
});
}

/**
* Whether a channel is visible to the roles we are previewing, if any
* @param channel Channel
*/
#previewCanView(channel: Channel): boolean {
return !this.previewRoles || channel.havePermission("ViewChannel");
}

/**
* Roles we are previewing in this server, from lowest to highest priority
*/
get previewRoles(): ServerRole[] | undefined {
const viewAs = this.#collection.client.viewAs();
if (viewAs?.server !== this.id) return;

return viewAs.roles
.map((id) => this.roles.get(id)!)
.filter((role) => role)
.sort((a, b) => b.rank! - a.rank!);
}

/**
* Permission the currently authenticated user has against this server
*/
Expand Down
20 changes: 18 additions & 2 deletions src/classes/ServerMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ export class ServerMember {
}

/**
* Member's ranking
* Member's ranking, ignoring any role preview
* Smaller values are ranked as higher priority
*/
get ranking(): number {
get realRanking(): number {
if (this.id.user === this.server?.ownerId) {
return -Infinity;
}
Expand All @@ -178,6 +178,22 @@ export class ServerMember {
}
}

/**
* Member's ranking
* Smaller values are ranked as higher priority
*/
get ranking(): number {
// Our own member is ranked as the previewed roles, if any
const preview =
this.id.user === this.#collection.client.user?.id
? this.server?.previewRoles
: undefined;

if (!preview) return this.realRanking;

return preview.length > 0 ? preview[preview.length - 1].rank! : Infinity;
}

/**
* Get the permissions that this member has against a certain object
* @param target Target object to check permissions against
Expand Down
19 changes: 19 additions & 0 deletions src/permissions/calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ export function bitwiseAndEq(a: bigint, ...b: bigint[]): boolean {
return (value & a) === value;
}

/**
* Build a stand-in member holding only the roles being previewed
* @param server Server the target belongs to
*/
function previewMember(server?: Server): ServerMember | undefined {
const orderedRoles = server?.previewRoles;
if (!orderedRoles) return;

return {
roles: orderedRoles.map((role) => role.id),
orderedRoles,
} as unknown as ServerMember;
}

/**
* Calculate permissions against a given object
* @param target Target object to check permissions against
Expand All @@ -33,6 +47,11 @@ export function calculatePermission(
member?: ServerMember;
},
): bigint {
const preview =
!options?.member &&
previewMember(target instanceof Server ? target : target.server);
if (preview) options = { member: preview };

const user = options?.member ? options?.member.user : client.user;
if (user?.privileged) {
return Permission.GrantAllSafe;
Expand Down
Loading