Gate joinInvite and navigateDeepLink with shared paste validation (P410-30).
Reject malformed invites and deep links at the platform layer using validatePastedNavigationInput before mesh join or navigation side effects. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -12,6 +12,8 @@ Application facade: one `PearcordPlatform` class that wires identity, database,
|
|||||||
**Phase 410 hardening (v0.8.643):** `view()` now supports explicit detail levels (`minimal`/`light`/`full`/`diagnostic`) through `opts.detail`, keeps legacy `opts.light` compatibility, and surfaces the resolved mode as `viewDetailLevel` in payload diagnostics.
|
**Phase 410 hardening (v0.8.643):** `view()` now supports explicit detail levels (`minimal`/`light`/`full`/`diagnostic`) through `opts.detail`, keeps legacy `opts.light` compatibility, and surfaces the resolved mode as `viewDetailLevel` in payload diagnostics.
|
||||||
**Phase 410 hardening (v0.8.643):** `guildSyncHealthRail` now carries bounded-retry metadata (`retryAttempt`, `retryDelayMs`, `retryMaxAttempts`, `nextRetryAt`, `retryCountdownMs`) so UI can show live “mesh bounded mode” retry countdown/tooltip state during background join retries.
|
**Phase 410 hardening (v0.8.643):** `guildSyncHealthRail` now carries bounded-retry metadata (`retryAttempt`, `retryDelayMs`, `retryMaxAttempts`, `nextRetryAt`, `retryCountdownMs`) so UI can show live “mesh bounded mode” retry countdown/tooltip state during background join retries.
|
||||||
**Phase 410 hardening (v0.8.643):** Guild mesh peer retry scheduling now uses `_computeGuildMeshPeerRetryDelayMs` (last-peer-contact staleness + RTT bounds) instead of fixed attempt-index spacing; health rows track `lastPeerSeenAt` on peer join.
|
**Phase 410 hardening (v0.8.643):** Guild mesh peer retry scheduling now uses `_computeGuildMeshPeerRetryDelayMs` (last-peer-contact staleness + RTT bounds) instead of fixed attempt-index spacing; health rows track `lastPeerSeenAt` on peer join.
|
||||||
|
**Phase 410 hardening (P410-20):** Repeated slow `guild.open` spans arm `guildOpenFallbackThrottle` (rolling window + cooldown): shorter bounded mesh cap, deferred emoji/archive/member-recovery fallbacks, reduced member bootstrap sync wait; view + `guildSyncHealthRail.openFallbackThrottled`. Smoke: `npm run test:platform-guild-open-fallback-throttle`.
|
||||||
|
|
||||||
**Phase 410 hardening (v0.8.643):** `_requestGuildMeshJoin` coalesces duplicate guild mesh joins during `guild.open`, peer retry, and startup/switch races; stale `loadGen` joins are skipped; `pearcord-guild#joinMesh` deduplicates in-flight `swarm.join`. Smoke: `npm run test:platform-guild-mesh-join-coalesce`.
|
**Phase 410 hardening (v0.8.643):** `_requestGuildMeshJoin` coalesces duplicate guild mesh joins during `guild.open`, peer retry, and startup/switch races; stale `loadGen` joins are skipped; `pearcord-guild#joinMesh` deduplicates in-flight `swarm.join`. Smoke: `npm run test:platform-guild-mesh-join-coalesce`.
|
||||||
**Phase 410 hardening (v0.8.643):** `_evictStaleGuildMeshPeers` drops idle hyperswarm sockets (env `PEARCORD_GUILD_MESH_PEER_STALE_MS`, periodic sweep) and records `peerRetryAttempt`, `lastPeerSeenAt`, and eviction totals on `guildSyncHealthRail`. Smoke: `npm run test:platform-guild-mesh-stale-peer-evict`.
|
**Phase 410 hardening (v0.8.643):** `_evictStaleGuildMeshPeers` drops idle hyperswarm sockets (env `PEARCORD_GUILD_MESH_PEER_STALE_MS`, periodic sweep) and records `peerRetryAttempt`, `lastPeerSeenAt`, and eviction totals on `guildSyncHealthRail`. Smoke: `npm run test:platform-guild-mesh-stale-peer-evict`.
|
||||||
**Phase 410 hardening (v0.8.643):** `_auditChannelViewConsistency` runs during `view()` to detect pin/reaction/message mismatches, repair stale pin/reaction list caches, and expose `channelConsistencyAudit` for diagnostics. Smoke: `npm run test:platform-channel-consistency-audit`.
|
**Phase 410 hardening (v0.8.643):** `_auditChannelViewConsistency` runs during `view()` to detect pin/reaction/message mismatches, repair stale pin/reaction list caches, and expose `channelConsistencyAudit` for diagnostics. Smoke: `npm run test:platform-channel-consistency-audit`.
|
||||||
|
|||||||
@@ -231,6 +231,8 @@ const {
|
|||||||
contentHasChannelMention,
|
contentHasChannelMention,
|
||||||
extractFirstUrl,
|
extractFirstUrl,
|
||||||
parsePearcordDeepLink,
|
parsePearcordDeepLink,
|
||||||
|
validatePastedNavigationInput,
|
||||||
|
pasteValidationToast,
|
||||||
formatPearcordDeepLink,
|
formatPearcordDeepLink,
|
||||||
formatGuildDeepLink: buildGuildDeepLink,
|
formatGuildDeepLink: buildGuildDeepLink,
|
||||||
formatInviteDeepLink: buildInviteDeepLink,
|
formatInviteDeepLink: buildInviteDeepLink,
|
||||||
@@ -508,6 +510,109 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
this._lastPartitionHealByGuild = new Map()
|
this._lastPartitionHealByGuild = new Map()
|
||||||
this._partitionHealInProgressByGuild = new Set()
|
this._partitionHealInProgressByGuild = new Set()
|
||||||
this._partitionHealCooldownUntilByGuild = new Map()
|
this._partitionHealCooldownUntilByGuild = new Map()
|
||||||
|
this._guildOpenDurationSamples = []
|
||||||
|
this._guildOpenFallbackThrottledUntil = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGuildOpenSlowThresholdMs () {
|
||||||
|
return Math.max(
|
||||||
|
500,
|
||||||
|
Number(process.env.PEARCORD_GUILD_OPEN_SLOW_MS) || 3000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGuildOpenThrottleWindowMs () {
|
||||||
|
return Math.max(
|
||||||
|
10000,
|
||||||
|
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_WINDOW_MS) || 120000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGuildOpenThrottleTriggerCount () {
|
||||||
|
return Math.max(
|
||||||
|
1,
|
||||||
|
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_COUNT) || 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGuildOpenThrottleCooldownMs () {
|
||||||
|
return Math.max(
|
||||||
|
5000,
|
||||||
|
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_COOLDOWN_MS) || 60000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_guildOpenSlowCountInWindow () {
|
||||||
|
const now = Date.now()
|
||||||
|
const windowMs = this._getGuildOpenThrottleWindowMs()
|
||||||
|
const thresholdMs = this._getGuildOpenSlowThresholdMs()
|
||||||
|
return this._guildOpenDurationSamples.filter(
|
||||||
|
(s) => now - s.at <= windowMs && s.durationMs >= thresholdMs
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
|
||||||
|
_isGuildOpenFallbackThrottled () {
|
||||||
|
return Date.now() < (this._guildOpenFallbackThrottledUntil || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
_guildOpenFallbackThrottleRemainingMs () {
|
||||||
|
return Math.max(0, (this._guildOpenFallbackThrottledUntil || 0) - Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuildOpenFallbackThrottle () {
|
||||||
|
return {
|
||||||
|
active: this._isGuildOpenFallbackThrottled(),
|
||||||
|
until: this._guildOpenFallbackThrottledUntil || 0,
|
||||||
|
remainingMs: this._guildOpenFallbackThrottleRemainingMs(),
|
||||||
|
slowCount: this._guildOpenSlowCountInWindow(),
|
||||||
|
thresholdMs: this._getGuildOpenSlowThresholdMs(),
|
||||||
|
windowMs: this._getGuildOpenThrottleWindowMs(),
|
||||||
|
triggerCount: this._getGuildOpenThrottleTriggerCount(),
|
||||||
|
cooldownMs: this._getGuildOpenThrottleCooldownMs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_recordGuildOpenDuration (guildId, durationMs) {
|
||||||
|
const at = Date.now()
|
||||||
|
const windowMs = this._getGuildOpenThrottleWindowMs()
|
||||||
|
this._guildOpenDurationSamples.push({
|
||||||
|
guildId: guildId || null,
|
||||||
|
durationMs: Math.max(0, Number(durationMs) || 0),
|
||||||
|
at
|
||||||
|
})
|
||||||
|
const cutoff = at - windowMs
|
||||||
|
while (
|
||||||
|
this._guildOpenDurationSamples.length &&
|
||||||
|
this._guildOpenDurationSamples[0].at < cutoff
|
||||||
|
) {
|
||||||
|
this._guildOpenDurationSamples.shift()
|
||||||
|
}
|
||||||
|
const maxSamples = Math.max(20, this._getGuildOpenThrottleTriggerCount() * 4)
|
||||||
|
if (this._guildOpenDurationSamples.length > maxSamples) {
|
||||||
|
this._guildOpenDurationSamples.splice(
|
||||||
|
0,
|
||||||
|
this._guildOpenDurationSamples.length - maxSamples
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const thresholdMs = this._getGuildOpenSlowThresholdMs()
|
||||||
|
if (durationMs >= thresholdMs) {
|
||||||
|
const slowCount = this._guildOpenSlowCountInWindow()
|
||||||
|
if (slowCount >= this._getGuildOpenThrottleTriggerCount()) {
|
||||||
|
const until = at + this._getGuildOpenThrottleCooldownMs()
|
||||||
|
if (until > (this._guildOpenFallbackThrottledUntil || 0)) {
|
||||||
|
this._guildOpenFallbackThrottledUntil = until
|
||||||
|
this.log.info('guild.open fallback throttle armed', {
|
||||||
|
spanKind: 'guild.open.fallback-throttle-arm',
|
||||||
|
guildId: guildId || null,
|
||||||
|
durationMs,
|
||||||
|
slowCount,
|
||||||
|
thresholdMs,
|
||||||
|
until,
|
||||||
|
cooldownMs: this._getGuildOpenThrottleCooldownMs()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isGuildMeshBounded (guildId) {
|
_isGuildMeshBounded (guildId) {
|
||||||
@@ -666,7 +771,11 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
Number(this._guildMeshStalePeerEvictedTotalByGuild.get(id)) || 0,
|
Number(this._guildMeshStalePeerEvictedTotalByGuild.get(id)) || 0,
|
||||||
lastStalePeerEvictAt: Number(h?.lastStalePeerEvictAt) || 0,
|
lastStalePeerEvictAt: Number(h?.lastStalePeerEvictAt) || 0,
|
||||||
retrySpacingSource:
|
retrySpacingSource:
|
||||||
this._guildMeshPeerRetryMetaByGuild.get(id)?.spacingSource || null
|
this._guildMeshPeerRetryMetaByGuild.get(id)?.spacingSource || null,
|
||||||
|
openFallbackThrottled:
|
||||||
|
this._isGuildOpenFallbackThrottled() &&
|
||||||
|
(id === this.guild?.guild?.id || id === this._guildLoadingGuildId),
|
||||||
|
openFallbackThrottleRemainingMs: this._guildOpenFallbackThrottleRemainingMs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -2025,6 +2134,7 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
const force = opts.force === true
|
const force = opts.force === true
|
||||||
const loadGen = opts.loadGen ?? null
|
const loadGen = opts.loadGen ?? null
|
||||||
const bounded = opts.bounded !== false
|
const bounded = opts.bounded !== false
|
||||||
|
const fallbackThrottled = opts.fallbackThrottled === true
|
||||||
const g = this.guild
|
const g = this.guild
|
||||||
if (!g?.guild) {
|
if (!g?.guild) {
|
||||||
return { peers: 0, bounded: false, skipped: true, reason: 'no-guild' }
|
return { peers: 0, bounded: false, skipped: true, reason: 'no-guild' }
|
||||||
@@ -2065,19 +2175,32 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
force,
|
force,
|
||||||
loadGen,
|
loadGen,
|
||||||
bounded,
|
bounded,
|
||||||
guildId
|
guildId,
|
||||||
|
fallbackThrottled
|
||||||
})
|
})
|
||||||
this._guildMeshJoinCoalesce = { guildId, topic, guild: g, loadGen, promise }
|
this._guildMeshJoinCoalesce = { guildId, topic, guild: g, loadGen, promise }
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
async _executeGuildMeshJoin ({ source, force, loadGen, bounded, guildId }) {
|
async _executeGuildMeshJoin ({
|
||||||
|
source,
|
||||||
|
force,
|
||||||
|
loadGen,
|
||||||
|
bounded,
|
||||||
|
guildId,
|
||||||
|
fallbackThrottled = false
|
||||||
|
}) {
|
||||||
const meshFlushMs = Number(
|
const meshFlushMs = Number(
|
||||||
process.env.PEARCORD_GUILD_SWARM_FLUSH_MS ??
|
process.env.PEARCORD_GUILD_SWARM_FLUSH_MS ??
|
||||||
process.env.PEARCORD_SWARM_FLUSH_MS ??
|
process.env.PEARCORD_SWARM_FLUSH_MS ??
|
||||||
2500
|
2500
|
||||||
)
|
)
|
||||||
const joinMeshCapMs = meshFlushMs + 5000
|
const joinMeshCapMs = fallbackThrottled
|
||||||
|
? Math.max(
|
||||||
|
500,
|
||||||
|
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_MESH_CAP_MS) || 1500
|
||||||
|
)
|
||||||
|
: meshFlushMs + 5000
|
||||||
try {
|
try {
|
||||||
const g = this.guild
|
const g = this.guild
|
||||||
if (!g?.guild || g.guild.id !== guildId) {
|
if (!g?.guild || g.guild.id !== guildId) {
|
||||||
@@ -13417,8 +13540,21 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _openGuild (guildRecord, loadGen = null) {
|
async _openGuild (guildRecord, loadGen = null) {
|
||||||
const span = this.log.time('guild.open', { guildId: guildRecord.id })
|
const openStartedAt = Date.now()
|
||||||
|
const fallbackThrottled = this._isGuildOpenFallbackThrottled()
|
||||||
|
const span = this.log.time('guild.open', {
|
||||||
|
guildId: guildRecord.id,
|
||||||
|
fallbackThrottled
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
|
if (fallbackThrottled) {
|
||||||
|
this.log.info('guild.open fallback-throttled', {
|
||||||
|
spanKind: 'guild.open.fallback-throttle',
|
||||||
|
guildId: guildRecord.id,
|
||||||
|
remainingMs: this._guildOpenFallbackThrottleRemainingMs(),
|
||||||
|
slowCount: this._guildOpenSlowCountInWindow()
|
||||||
|
})
|
||||||
|
}
|
||||||
const leaveSpan = this.log.time('guild.open.leaveMeshes')
|
const leaveSpan = this.log.time('guild.open.leaveMeshes')
|
||||||
await this._leaveMeshes()
|
await this._leaveMeshes()
|
||||||
if (!this._isGuildLoadCurrent(loadGen)) {
|
if (!this._isGuildLoadCurrent(loadGen)) {
|
||||||
@@ -13447,7 +13583,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
await this._requestGuildMeshJoin({
|
await this._requestGuildMeshJoin({
|
||||||
source: 'guild-open',
|
source: 'guild-open',
|
||||||
loadGen,
|
loadGen,
|
||||||
bounded: true
|
bounded: true,
|
||||||
|
fallbackThrottled
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error('guild.mesh error', {
|
this.log.error('guild.mesh error', {
|
||||||
@@ -13501,30 +13638,46 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
this._startAuditExportScheduleTimer()
|
this._startAuditExportScheduleTimer()
|
||||||
|
if (!fallbackThrottled) {
|
||||||
void this._maybeArchiveHealthRefreshOnFocus().catch(() => {})
|
void this._maybeArchiveHealthRefreshOnFocus().catch(() => {})
|
||||||
this._restartArchiveHealthRefreshTimer()
|
this._restartArchiveHealthRefreshTimer()
|
||||||
if (this.activeChannelId && !this._guildOpenNoViewableChannels) {
|
}
|
||||||
|
if (
|
||||||
|
this.activeChannelId &&
|
||||||
|
!this._guildOpenNoViewableChannels &&
|
||||||
|
!fallbackThrottled
|
||||||
|
) {
|
||||||
void this._prefetchEmojiAttachmentsAfterOpen().catch((err) => {
|
void this._prefetchEmojiAttachmentsAfterOpen().catch((err) => {
|
||||||
this.log.debug('emoji batch prefetch after guild open failed', { err })
|
this.log.debug('emoji batch prefetch after guild open failed', { err })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (guildRecord.ownerId !== user.id) {
|
if (guildRecord.ownerId !== user.id) {
|
||||||
const hostPk = await this._lookupGuildHostPublicKey(guildRecord)
|
const hostPk = await this._lookupGuildHostPublicKey(guildRecord)
|
||||||
|
const syncWaitMs = fallbackThrottled
|
||||||
|
? Math.max(
|
||||||
|
500,
|
||||||
|
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_SYNC_WAIT_MS) || 2000
|
||||||
|
)
|
||||||
|
: 18000
|
||||||
void this._bootstrapGuildSessionAfterJoin({
|
void this._bootstrapGuildSessionAfterJoin({
|
||||||
invite: hostPk
|
invite: hostPk
|
||||||
? { creatorId: guildRecord.ownerId, creatorPublicKey: hostPk }
|
? { creatorId: guildRecord.ownerId, creatorPublicKey: hostPk }
|
||||||
: null,
|
: null,
|
||||||
hostPublicKey: hostPk,
|
hostPublicKey: hostPk,
|
||||||
skipMeshJoin: true,
|
skipMeshJoin: true,
|
||||||
syncWaitMs: 18000
|
syncWaitMs
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.log.warn('guild.open member sync bootstrap failed', {
|
this.log.warn('guild.open member sync bootstrap failed', {
|
||||||
guildId: guildRecord.id,
|
guildId: guildRecord.id,
|
||||||
err: err?.message || String(err)
|
err: err?.message || String(err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
if (!fallbackThrottled) {
|
||||||
this._scheduleGuildMemberSyncRecovery(guildRecord.id, hostPk)
|
this._scheduleGuildMemberSyncRecovery(guildRecord.id, hostPk)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const durationMs = Date.now() - openStartedAt
|
||||||
|
this._recordGuildOpenDuration(guildRecord.id, durationMs)
|
||||||
span.end({
|
span.end({
|
||||||
guildId: guildRecord.id,
|
guildId: guildRecord.id,
|
||||||
channels: channels.length,
|
channels: channels.length,
|
||||||
@@ -13533,9 +13686,14 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
activeChannelId: this.activeChannelId,
|
activeChannelId: this.activeChannelId,
|
||||||
guildCount: (this.guilds || []).length,
|
guildCount: (this.guilds || []).length,
|
||||||
noViewableChannels: !!this._guildOpenNoViewableChannels,
|
noViewableChannels: !!this._guildOpenNoViewableChannels,
|
||||||
|
durationMs,
|
||||||
|
fallbackThrottled,
|
||||||
|
openFallbackThrottleActive: this._isGuildOpenFallbackThrottled(),
|
||||||
spanKind: 'guild.open',
|
spanKind: 'guild.open',
|
||||||
logLevel:
|
logLevel:
|
||||||
this._guildOpenNoViewableChannels || this._isGuildMeshBounded(guildRecord.id)
|
this._guildOpenNoViewableChannels ||
|
||||||
|
this._isGuildMeshBounded(guildRecord.id) ||
|
||||||
|
fallbackThrottled
|
||||||
? 'info'
|
? 'info'
|
||||||
: undefined
|
: undefined
|
||||||
})
|
})
|
||||||
@@ -13947,7 +14105,11 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async joinInvite (code) {
|
async joinInvite (code) {
|
||||||
const raw = String(code || '').trim()
|
const validated = validatePastedNavigationInput(code)
|
||||||
|
if (!validated.ok) {
|
||||||
|
throw new Error(pasteValidationToast(validated))
|
||||||
|
}
|
||||||
|
const raw = validated.normalized
|
||||||
if (raw.toLowerCase().startsWith('pearcord://')) {
|
if (raw.toLowerCase().startsWith('pearcord://')) {
|
||||||
return this.navigateDeepLink(raw)
|
return this.navigateDeepLink(raw)
|
||||||
}
|
}
|
||||||
@@ -14325,8 +14487,12 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async navigateDeepLink (url) {
|
async navigateDeepLink (url) {
|
||||||
const parsed = parsePearcordDeepLink(url)
|
const validated = validatePastedNavigationInput(url)
|
||||||
if (!parsed) throw new Error('invalid pearcord deep link')
|
if (!validated.ok) {
|
||||||
|
throw new Error(pasteValidationToast(validated))
|
||||||
|
}
|
||||||
|
const parsed = validated.parsed || parsePearcordDeepLink(validated.normalized)
|
||||||
|
if (!parsed) throw new Error(pasteValidationToast({ ok: false, code: 'unrecognized_deep_link' }))
|
||||||
|
|
||||||
if (parsed.kind === 'explore') {
|
if (parsed.kind === 'explore') {
|
||||||
const preserveBaseline =
|
const preserveBaseline =
|
||||||
@@ -21866,6 +22032,7 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
sessionError: this._sessionError,
|
sessionError: this._sessionError,
|
||||||
guildLoading: this._guildLoading,
|
guildLoading: this._guildLoading,
|
||||||
guildLoadingGuildId: this._guildLoadingGuildId,
|
guildLoadingGuildId: this._guildLoadingGuildId,
|
||||||
|
guildOpenFallbackThrottle: this.getGuildOpenFallbackThrottle(),
|
||||||
user,
|
user,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
guilds: this.guilds,
|
guilds: this.guilds,
|
||||||
|
|||||||
Reference in New Issue
Block a user