diff options
-rw-r--r-- | api/src/routes/channels/#channel_id/pins.ts | 3 | ||||
-rw-r--r-- | api/src/routes/channels/#channel_id/webhooks.ts | 9 | ||||
-rw-r--r-- | api/src/routes/guilds/#guild_id/roles.ts | 45 | ||||
-rw-r--r-- | api/src/routes/guilds/index.ts | 3 | ||||
-rw-r--r-- | api/src/routes/guilds/templates/index.ts | 3 | ||||
-rw-r--r-- | api/src/routes/users/@me/relationships.ts | 8 | ||||
-rw-r--r-- | api/src/util/ApiError.ts | 38 | ||||
-rw-r--r-- | api/src/util/Constants.ts | 114 | ||||
-rw-r--r-- | gateway/src/schema/Identify.ts | 53 |
9 files changed, 132 insertions, 144 deletions
diff --git a/api/src/routes/channels/#channel_id/pins.ts b/api/src/routes/channels/#channel_id/pins.ts index 96a3fdbf..fafb789f 100644 --- a/api/src/routes/channels/#channel_id/pins.ts +++ b/api/src/routes/channels/#channel_id/pins.ts @@ -1,6 +1,7 @@ import { Channel, ChannelPinsUpdateEvent, Config, emitEvent, getPermission, Message, MessageUpdateEvent } from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; +import { DiscordApiErrors } from "../../../util/Constants"; const router: Router = Router(); @@ -16,7 +17,7 @@ router.put("/:message_id", async (req: Request, res: Response) => { const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true }); const { maxPins } = Config.get().limits.channel; - if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins); + if (pinned_count >= maxPins) throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins); await Promise.all([ Message.update({ id: message_id }, { pinned: true }), diff --git a/api/src/routes/channels/#channel_id/webhooks.ts b/api/src/routes/channels/#channel_id/webhooks.ts index 775053ba..d2a4b9a0 100644 --- a/api/src/routes/channels/#channel_id/webhooks.ts +++ b/api/src/routes/channels/#channel_id/webhooks.ts @@ -1,11 +1,12 @@ import { Router, Response, Request } from "express"; import { check, Length } from "../../../util/instanceOf"; -import { Channel, getPermission, trimSpecial } from "@fosscord/util"; +import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { isTextChannel } from "./messages/index"; +import { DiscordApiErrors } from "../../../util/Constants"; const router: Router = Router(); -// TODO: +// TODO: webhooks // TODO: use Image Data Type for avatar instead of String router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req: Request, res: Response) => { @@ -15,6 +16,10 @@ router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), as isTextChannel(channel.type); if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); + const webhook_count = await Webhook.count({ channel_id }); + const { maxWebhooks } = Config.get().limits.channel; + if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); + const permission = await getPermission(req.user_id, channel.guild_id); permission.hasThrow("MANAGE_WEBHOOKS"); diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts index f6ac8caa..c3dd92dc 100644 --- a/api/src/routes/guilds/#guild_id/roles.ts +++ b/api/src/routes/guilds/#guild_id/roles.ts @@ -7,12 +7,14 @@ import { GuildRoleCreateEvent, GuildRoleUpdateEvent, GuildRoleDeleteEvent, - emitEvent + emitEvent, + Config } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { check } from "../../../util/instanceOf"; import { RoleModifySchema } from "../../../schema/Roles"; +import { DiscordApiErrors } from "../../../util/Constants"; const router: Router = Router(); @@ -33,24 +35,34 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_ROLES"); - const role = await new Role({ + const role_count = await Role.count({ guild_id }); + const { maxRoles } = Config.get().limits.guild; + + if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); + + const role = { + position: 0, + hoist: false, + color: 0, // default value ...body, id: Snowflake.generate(), guild_id: guild_id, managed: false, - position: 0, - tags: null, - permissions: String(perms.bitfield & (body.permissions || 0n)) - }).save(); - - await emitEvent({ - event: "GUILD_ROLE_CREATE", - guild_id, - data: { + permissions: String(perms.bitfield & (body.permissions || 0n)), + tags: undefined + }; + + await Promise.all([ + Role.insert(role), + emitEvent({ + event: "GUILD_ROLE_CREATE", guild_id, - role: role - } - } as GuildRoleCreateEvent); + data: { + guild_id, + role: role + } + } as GuildRoleCreateEvent) + ]); res.json(role); }); @@ -84,14 +96,13 @@ router.delete("/:role_id", async (req: Request, res: Response) => { // TODO: check role hierarchy router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const { role_id } = req.params; + const { role_id, guild_id } = req.params; const body = req.body as RoleModifySchema; const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_ROLES"); - const role = new Role({ ...body, id: role_id, guild_id, permissions: perms.bitfield & (body.permissions || 0n) }); + const role = new Role({ ...body, id: role_id, guild_id, permissions: String(perms.bitfield & (body.permissions || 0n)) }); await Promise.all([ role.save(), diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts index e4157384..a54b83ba 100644 --- a/api/src/routes/guilds/index.ts +++ b/api/src/routes/guilds/index.ts @@ -3,6 +3,7 @@ import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord import { HTTPError } from "lambert-server"; import { check } from "./../../util/instanceOf"; import { GuildCreateSchema } from "../../schema/Guild"; +import { DiscordApiErrors } from "../../util/Constants"; const router: Router = Router(); @@ -14,7 +15,7 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) = const { maxGuilds } = Config.get().limits.user; const guild_count = await Member.count({ id: req.user_id }); if (guild_count >= maxGuilds) { - throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403); + throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); } const guild_id = Snowflake.generate(); diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts index 7a8ac886..3a619278 100644 --- a/api/src/routes/guilds/templates/index.ts +++ b/api/src/routes/guilds/templates/index.ts @@ -4,6 +4,7 @@ import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscor import { HTTPError } from "lambert-server"; import { GuildTemplateCreateSchema } from "../../../schema/Guild"; import { check } from "../../../util/instanceOf"; +import { DiscordApiErrors } from "../../../util/Constants"; router.get("/:code", async (req: Request, res: Response) => { const { code } = req.params; @@ -21,7 +22,7 @@ router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res const guild_count = await Member.count({ id: req.user_id }); if (guild_count >= maxGuilds) { - throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403); + throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); } const template = await Template.findOneOrFail({ code: code }); diff --git a/api/src/routes/users/@me/relationships.ts b/api/src/routes/users/@me/relationships.ts index 0b864d88..2bd9c819 100644 --- a/api/src/routes/users/@me/relationships.ts +++ b/api/src/routes/users/@me/relationships.ts @@ -5,10 +5,12 @@ import { RelationshipType, RelationshipRemoveEvent, emitEvent, - Relationship + Relationship, + Config } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; +import { DiscordApiErrors } from "../../../util/Constants"; import { check, Length } from "../../../util/instanceOf"; @@ -31,6 +33,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ var relationship = user.relationships.find((x) => x.id === id); const friendRequest = friend.relationships.find((x) => x.id === req.user_id); + // TODO: you can add infinitely many blocked users (should this be prevented?) if (type === RelationshipType.blocked) { if (relationship) { if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user"); @@ -67,6 +70,9 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ return res.sendStatus(204); } + const { maxFriends } = Config.get().limits.user; + if (user.relationships.length >= maxFriends) throw DiscordApiErrors.MAXIMUM_FRIENDS.withParams(maxFriends); + var incoming_relationship = new Relationship({ nickname: undefined, type: RelationshipType.incoming, id: req.user_id }); var outgoing_relationship = new Relationship({ nickname: undefined, type: RelationshipType.outgoing, id }); diff --git a/api/src/util/ApiError.ts b/api/src/util/ApiError.ts index 2316cd71..c133e6e7 100644 --- a/api/src/util/ApiError.ts +++ b/api/src/util/ApiError.ts @@ -1,23 +1,27 @@ export class ApiError extends Error { - constructor(readonly message: string, public readonly code: number, public readonly httpStatus: number = 400, public readonly defaultParams?: string[]) { - super(message); - } + constructor( + readonly message: string, + public readonly code: number, + public readonly httpStatus: number = 400, + public readonly defaultParams?: string[] + ) { + super(message); + } - withDefaultParams(): ApiError { - if(this.defaultParams) - return new ApiError(applyParamsToString(this.message, this.defaultParams), this.code, this.httpStatus) - return this - } + withDefaultParams(): ApiError { + if (this.defaultParams) return new ApiError(applyParamsToString(this.message, this.defaultParams), this.code, this.httpStatus); + return this; + } - withParams(...params: string[]): ApiError { - return new ApiError(applyParamsToString(this.message, params), this.code, this.httpStatus) - } + withParams(...params: (string | number)[]): ApiError { + return new ApiError(applyParamsToString(this.message, params), this.code, this.httpStatus); + } } -export function applyParamsToString(s: string, params: string[]): string { - let newString = s - params.forEach(a => { - newString = newString.replace("{}", a) - }) - return newString +export function applyParamsToString(s: string, params: (string | number)[]): string { + let newString = s; + params.forEach((a) => { + newString = newString.replace("{}", "" + a); + }); + return newString; } diff --git a/api/src/util/Constants.ts b/api/src/util/Constants.ts index 15fdc519..f06b3d56 100644 --- a/api/src/util/Constants.ts +++ b/api/src/util/Constants.ts @@ -1,4 +1,4 @@ -import {ApiError} from "./ApiError"; +import { ApiError } from "./ApiError"; export const WSCodes = { 1000: "WS_CLOSE_REQUESTED", @@ -6,60 +6,7 @@ export const WSCodes = { 4010: "SHARDING_INVALID", 4011: "SHARDING_REQUIRED", 4013: "INVALID_INTENTS", - 4014: "DISALLOWED_INTENTS", -}; - -const AllowedImageFormats = ["webp", "png", "jpg", "jpeg", "gif"]; - -const AllowedImageSizes = Array.from({ length: 9 }, (e, i) => 2 ** (i + 4)); - -function makeImageUrl(root: string, { format = "webp", size = 512 } = {}) { - if (format && !AllowedImageFormats.includes(format)) throw new Error("IMAGE_FORMAT: " + format); - if (size && !AllowedImageSizes.includes(size)) throw new RangeError("IMAGE_SIZE: " + size); - return `${root}.${format}${size ? `?size=${size}` : ""}`; -} -/** - * Options for Image URLs. - * @typedef {Object} ImageURLOptions - * @property {string} [format] One of `webp`, `png`, `jpg`, `jpeg`, `gif`. If no format is provided, - * defaults to `webp`. - * @property {boolean} [dynamic] If true, the format will dynamically change to `gif` for - * animated avatars; the default is false. - * @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048`, `4096` - */ - -export const Endpoints = { - CDN(root: string) { - return { - Emoji: (emojiID: string, format = "png") => `${root}/emojis/${emojiID}.${format}`, - Asset: (name: string) => `${root}/assets/${name}`, - DefaultAvatar: (discriminator: string) => `${root}/embed/avatars/${discriminator}.png`, - Avatar: (user_id: string, hash: string, format = "webp", size: number, dynamic = false) => { - if (dynamic) format = hash.startsWith("a_") ? "gif" : format; - return makeImageUrl(`${root}/avatars/${user_id}/${hash}`, { format, size }); - }, - Banner: (guildID: string, hash: string, format = "webp", size: number) => - makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), - Icon: (guildID: string, hash: string, format = "webp", size: number, dynamic = false) => { - if (dynamic) format = hash.startsWith("a_") ? "gif" : format; - return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }); - }, - AppIcon: (clientID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) => - makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }), - AppAsset: (clientID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) => - makeImageUrl(`${root}/app-assets/${clientID}/${hash}`, { size, format }), - GDMIcon: (channelID: string, hash: string, format = "webp", size: number) => - makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }), - Splash: (guildID: string, hash: string, format = "webp", size: number) => - makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }), - DiscoverySplash: (guildID: string, hash: string, format = "webp", size: number) => - makeImageUrl(`${root}/discovery-splashes/${guildID}/${hash}`, { size, format }), - TeamIcon: (teamID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) => - makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }), - }; - }, - invite: (root: string, code: string) => `${root}/${code}`, - botGateway: "/gateway/bot", + 4014: "DISALLOWED_INTENTS" }; /** @@ -84,7 +31,7 @@ export const Status = { DISCONNECTED: 5, WAITING_FOR_GUILDS: 6, IDENTIFYING: 7, - RESUMING: 8, + RESUMING: 8 }; /** @@ -101,7 +48,7 @@ export const VoiceStatus = { CONNECTING: 1, AUTHENTICATING: 2, RECONNECTING: 3, - DISCONNECTED: 4, + DISCONNECTED: 4 }; export const OPCodes = { @@ -116,7 +63,7 @@ export const OPCodes = { REQUEST_GUILD_MEMBERS: 8, INVALID_SESSION: 9, HELLO: 10, - HEARTBEAT_ACK: 11, + HEARTBEAT_ACK: 11 }; export const VoiceOPCodes = { @@ -128,7 +75,7 @@ export const VoiceOPCodes = { SPEAKING: 5, HELLO: 8, CLIENT_CONNECT: 12, - CLIENT_DISCONNECT: 13, + CLIENT_DISCONNECT: 13 }; export const Events = { @@ -186,7 +133,7 @@ export const Events = { SHARD_READY: "shardReady", SHARD_RESUME: "shardResume", INVALIDATED: "invalidated", - RAW: "raw", + RAW: "raw" }; export const ShardEvents = { @@ -195,7 +142,7 @@ export const ShardEvents = { INVALID_SESSION: "invalidSession", READY: "ready", RESUMED: "resumed", - ALL_READY: "allReady", + ALL_READY: "allReady" }; /** @@ -287,7 +234,7 @@ export const WSEvents = keyMirror([ "TYPING_START", "VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE", - "WEBHOOKS_UPDATE", + "WEBHOOKS_UPDATE" ]); /** @@ -330,7 +277,7 @@ export const MessageTypes = [ null, null, null, - "REPLY", + "REPLY" ]; /** @@ -361,12 +308,12 @@ export const ChannelTypes = { GROUP: 3, CATEGORY: 4, NEWS: 5, - STORE: 6, + STORE: 6 }; export const ClientApplicationAssetTypes = { SMALL: 1, - BIG: 2, + BIG: 2 }; export const Colors = { @@ -398,7 +345,7 @@ export const Colors = { BLURPLE: 0x7289da, GREYPLE: 0x99aab5, DARK_BUT_NOT_BLACK: 0x2c2f33, - NOT_QUITE_BLACK: 0x23272a, + NOT_QUITE_BLACK: 0x23272a }; /** @@ -613,7 +560,10 @@ export const DiscordApiErrors = { ONLY_OWNER: new ApiError("Only the owner of this account can perform this action", 20018), ANNOUNCEMENT_RATE_LIMITS: new ApiError("This message cannot be edited due to announcement rate limits", 20022), CHANNEL_WRITE_RATELIMIT: new ApiError("The channel you are writing has hit the write rate limit", 20028), - WORDS_NOT_ALLOWED: new ApiError("Your Stage topic, server name, server description, or channel names contain words that are not allowed", 20031), + WORDS_NOT_ALLOWED: new ApiError( + "Your Stage topic, server name, server description, or channel names contain words that are not allowed", + 20031 + ), GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError("Guild premium subscription level too low", 20035), MAXIMUM_GUILDS: new ApiError("Maximum number of guilds reached ({})", 30001, undefined, ["100"]), MAXIMUM_FRIENDS: new ApiError("Maximum number of friends reached ({})", 30002, undefined, ["1000"]), @@ -659,7 +609,12 @@ export const DiscordApiErrors = { MISSING_PERMISSIONS: new ApiError("You lack permissions to perform that action", 50013), INVALID_AUTHENTICATION_TOKEN: new ApiError("Invalid authentication token provided", 50014), NOTE_TOO_LONG: new ApiError("Note was too long", 50015), - INVALID_BULK_DELETE_QUANTITY: new ApiError("Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", 50016, undefined, ["2","100"]), + INVALID_BULK_DELETE_QUANTITY: new ApiError( + "Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", + 50016, + undefined, + ["2", "100"] + ), CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError("A message can only be pinned to the channel it was sent in", 50019), INVALID_OR_TAKEN_INVITE_CODE: new ApiError("Invite code was either invalid or taken", 50020), CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError("Cannot execute action on a system message", 50021), @@ -670,7 +625,10 @@ export const DiscordApiErrors = { INVALID_ROLE: new ApiError("Invalid role", 50028), INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033), BULK_DELETE_MESSAGE_TOO_OLD: new ApiError("A message provided was too old to bulk delete", 50034), - INVALID_FORM_BODY: new ApiError("Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", 50035), + INVALID_FORM_BODY: new ApiError( + "Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", + 50035 + ), INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError("An invite was accepted to a guild the application's bot is not in", 50036), INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041), FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError("File uploaded exceeds the maximum size", 50045), @@ -679,7 +637,10 @@ export const DiscordApiErrors = { PAYMENT_SOURCE_REQUIRED: new ApiError("Payment source required to redeem gift", 50070), CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError("Cannot delete a channel required for Community guilds", 50074), INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081), - CANNOT_EDIT_ARCHIVED_THREAD: new ApiError("Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", 50083), + CANNOT_EDIT_ARCHIVED_THREAD: new ApiError( + "Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", + 50083 + ), INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError("Invalid thread notification settings", 50084), BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError("before value is earlier than the thread creation date", 50085), SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError("This server is not available in your location", 50095), @@ -701,17 +662,14 @@ export const DiscordApiErrors = { STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError("Sticker frame rate is either too small or too large", 170006), STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError("Sticker animation duration exceeds maximum of {} seconds", 170007, undefined, ["5"]), - //Other errors - UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404), -} + UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404) +}; /** * An error encountered while performing an API request (Fosscord only). Here are the potential errors: */ -export const FosscordApiErrors = { - -} +export const FosscordApiErrors = {}; /** * The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types: @@ -731,7 +689,7 @@ export const MembershipStates = [ // They start at 1 null, "INVITED", - "ACCEPTED", + "ACCEPTED" ]; /** @@ -744,7 +702,7 @@ export const WebhookTypes = [ // They start at 1 null, "Incoming", - "Channel Follower", + "Channel Follower" ]; function keyMirror(arr: string[]) { diff --git a/gateway/src/schema/Identify.ts b/gateway/src/schema/Identify.ts index f6d95204..cbd6630a 100644 --- a/gateway/src/schema/Identify.ts +++ b/gateway/src/schema/Identify.ts @@ -3,32 +3,33 @@ import { ActivitySchema } from "./Activity"; export const IdentifySchema = { token: String, $intents: BigInt, // discord uses a Integer for bitfields we use bigints tho. | instanceOf will automatically convert the Number to a BigInt - $properties: { - // bruh discord really uses $ in the property key for bots, so we need to double prefix it, because instanceOf treats $ (prefix) as a optional key - $os: String, - $os_arch: String, - $browser: String, - $device: String, - $$os: String, - $$browser: String, - $$device: String, - $browser_user_agent: String, - $browser_version: String, - $os_version: String, - $referrer: String, - $$referrer: String, - $referring_domain: String, - $$referring_domain: String, - $referrer_current: String, - $referring_domain_current: String, - $release_channel: String, - $client_build_number: Number, - $client_event_source: String, - $client_version: String, - $system_locale: String, - $window_manager: String, - $distro: String, - }, + $properties: Object, + // { + // // bruh discord really uses $ in the property key for bots, so we need to double prefix it, because instanceOf treats $ (prefix) as a optional key + // $os: String, + // $os_arch: String, + // $browser: String, + // $device: String, + // $$os: String, + // $$browser: String, + // $$device: String, + // $browser_user_agent: String, + // $browser_version: String, + // $os_version: String, + // $referrer: String, + // $$referrer: String, + // $referring_domain: String, + // $$referring_domain: String, + // $referrer_current: String, + // $referring_domain_current: String, + // $release_channel: String, + // $client_build_number: Number, + // $client_event_source: String, + // $client_version: String, + // $system_locale: String, + // $window_manager: String, + // $distro: String, + // }, $presence: ActivitySchema, $compress: Boolean, $large_threshold: Number, |