452 lines
10 KiB
TypeScript
452 lines
10 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { legalDocsSchema } from "./features/legal/schema";
|
|
import {
|
|
settingsStorageDefaults,
|
|
type SettingsStorageDefaults,
|
|
} from "./settings";
|
|
|
|
const fileFromMessage = z.object({
|
|
name: z.string(),
|
|
id: z.uuidv4(),
|
|
type: z.enum(["image", "image_top_right", "file"]),
|
|
});
|
|
|
|
const bytesLike = z.union([
|
|
z.instanceof(Uint8Array),
|
|
z.array(z.number().int().min(0).max(255)),
|
|
z.base64(),
|
|
]);
|
|
|
|
const protocolBytes = z.instanceof(Uint8Array);
|
|
|
|
const chatSecretResponse = z.object({
|
|
UserId: z.string(),
|
|
ChatId: z.string(),
|
|
SecretId: z.string(),
|
|
VersionNumber: z.number(),
|
|
EncryptedSecret: bytesLike,
|
|
KemCiphertext: bytesLike,
|
|
WrappingScheme: z.string(),
|
|
CreatedAt: z.number(),
|
|
UpdatedAt: z.number(),
|
|
});
|
|
|
|
const chatSecretRecipient = z.object({
|
|
UserId: z.string(),
|
|
EncryptedSecret: protocolBytes,
|
|
KemCiphertext: protocolBytes,
|
|
});
|
|
|
|
export const Message = z.object({
|
|
NotEncrypted: z.boolean().optional(),
|
|
SentBySelf: z.boolean().optional(),
|
|
SendTime: z.number(),
|
|
Content: z.base64(),
|
|
Files: z.array(fileFromMessage).optional(),
|
|
Tint: z.string().length(7).startsWith("#").optional(),
|
|
Avatar: z.boolean().optional(),
|
|
Display: z.boolean().optional(),
|
|
MessageState: z
|
|
.enum(["read", "received", "sent", "sending", "awaiting"]) // awaiting for 'internal' use
|
|
.default("received"),
|
|
});
|
|
|
|
export const failedUser = {
|
|
Display: "Failed",
|
|
IotaId: 0,
|
|
OmikronConnections: [],
|
|
OnlineStatus: "user_borked",
|
|
PublicKey: "",
|
|
SubEnd: 0,
|
|
SubLevel: 0,
|
|
UserId: 0,
|
|
Username: "unknown",
|
|
} as z.infer<typeof mtp.GetUserData.response>;
|
|
|
|
const authPayload = z.object({
|
|
Communities: z.array(z.object({})).default([]),
|
|
Contacts: z
|
|
.array(
|
|
z.object({
|
|
LastMessageAt: z.number(),
|
|
UserId: z.number(),
|
|
LastMessage: z
|
|
.object({
|
|
Content: z.base64(),
|
|
SenderId: z.number(),
|
|
})
|
|
.optional(),
|
|
Messages: z.array(Message),
|
|
}),
|
|
)
|
|
.default([]),
|
|
Calls: z
|
|
.array(
|
|
z.object({
|
|
CallId: z.string(),
|
|
CallSecret: z.base64().optional(),
|
|
CallMembers: z.array(z.number()),
|
|
}),
|
|
)
|
|
.default([]),
|
|
});
|
|
|
|
export type Contacts = z.infer<typeof authPayload.shape.Contacts>;
|
|
export type Communities = z.infer<typeof authPayload.shape.Communities>;
|
|
export type Calls = z.infer<typeof authPayload.shape.Calls>;
|
|
|
|
type Base16Palette = Record<
|
|
| "base00"
|
|
| "base01"
|
|
| "base02"
|
|
| "base03"
|
|
| "base04"
|
|
| "base05"
|
|
| "base06"
|
|
| "base07"
|
|
| "base08"
|
|
| "base09"
|
|
| "base0A"
|
|
| "base0B"
|
|
| "base0C"
|
|
| "base0D"
|
|
| "base0E"
|
|
| "base0F",
|
|
string
|
|
>;
|
|
|
|
// MTP
|
|
const user = z.object({
|
|
About: z.string().max(255).optional(),
|
|
Avatar: z.string().optional(),
|
|
Display: z.string().min(1).max(15),
|
|
IotaId: z.number(),
|
|
OmikronConnections: z.array(z.number()),
|
|
OmikronId: z.number().optional(),
|
|
OnlineStatus: z.enum([
|
|
"user_offline",
|
|
"user_online",
|
|
"user_dnd",
|
|
"user_idle",
|
|
"user_wc",
|
|
"user_borked",
|
|
"iota_offline",
|
|
"iota_online",
|
|
"iota_borked",
|
|
]),
|
|
PublicKey: z.base64(),
|
|
Status: z.string().max(15).optional(),
|
|
SubEnd: z.number(),
|
|
SubLevel: z.number(),
|
|
UserId: z.number(),
|
|
Username: z.string().min(1).max(15),
|
|
});
|
|
export const mtp = {
|
|
IdentificationResponse: {
|
|
request: z.object({}).optional(),
|
|
response: authPayload,
|
|
},
|
|
GetUserData: {
|
|
request: z.object({
|
|
UserId: z.number().optional(),
|
|
Username: z.string().optional(),
|
|
}),
|
|
response: user,
|
|
},
|
|
ChangeUserData: {
|
|
request: user.partial(),
|
|
response: z.object({}),
|
|
},
|
|
Ping: {
|
|
request: z.object({
|
|
LastPing: z.number(),
|
|
}),
|
|
response: z.object({
|
|
PingIota: z.number(),
|
|
}),
|
|
},
|
|
MessageLive: {
|
|
request: z.object({}).optional(),
|
|
response: z.object({
|
|
SenderId: z.number(),
|
|
Message,
|
|
}),
|
|
},
|
|
MessagesGet: {
|
|
request: z.object({
|
|
UserId: z.number(),
|
|
Amount: z.number(),
|
|
Offset: z.number(),
|
|
}),
|
|
response: z.object({
|
|
Messages: z.array(Message),
|
|
}),
|
|
},
|
|
MessageSend: {
|
|
request: z.object({
|
|
Content: z.base64(),
|
|
ReceiverId: z.number(),
|
|
SendTime: z.number(),
|
|
Files: z.array(fileFromMessage).optional(),
|
|
}),
|
|
response: z.object({}),
|
|
},
|
|
AddConversation: {
|
|
request: z.object({
|
|
ChatPartnerId: z.number().optional(),
|
|
ChatPartnerName: z.string().min(1).max(15).optional(),
|
|
}),
|
|
response: z.object({}),
|
|
},
|
|
MessageState: {
|
|
request: z
|
|
.object({
|
|
ChatPartnerId: z.number(),
|
|
SendTime: z.number(),
|
|
MessageState: Message.shape.MessageState,
|
|
})
|
|
.or(
|
|
z.object({
|
|
MessageState: Message.shape.MessageState,
|
|
}),
|
|
),
|
|
response: z.object({
|
|
ChatPartnerId: z.number(),
|
|
MessageState: Message.shape.MessageState,
|
|
SendTime: z.number(),
|
|
}),
|
|
},
|
|
|
|
LoadTxtRecord: {
|
|
request: z.object({
|
|
Path: z.string(),
|
|
}),
|
|
response: z.object({
|
|
Content: z.string(),
|
|
}),
|
|
},
|
|
AuthenticateApp: {
|
|
request: z.object({
|
|
AppIdentifier: z.string(),
|
|
}),
|
|
response: z.object({
|
|
Challenge: z.base64(),
|
|
}),
|
|
},
|
|
CreateApp: {
|
|
request: z.object({
|
|
AppPublicKey: z.base64(),
|
|
AppIdentifier: z.string(),
|
|
}),
|
|
response: z.object({}),
|
|
},
|
|
|
|
// Calls
|
|
CallToken: {
|
|
request: z.object({
|
|
CallId: z.string(),
|
|
}),
|
|
response: z.object({
|
|
CallToken: z.string(),
|
|
}),
|
|
},
|
|
CallData: {
|
|
request: z.object({
|
|
CallId: z.string(),
|
|
}),
|
|
response: z.object({
|
|
UserIds: z.array(z.number()),
|
|
}),
|
|
},
|
|
CallInvite: {
|
|
request: z.object({
|
|
CallId: z.string(),
|
|
CallSecret: z.base64(),
|
|
ReceiverId: z.number(),
|
|
}),
|
|
response: z.object({
|
|
CallId: z.string().optional(),
|
|
CallSecret: z.base64().optional(),
|
|
SenderId: z.number().optional(),
|
|
}),
|
|
},
|
|
SetChatSecret: {
|
|
request: z.object({
|
|
ChatId: z.string(),
|
|
SecretId: z.string(),
|
|
VersionNumber: z.number(),
|
|
WrappingScheme: z.string(),
|
|
CreatedAt: z.number(),
|
|
Recipients: z.array(chatSecretRecipient).min(1),
|
|
}),
|
|
response: z.object({}),
|
|
},
|
|
GetChatSecret: {
|
|
request: z.object({
|
|
UserId: z.string(),
|
|
ChatId: z.string(),
|
|
SecretId: z.string().optional(),
|
|
}),
|
|
response: chatSecretResponse,
|
|
},
|
|
ChatSecretResponse: {
|
|
request: z.object({}).optional(),
|
|
response: chatSecretResponse,
|
|
},
|
|
ChatSecretForward: {
|
|
request: z.object({
|
|
ChatId: z.string(),
|
|
SenderUserId: z.string(),
|
|
RecipientUserId: z.string(),
|
|
SecretId: z.string(),
|
|
VersionNumber: z.number(),
|
|
EncryptedSecret: protocolBytes,
|
|
KemCiphertext: protocolBytes,
|
|
WrappingScheme: z.string(),
|
|
CreatedAt: z.number(),
|
|
}),
|
|
response: z.object({}),
|
|
},
|
|
ErrorNoIota: {
|
|
request: z.object({}).optional(),
|
|
response: z.object({}),
|
|
},
|
|
ErrorNotSet: {
|
|
request: z.object({}).optional(),
|
|
response: z.object({}),
|
|
},
|
|
} satisfies Record<string, { request: z.ZodType; response: z.ZodType }>;
|
|
|
|
export type MTP = typeof mtp;
|
|
|
|
// Storage
|
|
export interface Storage extends SettingsStorageDefaults {
|
|
session_id: number;
|
|
user_id: number;
|
|
mtp_keyring: string;
|
|
ppandtos_done: boolean;
|
|
accepted_terms_of_service: boolean;
|
|
accepted_privacy_policy: boolean;
|
|
analytics_crash_reports: boolean;
|
|
analytics_usage_data: boolean;
|
|
analytics_done: boolean;
|
|
legal_docs: z.infer<typeof legalDocsSchema>;
|
|
cached_contacts: Contacts;
|
|
cached_communities: Communities;
|
|
omega_url: string;
|
|
forced_omikron_url: string | undefined;
|
|
forced_omikron_public_key: string | undefined;
|
|
call_mute_range_start: number;
|
|
call_mute_range_end: number;
|
|
theme_color: string;
|
|
theme_palette: Base16Palette | null;
|
|
theme_primary_color: string;
|
|
theme_polarity: "dark" | "light" | "system";
|
|
theme_tint: "soft" | "hard" | "extreme";
|
|
theme_border_radius: number;
|
|
theme_custom_css: string;
|
|
chat_trusted_domains: string[];
|
|
chat_picker_saved_media: string[];
|
|
chat_picker_last_tab: "gif" | "meme" | "saved";
|
|
chat_picker_size: {
|
|
width: number;
|
|
height: number;
|
|
} | null;
|
|
}
|
|
|
|
export const storageDefaults: Storage = {
|
|
session_id: 0,
|
|
user_id: 0,
|
|
mtp_keyring: "",
|
|
ppandtos_done: false,
|
|
accepted_terms_of_service: false,
|
|
accepted_privacy_policy: false,
|
|
analytics_crash_reports: true,
|
|
analytics_usage_data: true,
|
|
analytics_done: false,
|
|
...settingsStorageDefaults,
|
|
legal_docs: {
|
|
eula: {
|
|
version: "0.0",
|
|
hash: "000000000000",
|
|
unix: 0,
|
|
},
|
|
tos: {
|
|
version: "0.0",
|
|
hash: "000000000000",
|
|
unix: 0,
|
|
},
|
|
pp: {
|
|
version: "0.0",
|
|
hash: "000000000000",
|
|
unix: 0,
|
|
},
|
|
},
|
|
cached_contacts: [],
|
|
cached_communities: [],
|
|
omega_url: "https://omega.tensamin.net",
|
|
forced_omikron_url: undefined,
|
|
forced_omikron_public_key: undefined,
|
|
call_mute_range_start: -55,
|
|
call_mute_range_end: -45,
|
|
theme_color: "",
|
|
theme_palette: null,
|
|
theme_primary_color: "",
|
|
theme_polarity: "system",
|
|
theme_tint: "soft",
|
|
theme_border_radius: 0.55,
|
|
theme_custom_css: "",
|
|
chat_trusted_domains: [
|
|
// Other platforms
|
|
"cdn.discordapp.com",
|
|
|
|
// Tenor
|
|
"tenor.com",
|
|
"c.tenor.com",
|
|
|
|
// Klipy
|
|
"static.klipy.com",
|
|
"static1.klipy.com",
|
|
"static2.klipy.com",
|
|
|
|
// Giphy
|
|
"giphy.com",
|
|
"www.giphy.com",
|
|
"media.giphy.com",
|
|
"i.giphy.com",
|
|
"media0.giphy.com",
|
|
"media1.giphy.com",
|
|
"media2.giphy.com",
|
|
"media3.giphy.com",
|
|
"media4.giphy.com",
|
|
],
|
|
chat_picker_saved_media: [],
|
|
chat_picker_last_tab: "gif",
|
|
chat_picker_size: null,
|
|
};
|
|
|
|
// User Status
|
|
export function getStatusColor(
|
|
status: z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>,
|
|
) {
|
|
switch (status) {
|
|
case "user_online":
|
|
return "#22c55e";
|
|
case "iota_online":
|
|
return "#22c55e";
|
|
case "user_dnd":
|
|
return "#ef4444";
|
|
case "user_idle":
|
|
return "#f59e0b";
|
|
case "user_wc":
|
|
return "#3b82f6";
|
|
case "user_borked":
|
|
case "iota_borked":
|
|
return "#6b7280";
|
|
case "user_offline":
|
|
case "iota_offline":
|
|
default:
|
|
return "#9ca3af";
|
|
}
|
|
}
|