mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-07-09 17:38:35 +02:00
Update Ghostgram features
This commit is contained in:
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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), 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user