mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
[chat] updates to privacy and filters
This commit is contained in:
+164
-23
@@ -10,6 +10,7 @@ import { getThreadMessages } from './message-db'
|
||||
import {
|
||||
AUTHED,
|
||||
ChatMessageDto,
|
||||
ChatPrivacySettingRequest,
|
||||
ChatPrivacySettings,
|
||||
ChatResult,
|
||||
ChatThreadDto,
|
||||
@@ -50,7 +51,7 @@ import {
|
||||
} from './thread-db'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import type { App } from './context'
|
||||
import type { App, Env } from './context'
|
||||
import type { ChatMessage } from './message-db'
|
||||
|
||||
/**
|
||||
@@ -108,7 +109,10 @@ const PARTY_INVITE_LIFETIME_MINUTES = 60
|
||||
/**
|
||||
* Who may start a chat with a player — the client's `ChatPrivacy` enum, served numerically
|
||||
* like every other enum on this build. `Friends` is what a fresh account reports, and what
|
||||
* every account reports here: nothing stores a per-player setting yet.
|
||||
* a player who has never touched their privacy screen reads back here.
|
||||
*
|
||||
* The PUT spells the same enum by NAME (`directMessagePrivacySetting=Favorites`); only the
|
||||
* GET is numeric. Both directions go through `parseChatPrivacy`, which takes either.
|
||||
*/
|
||||
const ChatPrivacy = {
|
||||
Friends: 0,
|
||||
@@ -116,6 +120,92 @@ const ChatPrivacy = {
|
||||
NoOne: 2,
|
||||
} as const
|
||||
|
||||
type ChatPrivacyValue = (typeof ChatPrivacy)[keyof typeof ChatPrivacy]
|
||||
|
||||
/** The enum member names, indexed by ordinal — what a stored setting holds. */
|
||||
const CHAT_PRIVACY_NAMES = ['Friends', 'Favorites', 'NoOne'] as const
|
||||
|
||||
/**
|
||||
* The keys the two settings live under in the player's `playersettings` map. Chat has no
|
||||
* table of its own for them: they belong with the player's other toggles, and the settings
|
||||
* bag is already read and written per player.
|
||||
*/
|
||||
const DM_PRIVACY_KEY = 'directMessagePrivacySetting'
|
||||
const GROUP_PRIVACY_KEY = 'groupChatPrivacySetting'
|
||||
|
||||
/**
|
||||
* A `ChatPrivacy` out of whatever was stored or posted — the member name as the client
|
||||
* sends it (case-insensitively), or the ordinal as the GET serves it, since a value that
|
||||
* made a round trip through the settings bag could be spelled either way.
|
||||
*
|
||||
* `undefined` for anything unrecognized, which the read and the write treat differently: a
|
||||
* stored value that won't parse falls back to the default, but a posted one that won't
|
||||
* parse is a field worth leaving alone rather than a write of `Friends`.
|
||||
*/
|
||||
function parseChatPrivacy(value: string | undefined): ChatPrivacyValue | undefined {
|
||||
const raw = (value ?? '').trim()
|
||||
if (raw === '') return undefined
|
||||
|
||||
const byName = CHAT_PRIVACY_NAMES.findIndex((n) => n.toLowerCase() === raw.toLowerCase())
|
||||
if (byName !== -1) return byName as ChatPrivacyValue
|
||||
|
||||
const ordinal = Number.parseInt(raw, 10)
|
||||
return ordinal >= 0 && ordinal < CHAT_PRIVACY_NAMES.length
|
||||
? (ordinal as ChatPrivacyValue)
|
||||
: undefined
|
||||
}
|
||||
|
||||
/** The player's settings map from the KV the `playersettings` worker owns. */
|
||||
async function getPlayerSettings(
|
||||
env: Env,
|
||||
accountId: number
|
||||
): Promise<Record<string, string> | null> {
|
||||
return env.RECFLARE_PLAYER_SETTINGS.get<Record<string, string>>(
|
||||
`player:${accountId}`,
|
||||
'json'
|
||||
).catch(() => null)
|
||||
}
|
||||
|
||||
/**
|
||||
* A player's two chat privacy settings. Absent settings, an absent key and an unparseable
|
||||
* value all read `Friends` — the reference's default, and the safer of the two directions
|
||||
* to be wrong in: it describes a player as more private than this server enforces, rather
|
||||
* than less.
|
||||
*/
|
||||
async function readChatPrivacy(
|
||||
env: Env,
|
||||
accountId: number
|
||||
): Promise<{
|
||||
directMessagePrivacySetting: ChatPrivacyValue
|
||||
groupChatPrivacySetting: ChatPrivacyValue
|
||||
}> {
|
||||
const stored = (await getPlayerSettings(env, accountId)) ?? {}
|
||||
return {
|
||||
directMessagePrivacySetting: parseChatPrivacy(stored[DM_PRIVACY_KEY]) ?? ChatPrivacy.Friends,
|
||||
groupChatPrivacySetting: parseChatPrivacy(stored[GROUP_PRIVACY_KEY]) ?? ChatPrivacy.Friends,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the posted setting(s) back into the player's settings map.
|
||||
*
|
||||
* The write MERGES, exactly as the `playersettings` worker's own PUT does: the map holds
|
||||
* every setting the player has (OOBE state, tutorial mask, …), so storing these two on
|
||||
* their own would wipe the rest. Values are stored by NAME, the way the client posts them,
|
||||
* so the bag stays readable; `parseChatPrivacy` takes either spelling back.
|
||||
*/
|
||||
async function writeChatPrivacy(
|
||||
env: Env,
|
||||
accountId: number,
|
||||
settings: Partial<Record<typeof DM_PRIVACY_KEY | typeof GROUP_PRIVACY_KEY, ChatPrivacyValue>>
|
||||
): Promise<void> {
|
||||
const merged: Record<string, string> = { ...(await getPlayerSettings(env, accountId)) }
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
if (value !== undefined) merged[key] = CHAT_PRIVACY_NAMES[value]
|
||||
}
|
||||
await env.RECFLARE_PLAYER_SETTINGS.put(`player:${accountId}`, JSON.stringify(merged))
|
||||
}
|
||||
|
||||
/** The hub is a single global Durable Object instance, as every worker addresses it. */
|
||||
const HUB_INSTANCE = 'global'
|
||||
|
||||
@@ -525,14 +615,16 @@ const app = new Hono<App>()
|
||||
)
|
||||
|
||||
// The caller's chat privacy settings — who may DM them, and who may pull them into a
|
||||
// group chat. Both report `Friends`, which is the reference's default and the safer of
|
||||
// the two directions to be wrong in: it describes a player as more private than the
|
||||
// server actually enforces, rather than less.
|
||||
// group chat — read out of their `playersettings` map. A player who has never opened the
|
||||
// privacy screen reads `Friends` for both, the reference's default and the safer of the
|
||||
// two directions to be wrong in: it describes a player as more private than the server
|
||||
// actually enforces, rather than less.
|
||||
//
|
||||
// REPORTED, NOT ENFORCED. Nothing here stores a per-player setting or checks one — the
|
||||
// DM check below allows every message regardless — so this is what the client renders on
|
||||
// its privacy screen. Wire the two together if this ever becomes real: a screen that says
|
||||
// "Friends" while anyone can message you is worse than one that says nothing.
|
||||
// STORED, NOT ENFORCED. The PUT below keeps the player's choice, but nothing checks it:
|
||||
// the DM check further down allows every message regardless, because this server has no
|
||||
// friends/favorites list to test a sender against. Wire the two together once it does —
|
||||
// a screen that says "Favorites" while anyone can message you is worse than one that
|
||||
// says nothing.
|
||||
//
|
||||
// `playerId` comes off the TOKEN, not a query param: the answer is about the caller.
|
||||
.get(
|
||||
@@ -542,31 +634,79 @@ const app = new Hono<App>()
|
||||
summary: 'The caller’s chat privacy settings',
|
||||
description: [
|
||||
'Who may direct-message the caller and who may add them to a group chat, as the',
|
||||
'`ChatPrivacy` enum by NUMBER (0 Friends · 1 Favorites · 2 NoOne). Both are `Friends`',
|
||||
'here — nothing stores a per-player setting — and nothing enforces them either: the',
|
||||
'DM check allows every message. `playerId` is the caller, read from the token.',
|
||||
'`ChatPrivacy` enum by NUMBER (0 Friends · 1 Favorites · 2 NoOne) — note the PUT takes',
|
||||
'the same enum by NAME. Read from the caller’s `playersettings` map; a player who has',
|
||||
'never set them reads `Friends` for both, as does one whose stored value won’t parse.',
|
||||
'Stored but not enforced: the DM check allows every message. `playerId` is the caller,',
|
||||
'read from the token.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
responses: {
|
||||
200: json(ChatPrivacySettings, 'The caller’s settings — always Friends/Friends'),
|
||||
200: json(ChatPrivacySettings, 'The caller’s stored settings (Friends/Friends by default)'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return c.body(null, 401)
|
||||
return c.json({
|
||||
playerId: id,
|
||||
directMessagePrivacySetting: ChatPrivacy.Friends,
|
||||
groupChatPrivacySetting: ChatPrivacy.Friends,
|
||||
})
|
||||
return c.json({ playerId: id, ...(await readChatPrivacy(c.env, id)) })
|
||||
}
|
||||
)
|
||||
|
||||
// Set one of the two settings. The client PUTs whichever row of its privacy screen the
|
||||
// player just changed — `directMessagePrivacySetting=Favorites` OR
|
||||
// `groupChatPrivacySetting=Favorites`, never both — so a field that isn't in the body is
|
||||
// left alone rather than reset to the default, which would silently undo the other row.
|
||||
//
|
||||
// The body spells the enum by NAME while the GET answers the ordinal; that asymmetry is
|
||||
// the client's, not a mistake here. An unrecognized value writes nothing.
|
||||
//
|
||||
// Answers the RESULTING settings, the same body the GET serves, rather than an empty
|
||||
// ack: the client has just changed a toggle it renders, and a body it can read back
|
||||
// can't disagree with what was stored.
|
||||
.put(
|
||||
'/thread/chatPrivacySetting',
|
||||
describeRoute({
|
||||
tags: ['Threads'],
|
||||
summary: 'Set the caller’s chat privacy settings',
|
||||
description: [
|
||||
'Stores the posted setting(s) in the caller’s `playersettings` map and answers the',
|
||||
'resulting settings — the same body `GET /thread/chatPrivacySetting` serves, with the',
|
||||
'enum by NUMBER. The body names the enum by NAME',
|
||||
'(`directMessagePrivacySetting=Favorites`); the ordinal is accepted too. The client',
|
||||
'sends one field per call, so an absent field leaves that setting as it was, and the',
|
||||
'write merges into the settings map so the player’s other settings are untouched. A',
|
||||
'body with nothing readable in it is a no-op 200 answering the stored settings, not a',
|
||||
'400. Stored, not enforced: nothing checks these when a message is sent.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
requestBody: form(ChatPrivacySettingRequest, 'The setting(s) to store'),
|
||||
responses: {
|
||||
200: json(ChatPrivacySettings, 'The caller’s settings as they now stand'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return c.body(null, 401)
|
||||
|
||||
const posted = {
|
||||
[DM_PRIVACY_KEY]: parseChatPrivacy(await formField(c, DM_PRIVACY_KEY)),
|
||||
[GROUP_PRIVACY_KEY]: parseChatPrivacy(await formField(c, GROUP_PRIVACY_KEY)),
|
||||
}
|
||||
if (posted[DM_PRIVACY_KEY] !== undefined || posted[GROUP_PRIVACY_KEY] !== undefined) {
|
||||
await writeChatPrivacy(c.env, id, posted)
|
||||
}
|
||||
|
||||
return c.json({ playerId: id, ...(await readChatPrivacy(c.env, id)) })
|
||||
}
|
||||
)
|
||||
|
||||
// May the caller DM this player? Asked before the client opens a new direct message, so
|
||||
// it can grey the button out rather than let the send fail. Always 0 (Success): nothing
|
||||
// here stores the who-can-message-me privacy setting the name refers to, so there is no
|
||||
// setting to refuse on.
|
||||
// it can grey the button out rather than let the send fail. Always 0 (Success): the
|
||||
// setting the name refers to is stored (see `/thread/chatPrivacySetting`) but can't be
|
||||
// checked, since Friends and Favorites both need a friends list this server doesn't
|
||||
// keep. Enforce it here the moment one exists.
|
||||
//
|
||||
// The body is a bare ChatResult INTEGER — the client instantiates its response wrapper
|
||||
// with the ChatResult enum, not a bool, so `true` decodes as nothing. The refusals this
|
||||
@@ -582,8 +722,9 @@ const app = new Hono<App>()
|
||||
description: [
|
||||
'Whether the caller may open a direct message with `receivingPlayerId`, as a bare',
|
||||
'ChatResult integer — 0 (Success) means allowed; a real refusal would be 15 (the',
|
||||
'caller’s own privacy setting) or 16 (the other player’s). Always 0 here: this server',
|
||||
'stores no who-can-message-me privacy setting, so there is nothing to refuse on.',
|
||||
'caller’s own privacy setting) or 16 (the other player’s). Always 0 here: the settings',
|
||||
'`/thread/chatPrivacySetting` stores are not enforced, since Friends and Favorites both',
|
||||
'need a friends list this server doesn’t keep.',
|
||||
'`receivingPlayerId` is accepted and ignored; the answer is the same for every player,',
|
||||
'and the client asks again for the next one.',
|
||||
].join(' '),
|
||||
|
||||
@@ -11,6 +11,11 @@ export type Env = SharedHonoEnv & {
|
||||
JWT_SECRET: SecretsStoreSecret
|
||||
/** Shared `recflare` D1 — this worker owns the `message` and thread tables. */
|
||||
DB: D1Database
|
||||
/**
|
||||
* Per-player settings KV, owned by the `playersettings` worker. Holds the caller's
|
||||
* chat privacy settings (`/thread/chatPrivacySetting`) alongside their other toggles.
|
||||
*/
|
||||
RECFLARE_PLAYER_SETTINGS: KVNamespace
|
||||
/** The `notify` worker's NotificationsHub DO — pushes ChatMessageReceived to members. */
|
||||
RECFLARE_NOTIFICATIONS_HUB: DurableObjectNamespace<NotificationsHub>
|
||||
}
|
||||
|
||||
@@ -165,14 +165,16 @@ export const PartyInviteSettings = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* `GET /thread/chatPrivacySetting` — who may start a chat with the caller. camelCase, unlike
|
||||
* the PascalCase thread DTOs, and the two settings are the `ChatPrivacy` enum served
|
||||
* `GET|PUT /thread/chatPrivacySetting` — who may start a chat with the caller. camelCase,
|
||||
* unlike the PascalCase thread DTOs, and the two settings are the `ChatPrivacy` enum served
|
||||
* NUMERICALLY (0 Friends · 1 Favorites · 2 NoOne): this client build carries no by-name enum
|
||||
* formatter, so a string would decode as nothing.
|
||||
* formatter, so a string would decode as nothing. Note the asymmetry with the PUT, which
|
||||
* sends the enum by NAME (`directMessagePrivacySetting=Favorites`).
|
||||
*
|
||||
* Reported, not enforced. Nothing on this server stores a per-player privacy setting or
|
||||
* checks one — `GET /thread/checkCanSendDirectMessageWithPrivacySetting` allows every DM —
|
||||
* so these are the values the client renders its privacy screen from.
|
||||
* STORED, NOT ENFORCED. The PUT keeps the player's choice in the `playersettings` KV and this
|
||||
* is what the client renders its privacy screen from, but nothing checks it —
|
||||
* `GET /thread/checkCanSendDirectMessageWithPrivacySetting` still allows every DM, since this
|
||||
* server has no friends/favorites list to test a sender against.
|
||||
*/
|
||||
export const ChatPrivacySettings = z.object({
|
||||
playerId: z.int().describe('The caller — read from the token, not from the query'),
|
||||
@@ -257,6 +259,27 @@ export const SnoozeThreadRequest = z.object({
|
||||
.describe('`True`/`False` as the client spells it (`1`/`yes` also count as true)'),
|
||||
})
|
||||
|
||||
/**
|
||||
* `PUT /thread/chatPrivacySetting` form body. The client sends ONE of the two fields per
|
||||
* call — it PUTs whichever row of its privacy screen the player just changed — so a field
|
||||
* that isn't in the body leaves that setting as it was rather than resetting it.
|
||||
*
|
||||
* The value is the `ChatPrivacy` enum by NAME (`Favorites`), which is how the client spells
|
||||
* it here even though the GET answers with the ordinal; the ordinal is accepted too.
|
||||
*/
|
||||
export const ChatPrivacySettingRequest = z.object({
|
||||
directMessagePrivacySetting: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Who may DM the caller: `Friends` · `Favorites` · `NoOne` (or 0 · 1 · 2)'),
|
||||
groupChatPrivacySetting: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Who may add the caller to a group chat: `Friends` · `Favorites` · `NoOne` (or 0 · 1 · 2)'
|
||||
),
|
||||
})
|
||||
|
||||
/** `PUT|POST /thread/:id/favorite` form body. */
|
||||
export const FavoriteThreadRequest = z.object({
|
||||
favorite: z
|
||||
|
||||
@@ -410,7 +410,7 @@ describe('GET /thread/party', () => {
|
||||
})
|
||||
|
||||
describe('GET /thread/chatPrivacySetting', () => {
|
||||
it('reports Friends for both settings, keyed to the caller', async () => {
|
||||
it('reports Friends for both settings by default, keyed to the caller', async () => {
|
||||
const res = await SELF.fetch(`${ORIGIN}/thread/chatPrivacySetting`, {
|
||||
headers: await bearer(886001),
|
||||
})
|
||||
@@ -424,6 +424,37 @@ describe('GET /thread/chatPrivacySetting', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('reads the stored settings out of the player settings map', async () => {
|
||||
await env.RECFLARE_PLAYER_SETTINGS.put(
|
||||
'player:886003',
|
||||
JSON.stringify({
|
||||
directMessagePrivacySetting: 'Favorites',
|
||||
groupChatPrivacySetting: 'NoOne',
|
||||
})
|
||||
)
|
||||
const res = await SELF.fetch(`${ORIGIN}/thread/chatPrivacySetting`, {
|
||||
headers: await bearer(886003),
|
||||
})
|
||||
expect(await res.json()).toEqual({
|
||||
playerId: 886003,
|
||||
directMessagePrivacySetting: 1,
|
||||
groupChatPrivacySetting: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to Friends for a stored value it can’t parse', async () => {
|
||||
await env.RECFLARE_PLAYER_SETTINGS.put(
|
||||
'player:886004',
|
||||
JSON.stringify({ directMessagePrivacySetting: 'Nobody at all' })
|
||||
)
|
||||
const res = await SELF.fetch(`${ORIGIN}/thread/chatPrivacySetting`, {
|
||||
headers: await bearer(886004),
|
||||
})
|
||||
expect(
|
||||
((await res.json()) as { directMessagePrivacySetting: number }).directMessagePrivacySetting
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
it('reads playerId off the token, not a query param', async () => {
|
||||
const res = await SELF.fetch(`${ORIGIN}/thread/chatPrivacySetting?playerId=999999`, {
|
||||
headers: await bearer(886002),
|
||||
@@ -437,6 +468,99 @@ describe('GET /thread/chatPrivacySetting', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /thread/chatPrivacySetting', () => {
|
||||
const path = `${ORIGIN}/thread/chatPrivacySetting`
|
||||
|
||||
/** The client's PUT: one form field, the enum by NAME. */
|
||||
async function put(playerId: number, body: Record<string, string>) {
|
||||
return SELF.fetch(path, {
|
||||
method: 'PUT',
|
||||
headers: await bearer(playerId),
|
||||
body: new URLSearchParams(body),
|
||||
})
|
||||
}
|
||||
|
||||
const settings = async (playerId: number) =>
|
||||
env.RECFLARE_PLAYER_SETTINGS.get<Record<string, string>>(`player:${playerId}`, 'json')
|
||||
|
||||
it('stores the DM setting and answers the resulting settings', async () => {
|
||||
const res = await put(887001, { directMessagePrivacySetting: 'Favorites' })
|
||||
expect(res.status).toBe(200)
|
||||
// The same body the GET serves — the enum by NUMBER, not the name that was posted.
|
||||
expect(await res.json()).toEqual({
|
||||
playerId: 887001,
|
||||
directMessagePrivacySetting: 1,
|
||||
groupChatPrivacySetting: 0,
|
||||
})
|
||||
// Stored by NAME in the player settings map the `playersettings` worker owns.
|
||||
expect((await settings(887001))?.directMessagePrivacySetting).toBe('Favorites')
|
||||
})
|
||||
|
||||
it('stores the group chat setting on its own', async () => {
|
||||
const res = await put(887002, { groupChatPrivacySetting: 'NoOne' })
|
||||
expect(await res.json()).toEqual({
|
||||
playerId: 887002,
|
||||
directMessagePrivacySetting: 0,
|
||||
groupChatPrivacySetting: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves the other setting alone — the client sends one field per call', async () => {
|
||||
await put(887003, { directMessagePrivacySetting: 'NoOne' })
|
||||
const res = await put(887003, { groupChatPrivacySetting: 'Favorites' })
|
||||
expect(await res.json()).toEqual({
|
||||
playerId: 887003,
|
||||
directMessagePrivacySetting: 2,
|
||||
groupChatPrivacySetting: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('merges, leaving the player’s other settings untouched', async () => {
|
||||
await env.RECFLARE_PLAYER_SETTINGS.put(
|
||||
'player:887004',
|
||||
JSON.stringify({ 'Recroom.OOBE': '77' })
|
||||
)
|
||||
await put(887004, { directMessagePrivacySetting: 'Favorites' })
|
||||
expect(await settings(887004)).toEqual({
|
||||
'Recroom.OOBE': '77',
|
||||
directMessagePrivacySetting: 'Favorites',
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts the enum by ordinal too, and is case-insensitive about the name', async () => {
|
||||
expect(await (await put(887005, { directMessagePrivacySetting: '2' })).json()).toMatchObject({
|
||||
directMessagePrivacySetting: 2,
|
||||
})
|
||||
expect(
|
||||
await (await put(887005, { directMessagePrivacySetting: 'favorites' })).json()
|
||||
).toMatchObject({ directMessagePrivacySetting: 1 })
|
||||
})
|
||||
|
||||
it('accepts the fields in the query string as well as the body', async () => {
|
||||
const res = await SELF.fetch(`${path}?groupChatPrivacySetting=NoOne`, {
|
||||
method: 'PUT',
|
||||
headers: await bearer(887006),
|
||||
})
|
||||
expect(await res.json()).toMatchObject({ groupChatPrivacySetting: 2 })
|
||||
})
|
||||
|
||||
it('ignores an unreadable value rather than 400ing or writing a default', async () => {
|
||||
await put(887007, { directMessagePrivacySetting: 'Favorites' })
|
||||
const res = await put(887007, { directMessagePrivacySetting: 'Whoever' })
|
||||
expect(res.status).toBe(200)
|
||||
// Unchanged — a value that won't parse is not a write of `Friends`.
|
||||
expect(await res.json()).toMatchObject({ directMessagePrivacySetting: 1 })
|
||||
})
|
||||
|
||||
it('401s without a token', async () => {
|
||||
const res = await SELF.fetch(path, {
|
||||
method: 'PUT',
|
||||
body: new URLSearchParams({ directMessagePrivacySetting: 'NoOne' }),
|
||||
})
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /thread/checkCanSendDirectMessageWithPrivacySetting', () => {
|
||||
const path = `${ORIGIN}/thread/checkCanSendDirectMessageWithPrivacySetting`
|
||||
|
||||
@@ -1406,6 +1530,7 @@ describe('openapi', () => {
|
||||
'POST /thread/{id}/read',
|
||||
'POST /thread/{id}/rename',
|
||||
'POST /thread/{id}/snooze',
|
||||
'PUT /thread/chatPrivacySetting',
|
||||
'PUT /thread/{id}/favorite',
|
||||
'PUT /thread/{id}/message/{messageId}/read',
|
||||
'PUT /thread/{id}/read',
|
||||
|
||||
Reference in New Issue
Block a user