using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
public sealed class ReplikaClient : IDisposable
private static readonly object _lock = new object();
private static ReplikaClient _instance;
public static ReplikaClient Instance
return _instance ??= new ReplikaClient();
private const string BaseDomain = "my.replika.com";
private const string BaseApiUrl = "https://my.replika.com/api";
private const string ApiVersionPath = "mobile/1.5";
private const string WebSocketUrl = "wss://ws.replika.ai/v17";
private readonly HttpClientHandler _httpHandler;
private readonly HttpClient _httpClient;
private ClientWebSocket _webSocket;
private bool _wsConnected;
private string _authToken;
private string _deviceId;
private string _timestampHash;
private CancellationTokenSource _wsCts;
private string _password;
public event EventHandler<ConnectionStateEventArgs> OnConnectionStateChanged;
public event EventHandler<MessageEventArgs> OnMessageReceived;
public event EventHandler<MessageEventArgs> OnMessageSent;
public event EventHandler<HistoryEventArgs> OnHistoryReceived;
public event EventHandler<DiaryEventArgs> OnDiaryFetched;
public event EventHandler<ErrorEventArgs> OnError;
_authToken = Environment.GetEnvironmentVariable("X_AUTH_TOKEN");
_userId = Environment.GetEnvironmentVariable("X_USER_ID");
_deviceId = Environment.GetEnvironmentVariable("X_DEVICE_ID");
_timestampHash = Environment.GetEnvironmentVariable("X_TIMESTAMP_HASH");
_email = Environment.GetEnvironmentVariable("REPLIKA_EMAIL");
_password = Environment.GetEnvironmentVariable("REPLIKA_PASSWORD");
_chatId = Environment.GetEnvironmentVariable("REPLIKA_CHAT_ID");
if (string.IsNullOrEmpty(_deviceId))
_deviceId = Guid.NewGuid().ToString().ToUpper();
if (string.IsNullOrEmpty(_timestampHash))
_timestampHash = ComputeTimestampHash(_deviceId);
_httpHandler = new HttpClientHandler
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
CookieContainer = new CookieContainer(),
_httpClient = new HttpClient(_httpHandler);
_httpClient.BaseAddress = new Uri($"{BaseApiUrl}/{ApiVersionPath}/");
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36");
_httpClient.DefaultRequestHeaders.Add("Origin", $"https://{BaseDomain}");
_httpClient.DefaultRequestHeaders.Add("x-device-type", "web");
if (!string.IsNullOrEmpty(_authToken) && !string.IsNullOrEmpty(_userId))
_httpClient.DefaultRequestHeaders.Add("x-auth-token", _authToken);
_httpClient.DefaultRequestHeaders.Add("x-user-id", _userId);
_httpClient.DefaultRequestHeaders.Add("x-device-id", _deviceId);
_httpClient.DefaultRequestHeaders.Add("x-timestamp-hash", _timestampHash);
#region Public Login Method
public async Task LoginIfNeededAsync()
if (!string.IsNullOrEmpty(_authToken) && !string.IsNullOrEmpty(_userId))
Console.WriteLine("Using existing X_AUTH_TOKEN + X_USER_ID from environment.");
if (string.IsNullOrEmpty(_email) || string.IsNullOrEmpty(_password))
throw new InvalidOperationException(
"No X_AUTH_TOKEN / X_USER_ID in env. Also missing REPLIKA_EMAIL/PASSWORD. Cannot login!");
var checkPayload = new { id_string = _email };
var resp = await _httpClient.PostAsync("auth/sign_in/actions/get_auth_type",
MakeJsonContent(checkPayload));
resp.EnsureSuccessStatusCode();
var text = await resp.Content.ReadAsStringAsync();
if (!text.Contains("\"auth_type\":\"password\""))
throw new NotSupportedException("Replika responded with non-password auth_type. Cannot proceed.");
throw new Exception("Failed to get_auth_type from Replika. " + ex.Message, ex);
"widget.multiselect", "widget.scale", "widget.titled_text_field", "widget.new_onboarding",
"widget.app_navigation", "widget.mission_recommendation", "journey2.daily_mission_activity",
"journey2.replika_phrases", "new_payment_subscriptions", "navigation.relationship_settings",
"avatar", "diaries.images", "wallet", "store.dialog_items", "chat_suggestions",
"3d_customization", "3d_customization_v2", "3d_customization_v3", "store_customization",
"blurred_messages", "item_daily_reward", "romantic_photos", "voice_messages",
"stable_diffusion2", "subscription_request", "sale_screen", "multiple_bot_selfies",
"hot_photos_v2", "store_age_and_body_type", "diary_generation_by_session",
"store_voice", "coaching_prompts", "voice_sample_streaming", "pets_in_memory",
"quests", "ai_art_ui", "name_moderation", "special_offer_screen", "bot_photo_no_content",
"gifts_mood_XP_calculation_logic",
unity_bundle_version = 286
var loginResp = await _httpClient.PostAsync("auth/sign_in/actions/auth_by_password",
MakeJsonContent(loginBody));
if (!loginResp.IsSuccessStatusCode)
var failText = await loginResp.Content.ReadAsStringAsync();
throw new Exception($"Login failed. Status: {loginResp.StatusCode}, body={failText}");
var loginJson = await loginResp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(loginJson);
if (doc.RootElement.TryGetProperty("user_id", out var uidEl) &&
doc.RootElement.TryGetProperty("auth_token", out var tokEl))
_userId = uidEl.GetString();
_authToken = tokEl.GetString();
throw new Exception("Could not parse user_id/auth_token from login response!");
_httpClient.DefaultRequestHeaders.Remove("x-user-id");
_httpClient.DefaultRequestHeaders.Remove("x-auth-token");
_httpClient.DefaultRequestHeaders.Add("x-user-id", _userId);
_httpClient.DefaultRequestHeaders.Add("x-auth-token", _authToken);
Console.WriteLine($"Logged in as userId={_userId}, token=***");
#region Connect WebSocket
public async Task ConnectWebSocketAsync()
if (_wsConnected) return;
var initResp = await _httpClient.GetAsync($"https://{BaseDomain}/");
_webSocket = new ClientWebSocket();
var cookieUri = new Uri($"https://{BaseDomain}/");
var cookieCollection = _httpHandler.CookieContainer.GetCookies(cookieUri);
if (cookieCollection != null)
var cookieHeader = new StringBuilder();
foreach (Cookie c in cookieCollection)
cookieHeader.Append($"{c.Name}={c.Value}; ");
if (cookieHeader.Length > 0)
_webSocket.Options.SetRequestHeader("Cookie", cookieHeader.ToString());
_webSocket.Options.SetRequestHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0.0.0 Safari/537.36");
_webSocket.Options.SetRequestHeader("Origin", $"https://{BaseDomain}");
_webSocket.Options.SetRequestHeader("x-user-id", _userId);
_webSocket.Options.SetRequestHeader("x-auth-token", _authToken);
_webSocket.Options.SetRequestHeader("x-device-id", _deviceId);
_webSocket.Options.SetRequestHeader("x-timestamp-hash", _timestampHash);
_wsCts = new CancellationTokenSource();
await _webSocket.ConnectAsync(new Uri(WebSocketUrl), _wsCts.Token);
OnConnectionStateChanged?.Invoke(this, new ConnectionStateEventArgs(ConnectionState.Connected));
_ = Task.Run(() => ReceiveLoopAsync(_wsCts.Token));
#region WebSocket Send & Handshake
private async Task ReceiveLoopAsync(CancellationToken cancelToken)
var buffer = new byte[8192];
while (!cancelToken.IsCancellationRequested && _webSocket.State == WebSocketState.Open)
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancelToken);
if (result.MessageType == WebSocketMessageType.Close)
Console.WriteLine("Socket closed by server. State=" + _webSocket.State);
OnConnectionStateChanged?.Invoke(this, new ConnectionStateEventArgs(ConnectionState.Disconnected));
int count = result.Count;
while (!result.EndOfMessage)
if (count >= buffer.Length)
throw new Exception("Message too long for buffer!");
var next = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, count, buffer.Length - count), cancelToken);
var msg = Encoding.UTF8.GetString(buffer, 0, count);
HandleIncomingWsEvent(msg);
Console.WriteLine("WS receive loop error: " + ex);
OnError?.Invoke(this, new ErrorEventArgs(ex));
OnConnectionStateChanged?.Invoke(this, new ConnectionStateEventArgs(ConnectionState.Disconnected));
private void HandleIncomingWsEvent(string json)
evt = JsonSerializer.Deserialize<EventMessage>(json);
Console.WriteLine("Error deserializing WS message: " + ex + "\nMsg=" + json);
if (evt == null || evt.EventName == null)
Console.WriteLine("Ignoring malformed event: " + json);
if (evt.EventName == "init")
SendWsEvent("chat_screen", new { }, Guid.NewGuid().ToString());
else if (evt.EventName == "chat_screen")
SendWsEvent("application_started", new { }, Guid.NewGuid().ToString());
else if (evt.EventName == "application_started")
SendWsEvent("app_foreground", new { }, Guid.NewGuid().ToString());
else if (evt.EventName == "app_foreground")
Console.WriteLine("Replika handshake completed!");
if (evt.EventName == "history")
if (evt.Payload != null && evt.Payload.TryGetValue("messages", out var messagesElem))
var msgArr = JsonSerializer.Deserialize<List<ChatMessage>>(messagesElem.GetRawText());
var histArgs = new HistoryEventArgs(msgArr ?? new List<ChatMessage>());
OnHistoryReceived?.Invoke(this, histArgs);
else if (evt.EventName == "message")
if (evt.Payload.TryGetValue("message", out var singleMsg))
var cm = JsonSerializer.Deserialize<ChatMessage>(singleMsg.GetRawText());
OnMessageReceived?.Invoke(this, new MessageEventArgs(cm));
OnMessageSent?.Invoke(this, new MessageEventArgs(cm));
else if (evt.Payload.TryGetValue("messages", out var multiple))
var arr = JsonSerializer.Deserialize<List<ChatMessage>>(multiple.GetRawText());
OnMessageReceived?.Invoke(this, new MessageEventArgs(cm));
OnMessageSent?.Invoke(this, new MessageEventArgs(cm));
Console.WriteLine("Received unhandled event: " + evt.EventName + " => " + evt.Payload);
private bool IsBotMessage(ChatMessage cm)
if (cm?.Meta == null) return false;
return cm.Meta.Nature != null && cm.Meta.Nature.Equals("Robot", StringComparison.OrdinalIgnoreCase);
private void SendWsEvent(string eventName, object payload, string token)
var json = JsonSerializer.Serialize(obj);
var bytes = Encoding.UTF8.GetBytes(json);
_webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text,
true, CancellationToken.None).Wait();
public async Task<List<DiaryItem>> FetchAllDiaryEntriesAsync()
var diaries = new List<DiaryItem>();
var previews = new List<SavedItemPreview>();
var url = $"saved_chat_items/previews?t=diary&offset={offset}&limit={limit}";
var resp = await _httpClient.GetAsync(url);
if (!resp.IsSuccessStatusCode) break;
var text = await resp.Content.ReadAsStringAsync();
var subset = JsonSerializer.Deserialize<List<SavedItemPreview>>(text) ?? new List<SavedItemPreview>();
previews.AddRange(subset);
OnDiaryFetched?.Invoke(this, new DiaryEventArgs(diaries));
var body = new { ids = new List<string>() };
foreach (var p in previews) body.ids.Add(p.Id);
var bodyJson = JsonSerializer.Serialize(body);
var detailResp = await _httpClient.PostAsync("saved_chat_items/actions/get_by_ids",
new StringContent(bodyJson, Encoding.UTF8, "application/json"));
if (detailResp.IsSuccessStatusCode)
var detailText = await detailResp.Content.ReadAsStringAsync();
diaries = JsonSerializer.Deserialize<List<DiaryItem>>(detailText) ?? new List<DiaryItem>();
OnDiaryFetched?.Invoke(this, new DiaryEventArgs(diaries));
private async Task EnsureChatIdAsync()
if (!string.IsNullOrEmpty(_chatId)) return;
var resp = await _httpClient.GetAsync("personal_bot");
if (!resp.IsSuccessStatusCode) return;
var text = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(text);
if (doc.RootElement.TryGetProperty("last_message", out var lastMsg)
&& lastMsg.TryGetProperty("meta", out var meta)
&& meta.TryGetProperty("chat_id", out var cElem))
_chatId = cElem.GetString();
public async Task RequestHistoryAsync(int limit = 20, string lastMessageId = null)
await EnsureChatIdAsync();
if (string.IsNullOrEmpty(_chatId))
throw new Exception("No chat_id found from personal_bot. Provide REPLIKA_CHAT_ID or let me fetch from /personal_bot");
last_message_id = lastMessageId
SendWsEvent("history", payloadObj, Guid.NewGuid().ToString());
public async Task SendMessageAsync(string text)
await EnsureChatIdAsync();
if (string.IsNullOrEmpty(_chatId))
throw new Exception("No chat_id found. can't send message.");
var token = Guid.NewGuid().ToString();
SendWsEvent("message", payloadObj, token);
#region Helper & Disposal
private static string ComputeTimestampHash(string deviceId)
var input = $"time_covfefe_prefix=2020_{deviceId}";
using var md5 = MD5.Create();
var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
var sb = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes) sb.Append(b.ToString("x2"));
private static StringContent MakeJsonContent(object o)
var j = JsonSerializer.Serialize(o);
return new StringContent(j, Encoding.UTF8, "application/json");
if (_webSocket.State == WebSocketState.Open)
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).Wait();
public class EventMessage
[JsonPropertyName("event_name")]
public string EventName { get; set; }
[JsonPropertyName("token")]
public string Token { get; set; }
[JsonPropertyName("auth")]
public Dictionary<string, object> Auth { get; set; }
[JsonPropertyName("payload")]
public Dictionary<string, JsonElement> Payload { get; set; }
public string Id { get; set; }
[JsonPropertyName("content")]
public ChatContent Content { get; set; }
[JsonPropertyName("meta")]
public ChatMeta Meta { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("text")]
public string Text { get; set; }
[JsonPropertyName("nature")]
public string Nature { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("chat_id")]
public string ChatId { get; set; }
public class SavedItemPreview
public string Id { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("created_at")]
public string CreatedAt { get; set; }
public string Id { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("content")]
public string Content { get; set; }
[JsonPropertyName("images")]
public List<string> Images { get; set; }
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; }
#region Event Args and ConnectionState
public enum ConnectionState
public class ConnectionStateEventArgs : EventArgs
public ConnectionState State { get; }
public ConnectionStateEventArgs(ConnectionState s)
public class MessageEventArgs : EventArgs
public ChatMessage Message { get; }
public MessageEventArgs(ChatMessage msg)
public class HistoryEventArgs : EventArgs
public List<ChatMessage> Messages { get; }
public HistoryEventArgs(List<ChatMessage> msgs)
public class DiaryEventArgs : EventArgs
public List<DiaryItem> Diaries { get; }
public DiaryEventArgs(List<DiaryItem> items)
public class ErrorEventArgs : EventArgs
public Exception Exception { get; }
public ErrorEventArgs(Exception ex)