Greetings to everyone who cares! One boring day, I decided to try my hand at creating a project using the Telegram API, not expecting it to turn into what it did. I explored several integration approaches and came to some interesting conclusions along the way. Eventually, I began developing an open-source library called
In this series of articles, I’ll share the challenges I faced and the new discoveries I made throughout this journey.
MTProto or the Story of Big Crutch
At first, I decided to take the easy route and simply used libraries designed for the browser, specifically ”
However, it didn’t take long to realize that this approach wasn’t cutting it. Since it relies on a browser running in the background, the performance takes a significant hit — responses become very slow, and some functions are outright impossible to implement.
Because of these limitations, I quickly abandoned this approach and decided it was time to dive into native code.
TDLib Pre-built Library
I’ll be honest — this was my first experience working with a prebuilt library. To use it, you need to build the library separately for each platform (
Once built, you end up with a library for each platform, which you then need to manually import into your project (in my case, into my own library).
If you’re curious, you can check out my
Problem: The library is quite large — around 400 MB for both platforms — which makes storing it in the repository impractical. For now, the pre-built library is included in the repo since I haven’t found a better solution yet.
In the future, I plan to upload it to an external storage service and set it up to import automatically during installation. Honestly, I’m still exploring the best way to handle this, as it’s the first time I’ve encountered a problem like this.
Native Module
In the first step, I decided to try to wrap basic TDLib methods without additional logic and try to implement something this way.
Let’s break this down using the td_json_client_receive example method. Below, I provide the code for this function in Java and Objective-C.
@ReactMethod
public void td_json_client_receive(Promise promise) {
try {
if (client == null) {
promise.reject("CLIENT_NOT_INITIALIZED", "TDLib client is not initialized");
return;
}
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<TdApi.Object> responseRef = new AtomicReference<>();
client.send(null, new Client.ResultHandler() {
@Override
public void onResult(TdApi.Object object) {
responseRef.set(object);
latch.countDown();
}
});
boolean awaitSuccess = latch.await(10, TimeUnit.SECONDS);
if (awaitSuccess && responseRef.get() != null) {
promise.resolve(gson.toJson(responseRef.get()));
} else {
promise.reject("RECEIVE_ERROR", "No response from TDLib");
}
} catch (Exception e) {
promise.reject("RECEIVE_EXCEPTION", e.getMessage());
}
}
RCT_EXPORT_METHOD(td_json_client_receive:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
if (_client == NULL) {
reject(@"CLIENT_NOT_INITIALIZED", @"TDLib client not initialized", nil);
return;
}
const char *response = td_json_client_receive(_client, 10.0);
if (response) {
NSString *responseString = [NSString stringWithUTF8String:response];
resolve(responseString);
} else {
reject(@"RECEIVE_ERROR", @"No response from TDLib", nil);
}
}
Problem: The receive function returns all events from TDLib, and to catch the specific event we need, we have to use a loop. This approach isn’t very efficient at the JavaScript layer. I’ll attach the implementation code in JS below, but I wouldn’t recommend using this method.
/**
* Fetches the list of supported languages from TDLib.
*/
const fetchSupportedLanguages = async () => {
try {
await setLocalizationTargetOption();
const request = {
'@type': 'getLocalizationTargetInfo',
only_locales: true,
};
TdLib.td_json_client_send(request);
while (true) {
const response = await TdLib.td_json_client_receive();
if (response) {
const parsedResponse = JSON.parse(response);
if (parsedResponse['@type'] === 'localizationTargetInfo') {
return parsedResponse;
}
if (parsedResponse['@type'] === 'error') {
throw new Error(
`Error fetching supported languages: ${parsedResponse.message}`,
);
}
} else {
throw new Error('No response from TDLib');
}
}
} catch (error) {
console.error('Error in fetchSupportedLanguages:', error);
throw error;
}
};
Now, we’ve finally reached the final solution: the complex logic needs to be moved into native code. This involves creating a Client and handling everything there. I understand that this approach requires implementing a lot of logic and methods, but I don’t see any better alternatives.
Below is an example implementation of the getAuthorizationState function, where we loop through events to find the one we need.
RCT_EXPORT_METHOD(getAuthorizationState:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
@try {
if (_client == NULL) {
reject(@"TDLIB_NOT_STARTED", @"TDLib client is not initialized. Call startTdLibService first.", nil);
return;
}
NSString *request = @"{"@type":"getAuthorizationState"}";
td_json_client_send(_client, [request UTF8String]);
while (true) {
const char *response = td_json_client_receive(_client, 10.0);
if (response != NULL) {
NSString *responseString = [NSString stringWithUTF8String:response];
NSLog(@"TDLib response: %@", responseString);
NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
NSString *type = responseDict[@"@type"];
if ([type isEqualToString:@"authorizationStateWaitPhoneNumber"] ||
[type isEqualToString:@"authorizationStateWaitCode"] ||
[type isEqualToString:@"authorizationStateReady"] ||
[type isEqualToString:@"authorizationStateWaitOtherDeviceConfirmation"] ||
[type isEqualToString:@"authorizationStateClosed"]) {
resolve(responseString);
return;
}
if ([type containsString:@"update"]) {
NSLog(@"Ignoring update: %@", type);
continue;
}
} else {
reject(@"NO_RESPONSE", @"No response from TDLib", nil);
return;
}
}
} @catch (NSException *exception) {
reject(@"GET_AUTH_STATE_EXCEPTION", exception.reason, nil);
}
}
@ReactMethod
public void getAuthorizationState(Promise promise) {
try {
if (client == null) {
promise.reject("CLIENT_NOT_INITIALIZED", "TDLib client is not initialized");
return;
}
client.send(new TdApi.GetAuthorizationState(), object -> {
if (object instanceof TdApi.AuthorizationState) {
try {
Map<String, Object> responseMap = new HashMap<>();
String originalType = object.getClass().getSimpleName();
String formattedType = originalType.substring(0, 1).toLowerCase() + originalType.substring(1);
responseMap.put("@type", formattedType);
promise.resolve(new JSONObject(responseMap).toString());
} catch (Exception e) {
promise.reject("JSON_CONVERT_ERROR", "Error converting object to JSON: " + e.getMessage());
}
} else if (object instanceof TdApi.Error) {
TdApi.Error error = (TdApi.Error) object;
promise.reject("AUTH_STATE_ERROR", error.message);
} else {
promise.reject("AUTH_STATE_UNEXPECTED_RESPONSE", "Unexpected response from TDLib.");
}
});
} catch (Exception e) {
promise.reject("GET_AUTH_STATE_EXCEPTION", e.getMessage());
}
}
You can find the rest of the implemented functions in the
useEffect(() => {
// Initializes TDLib with the provided parameters and checks the authorization state
TdLib.startTdLib(parameters).then(r => {
TdLib.getAuthorizationState().then(r => {
if (JSON.parse(r)['@type'] === 'authorizationStateReady') {
TdLib.getProfile(); // Fetches the user's profile if authorization is ready
}
});
});
}, []);
At this point, I’ve completed the development of methods for authorization and retrieving user profiles. I’ll dive deeper into these topics in the next article. Thank you for your attention!