(feat): prepare for new ident flow
Some checks failed
/ build-web (push) Failing after 3m21s
/ build-desktop (linux) (push) Failing after 3m37s
/ build-mobile (push) Failing after 11m20s
/ release (push) Has been skipped

(fix): some bugs
This commit is contained in:
Alois 2026-07-02 23:56:49 +02:00
commit 8e831fd993
8 changed files with 177 additions and 232 deletions

View file

@ -238,19 +238,9 @@ function RootShell() {
<LoginWrapper> <LoginWrapper>
<LegalWrapper> <LegalWrapper>
<Crypto> <Crypto>
<MTPProvider> <DesktopMediaProvider>
<Session> <Outlet />
<UserContext> </DesktopMediaProvider>
<CallInit />
<CallPopout />
<TAuthWrapper>
<DesktopMediaProvider>
<Outlet />
</DesktopMediaProvider>
</TAuthWrapper>
</UserContext>
</Session>
</MTPProvider>
</Crypto> </Crypto>
</LegalWrapper> </LegalWrapper>
</LoginWrapper> </LoginWrapper>
@ -264,13 +254,23 @@ function RootShell() {
function AppShell() { function AppShell() {
return ( return (
<AppLayout> <MTPProvider>
<ChatContext> <Session>
<NotificationsProvider> <UserContext>
<Outlet /> <CallInit />
</NotificationsProvider> <CallPopout />
</ChatContext> <TAuthWrapper>
</AppLayout> <AppLayout>
<ChatContext>
<NotificationsProvider>
<Outlet />
</NotificationsProvider>
</ChatContext>
</AppLayout>
</TAuthWrapper>
</UserContext>
</Session>
</MTPProvider>
); );
} }

View file

@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineConfig, type Plugin } from "vite"; import { defineConfig, normalizePath, type Plugin } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { mtp } from "mtp/vite"; import { mtp } from "mtp/vite";
@ -51,6 +51,38 @@ function deepFilterAssetHeaders(rootDir: string): Plugin {
}; };
} }
function restartOnMtpSourceChange(srcDir: string): Plugin {
const watchedDir = normalizePath(realpathSync(srcDir));
return {
name: "restart-on-mtp-source-change",
apply: "serve",
configureServer(server) {
server.watcher.add(watchedDir);
let restartTimer: ReturnType<typeof setTimeout> | null = null;
const restart = (file: string) => {
if (!normalizePath(file).startsWith(`${watchedDir}/`)) {
return;
}
if (restartTimer) {
clearTimeout(restartTimer);
}
restartTimer = setTimeout(() => {
restartTimer = null;
void server.restart();
}, 50);
};
server.watcher.on("add", restart);
server.watcher.on("change", restart);
server.watcher.on("unlink", restart);
},
};
}
export default defineConfig({ export default defineConfig({
base: "./", base: "./",
clearScreen: false, clearScreen: false,
@ -105,7 +137,6 @@ export default defineConfig({
}, },
envPrefix: ["VITE_", "TAURI_ENV_*"], envPrefix: ["VITE_", "TAURI_ENV_*"],
optimizeDeps: { optimizeDeps: {
// Annoying Vite 8 stuff
include: [ include: [
"react-redux", "react-redux",
"use-sync-external-store/shim/with-selector", "use-sync-external-store/shim/with-selector",
@ -114,7 +145,28 @@ export default defineConfig({
"eventemitter3", "eventemitter3",
"react-is", "react-is",
], ],
exclude: ["mtp", "mtp/raw", "mtp/type-map", "@tensamin/shared"], exclude: [
"mtp",
"mtp/raw",
"mtp/type-map",
"@tensamin/call",
"@tensamin/call/utils",
"@tensamin/chat",
"@tensamin/crypto",
"@tensamin/crypto/context",
"@tensamin/crypto/worker",
"@tensamin/markdown",
"@tensamin/mtp",
"@tensamin/notifications",
"@tensamin/shared",
"@tensamin/shared/data",
"@tensamin/shared/log",
"@tensamin/storage",
"@tensamin/storage/context",
"@tensamin/tauri",
"@tensamin/tauth",
"@tensamin/user",
],
}, },
build: { build: {
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
@ -122,6 +174,7 @@ export default defineConfig({
}, },
plugins: [ plugins: [
deepFilterAssetHeaders(resolve(appDir, "public")), deepFilterAssetHeaders(resolve(appDir, "public")),
restartOnMtpSourceChange(resolve(appDir, "../../packages/mtp/src")),
mtp({ typeMaps: resolve(appDir, "../../type-maps.yaml") }), mtp({ typeMaps: resolve(appDir, "../../type-maps.yaml") }),
react(), react(),
tailwindcss(), tailwindcss(),

View file

@ -347,7 +347,7 @@
}, },
"overrides": { "overrides": {
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz",
"mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-c4a52c6/mtp-0.1.0.tgz", "mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-8ae8377/mtp-0.1.0.tgz",
}, },
"packages": { "packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
@ -1540,7 +1540,7 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mtp": ["mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-c4a52c6/mtp-0.1.0.tgz", {}, "sha512-KIhmkIlALo0PdMnx/QzhM87jNCuC90DaQ7uEWpMjbkI3xvGWmkiX75zqt0UZcO03JYWfBAsCO4+51egoOUq6TA=="], "mtp": ["mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-8ae8377/mtp-0.1.0.tgz", {}, "sha512-W0l7Jc+1XI8BdrmRP9rlY/di6kZ5a/srv4mQvzwyJd+Tfg7BWZ9IUYUXIPze6ayx9dLY1f9Y71OgbhjstMzYjA=="],
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],

View file

@ -44,7 +44,7 @@
}, },
"overrides": { "overrides": {
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz",
"mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-c4a52c6/mtp-0.1.0.tgz" "mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-8ae8377/mtp-0.1.0.tgz"
}, },
"dependencies": { "dependencies": {
"@tensamin/ui": "*", "@tensamin/ui": "*",

View file

@ -52,8 +52,7 @@ type IncomingCallInvite = {
senderId: number; senderId: number;
}; };
type CurrentCallData = type CurrentCallData =
| (z.infer<typeof mtp.call_data.response> & { exists: boolean }) (z.infer<typeof mtp.call_data.response> & { exists: boolean }) | null;
| null;
type NavigateFn = (options: { type NavigateFn = (options: {
to: string; to: string;

View file

@ -13,8 +13,6 @@ import { onResume } from "tauri-plugin-app-events-api";
import { MTPClient } from "mtp"; import { MTPClient } from "mtp";
import type { z } from "zod"; import type { z } from "zod";
import { decryptText } from "@tensamin/crypto/worker";
import { useCrypto } from "@tensamin/crypto/context";
import { import {
type Calls, type Calls,
type Communities, type Communities,
@ -33,19 +31,21 @@ import {
RETRY_INTERVAL, RETRY_INTERVAL,
} from "./values"; } from "./values";
const APP_VERSION = "0.0.10";
const READY_STATE = { const READY_STATE = {
CLOSED: 0, CLOSED: 0,
CONNECTING: 1, CONNECTING: 1,
OPEN: 2, OPEN: 2,
} as const; } as const;
const PUSH_TYPES = ["message_live", "message_state", "call_invite", "error_no_iota"] as const; const PUSH_TYPES = [
"message_live",
"message_state",
"call_invite",
"error_no_iota",
] as const;
const WIRE_TYPES = { const WIRE_TYPES = {
identification: "AppIdentification", temp_cool_type: "TempCoolType",
challenge_response: "AppChallengeResponse",
get_user_data: "GetUserData", get_user_data: "GetUserData",
change_user_data: "ChangeUserData", change_user_data: "ChangeUserData",
ping: "AppPing", ping: "AppPing",
@ -67,18 +67,9 @@ const APP_TYPES = Object.fromEntries(
Object.entries(WIRE_TYPES).map(([appType, wireType]) => [wireType, appType]), Object.entries(WIRE_TYPES).map(([appType, wireType]) => [wireType, appType]),
) as Record<string, keyof Schemas & string>; ) as Record<string, keyof Schemas & string>;
const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([ export type ProtocolMessage<
"error", T extends keyof Schemas & string = keyof Schemas & string,
"error_invalid_user_id", > = {
"error_no_user_id",
"error_invalid_challenge",
"error_invalid_secret",
"error_invalid_private_key",
"error_invalid_public_key",
"error_not_authenticated",
]);
export type ProtocolMessage<T extends keyof Schemas & string = keyof Schemas & string> = {
id?: number; id?: number;
type: T | string; type: T | string;
data: z.infer<Schemas[T]["response"]>; data: z.infer<Schemas[T]["response"]>;
@ -114,29 +105,16 @@ function isTauriMobile() {
return isTauri() && /Android|iPhone|iPad|iPod/.test(navigator.userAgent); return isTauri() && /Android|iPhone|iPad|iPod/.test(navigator.userAgent);
} }
function isFatalIdentificationError(error: unknown) {
if (typeof error === "object" && error !== null && "type" in error) {
const type = (error as { type?: unknown }).type;
if (typeof type === "string" && FATAL_IDENTIFICATION_ERROR_TYPES.has(type)) {
return true;
}
}
return error instanceof Error && (
error.message.includes("Missing or invalid user id") ||
error.message.includes("Missing private key") ||
error.message.includes("Identification challenge was rejected") ||
error.message.includes("timed out after") ||
error.message.includes("Response validation failed")
);
}
function getProtocolErrorDetails(error: unknown) { function getProtocolErrorDetails(error: unknown) {
if (typeof error !== "object" || error === null || !("type" in error)) { if (typeof error !== "object" || error === null || !("type" in error)) {
return null; return null;
} }
const protocolError = error as { id?: unknown; type?: unknown; data?: unknown }; const protocolError = error as {
id?: unknown;
type?: unknown;
data?: unknown;
};
return { return {
id: protocolError.id, id: protocolError.id,
type: protocolError.type, type: protocolError.type,
@ -193,7 +171,9 @@ function validateResponse<T extends keyof Schemas & string>(
const parsed = schema.safeParse(data); const parsed = schema.safeParse(data);
if (!parsed.success) { if (!parsed.success) {
throw new Error(`Response validation failed for ${type}: ${parsed.error.message}`); throw new Error(
`Response validation failed for ${type}: ${parsed.error.message}`,
);
} }
return { return {
@ -203,9 +183,11 @@ function validateResponse<T extends keyof Schemas & string>(
} as ProtocolMessage<T>; } as ProtocolMessage<T>;
} }
export function Provider(props: { children: ReactNode; blockConnection?: boolean }) { export function Provider(props: {
children: ReactNode;
blockConnection?: boolean;
}) {
const { load } = useStorage(); const { load } = useStorage();
const { decrypt, getSharedSecret } = useCrypto();
const [readyState, setReadyState] = useState<number>(READY_STATE.CLOSED); const [readyState, setReadyState] = useState<number>(READY_STATE.CLOSED);
const [connected, setConnected] = useState<boolean>(false); const [connected, setConnected] = useState<boolean>(false);
@ -222,9 +204,9 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
const [freshContacts, setFreshContacts] = useState<Contacts>([]); const [freshContacts, setFreshContacts] = useState<Contacts>([]);
const [freshCalls, setFreshCalls] = useState<Calls>([]); const [freshCalls, setFreshCalls] = useState<Calls>([]);
const clientRef = useRef<Awaited<ReturnType<typeof MTPClient.create>> | null>(null); const clientRef = useRef<Awaited<ReturnType<typeof MTPClient.create>> | null>(
const identificationStartedRef = useRef(false); null,
const identificationCancelRef = useRef(false); );
const [mtpUrl, setMtpUrl] = useState<string | null>(null); const [mtpUrl, setMtpUrl] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@ -281,7 +263,6 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
if (!connected) return; if (!connected) return;
return subscribe("error_no_iota", () => { return subscribe("error_no_iota", () => {
identificationCancelRef.current = true;
setIdentified(false); setIdentified(false);
setIdentifying(false); setIdentifying(false);
setError("We couldn't reach your Iota"); setError("We couldn't reach your Iota");
@ -381,13 +362,14 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
setConnected(false); setConnected(false);
setIdentified(false); setIdentified(false);
setIdentifying(false); setIdentifying(false);
identificationStartedRef.current = false;
await MTPClient.init(); await MTPClient.init();
const client = await MTPClient.create({ const client = await MTPClient.create({
url, url,
pings: true, pings: true,
logger: (event) => log(event.hint === "error" ? 0 : 2, "mtp", "blue", event), logger: (event) => {
log(2, "mtp", event.type === "state" ? "cyan" : "blue", event.type === "state" ? event.data : event.type, event);
},
}); });
if (disposed) { if (disposed) {
@ -403,13 +385,40 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
return; return;
} }
const authPayload = new Promise<ProtocolMessage<"temp_cool_type">>(
(resolve, reject) => {
const unsubscribe = client.subscribe("TempCoolType", (message) => {
try {
unsubscribe();
resolve(validateResponse("temp_cool_type", message));
} catch (authPayloadError) {
unsubscribe();
reject(authPayloadError);
}
});
},
);
clearReconnectTimer(); clearReconnectTimer();
scheduleReconnectReset(); scheduleReconnectReset();
identificationCancelRef.current = false;
setReadyState(READY_STATE.OPEN); setReadyState(READY_STATE.OPEN);
setConnected(true); setConnected(true);
setIdentifying(true);
setError(""); setError("");
setErrorDescription(""); setErrorDescription("");
await client.auth();
const finalResponse = await authPayload;
if (disposed || clientRef.current !== client) {
return;
}
setFreshContacts(finalResponse.data.contacts);
setFreshCommunities(finalResponse.data.communities ?? []);
setFreshCalls(finalResponse.data.calls);
setIdentifying(false);
setIdentified(true);
} catch (connectError) { } catch (connectError) {
if (disposed) return; if (disposed) return;
@ -420,7 +429,13 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
setConnected(false); setConnected(false);
setIdentified(false); setIdentified(false);
setIdentifying(false); setIdentifying(false);
log(0, "mtp", "red", "Connection attempt failed", connectError); log(
0,
"mtp",
"red",
"Connection/authentication attempt failed",
getProtocolErrorDetails(connectError) ?? connectError,
);
scheduleReconnect(connectError); scheduleReconnect(connectError);
} }
} }
@ -464,116 +479,9 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
setConnected(false); setConnected(false);
setIdentified(false); setIdentified(false);
setIdentifying(false); setIdentifying(false);
identificationStartedRef.current = false;
}; };
}, [mtpUrl, props.blockConnection]); }, [mtpUrl, props.blockConnection]);
useEffect(() => {
if (!connected) {
identificationStartedRef.current = false;
return;
}
if (identificationCancelRef.current || identificationStartedRef.current) {
return;
}
identificationStartedRef.current = true;
let cancelled = false;
setIdentifying(true);
setIdentified(false);
const identify = async () => {
try {
const sessionId = await load("session_id");
const userId = await load("user_id");
const privateKey = await load("private_key");
if (
!Number.isSafeInteger(userId) ||
userId <= 0 ||
privateKey.trim() === "" ||
!Number.isSafeInteger(sessionId) ||
sessionId <= 0
) {
throw new Error("Invalid credentials");
}
const challengeEnvelope = await send("identification", {
version: APP_VERSION,
session_id: sessionId,
user_id: userId,
});
const sharedSecret = await getSharedSecret(
privateKey,
"",
challengeEnvelope.data.public_key,
);
const decryptedChallenge = await decryptText(
sharedSecret,
challengeEnvelope.data.challenge,
);
const finalResponse = await send("challenge_response", {
challenge: decryptedChallenge,
}).catch((challengeError) => {
if (!identificationCancelRef.current) {
setError("Identification Failed");
setErrorDescription(
"Unable to complete secure identification. Please verify your credentials and try again.",
);
}
throw challengeError;
});
setFreshContacts(finalResponse.data.contacts);
setFreshCommunities(finalResponse.data.communities ?? []);
setFreshCalls(finalResponse.data.calls);
if (cancelled || identificationCancelRef.current) {
return;
}
setError("");
setErrorDescription("");
setIdentified(true);
} catch (identificationError) {
if (cancelled || identificationCancelRef.current) {
return;
}
const isFatal = isFatalIdentificationError(identificationError);
log(
isFatal ? 0 : 1,
"mtp",
isFatal ? "red" : "yellow",
"Identification handshake failed",
getProtocolErrorDetails(identificationError) ?? identificationError,
);
setIdentified(false);
setError("Identification Failed");
setErrorDescription(
isFatal
? "Unable to complete secure identification. Please verify your credentials and try again."
: "Unable to complete secure identification because the transport request failed.",
);
} finally {
if (!cancelled && !identificationCancelRef.current) {
setIdentifying(false);
}
}
};
void identify();
return () => {
cancelled = true;
};
}, [connected, decrypt, getSharedSecret, load, send]);
const progress = useMemo(() => { const progress = useMemo(() => {
if (!mtpUrl) return 10; if (!mtpUrl) return 10;
if (readyState === READY_STATE.CONNECTING) return 30; if (readyState === READY_STATE.CONNECTING) return 30;
@ -585,7 +493,8 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
const loadingTitle = useMemo(() => { const loadingTitle = useMemo(() => {
if (!mtpUrl) return "Looking up configuration"; if (!mtpUrl) return "Looking up configuration";
if (readyState === READY_STATE.CONNECTING || !connected) return "Connecting to Tensamin"; if (readyState === READY_STATE.CONNECTING || !connected)
return "Connecting to Tensamin";
if (identifying || !identified) return "Identifying secure session"; if (identifying || !identified) return "Identifying secure session";
return "Loading"; return "Loading";
}, [connected, identified, identifying, readyState, mtpUrl]); }, [connected, identified, identifying, readyState, mtpUrl]);
@ -595,7 +504,7 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
if (readyState === READY_STATE.CONNECTING || !connected) { if (readyState === READY_STATE.CONNECTING || !connected) {
return "Establishing transport channel"; return "Establishing transport channel";
} }
if (identifying || !identified) return "Verifying challenge-response handshake"; if (identifying || !identified) return "Waiting for authenticated session";
return undefined; return undefined;
}, [connected, identified, identifying, readyState, mtpUrl]); }, [connected, identified, identifying, readyState, mtpUrl]);

View file

@ -39,13 +39,33 @@ export const failedUser = {
username: "unknown", username: "unknown",
} as z.infer<typeof mtp.get_user_data.response>; } as z.infer<typeof mtp.get_user_data.response>;
export type Contacts = z.infer< const authPayload = z.object({
typeof mtp.challenge_response.response.shape.contacts communities: z.array(z.object({})).optional(),
>; contacts: z.array(
export type Communities = z.infer< z.object({
typeof mtp.challenge_response.response.shape.communities last_message_at: z.number(),
>; user_id: z.number(),
export type Calls = z.infer<typeof mtp.challenge_response.response.shape.calls>; last_message: z
.object({
content: z.base64(),
sender_id: z.number(),
})
.optional(),
messages: z.array(message),
}),
),
calls: z.array(
z.object({
call_id: z.string(),
call_secret: z.base64().optional(),
call_members: z.array(z.number()),
}),
),
});
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< type Base16Palette = Record<
| "base00" | "base00"
@ -94,44 +114,9 @@ const user = z.object({
username: z.string().min(1).max(15), username: z.string().min(1).max(15),
}); });
export const mtp = { export const mtp = {
identification: { temp_cool_type: {
request: z.object({ request: z.object({}).optional(),
version: z.string(), response: authPayload,
session_id: z.number(),
user_id: z.number(),
}),
response: z.object({
challenge: z.string(),
public_key: z.base64(),
}),
},
challenge_response: {
request: z.object({
challenge: z.base64(),
}),
response: z.object({
communities: z.array(z.object({})).optional(),
contacts: z.array(
z.object({
last_message_at: z.number(),
user_id: z.number(),
last_message: z
.object({
content: z.base64(),
sender_id: z.number(),
})
.optional(),
messages: z.array(message),
}),
),
calls: z.array(
z.object({
call_id: z.string(),
call_secret: z.base64().optional(),
call_members: z.array(z.number()),
}),
),
}),
}, },
get_user_data: { get_user_data: {
request: z.object({ request: z.object({

View file

@ -3,8 +3,7 @@ protocol_version: "0.0"
type_maps: type_maps:
"0.0": "0.0":
CommunicationTypes: CommunicationTypes:
AppIdentification: 32 TempCoolType: 32
AppChallengeResponse: 33
GetUserData: 34 GetUserData: 34
ChangeUserData: 35 ChangeUserData: 35
MessageLive: 36 MessageLive: 36