GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "WatchBridge",
module_name = "WatchBridge",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/SSignalKit/SSignalKit:SSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/WatchCommon/Host:WatchCommon",
"//submodules/WatchBridgeAudio:WatchBridgeAudio",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/AvatarNode:AvatarNode",
"//submodules/StickerResources:StickerResources",
"//submodules/PhotoResources:PhotoResources",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyUI:LegacyUI",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/WatchBridge/Impl:WatchBridgeImpl",
],
visibility = [
"//visibility:public",
],
)
+27
View File
@@ -0,0 +1,27 @@
objc_library(
name = "WatchBridgeImpl",
enable_modules = True,
module_name = "WatchBridgeImpl",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.h",
], allow_empty=True),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
deps = [
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/WatchCommon/Host:WatchCommon",
],
sdk_frameworks = [
"Foundation",
"WatchConnectivity",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,29 @@
#import <SSignalKit/SSignalKit.h>
#import <UIKit/UIKit.h>
@class TGBridgeSubscription;
@interface TGBridgeServer : NSObject
@property (nonatomic, readonly) NSURL * _Nullable temporaryFilesURL;
@property (nonatomic, readonly) bool isRunning;
- (instancetype _Nonnull)initWithHandler:(SSignal * _Nullable (^ _Nonnull)(TGBridgeSubscription * _Nullable))handler fileHandler:(void (^ _Nonnull)(NSString * _Nullable, NSDictionary * _Nullable))fileHandler dispatchOnQueue:(void (^ _Nonnull)(void (^ _Nonnull)(void)))dispatchOnQueue logFunction:(void (^ _Nonnull)(NSString * _Nullable))logFunction allowBackgroundTimeExtension:(void (^ _Nonnull)())allowBackgroundTimeExtension;
- (void)startRunning;
- (SSignal * _Nonnull)watchAppInstalledSignal;
- (SSignal * _Nonnull)runningRequestsSignal;
- (void)setAuthorized:(bool)authorized userId:(int64_t)userId;
- (void)setMicAccessAllowed:(bool)allowed;
- (void)setStartupData:(NSDictionary * _Nullable)data;
- (void)pushContext;
- (void)sendFileWithURL:(NSURL * _Nonnull)url metadata:(NSDictionary * _Nullable)metadata asMessageData:(bool)asMessageData;
- (void)sendFileWithData:(NSData * _Nonnull)data metadata:(NSDictionary * _Nullable)metadata errorHandler:(void (^ _Nullable)(void))errorHandler;
- (NSInteger)wakeupNetwork;
- (void)suspendNetworkIfReady:(NSInteger)token;
@end
@@ -0,0 +1,3 @@
#import <UIKit/UIKit.h>
#import <WatchBridgeImpl/TGBridgeServer.h>
@@ -0,0 +1,770 @@
#import <WatchBridgeImpl/TGBridgeServer.h>
#import <LegacyComponents/LegacyComponents.h>
#import <WatchConnectivity/WatchConnectivity.h>
#import <os/lock.h>
#import <WatchCommon/WatchCommon.h>
@interface TGBridgeSignalManager : NSObject
- (bool)startSignalForKey:(NSString *)key producer:(SSignal *(^)())producer;
- (void)haltSignalForKey:(NSString *)key;
- (void)haltAllSignals;
@end
@interface TGBridgeServer () <WCSessionDelegate>
{
SSignal *(^_handler)(TGBridgeSubscription *);
void (^_fileHandler)(NSString *, NSDictionary *);
void (^_logFunction)(NSString *);
void (^_dispatch)(void (^)(void));
bool _pendingStart;
bool _processingNotification;
int32_t _sessionId;
volatile int32_t _tasksVersion;
TGBridgeContext *_activeContext;
TGBridgeSignalManager *_signalManager;
os_unfair_lock _incomingQueueLock;
NSMutableArray *_incomingMessageQueue;
bool _requestSubscriptionList;
NSArray *_initialSubscriptionList;
os_unfair_lock _outgoingQueueLock;
NSMutableArray *_outgoingMessageQueue;
os_unfair_lock _replyHandlerMapLock;
NSMutableDictionary *_replyHandlerMap;
SPipe *_appInstalled;
NSMutableDictionary *_runningTasks;
SVariable *_hasRunningTasks;
void (^_allowBackgroundTimeExtension)();
}
@property (nonatomic, readonly) WCSession *session;
@end
@implementation TGBridgeServer
- (instancetype)initWithHandler:(SSignal *(^)(TGBridgeSubscription *))handler fileHandler:(void (^)(NSString *, NSDictionary *))fileHandler dispatchOnQueue:(void (^)(void (^)(void)))dispatchOnQueue logFunction:(void (^)(NSString *))logFunction allowBackgroundTimeExtension:(void (^)())allowBackgroundTimeExtension
{
self = [super init];
if (self != nil)
{
_handler = [handler copy];
_fileHandler = [fileHandler copy];
_dispatch = [dispatchOnQueue copy];
_logFunction = [logFunction copy];
_allowBackgroundTimeExtension = [allowBackgroundTimeExtension copy];
_runningTasks = [[NSMutableDictionary alloc] init];
_hasRunningTasks = [[SVariable alloc] init];
[_hasRunningTasks set:[SSignal single:@false]];
_signalManager = [[TGBridgeSignalManager alloc] init];
_incomingMessageQueue = [[NSMutableArray alloc] init];
self.session.delegate = self;
[self.session activateSession];
_replyHandlerMap = [[NSMutableDictionary alloc] init];
_appInstalled = [[SPipe alloc] init];
_activeContext = [[TGBridgeContext alloc] initWithDictionary:[self.session applicationContext]];
}
return self;
}
- (void)log:(NSString *)message
{
_logFunction(message);
}
- (void)dispatch:(void (^)(void))action
{
_dispatch(action);
}
- (void)startRunning
{
if (self.isRunning)
return;
os_unfair_lock_lock(&_incomingQueueLock);
_isRunning = true;
for (id message in _incomingMessageQueue)
[self handleMessage:message replyHandler:nil finishTask:nil completion:nil];
[_incomingMessageQueue removeAllObjects];
os_unfair_lock_unlock(&_incomingQueueLock);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self dispatch:^{
_appInstalled.sink(@(self.session.isWatchAppInstalled));
}];
});
}
- (NSURL *)temporaryFilesURL
{
return self.session.watchDirectoryURL;
}
- (SSignal *)watchAppInstalledSignal
{
return [[SSignal single:@(self.session.watchAppInstalled)] then:_appInstalled.signalProducer()];
}
- (SSignal *)runningRequestsSignal
{
return _hasRunningTasks.signal;
}
#pragma mark -
- (void)setAuthorized:(bool)authorized userId:(int64_t)userId
{
_activeContext = [_activeContext updatedWithAuthorized:authorized peerId:userId];
}
- (void)setMicAccessAllowed:(bool)allowed
{
_activeContext = [_activeContext updatedWithMicAccessAllowed:allowed];
}
- (void)setStartupData:(NSDictionary *)data
{
_activeContext = [_activeContext updatedWithPreheatData:data];
}
- (void)pushContext
{
NSError *error;
[self.session updateApplicationContext:[_activeContext dictionary] error:&error];
//if (error != nil)
//TGLog(@"[BridgeServer][ERROR] Failed to push active application context: %@", error.localizedDescription);
}
#pragma mark -
- (void)handleMessageData:(NSData *)messageData task:(id<SDisposable>)task replyHandler:(void (^)(NSData *))replyHandler completion:(void (^)(void))completion
{
if (_allowBackgroundTimeExtension) {
_allowBackgroundTimeExtension();
}
__block id<SDisposable> runningTask = task;
void (^finishTask)(NSTimeInterval) = ^(NSTimeInterval delay)
{
if (runningTask == nil)
return;
void (^block)(void) = ^
{
[self dispatch:^{
[runningTask dispose];
//TGLog(@"[BridgeServer]: ended taskid: %d", runningTask);
runningTask = nil;
}];
};
if (delay > DBL_EPSILON)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((delay) * NSEC_PER_SEC)), dispatch_get_main_queue(), block);
else
block();
};
id message = [NSKeyedUnarchiver unarchiveObjectWithData:messageData];
os_unfair_lock_lock(&_incomingQueueLock);
if (!self.isRunning)
{
[_incomingMessageQueue addObject:message];
if (replyHandler != nil)
replyHandler([NSData data]);
finishTask(4.0);
os_unfair_lock_unlock(&_incomingQueueLock);
return;
}
os_unfair_lock_unlock(&_incomingQueueLock);
[self handleMessage:message replyHandler:replyHandler finishTask:finishTask completion:completion];
}
- (void)handleMessage:(id)message replyHandler:(void (^)(NSData *))replyHandler finishTask:(void (^)(NSTimeInterval))finishTask completion:(void (^)(void))completion
{
if ([message isKindOfClass:[TGBridgeSubscription class]])
{
TGBridgeSubscription *subcription = (TGBridgeSubscription *)message;
[self _createSubscription:subcription replyHandler:replyHandler finishTask:finishTask completion:completion];
//TGLog(@"[BridgeServer] Create subscription: %@", subcription);
}
else if ([message isKindOfClass:[TGBridgeDisposal class]])
{
TGBridgeDisposal *disposal = (TGBridgeDisposal *)message;
[_signalManager haltSignalForKey:[NSString stringWithFormat:@"%lld", disposal.identifier]];
if (replyHandler != nil)
replyHandler([NSData data]);
if (completion != nil)
completion();
//TGLog(@"[BridgeServer] Dispose subscription %lld", disposal.identifier);
if (finishTask != nil)
finishTask(0);
}
else if ([message isKindOfClass:[TGBridgeSubscriptionList class]])
{
TGBridgeSubscriptionList *list = (TGBridgeSubscriptionList *)message;
for (TGBridgeSubscription *subscription in list.subscriptions)
[self _createSubscription:subscription replyHandler:nil finishTask:nil completion:nil];
//TGLog(@"[BridgeServer] Received subscription list, applying");
if (replyHandler != nil)
replyHandler([NSData data]);
if (finishTask != nil)
finishTask(4.0);
if (completion != nil)
completion();
}
else if ([message isKindOfClass:[TGBridgePing class]])
{
TGBridgePing *ping = (TGBridgePing *)message;
if (_sessionId != ping.sessionId)
{
//TGLog(@"[BridgeServer] Session id mismatch");
if (_sessionId != 0)
{
//TGLog(@"[BridgeServer] Halt all active subscriptions");
[_signalManager haltAllSignals];
os_unfair_lock_lock(&_outgoingQueueLock);
[_outgoingMessageQueue removeAllObjects];
os_unfair_lock_unlock(&_outgoingQueueLock);
}
_sessionId = ping.sessionId;
if (self.session.isReachable)
[self _requestSubscriptionList];
else
_requestSubscriptionList = true;
}
else
{
if (_requestSubscriptionList)
{
_requestSubscriptionList = false;
[self _requestSubscriptionList];
}
[self _sendQueuedResponses];
if (replyHandler != nil)
replyHandler([NSData data]);
}
if (completion != nil)
completion();
if (finishTask != nil)
finishTask(4.0);
}
else
{
if (completion != nil)
completion();
if (finishTask != nil)
finishTask(1.0);
}
}
- (void)_createSubscription:(TGBridgeSubscription *)subscription replyHandler:(void (^)(NSData *))replyHandler finishTask:(void (^)(NSTimeInterval))finishTask completion:(void (^)(void))completion
{
SSignal *subscriptionHandler = _handler(subscription);
if (replyHandler != nil)
{
os_unfair_lock_lock(&_replyHandlerMapLock);
_replyHandlerMap[@(subscription.identifier)] = replyHandler;
os_unfair_lock_unlock(&_replyHandlerMapLock);
}
if (subscriptionHandler != nil)
{
[_signalManager startSignalForKey:[NSString stringWithFormat:@"%lld", subscription.identifier] producer:^SSignal *
{
STimer *timer = [[STimer alloc] initWithTimeout:2.0 repeat:false completion:^(__unused STimer *timer)
{
os_unfair_lock_lock(&_replyHandlerMapLock);
void (^reply)(NSData *) = _replyHandlerMap[@(subscription.identifier)];
if (reply == nil)
{
os_unfair_lock_unlock(&_replyHandlerMapLock);
if (finishTask != nil)
finishTask(2.0);
return;
}
reply([NSData data]);
[_replyHandlerMap removeObjectForKey:@(subscription.identifier)];
os_unfair_lock_unlock(&_replyHandlerMapLock);
if (finishTask != nil)
finishTask(4.0);
//TGLog(@"[BridgeServer]: subscription 0x%x hit 2.0s timeout, releasing reply handler", subscription.identifier);
} queue:[SQueue mainQueue]];
[timer start];
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(__unused SSubscriber *subscriber)
{
return [subscriptionHandler startWithNext:^(id next)
{
[timer invalidate];
[self _responseToSubscription:subscription message:next type:TGBridgeResponseTypeNext completion:completion];
if (finishTask != nil)
finishTask(4.0);
} error:^(id error)
{
[timer invalidate];
[self _responseToSubscription:subscription message:error type:TGBridgeResponseTypeFailed completion:completion];
if (finishTask != nil)
finishTask(4.0);
} completed:^
{
[timer invalidate];
[self _responseToSubscription:subscription message:nil type:TGBridgeResponseTypeCompleted completion:completion];
if (finishTask != nil)
finishTask(4.0);
}];
}];
}];
}
else
{
os_unfair_lock_lock(&_replyHandlerMapLock);
void (^reply)(NSData *) = _replyHandlerMap[@(subscription.identifier)];
if (reply == nil)
{
os_unfair_lock_unlock(&_replyHandlerMapLock);
if (finishTask != nil)
finishTask(2.0);
return;
}
reply([NSData data]);
[_replyHandlerMap removeObjectForKey:@(subscription.identifier)];
os_unfair_lock_unlock(&_replyHandlerMapLock);
if (finishTask != nil)
finishTask(2.0);
}
}
- (void)_responseToSubscription:(TGBridgeSubscription *)subscription message:(id<NSCoding>)message type:(TGBridgeResponseType)type completion:(void (^)(void))completion
{
TGBridgeResponse *response = nil;
switch (type)
{
case TGBridgeResponseTypeNext:
response = [TGBridgeResponse single:message forSubscription:subscription];
break;
case TGBridgeResponseTypeFailed:
response = [TGBridgeResponse fail:message forSubscription:subscription];
break;
case TGBridgeResponseTypeCompleted:
response = [TGBridgeResponse completeForSubscription:subscription];
break;
default:
break;
}
os_unfair_lock_lock(&_replyHandlerMapLock);
void (^reply)(NSData *) = _replyHandlerMap[@(subscription.identifier)];
if (reply != nil)
[_replyHandlerMap removeObjectForKey:@(subscription.identifier)];
os_unfair_lock_unlock(&_replyHandlerMapLock);
if (_processingNotification)
{
[self _enqueueResponse:response forSubscription:subscription];
if (completion != nil)
completion();
return;
}
NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:response];
if (reply != nil && messageData.length < 64000)
{
reply(messageData);
if (completion != nil)
completion();
}
else
{
if (reply != nil)
reply([NSData data]);
if (self.session.isReachable)
{
[self.session sendMessageData:messageData replyHandler:nil errorHandler:^(NSError *error)
{
//if (error != nil)
// TGLog(@"[BridgeServer]: send response for subscription %lld failed with error %@", subscription.identifier, error);
}];
}
else
{
//TGLog(@"[BridgeServer]: client out of reach, queueing response for subscription %lld", subscription.identifier);
[self _enqueueResponse:response forSubscription:subscription];
}
if (completion != nil)
completion();
}
}
- (void)_enqueueResponse:(TGBridgeResponse *)response forSubscription:(TGBridgeSubscription *)subscription
{
os_unfair_lock_lock(&_outgoingQueueLock);
NSMutableArray *updatedResponses = (_outgoingMessageQueue != nil) ? [_outgoingMessageQueue mutableCopy] : [[NSMutableArray alloc] init];
if (subscription.dropPreviouslyQueued)
{
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init];
[updatedResponses enumerateObjectsUsingBlock:^(TGBridgeResponse *queuedResponse, NSUInteger index, __unused BOOL *stop)
{
if (queuedResponse.subscriptionIdentifier == subscription.identifier)
[indexSet addIndex:index];
}];
[updatedResponses removeObjectsAtIndexes:indexSet];
}
[updatedResponses addObject:response];
_outgoingMessageQueue = updatedResponses;
os_unfair_lock_unlock(&_outgoingQueueLock);
}
- (void)_sendQueuedResponses
{
if (_processingNotification)
return;
os_unfair_lock_lock(&_outgoingQueueLock);
if (_outgoingMessageQueue.count > 0)
{
//TGLog(@"[BridgeServer] Sending queued responses");
for (TGBridgeResponse *response in _outgoingMessageQueue)
{
NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:response];
[self.session sendMessageData:messageData replyHandler:nil errorHandler:nil];
}
[_outgoingMessageQueue removeAllObjects];
}
os_unfair_lock_unlock(&_outgoingQueueLock);
}
- (void)_requestSubscriptionList
{
TGBridgeSubscriptionListRequest *request = [[TGBridgeSubscriptionListRequest alloc] initWithSessionId:_sessionId];
NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:request];
[self.session sendMessageData:messageData replyHandler:nil errorHandler:nil];
}
- (void)sendFileWithURL:(NSURL *)url metadata:(NSDictionary *)metadata asMessageData:(bool)asMessageData
{
//TGLog(@"[BridgeServer] Sent file with metadata %@", metadata);
if (asMessageData && self.session.isReachable) {
NSData *data = [NSData dataWithContentsOfURL:url];
[self sendFileWithData:data metadata:metadata errorHandler:^{
[self.session transferFile:url metadata:metadata];
}];
} else {
[self.session transferFile:url metadata:metadata];
}
}
- (void)sendFileWithData:(NSData *)data metadata:(NSDictionary *)metadata errorHandler:(void (^)(void))errorHandler
{
TGBridgeFile *file = [[TGBridgeFile alloc] initWithData:data metadata:metadata];
NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:file];
[self.session sendMessageData:messageData replyHandler:nil errorHandler:^(NSError *error) {
if (errorHandler != nil)
errorHandler();
}];
}
#pragma mark - Tasks
- (id<SDisposable>)beginTask
{
int64_t randomId = 0;
arc4random_buf(&randomId, 8);
NSNumber *taskId = @(randomId);
_runningTasks[taskId] = @true;
[_hasRunningTasks set:[SSignal single:@{@"version": @(_tasksVersion++), @"running": @true}]];
SBlockDisposable *taskDisposable = [[SBlockDisposable alloc] initWithBlock:^{
[_runningTasks removeObjectForKey:taskId];
[_hasRunningTasks set:[SSignal single:@{@"version": @(_tasksVersion++), @"running": @(_runningTasks.count > 0)}]];
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((4.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self dispatch:^{
[taskDisposable dispose];
}];
});
return taskDisposable;
}
#pragma mark - Session Delegate
- (void)handleReceivedData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler
{
if (messageData.length == 0)
{
if (replyHandler != nil)
replyHandler([NSData data]);
return;
}
// __block UIBackgroundTaskIdentifier backgroundTask;
// backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
// {
// if (replyHandler != nil)
// replyHandler([NSData data]);
// [[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
// }];
//
[self handleMessageData:messageData task:[self beginTask] replyHandler:replyHandler completion:^{}];
}
- (void)session:(WCSession *)__unused session didReceiveMessageData:(NSData *)messageData
{
[self dispatch:^{
[self handleReceivedData:messageData replyHandler:nil];
}];
}
- (void)session:(WCSession *)__unused session didReceiveMessageData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler
{
[self dispatch:^{
[self handleReceivedData:messageData replyHandler:replyHandler];
}];
}
- (void)session:(WCSession *)__unused session didReceiveFile:(WCSessionFile *)file
{
NSDictionary *metadata = file.metadata;
if (metadata == nil || ![metadata[TGBridgeIncomingFileTypeKey] isEqualToString:TGBridgeIncomingFileTypeAudio])
return;
NSError *error;
NSURL *tempURL = [NSURL URLWithString:file.fileURL.lastPathComponent relativeToURL:self.temporaryFilesURL];
[[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryFilesURL.path withIntermediateDirectories:true attributes:nil error:&error];
[[NSFileManager defaultManager] moveItemAtURL:file.fileURL toURL:tempURL error:&error];
[self dispatch:^{
_fileHandler(tempURL.path, file.metadata);
}];
}
- (void)session:(WCSession *)__unused session didFinishFileTransfer:(WCSessionFileTransfer *)__unused fileTransfer error:(NSError *)__unused error
{
}
- (void)session:(nonnull WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error {
}
- (void)sessionDidBecomeInactive:(nonnull WCSession *)session {
}
- (void)sessionDidDeactivate:(nonnull WCSession *)session {
}
- (void)sessionWatchStateDidChange:(WCSession *)session
{
[self dispatch:^{
if (session.isWatchAppInstalled)
[self pushContext];
_appInstalled.sink(@(session.isWatchAppInstalled));
}];
}
- (void)sessionReachabilityDidChange:(WCSession *)session
{
NSLog(@"[TGBridgeServer] Reachability changed: %d", session.isReachable);
}
#pragma mark -
- (NSInteger)wakeupNetwork
{
return 0;
}
- (void)suspendNetworkIfReady:(NSInteger)token
{
}
#pragma mark -
- (WCSession *)session
{
return [WCSession defaultSession];
}
@end
@interface TGBridgeSignalManager()
{
os_unfair_lock _lock;
NSMutableDictionary *_disposables;
}
@end
@implementation TGBridgeSignalManager
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_disposables = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc
{
NSArray *disposables = nil;
os_unfair_lock_lock(&_lock);
disposables = [_disposables allValues];
os_unfair_lock_unlock(&_lock);
for (id<SDisposable> disposable in disposables)
{
[disposable dispose];
}
}
- (bool)startSignalForKey:(NSString *)key producer:(SSignal *(^)())producer
{
if (key == nil)
return false;
bool produce = false;
os_unfair_lock_lock(&_lock);
if (_disposables[key] == nil)
{
_disposables[key] = [[SMetaDisposable alloc] init];
produce = true;
}
os_unfair_lock_unlock(&_lock);
if (produce)
{
__weak TGBridgeSignalManager *weakSelf = self;
id<SDisposable> disposable = [producer() startWithNext:nil error:^(__unused id error)
{
__strong TGBridgeSignalManager *strongSelf = weakSelf;
if (strongSelf != nil)
{
os_unfair_lock_lock(&strongSelf->_lock);
[strongSelf->_disposables removeObjectForKey:key];
os_unfair_lock_unlock(&strongSelf->_lock);
}
} completed:^
{
__strong TGBridgeSignalManager *strongSelf = weakSelf;
if (strongSelf != nil)
{
os_unfair_lock_lock(&strongSelf->_lock);
[strongSelf->_disposables removeObjectForKey:key];
os_unfair_lock_unlock(&strongSelf->_lock);
}
}];
os_unfair_lock_lock(&_lock);
[(SMetaDisposable *)_disposables[key] setDisposable:disposable];
os_unfair_lock_unlock(&_lock);
}
return produce;
}
- (void)haltSignalForKey:(NSString *)key
{
if (key == nil)
return;
os_unfair_lock_lock(&_lock);
if (_disposables[key] != nil)
{
[_disposables[key] dispose];
[_disposables removeObjectForKey:key];
}
os_unfair_lock_unlock(&_lock);
}
- (void)haltAllSignals
{
os_unfair_lock_lock(&_lock);
for (NSObject <SDisposable> *disposable in _disposables.allValues)
[disposable dispose];
[_disposables removeAllObjects];
os_unfair_lock_unlock(&_lock);
}
@end
@@ -0,0 +1,553 @@
import Foundation
import Postbox
import TelegramCore
import WatchCommon
import TelegramPresentationData
import LegacyUI
import PhoneNumberFormat
private func legacyImageLocationUri(resource: MediaResource) -> String? {
if let resource = resource as? CloudPeerPhotoSizeMediaResource {
return resource.id.stringRepresentation
}
return nil
}
func makePeerIdFromBridgeIdentifier(_ identifier: Int64) -> PeerId? {
if identifier < 0 && identifier > Int32.min {
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(-identifier))
} else if identifier < Int64(Int32.min) * 2 && identifier > Int64(Int32.min) * 3 {
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(Int64(Int32.min) &* 2 &- identifier))
// MARK: Swiftgram
// supports 52 bits
} else if identifier > 0 && identifier < (1 << 52) {
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(identifier))
} else {
return nil
}
}
func makeBridgeIdentifier(_ peerId: PeerId) -> Int64 {
switch peerId.namespace {
case Namespaces.Peer.CloudGroup:
return -Int64(peerId.id._internalGetInt64Value())
case Namespaces.Peer.CloudChannel:
return Int64(Int32.min) * 2 - Int64(peerId.id._internalGetInt64Value())
default:
return Int64(peerId.id._internalGetInt64Value())
}
}
func makeBridgeDeliveryState(_ message: Message?) -> TGBridgeMessageDeliveryState {
if let message = message {
if message.flags.contains(.Failed) {
return .failed
}
else if message.flags.contains(.Sending) {
return .pending
}
}
return .delivered
}
private func makeBridgeImage(_ image: TelegramMediaImage?) -> TGBridgeImageMediaAttachment? {
if let image = image, let representation = largestImageRepresentation(image.representations) {
let bridgeImage = TGBridgeImageMediaAttachment()
bridgeImage.imageId = image.imageId.id
bridgeImage.dimensions = representation.dimensions.cgSize
return bridgeImage
} else {
return nil
}
}
func makeBridgeDocument(_ file: TelegramMediaFile?) -> TGBridgeDocumentMediaAttachment? {
if let file = file {
let bridgeDocument = TGBridgeDocumentMediaAttachment()
bridgeDocument.documentId = file.fileId.id
bridgeDocument.fileSize = Int32(file.size ?? 0)
for attribute in file.attributes {
switch attribute {
case let .FileName(fileName):
bridgeDocument.fileName = fileName
case .Animated:
bridgeDocument.isAnimated = true
case let .ImageSize(size):
bridgeDocument.imageSize = NSValue(cgSize: size.cgSize)
case let .Sticker(displayText, packReference, _):
bridgeDocument.isSticker = true
bridgeDocument.stickerAlt = displayText
if let packReference = packReference, case let .id(id, accessHash) = packReference {
bridgeDocument.stickerPackId = id
bridgeDocument.stickerPackAccessHash = accessHash
}
case let .Audio(_, duration, title, performer, _):
bridgeDocument.duration = Int32(clamping: duration)
bridgeDocument.title = title
bridgeDocument.performer = performer
default:
break
}
}
return bridgeDocument
}
return nil
}
func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: Peer? = nil, filterUnsupportedActions: Bool = true) -> [TGBridgeMediaAttachment] {
var bridgeMedia: [TGBridgeMediaAttachment] = []
if let forward = message.forwardInfo {
let bridgeForward = TGBridgeForwardedMessageMediaAttachment()
bridgeForward.peerId = forward.author.flatMap({ makeBridgeIdentifier($0.id) }) ?? 0
if let sourceMessageId = forward.sourceMessageId {
bridgeForward.mid = sourceMessageId.id
}
bridgeForward.date = forward.date
bridgeMedia.append(bridgeForward)
}
for attribute in message.attributes {
if let reply = attribute as? ReplyMessageAttribute, let replyMessage = message.associatedMessages[reply.messageId] {
let bridgeReply = TGBridgeReplyMessageMediaAttachment()
bridgeReply.mid = reply.messageId.id
bridgeReply.message = makeBridgeMessage(replyMessage, strings: strings)
bridgeMedia.append(bridgeReply)
} else if let entities = attribute as? TextEntitiesMessageAttribute {
var bridgeEntities: [Any] = []
for entity in entities.entities {
var bridgeEntity: TGBridgeMessageEntity? = nil
switch entity.type {
case .Url:
bridgeEntity = TGBridgeMessageEntityUrl()
bridgeEntity?.range = NSRange(entity.range)
case .TextUrl:
bridgeEntity = TGBridgeMessageEntityTextUrl()
bridgeEntity?.range = NSRange(entity.range)
case .Email:
bridgeEntity = TGBridgeMessageEntityEmail()
bridgeEntity?.range = NSRange(entity.range)
case .Mention:
bridgeEntity = TGBridgeMessageEntityMention()
bridgeEntity?.range = NSRange(entity.range)
case .Hashtag:
bridgeEntity = TGBridgeMessageEntityHashtag()
bridgeEntity?.range = NSRange(entity.range)
case .BotCommand:
bridgeEntity = TGBridgeMessageEntityBotCommand()
bridgeEntity?.range = NSRange(entity.range)
case .Bold:
bridgeEntity = TGBridgeMessageEntityBold()
bridgeEntity?.range = NSRange(entity.range)
case .Italic:
bridgeEntity = TGBridgeMessageEntityItalic()
bridgeEntity?.range = NSRange(entity.range)
case .Code:
bridgeEntity = TGBridgeMessageEntityCode()
bridgeEntity?.range = NSRange(entity.range)
case .Pre:
bridgeEntity = TGBridgeMessageEntityPre()
bridgeEntity?.range = NSRange(entity.range)
default:
break
}
if let bridgeEntity = bridgeEntity {
bridgeEntities.append(bridgeEntity)
}
}
if !bridgeEntities.isEmpty {
let attachment = TGBridgeMessageEntitiesAttachment()
attachment.entities = bridgeEntities
bridgeMedia.append(attachment)
}
}
}
for m in message.media {
if let image = m as? TelegramMediaImage, let bridgeImage = makeBridgeImage(image) {
bridgeMedia.append(bridgeImage)
}
else if let file = m as? TelegramMediaFile {
if file.isVideo {
let bridgeVideo = TGBridgeVideoMediaAttachment()
bridgeVideo.videoId = file.fileId.id
for attribute in file.attributes {
switch attribute {
case let .Video(duration, size, flags, _, _, _):
bridgeVideo.duration = Int32(duration)
bridgeVideo.dimensions = size.cgSize
bridgeVideo.round = flags.contains(.instantRoundVideo)
default:
break
}
}
bridgeMedia.append(bridgeVideo)
} else if file.isVoice {
let bridgeAudio = TGBridgeAudioMediaAttachment()
bridgeAudio.audioId = file.fileId.id
bridgeAudio.fileSize = Int32(clamping: file.size ?? 0)
for attribute in file.attributes {
switch attribute {
case let .Audio(_, duration, _, _, _):
bridgeAudio.duration = Int32(clamping: duration)
default:
break
}
}
bridgeMedia.append(bridgeAudio)
} else if let bridgeDocument = makeBridgeDocument(file) {
bridgeMedia.append(bridgeDocument)
}
} else if let action = m as? TelegramMediaAction {
var bridgeAction: TGBridgeActionMediaAttachment? = nil
var consumed = false
switch action.action {
case let .groupCreated(title):
bridgeAction = TGBridgeActionMediaAttachment()
if chatPeer is TelegramGroup {
bridgeAction?.actionType = .createChat
bridgeAction?.actionData = ["title": title]
} else if let channel = chatPeer as? TelegramChannel {
if case .group = channel.info {
bridgeAction?.actionType = .createChat
bridgeAction?.actionData = ["title": title]
} else {
bridgeAction?.actionType = .channelCreated
}
}
case let .phoneCall(_, discardReason, _, _):
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
let incoming = message.flags.contains(.Incoming)
var compactTitle: String = ""
var subTitle: String = ""
if let discardReason = discardReason {
switch discardReason {
case .busy, .disconnect:
compactTitle = strings.Notification_CallCanceled
subTitle = strings.Notification_CallCanceledShort
case .missed:
compactTitle = incoming ? strings.Notification_CallMissed : strings.Notification_CallCanceled
subTitle = incoming ? strings.Notification_CallMissedShort : strings.Notification_CallCanceledShort
case .hangup:
break
}
}
if compactTitle.isEmpty {
compactTitle = incoming ? strings.Notification_CallIncoming : strings.Notification_CallOutgoing
subTitle = incoming ? strings.Notification_CallIncomingShort : strings.Notification_CallOutgoingShort
}
bridgeAttachment.compactTitle = compactTitle
bridgeAttachment.title = strings.Watch_Message_Call
bridgeAttachment.subtitle = subTitle
bridgeMedia.append(bridgeAttachment)
consumed = true
default:
break
}
if let bridgeAction = bridgeAction {
bridgeMedia.append(bridgeAction)
} else if !consumed && !filterUnsupportedActions {
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
bridgeAttachment.compactTitle = ""
bridgeAttachment.title = ""
bridgeMedia.append(bridgeAttachment)
}
} else if let poll = m as? TelegramMediaPoll {
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
bridgeAttachment.compactTitle = strings.Watch_Message_Poll
bridgeAttachment.title = strings.Watch_Message_Poll
bridgeAttachment.subtitle = poll.text
bridgeMedia.append(bridgeAttachment)
} else if let contact = m as? TelegramMediaContact {
let bridgeContact = TGBridgeContactMediaAttachment()
if let peerId = contact.peerId {
bridgeContact.uid = Int32(clamping: makeBridgeIdentifier(peerId))
}
bridgeContact.firstName = contact.firstName
bridgeContact.lastName = contact.lastName
bridgeContact.phoneNumber = contact.phoneNumber
bridgeContact.prettyPhoneNumber = formatPhoneNumber(contact.phoneNumber)
bridgeMedia.append(bridgeContact)
} else if let map = m as? TelegramMediaMap {
let bridgeLocation = TGBridgeLocationMediaAttachment()
bridgeLocation.latitude = map.latitude
bridgeLocation.longitude = map.longitude
if let venue = map.venue {
let bridgeVenue = TGBridgeVenueAttachment()
bridgeVenue.title = venue.title
bridgeVenue.address = venue.address
bridgeVenue.provider = venue.provider
bridgeVenue.venueId = venue.id
bridgeLocation.venue = bridgeVenue
}
bridgeMedia.append(bridgeLocation)
} else if let webpage = m as? TelegramMediaWebpage {
if case let .Loaded(content) = webpage.content {
let bridgeWebpage = TGBridgeWebPageMediaAttachment()
bridgeWebpage.webPageId = webpage.id?.id ?? 0
bridgeWebpage.url = content.url
bridgeWebpage.displayUrl = content.displayUrl
bridgeWebpage.pageType = content.type
bridgeWebpage.siteName = content.websiteName
bridgeWebpage.title = content.title
bridgeWebpage.pageDescription = content.text
bridgeWebpage.photo = makeBridgeImage(content.image)
bridgeWebpage.embedUrl = content.embedUrl
bridgeWebpage.embedType = content.embedType
bridgeWebpage.embedSize = content.embedSize?.cgSize ?? CGSize()
bridgeWebpage.duration = NSNumber(integerLiteral: content.duration ?? 0)
bridgeWebpage.author = content.author
bridgeMedia.append(bridgeWebpage)
}
} else if let game = m as? TelegramMediaGame {
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
bridgeAttachment.compactTitle = game.title
bridgeAttachment.title = strings.Watch_Message_Game
bridgeAttachment.subtitle = game.title
bridgeMedia.append(bridgeAttachment)
} else if let invoice = m as? TelegramMediaInvoice {
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
bridgeAttachment.compactTitle = invoice.title
bridgeAttachment.title = strings.Watch_Message_Invoice
bridgeAttachment.subtitle = invoice.title
bridgeMedia.append(bridgeAttachment)
} else if let _ = m as? TelegramMediaUnsupported {
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
bridgeAttachment.compactTitle = strings.Watch_Message_Unsupported
bridgeAttachment.title = strings.Watch_Message_Unsupported
bridgeAttachment.subtitle = ""
bridgeMedia.append(bridgeAttachment)
}
}
return bridgeMedia
}
func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TGBridgeChat, [Int64 : TGBridgeUser])? {
if case let .MessageEntry(entryData) = entry {
let index = entryData.index
let messages = entryData.messages
let readState = entryData.readState
let renderedPeer = entryData.renderedPeer
let hasFailed = entryData.hasFailed
guard index.messageIndex.id.peerId.namespace != Namespaces.Peer.SecretChat else {
return nil
}
let message = messages.last
let (bridgeChat, participants) = makeBridgeChat(renderedPeer.peer)
bridgeChat.date = TimeInterval(index.messageIndex.timestamp)
if let message = message {
if let author = message.author {
bridgeChat.fromUid = Int32(clamping: makeBridgeIdentifier(author.id))
}
bridgeChat.text = message.text
bridgeChat.outgoing = !message.flags.contains(.Incoming)
bridgeChat.deliveryState = makeBridgeDeliveryState(message)
bridgeChat.deliveryError = hasFailed
bridgeChat.media = makeBridgeMedia(message: message, strings: strings, filterUnsupportedActions: false)
}
bridgeChat.unread = readState?.state.isUnread ?? false
bridgeChat.unreadCount = readState?.state.count ?? 0
var bridgeUsers: [Int64 : TGBridgeUser] = participants
if let bridgeUser = makeBridgeUser(message?.author, presence: nil) {
bridgeUsers[bridgeUser.identifier] = bridgeUser
}
if let user = renderedPeer.peer as? TelegramUser, user.id != message?.author?.id, let bridgeUser = makeBridgeUser(user, presence: nil) {
bridgeUsers[bridgeUser.identifier] = bridgeUser
}
return (bridgeChat, bridgeUsers)
}
return nil
}
func makeBridgeChat(_ peer: Peer?, view: PeerView? = nil) -> (TGBridgeChat, [Int64 : TGBridgeUser]) {
let bridgeChat = TGBridgeChat()
var bridgeUsers: [Int64 : TGBridgeUser] = [:]
if let peer = peer {
bridgeChat.identifier = makeBridgeIdentifier(peer.id)
bridgeChat.userName = peer.addressName
}
if let group = peer as? TelegramGroup {
bridgeChat.isGroup = true
bridgeChat.groupTitle = group.title
bridgeChat.participantsCount = Int32(clamping: group.participantCount)
if let representation = smallestImageRepresentation(group.photo) {
bridgeChat.groupPhotoSmall = legacyImageLocationUri(resource: representation.resource)
}
if let representation = largestImageRepresentation(group.photo) {
bridgeChat.groupPhotoBig = legacyImageLocationUri(resource: representation.resource)
}
if let view = view, let cachedData = view.cachedData as? CachedGroupData, let participants = cachedData.participants {
bridgeChat.participantsCount = Int32(clamping: participants.participants.count)
var bridgeParticipants: [Int64] = []
for participant in participants.participants {
if let user = view.peers[participant.peerId], let bridgeUser = makeBridgeUser(user, presence: view.peerPresences[user.id]) {
bridgeParticipants.append(bridgeUser.identifier)
bridgeUsers[bridgeUser.identifier] = bridgeUser
}
}
bridgeChat.participants = bridgeParticipants
}
} else if let channel = peer as? TelegramChannel {
bridgeChat.isChannel = true
bridgeChat.groupTitle = channel.title
if case .group = channel.info {
bridgeChat.isChannelGroup = true
}
bridgeChat.verified = channel.flags.contains(.isVerified)
if let representation = smallestImageRepresentation(channel.photo) {
bridgeChat.groupPhotoSmall = legacyImageLocationUri(resource: representation.resource)
}
if let representation = largestImageRepresentation(channel.photo) {
bridgeChat.groupPhotoBig = legacyImageLocationUri(resource: representation.resource)
}
if let view = view, let cachedData = view.cachedData as? CachedChannelData {
bridgeChat.about = cachedData.about
}
}
// _hasLeftGroup = [aDecoder decodeBoolForKey:TGBridgeChatHasLeftGroupKey];
// _isKickedFromGroup = [aDecoder decodeBoolForKey:TGBridgeChatIsKickedFromGroupKey];
return (bridgeChat, bridgeUsers)
}
func makeBridgeUser(_ peer: Peer?, presence: PeerPresence? = nil, cachedData: CachedPeerData? = nil) -> TGBridgeUser? {
if let user = peer as? TelegramUser {
let bridgeUser = TGBridgeUser()
bridgeUser.identifier = makeBridgeIdentifier(user.id)
bridgeUser.firstName = user.firstName
bridgeUser.lastName = user.lastName
bridgeUser.userName = user.addressName
bridgeUser.phoneNumber = user.phone
if let phone = user.phone {
bridgeUser.prettyPhoneNumber = formatPhoneNumber(phone)
}
if let presence = presence as? TelegramUserPresence {
let timestamp = 0
switch presence.status {
case .recently:
bridgeUser.lastSeen = -2
case .lastWeek:
bridgeUser.lastSeen = -3
case .lastMonth:
bridgeUser.lastSeen = -4
case .none:
bridgeUser.lastSeen = -5
case let .present(statusTimestamp):
if statusTimestamp > timestamp {
bridgeUser.online = true
}
bridgeUser.lastSeen = TimeInterval(statusTimestamp)
}
}
if let cachedData = cachedData as? CachedUserData {
bridgeUser.about = cachedData.about
}
if let representation = smallestImageRepresentation(user.photo) {
bridgeUser.photoSmall = legacyImageLocationUri(resource: representation.resource)
}
if let representation = largestImageRepresentation(user.photo) {
bridgeUser.photoBig = legacyImageLocationUri(resource: representation.resource)
}
if user.botInfo != nil {
bridgeUser.kind = .bot
bridgeUser.botKind = .generic
}
bridgeUser.verified = user.flags.contains(.isVerified)
return bridgeUser
} else {
return nil
}
}
func makeBridgePeers(_ message: Message) -> [Int64 : Any] {
var bridgeUsers: [Int64 : Any] = [:]
for (_, peer) in message.peers {
if peer is TelegramUser, let bridgeUser = makeBridgeUser(peer, presence: nil) {
bridgeUsers[bridgeUser.identifier] = bridgeUser
} else if peer is TelegramGroup || peer is TelegramChannel {
let bridgeChat = makeBridgeChat(peer)
bridgeUsers[bridgeChat.0.identifier] = bridgeChat.0
}
}
if let author = message.author, let bridgeUser = makeBridgeUser(author) {
bridgeUsers[bridgeUser.identifier] = bridgeUser
}
return bridgeUsers
}
func makeBridgeMessage(_ entry: MessageHistoryEntry, strings: PresentationStrings) -> (TGBridgeMessage, [Int64 : TGBridgeUser])? {
guard let bridgeMessage = makeBridgeMessage(entry.message, strings: strings) else {
return nil
}
if entry.message.id.namespace == Namespaces.Message.Local && !entry.message.flags.contains(.Failed) {
return nil
}
bridgeMessage.unread = !entry.isRead
var bridgeUsers: [Int64 : TGBridgeUser] = [:]
if let bridgeUser = makeBridgeUser(entry.message.author, presence: nil) {
bridgeUsers[bridgeUser.identifier] = bridgeUser
}
for (_, peer) in entry.message.peers {
if let bridgeUser = makeBridgeUser(peer, presence: nil) {
bridgeUsers[bridgeUser.identifier] = bridgeUser
}
}
return (bridgeMessage, bridgeUsers)
}
func makeBridgeMessage(_ message: Message, strings: PresentationStrings, chatPeer: Peer? = nil) -> TGBridgeMessage? {
var chatPeer = chatPeer
if chatPeer == nil {
chatPeer = message.peers[message.id.peerId]
}
let bridgeMessage = TGBridgeMessage()
bridgeMessage.identifier = message.id.id
bridgeMessage.date = TimeInterval(message.timestamp)
bridgeMessage.randomId = message.globallyUniqueId ?? 0
// bridgeMessage.unread = false
bridgeMessage.outgoing = !message.flags.contains(.Incoming)
if let author = message.author {
bridgeMessage.fromUid = makeBridgeIdentifier(author.id)
}
bridgeMessage.toUid = makeBridgeIdentifier(message.id.peerId)
bridgeMessage.cid = makeBridgeIdentifier(message.id.peerId)
bridgeMessage.text = message.text
bridgeMessage.deliveryState = makeBridgeDeliveryState(message)
bridgeMessage.media = makeBridgeMedia(message: message, strings: strings, chatPeer: chatPeer)
return bridgeMessage
}
func makeVenue(from bridgeVenue: TGBridgeVenueAttachment?) -> MapVenue? {
if let bridgeVenue = bridgeVenue {
return MapVenue(title: bridgeVenue.title, address: bridgeVenue.address, provider: bridgeVenue.provider, id: bridgeVenue.venueId, type: "")
}
return nil
}
func makeBridgeLocationVenue(_ contextResult: ChatContextResultMessage) -> TGBridgeLocationVenue? {
if case let .mapLocation(mapMedia, _) = contextResult {
let bridgeVenue = TGBridgeLocationVenue()
bridgeVenue.coordinate = CLLocationCoordinate2D(latitude: mapMedia.latitude, longitude: mapMedia.longitude)
if let venue = mapMedia.venue {
bridgeVenue.name = venue.title
bridgeVenue.address = venue.address
bridgeVenue.provider = venue.provider
bridgeVenue.identifier = venue.id
}
return bridgeVenue
}
return nil
}
@@ -0,0 +1,219 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import WatchCommon
import SSignalKit
import TelegramUIPreferences
import AccountContext
import WatchBridgeImpl
public final class WatchCommunicationManagerContext {
public let context: AccountContext
public init(context: AccountContext) {
self.context = context
}
}
public final class WatchManagerArguments {
public let appInstalled: Signal<Bool, NoError>
public let navigateToMessageRequested: Signal<MessageId, NoError>
public let runningTasks: Signal<WatchRunningTasks?, NoError>
public init(appInstalled: Signal<Bool, NoError>, navigateToMessageRequested: Signal<MessageId, NoError>, runningTasks: Signal<WatchRunningTasks?, NoError>) {
self.appInstalled = appInstalled
self.navigateToMessageRequested = navigateToMessageRequested
self.runningTasks = runningTasks
}
}
public final class WatchCommunicationManager {
private let queue: Queue
private let allowBackgroundTimeExtension: (Double) -> Void
private var server: TGBridgeServer!
private let contextDisposable = MetaDisposable()
private let presetsDisposable = MetaDisposable()
let accountContext = Promise<AccountContext?>(nil)
private let presets = Promise<WatchPresetSettings?>(nil)
private let navigateToMessagePipe = ValuePipe<MessageId>()
public init(queue: Queue, context: Signal<WatchCommunicationManagerContext?, NoError>, allowBackgroundTimeExtension: @escaping (Double) -> Void) {
self.queue = queue
self.allowBackgroundTimeExtension = allowBackgroundTimeExtension
let handlers = allWatchRequestHandlers.reduce([String : AnyClass]()) { (map, handler) -> [String : AnyClass] in
var map = map
if let handler = handler as? WatchRequestHandler.Type {
for case let subscription as TGBridgeSubscription.Type in handler.handledSubscriptions {
if let name = subscription.subscriptionName() {
map[name] = handler
}
}
}
return map
}
self.server = TGBridgeServer(handler: { [weak self] subscription -> SSignal? in
guard let strongSelf = self, let subscription = subscription, let handler = handlers[subscription.name] as? WatchRequestHandler.Type else {
return nil
}
return handler.handle(subscription: subscription, manager: strongSelf)
}, fileHandler: { [weak self] path, metadata in
guard let strongSelf = self, let path = path, let metadata = metadata as? [String : Any] else {
return
}
if metadata[TGBridgeIncomingFileTypeKey] as? String == TGBridgeIncomingFileTypeAudio {
let _ = WatchAudioHandler.handleFile(path: path, metadata: metadata, manager: strongSelf).start()
}
}, dispatchOnQueue: { [weak self] block in
if let strongSelf = self {
strongSelf.queue.justDispatch(block)
}
}, logFunction: { value in
if let value = value {
Logger.shared.log("WatchBridge", value)
}
}, allowBackgroundTimeExtension: {
allowBackgroundTimeExtension(4.0)
})
self.server.startRunning()
self.contextDisposable.set((combineLatest(self.watchAppInstalled, context |> deliverOn(self.queue))).start(next: { [weak self] appInstalled, appContext in
guard let strongSelf = self, appInstalled else {
return
}
if let context = appContext {
strongSelf.accountContext.set(.single(context.context))
strongSelf.server.setAuthorized(true, userId: context.context.account.peerId.id._internalGetInt64Value())
strongSelf.server.setMicAccessAllowed(false)
strongSelf.server.pushContext()
strongSelf.server.setMicAccessAllowed(true)
strongSelf.server.pushContext()
strongSelf.presets.set(context.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.watchPresetSettings])
|> map({ sharedData -> WatchPresetSettings in
return sharedData.entries[ApplicationSpecificSharedDataKeys.watchPresetSettings]?.get(WatchPresetSettings.self) ?? WatchPresetSettings.defaultSettings
}))
} else {
strongSelf.accountContext.set(.single(nil))
strongSelf.server.setAuthorized(false, userId: 0)
strongSelf.server.pushContext()
strongSelf.presets.set(.single(nil))
}
}))
self.presetsDisposable.set((combineLatest(self.watchAppInstalled, self.presets.get() |> distinctUntilChanged |> deliverOn(self.queue), context |> deliverOn(self.queue))).start(next: { [weak self] appInstalled, presets, appContext in
guard let strongSelf = self, let presets = presets, let context = appContext, appInstalled, let tempPath = strongSelf.watchTemporaryStorePath else {
return
}
let presentationData = context.context.sharedContext.currentPresentationData.with { $0 }
let defaultSuggestions: [String : String] = [
"OK": presentationData.strings.Watch_Suggestion_OK,
"Thanks": presentationData.strings.Watch_Suggestion_Thanks,
"WhatsUp": presentationData.strings.Watch_Suggestion_WhatsUp,
"TalkLater": presentationData.strings.Watch_Suggestion_TalkLater,
"CantTalk": presentationData.strings.Watch_Suggestion_CantTalk,
"HoldOn": presentationData.strings.Watch_Suggestion_HoldOn,
"BRB": presentationData.strings.Watch_Suggestion_BRB,
"OnMyWay": presentationData.strings.Watch_Suggestion_OnMyWay
]
var suggestions: [String : String] = [:]
for (key, defaultValue) in defaultSuggestions {
suggestions[key] = presets.customPresets[key] ?? defaultValue
}
let fileManager = FileManager.default
let presetsFileUrl = URL(fileURLWithPath: tempPath + "/presets.dat")
if fileManager.fileExists(atPath: presetsFileUrl.path) {
try? fileManager.removeItem(atPath: presetsFileUrl.path)
}
let data = try? NSKeyedArchiver.archivedData(withRootObject: suggestions, requiringSecureCoding: false)
try? data?.write(to: presetsFileUrl)
let _ = strongSelf.sendFile(url: presetsFileUrl, metadata: [TGBridgeIncomingFileIdentifierKey: "presets"]).start()
}))
}
deinit {
self.contextDisposable.dispose()
self.presetsDisposable.dispose()
}
public var arguments: WatchManagerArguments {
return WatchManagerArguments(appInstalled: self.watchAppInstalled, navigateToMessageRequested: self.navigateToMessagePipe.signal(), runningTasks: self.runningTasks)
}
public func requestNavigateToMessage(messageId: MessageId) {
self.navigateToMessagePipe.putNext(messageId)
}
private var watchAppInstalled: Signal<Bool, NoError> {
return Signal { subscriber in
let disposable = self.server.watchAppInstalledSignal().start(next: { value in
if let value = value as? NSNumber {
subscriber.putNext(value.boolValue)
}
})
return ActionDisposable {
disposable?.dispose()
}
} |> deliverOn(self.queue)
}
private var runningTasks: Signal<WatchRunningTasks?, NoError> {
return Signal { subscriber in
let disposable = self.server.runningRequestsSignal().start(next: { value in
if let value = value as? Dictionary<String, Any> {
if let running = value["running"] as? Bool, let version = value["version"] as? Int32 {
subscriber.putNext(WatchRunningTasks(running: running, version: version))
}
}
})
return ActionDisposable {
disposable?.dispose()
}
} |> deliverOn(self.queue)
}
public var watchTemporaryStorePath: String? {
return self.server.temporaryFilesURL?.path
}
public func sendFile(url: URL, metadata: Dictionary<AnyHashable, Any>, asMessageData: Bool = false) -> Signal<Void, NoError> {
return Signal { subscriber in
self.server.sendFile(with: url, metadata: metadata, asMessageData: asMessageData)
subscriber.putCompletion()
return EmptyDisposable
} |> runOn(self.queue)
}
public func sendFile(data: Data, metadata: Dictionary<AnyHashable, Any>) -> Signal<Void, NoError> {
return Signal { subscriber in
self.server.sendFile(with: data, metadata: metadata, errorHandler: {})
subscriber.putCompletion()
return EmptyDisposable
} |> runOn(self.queue)
}
}
public func watchCommunicationManager(context: Signal<WatchCommunicationManagerContext?, NoError>, allowBackgroundTimeExtension: @escaping (Double) -> Void) -> Signal<WatchCommunicationManager?, NoError> {
return Signal { subscriber in
let queue = Queue()
queue.async {
if #available(iOSApplicationExtension 9.0, *) {
subscriber.putNext(WatchCommunicationManager(queue: queue, context: context, allowBackgroundTimeExtension: allowBackgroundTimeExtension))
} else {
subscriber.putNext(nil)
}
subscriber.putCompletion()
}
return EmptyDisposable
}
}
@@ -0,0 +1,884 @@
import Foundation
import SwiftSignalKit
import Postbox
import Display
import TelegramCore
import LegacyComponents
import WatchCommon
import TelegramPresentationData
import AvatarNode
import StickerResources
import PhotoResources
import AccountContext
import WatchBridgeAudio
let allWatchRequestHandlers: [AnyClass] = [
WatchChatListHandler.self,
WatchChatMessagesHandler.self,
WatchSendMessageHandler.self,
WatchPeerInfoHandler.self,
WatchMediaHandler.self,
WatchStickersHandler.self,
WatchAudioHandler.self,
WatchLocationHandler.self,
WatchPeerSettingsHandler.self,
WatchContinuationHandler.self,
]
protocol WatchRequestHandler: AnyObject {
static var handledSubscriptions: [Any] { get }
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal
}
final class WatchChatListHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [TGBridgeChatListSubscription.self]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeChatListSubscription {
let limit = Int(args.limit)
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<(ChatListView, PresentationData), NoError> in
if let context = context {
return context.account.viewTracker.tailChatListView(groupId: .root, count: limit)
|> map { chatListView, _ -> (ChatListView, PresentationData) in
return (chatListView, context.sharedContext.currentPresentationData.with { $0 })
}
} else {
return .complete()
}
})
let disposable = signal.start(next: { chatListView, presentationData in
var chats: [TGBridgeChat] = []
var users: [Int64 : TGBridgeUser] = [:]
for entry in chatListView.entries.reversed() {
if let (chat, chatUsers) = makeBridgeChat(entry, strings: presentationData.strings) {
chats.append(chat)
users = users.merging(chatUsers, uniquingKeysWith: { (_, last) in last })
}
}
subscriber.putNext([ TGBridgeChatsArrayKey: chats, TGBridgeUsersDictionaryKey: users ] as [String: Any])
})
return SBlockDisposable {
disposable.dispose()
}
}
} else {
return SSignal.fail(nil)
}
}
}
final class WatchChatMessagesHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [
TGBridgeChatMessageListSubscription.self,
TGBridgeChatMessageSubscription.self,
TGBridgeReadChatMessageListSubscription.self
]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeChatMessageListSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
return SSignal { subscriber in
let limit = Int(args.rangeMessageCount)
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<(MessageHistoryView, Bool, PresentationData), NoError> in
if let context = context {
return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: limit, fixedCombinedReadStates: nil)
|> map { messageHistoryView, _, _ -> (MessageHistoryView, Bool, PresentationData) in
return (messageHistoryView, peerId == context.account.peerId, context.sharedContext.currentPresentationData.with { $0 })
}
} else {
return .complete()
}
})
let disposable = signal.start(next: { messageHistoryView, savedMessages, presentationData in
var messages: [TGBridgeMessage] = []
var users: [Int64 : TGBridgeUser] = [:]
for entry in messageHistoryView.entries.reversed() {
if let (message, messageUsers) = makeBridgeMessage(entry, strings: presentationData.strings) {
messages.append(message)
users = users.merging(messageUsers, uniquingKeysWith: { (_, last) in last })
}
}
subscriber.putNext([ TGBridgeMessagesArrayKey: messages, TGBridgeUsersDictionaryKey: users ] as [String: Any])
})
return SBlockDisposable {
disposable.dispose()
}
}
} else if let args = subscription as? TGBridgeReadChatMessageListSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<Void, NoError> in
if let context = context {
let messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)
return context.engine.messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0))
} else {
return .complete()
}
})
let disposable = signal.start(next: { _ in
subscriber.putNext(true)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
} else if let args = subscription as? TGBridgeChatMessageSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<(Message, PresentationData)?, NoError> in
if let context = context {
let messageSignal = context.engine.messages.downloadMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId))
|> map { message -> (Message, PresentationData)? in
if let message = message {
return (message, context.sharedContext.currentPresentationData.with { $0 })
} else {
return nil
}
}
return messageSignal |> timeout(3.5, queue: Queue.concurrentDefaultQueue(), alternate: .single(nil))
} else {
return .single(nil)
}
})
let disposable = signal.start(next: { messageAndPresentationData in
if let (message, presentationData) = messageAndPresentationData, let bridgeMessage = makeBridgeMessage(message, strings: presentationData.strings) {
let peers = makeBridgePeers(message)
var response: [String : Any] = [TGBridgeMessageKey: bridgeMessage, TGBridgeUsersDictionaryKey: peers]
if peerId.namespace != Namespaces.Peer.CloudUser {
response[TGBridgeChatKey] = peers[makeBridgeIdentifier(peerId)]
}
subscriber.putNext(response)
}
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
}
return SSignal.fail(nil)
}
}
final class WatchSendMessageHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [
TGBridgeSendTextMessageSubscription.self,
TGBridgeSendLocationMessageSubscription.self,
TGBridgeSendStickerMessageSubscription.self,
TGBridgeSendForwardedMessageSubscription.self
]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<Bool, NoError> in
if let context = context {
var messageSignal: Signal<(EnqueueMessage?, PeerId?), NoError>?
if let args = subscription as? TGBridgeSendTextMessageSubscription {
let peerId = makePeerIdFromBridgeIdentifier(args.peerId)
var replyMessageId: MessageId?
if args.replyToMid != 0, let peerId = peerId {
replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.replyToMid)
}
messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId))
} else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location {
let peerId = makePeerIdFromBridgeIdentifier(args.peerId)
let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)
messageSignal = .single((.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: map), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId))
} else if let args = subscription as? TGBridgeSendStickerMessageSubscription {
let peerId = makePeerIdFromBridgeIdentifier(args.peerId)
messageSignal = mediaForSticker(documentId: args.document.documentId, account: context.account)
|> map({ media -> (EnqueueMessage?, PeerId?) in
if let media = media {
return (.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)
} else {
return (nil, nil)
}
})
} else if let args = subscription as? TGBridgeSendForwardedMessageSubscription {
let peerId = makePeerIdFromBridgeIdentifier(args.targetPeerId)
if let forwardPeerId = makePeerIdFromBridgeIdentifier(args.peerId) {
messageSignal = .single((.forward(source: MessageId(peerId: forwardPeerId, namespace: Namespaces.Message.Cloud, id: args.messageId), threadId: nil, grouping: .none, attributes: [], correlationId: nil, asCopy: false), peerId))
}
}
if let messageSignal = messageSignal {
return messageSignal |> mapToSignal({ message, peerId -> Signal<Bool, NoError> in
if let message = message, let peerId = peerId {
return enqueueMessages(account: context.account, peerId: peerId, messages: [message]) |> mapToSignal({ _ in
return .single(true)
})
} else {
return .complete()
}
})
}
}
return .complete()
})
let disposable = signal.start(next: { _ in
subscriber.putNext(true)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
}
}
final class WatchPeerInfoHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [
TGBridgeUserInfoSubscription.self,
TGBridgeUserBotInfoSubscription.self,
TGBridgeConversationSubscription.self
]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeUserInfoSubscription {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<PeerView, NoError> in
if let context = context, let userId = args.userIds.first as? Int64, let peerId = makePeerIdFromBridgeIdentifier(userId) {
return context.account.viewTracker.peerView(peerId)
} else {
return .complete()
}
})
let disposable = signal.start(next: { view in
if let user = makeBridgeUser(peerViewMainPeer(view), presence: view.peerPresences[view.peerId], cachedData: view.cachedData) {
subscriber.putNext([user.identifier: user])
} else {
subscriber.putCompletion()
}
})
return SBlockDisposable {
disposable.dispose()
}
}
} else if let _ = subscription as? TGBridgeUserBotInfoSubscription {
return SSignal.complete()
} else if let args = subscription as? TGBridgeConversationSubscription {
return SSignal { subscriber in
let signal = manager.accountContext.get() |> take(1) |> mapToSignal({ context -> Signal<PeerView, NoError> in
if let context = context, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
return context.account.viewTracker.peerView(peerId)
} else {
return .complete()
}
})
let disposable = signal.start(next: { view in
let (chat, users) = makeBridgeChat(peerViewMainPeer(view), view: view)
subscriber.putNext([ TGBridgeChatKey: chat, TGBridgeUsersDictionaryKey: users ] as [String: Any])
})
return SBlockDisposable {
disposable.dispose()
}
}
}
return SSignal.fail(nil)
}
}
private func mediaForSticker(documentId: Int64, account: Account) -> Signal<TelegramMediaFile?, NoError> {
return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50)
|> take(1)
|> map { view -> TelegramMediaFile? in
for view in view.orderedItemListsViews {
for entry in view.items {
if let file = entry.contents.get(SavedStickerItem.self)?.file {
if file.id.id == documentId {
return file._parse()
}
} else if let file = entry.contents.get(RecentMediaItem.self)?.media {
if file.id.id == documentId {
return file._parse()
}
}
}
}
return nil
}
}
private let roundCorners = { () -> UIImage in
let diameter: CGFloat = 44.0
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
UIGraphicsEndImageContext()
return image
}()
private func sendData(manager: WatchCommunicationManager, data: Data, key: String, ext: String, type: String, forceAsData: Bool = false) {
if let tempPath = manager.watchTemporaryStorePath, !forceAsData {
let tempFileUrl = URL(fileURLWithPath: tempPath + "/\(key)\(ext)")
let _ = try? data.write(to: tempFileUrl)
let _ = manager.sendFile(url: tempFileUrl, metadata: [TGBridgeIncomingFileTypeKey: type, TGBridgeIncomingFileIdentifierKey: key]).start()
} else {
let _ = manager.sendFile(data: data, metadata: [TGBridgeIncomingFileTypeKey: type, TGBridgeIncomingFileIdentifierKey: key]).start()
}
}
final class WatchMediaHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [
TGBridgeMediaThumbnailSubscription.self,
TGBridgeMediaAvatarSubscription.self,
TGBridgeMediaStickerSubscription.self
]
}
static private let disposable = DisposableSet()
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeMediaAvatarSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
let key = "\(args.url!)_\(args.type.rawValue)"
let targetSize: CGSize
var compressionRate: CGFloat = 0.5
var round = false
switch args.type {
case .small:
targetSize = CGSize(width: 19, height: 19);
compressionRate = 0.5
case .profile:
targetSize = CGSize(width: 44, height: 44);
round = true
case .large:
targetSize = CGSize(width: 150, height: 150);
@unknown default:
fatalError()
}
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<UIImage?, NoError> in
if let context = context {
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer?, NoError> in
if let peer = peer, case let .secretChat(secretChat) = peer {
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: secretChat.regularPeerId))
} else {
return .single(peer)
}
}
|> mapToSignal({ peer -> Signal<UIImage?, NoError> in
if let peer = peer, let representation = peer.smallProfileImage {
let imageData = peerAvatarImageData(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, synchronousLoad: false)
if let imageData = imageData {
return imageData
|> map { data -> UIImage? in
if let (data, _) = data, let image = generateImage(targetSize, contextGenerator: { size, context -> Void in
if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
context.setBlendMode(.copy)
context.draw(dataImage, in: CGRect(origin: CGPoint(), size: targetSize))
if round {
context.setBlendMode(.normal)
context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: targetSize))
}
}
}, scale: 2.0) {
return image
}
return nil
}
}
}
return .single(nil)
})
} else {
return .complete()
}
})
let disposable = signal.start(next: { image in
if let image = image, let imageData = image.jpegData(compressionQuality: compressionRate) {
sendData(manager: manager, data: imageData, key: key, ext: ".jpg", type: TGBridgeIncomingFileTypeImage, forceAsData: true)
}
subscriber.putNext(key)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
} else if let args = subscription as? TGBridgeMediaStickerSubscription {
let key = "sticker_\(args.documentId)_\(Int(args.size.width))x\(Int(args.size.height))_\(args.notification ? 1 : 0)"
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<UIImage?, NoError> in
if let context = context {
var mediaSignal: Signal<(TelegramMediaFile, FileMediaReference)?, NoError>? = nil
if args.stickerPackId != 0 {
mediaSignal = mediaForSticker(documentId: args.documentId, account: context.account)
|> map { media -> (TelegramMediaFile, FileMediaReference)? in
if let media = media {
return (media, .standalone(media: media))
} else {
return nil
}
}
} else if args.stickerPeerId != 0, let peerId = makePeerIdFromBridgeIdentifier(args.stickerPeerId) {
mediaSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.stickerMessageId)))
|> map { message -> (TelegramMediaFile, FileMediaReference)? in
if let message = message {
for media in message.media {
if let media = media as? TelegramMediaFile {
return (media, .message(message: MessageReference(message._asMessage()), media: media))
}
}
}
return nil
}
}
var size: CGSize = args.size
if let mediaSignal = mediaSignal {
return mediaSignal
|> mapToSignal { mediaAndFileReference -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let (media, fileReference) = mediaAndFileReference {
if let dimensions = media.dimensions {
size = dimensions.cgSize
}
self.disposable.add(freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).start())
return chatMessageSticker(account: context.account, userLocation: .other, file: media, small: false, fetched: true, onlyFullSize: true)
}
return .complete()
}
|> map{ f -> UIImage? in
let context = f(TransformImageArguments(corners: ImageCorners(), imageSize: size.fitted(args.size), boundingSize: args.size, intrinsicInsets: UIEdgeInsets(), emptyColor: args.notification ? UIColor(rgb: 0xe5e5ea) : .black, scale: 2.0))
return context?.generateImage()
}
}
}
return .complete()
})
let disposable = signal.start(next: { image in
if let image = image, let imageData = image.jpegData(compressionQuality: 0.2) {
sendData(manager: manager, data: imageData, key: key, ext: ".jpg", type: TGBridgeIncomingFileTypeImage, forceAsData: args.notification)
}
subscriber.putNext(key)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
} else if let args = subscription as? TGBridgeMediaThumbnailSubscription {
let key = "\(args.peerId)_\(args.messageId)"
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<UIImage?, NoError> in
if let context = context, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
var roundVideo = false
return context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)))
|> mapToSignal { message -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let message = message, !message._asMessage().containsSecretMedia {
var imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var updatedMediaReference: AnyMediaReference?
var candidateMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
for media in message.media {
if let image = media as? TelegramMediaImage, let resource = largestImageRepresentation(image.representations)?.resource {
self.disposable.add(messageMediaImageInteractiveFetched(context: context, message: message._asMessage(), image: image, resource: resource, storeToDownloadsPeerId: nil).start())
candidateMediaReference = .message(message: MessageReference(message._asMessage()), media: media)
break
} else if let _ = media as? TelegramMediaFile {
candidateMediaReference = .message(message: MessageReference(message._asMessage()), media: media)
break
} else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let resource = largestImageRepresentation(image.representations)?.resource {
self.disposable.add(messageMediaImageInteractiveFetched(context: context, message: message._asMessage(), image: image, resource: resource, storeToDownloadsPeerId: nil).start())
candidateMediaReference = .webPage(webPage: WebpageReference(webPage), media: image)
break
}
}
if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) {
updatedMediaReference = imageReference.abstract
if let representation = largestRepresentationForPhoto(imageReference.media) {
imageDimensions = representation.dimensions.cgSize
}
} else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) {
updatedMediaReference = fileReference.abstract
if let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker {
imageDimensions = representation.dimensions.cgSize
}
}
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
imageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .other, photoReference: imageReference, onlyFullSize: true)
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isVideo {
imageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .other, fileReference: fileReference)
roundVideo = fileReference.media.isInstantVideo
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
imageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .other, mediaReference: fileReference.abstract, representation: iconImageRepresentation)
}
}
}
if let signal = imageSignal {
return signal
}
}
return .complete()
} |> map{ f -> UIImage? in
var insets = UIEdgeInsets()
if roundVideo {
insets = UIEdgeInsets(top: -2, left: -2, bottom: -2, right: -2)
}
let context = f(TransformImageArguments(corners: ImageCorners(), imageSize: args.size, boundingSize: args.size, intrinsicInsets: insets, scale: 2.0))
return context?.generateImage()
}
} else {
return .complete()
}
})
let disposable = signal.start(next: { image in
if let image = image, let imageData = image.jpegData(compressionQuality: 0.5) {
sendData(manager: manager, data: imageData, key: key, ext: ".jpg", type: TGBridgeIncomingFileTypeImage, forceAsData: args.notification)
}
subscriber.putNext(key)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
}
return SSignal.fail(nil)
}
}
final class WatchStickersHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [TGBridgeRecentStickersSubscription.self]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeRecentStickersSubscription {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<ItemCollectionsView, NoError> in
if let context = context {
return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) |> take(1)
} else {
return .complete()
}
})
let disposable = signal.start(next: { view in
var stickers: [TGBridgeDocumentMediaAttachment] = []
var added: Set<Int64> = []
outer: for view in view.orderedItemListsViews {
for entry in view.items {
if let file = entry.contents.get(SavedStickerItem.self)?.file {
if let sticker = makeBridgeDocument(file._parse()), !added.contains(sticker.documentId) {
stickers.append(sticker)
added.insert(sticker.documentId)
}
} else if let file = entry.contents.get(RecentMediaItem.self)?.media {
if let sticker = makeBridgeDocument(file._parse()), !added.contains(sticker.documentId) {
stickers.append(sticker)
added.insert(sticker.documentId)
}
}
if stickers.count == args.limit {
break outer
}
}
}
subscriber.putNext(stickers)
})
return SBlockDisposable {
disposable.dispose()
}
}
}
return SSignal.fail(nil)
}
}
final class WatchAudioHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [
TGBridgeAudioSubscription.self,
TGBridgeAudioSentSubscription.self
]
}
static private let disposable = DisposableSet()
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeAudioSubscription {
let key = "audio_\(args.peerId)_\(args.messageId)"
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<String, NoError> in
if let context = context, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
return context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)))
|> mapToSignal { message -> Signal<String, NoError> in
if let message = message {
for media in message.media {
if let file = media as? TelegramMediaFile {
self.disposable.add(messageMediaFileInteractiveFetched(context: context, message: message._asMessage(), file: file, userInitiated: true).start())
return context.account.postbox.mediaBox.resourceData(file.resource)
|> mapToSignal({ data -> Signal<String, NoError> in
if let tempPath = manager.watchTemporaryStorePath, data.complete {
let outputPath = tempPath + "/\(key).m4a"
return legacyDecodeOpusAudio(path: data.path, outputPath: outputPath)
} else {
return .complete()
}
})
}
}
}
return .complete()
}
} else {
return .complete()
}
})
let disposable = signal.start(next: { path in
let _ = manager.sendFile(url: URL(fileURLWithPath: path), metadata: [TGBridgeIncomingFileTypeKey: TGBridgeIncomingFileTypeAudio, TGBridgeIncomingFileIdentifierKey: key]).start()
subscriber.putNext(key)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
//let outputPath = manager.watchTemporaryStorePath + "/\(key).opus"
} else if let _ = subscription as? TGBridgeAudioSentSubscription {
}
return SSignal.fail(nil)
}
static func handleFile(path: String, metadata: Dictionary<String, Any>, manager: WatchCommunicationManager) -> Signal<Void, NoError> {
let randomId = metadata[TGBridgeIncomingFileRandomIdKey] as? Int64
let peerId = metadata[TGBridgeIncomingFilePeerIdKey] as? Int64
let replyToMid = metadata[TGBridgeIncomingFileReplyToMidKey] as? Int32
if let randomId = randomId, let id = peerId, let peerId = makePeerIdFromBridgeIdentifier(id) {
return combineLatest(manager.accountContext.get() |> take(1), legacyEncodeOpusAudio(path: path))
|> map({ context, pathAndDuration -> Void in
let (path, duration) = pathAndDuration
if let context = context, let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
let resource = LocalFileMediaResource(fileId: randomId)
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
var replyMessageId: MessageId? = nil
if let replyToMid = replyToMid, replyToMid != 0 {
replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid)
}
let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
}
})
} else {
return .complete()
}
}
}
final class WatchLocationHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [TGBridgeNearbyVenuesSubscription.self]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeNearbyVenuesSubscription {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<[ChatContextResultMessage], NoError> in
if let context = context {
return context.engine.peers.resolvePeerByName(name: "foursquare", referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> take(1)
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer?._asPeer() else {
return .single(nil)
}
return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: "", location: .single((args.coordinate.latitude, args.coordinate.longitude)), offset: "")
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
|> mapToSignal { contextResult -> Signal<[ChatContextResultMessage], NoError> in
guard let contextResult = contextResult else {
return .single([])
}
return .single(contextResult.results.map { $0.message })
}
} else {
return .complete()
}
})
let disposable = signal.start(next: { results in
var venues: [TGBridgeLocationVenue] = []
for result in results {
if let venue = makeBridgeLocationVenue(result) {
venues.append(venue)
}
}
subscriber.putNext(venues)
})
return SBlockDisposable {
disposable.dispose()
}
}
}
return SSignal.fail(nil)
}
}
final class WatchPeerSettingsHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [
TGBridgePeerSettingsSubscription.self,
TGBridgePeerUpdateNotificationSettingsSubscription.self,
TGBridgePeerUpdateBlockStatusSubscription.self
]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgePeerSettingsSubscription {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<PeerView, NoError> in
if let context = context, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
return context.account.viewTracker.peerView(peerId)
} else {
return .complete()
}
})
let disposable = signal.start(next: { view in
var muted = false
var blocked = false
if let notificationSettings = view.notificationSettings as? TelegramPeerNotificationSettings, case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
muted = true
}
if let cachedData = view.cachedData as? CachedUserData {
blocked = cachedData.isBlocked
}
subscriber.putNext([ "muted": muted, "blocked": blocked ])
})
return SBlockDisposable {
disposable.dispose()
}
}
} else {
return SSignal { subscriber in
let signal = manager.accountContext.get()
|> take(1)
|> mapToSignal({ context -> Signal<Bool, NoError> in
if let context = context {
var signal: Signal<Void, NoError>?
if let args = subscription as? TGBridgePeerUpdateNotificationSettingsSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
signal = context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil)
} else if let args = subscription as? TGBridgePeerUpdateBlockStatusSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
signal = context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: args.blocked)
}
if let signal = signal {
return signal |> mapToSignal({ _ in
return .single(true)
})
} else {
return .complete()
}
} else {
return .complete()
}
})
let disposable = signal.start(next: { _ in
subscriber.putNext(true)
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable {
disposable.dispose()
}
}
}
}
}
final class WatchContinuationHandler: WatchRequestHandler {
static var handledSubscriptions: [Any] {
return [TGBridgeRemoteSubscription.self]
}
static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal {
if let args = subscription as? TGBridgeRemoteSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) {
manager.requestNavigateToMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId))
}
return SSignal.fail(nil)
}
}