Skip to content
Merged
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
958 changes: 592 additions & 366 deletions src/components/ui/ChannelSettingsModal.tsx

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions src/lib/irc/IRCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,49 @@ export interface EventMap {
url: string;
description: string;
};
// IRCv3 draft/named-modes (PROP command + RPL_CHMODELIST etc.).
// The "registry" events (CHANMODE_LIST / UMODE_LIST) carry the
// server's long-form mode name table; the "mode change" event
// (PROP) carries an actual mode change; the rest pair with the
// listing forms of PROP <chan> [<listmode>].
NAMED_MODES_CHANMODE_LIST: BaseIRCEvent & {
entries: Array<{
type: 1 | 2 | 3 | 4 | 5;
name: string;
letter?: string;
}>;
isFinal: boolean;
};
NAMED_MODES_UMODE_LIST: BaseIRCEvent & {
entries: Array<{
type: 1 | 2 | 3 | 4 | 5;
name: string;
letter?: string;
}>;
isFinal: boolean;
};
NAMED_MODES_PROPLIST: BaseIRCEvent & {
channel: string;
items: string[];
};
NAMED_MODES_PROPLIST_END: BaseIRCEvent & { channel: string };
NAMED_MODES_LISTPROPLIST: BaseIRCEvent & {
channel: string;
modeName: string;
mask: string;
setter: string;
settime: number;
};
NAMED_MODES_LISTPROPLIST_END: BaseIRCEvent & {
channel: string;
modeName: string;
};
NAMED_MODES_PROP: EventWithTags & {
sender: string;
target: string;
items: Array<{ sign: "+" | "-"; name: string; param?: string }>;
timestamp: Date;
};
TWOFA: EventWithTags & {
subcommand: string;
status: string;
Expand Down Expand Up @@ -541,6 +584,7 @@ export class IRCClient implements IRCClientContext {
"invite-notify",
"monitor",
"extended-monitor",
"draft/named-modes",
"labeled-response",
"draft/read-marker",
"obsidianirc/cmdslist",
Expand Down Expand Up @@ -1388,6 +1432,85 @@ export class IRCClient implements IRCClientContext {
this.sendRaw(serverId, `METADATA ${target} SYNC`);
}

/**
* IRCv3 draft/named-modes: send a mode change addressed by long-form
* mode names instead of single letters.
*
* When the negotiated registry exposes a letter for every requested
* name, we build a `MODE` line so the change works on legacy
* (non-cap) servers too. When the cap is negotiated AND any item
* has no letter equivalent (a name-only mode), we send a `PROP`
* line, which is the only wire form that can carry such modes.
*
* `target` is a channel name or a nick. `items` is a list of
* `{sign, name, param?}` triples. `registry` is the per-server
* NamedModes object the caller already has from the store; passing
* it in keeps this layer free of a store dependency.
*/
sendNamedMode(
serverId: string,
target: string,
items: Array<{ sign: "+" | "-"; name: string; param?: string }>,
registry?: { supported: boolean } & {
channelModes: Array<{ name: string; letter?: string }>;
userModes: Array<{ name: string; letter?: string }>;
},
): void {
if (!items.length) return;

const isChannelTarget =
target.startsWith("#") ||
target.startsWith("^") ||
target.startsWith("$");
const list = isChannelTarget
? (registry?.channelModes ?? [])
: (registry?.userModes ?? []);

// Decide which form to send. Prefer PROP when the cap is
// negotiated AND any item is name-only (no letter); fall back to
// MODE otherwise so legacy servers / clients in mixed deployments
// keep working.
const capSupported = !!registry?.supported;
const anyNameOnly = items.some((it) => {
const spec = list.find((m) => m.name === it.name);
return !spec?.letter;
});

if (capSupported && anyNameOnly) {
const tail = items
.map((it) =>
it.param !== undefined
? `${it.sign}${it.name}=${it.param}`
: `${it.sign}${it.name}`,
)
.join(" ");
this.sendRaw(serverId, `PROP ${target} ${tail}`);
return;
}

// Build a MODE line. Drop any items that map to no letter (only
// happens when the cap isn't negotiated; the user just can't
// reach name-only modes on a legacy server).
let modestring = "";
const params: string[] = [];
let lastSign: "+" | "-" | "" = "";
for (const it of items) {
const spec = list.find((m) => m.name === it.name);
if (!spec?.letter) continue;
if (it.sign !== lastSign) {
modestring += it.sign;
lastSign = it.sign;
}
modestring += spec.letter;
if (it.param !== undefined) params.push(it.param);
}
if (!modestring) return;
const cmd = params.length
? `MODE ${target} ${modestring} ${params.join(" ")}`
: `MODE ${target} ${modestring}`;
this.sendRaw(serverId, cmd);
}

// draft/authtoken: ask the server to mint a bearer token for `service`.
// Server reply is `:server TOKEN GENERATE <service> <url> :<token>`.
requestToken(serverId: string, service: string, scope?: string): void {
Expand Down
24 changes: 24 additions & 0 deletions src/lib/irc/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ import {
handleMonOffline,
handleMonOnline,
} from "./monitoring";
import {
handleProp,
handleRplChmodelist,
handleRplEndOfListProplist,
handleRplEndOfProplist,
handleRplListProplist,
handleRplProplist,
handleRplUmodelist,
} from "./named-modes";
import { handleMarkread } from "./readMarker";
import {
handleAway,
Expand Down Expand Up @@ -304,6 +313,21 @@ export const IRC_DISPATCH: Record<string, HandlerFn> = {
CMDSLIST: (ctx, serverId, source, parv, mtags) =>
handleCmdslist(ctx, serverId, source, parv, mtags),

PROP: (ctx, serverId, source, parv, mtags) =>
handleProp(ctx, serverId, source, parv, mtags),
"960": (ctx, serverId, source, parv, mtags) =>
handleRplEndOfProplist(ctx, serverId, source, parv, mtags),
"961": (ctx, serverId, source, parv, mtags) =>
handleRplProplist(ctx, serverId, source, parv, mtags),
"962": (ctx, serverId, source, parv, mtags) =>
handleRplEndOfListProplist(ctx, serverId, source, parv, mtags),
"963": (ctx, serverId, source, parv, mtags) =>
handleRplListProplist(ctx, serverId, source, parv, mtags),
"964": (ctx, serverId, source, parv, mtags) =>
handleRplChmodelist(ctx, serverId, source, parv, mtags),
"965": (ctx, serverId, source, parv, mtags) =>
handleRplUmodelist(ctx, serverId, source, parv, mtags),

"730": (ctx, serverId, source, parv, mtags) =>
handleMonOnline(ctx, serverId, source, parv, mtags),
"731": (ctx, serverId, source, parv, mtags) =>
Expand Down
Loading
Loading