webclient.ts 102 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. /// <reference types="@saltyrtc/task-webrtc" />
  18. /// <reference types="@saltyrtc/task-relayed-data" />
  19. import * as msgpack from 'msgpack-lite';
  20. import {hexToU8a, msgpackVisualizer} from '../helpers';
  21. import {isContactReceiver, isDistributionListReceiver, isGroupReceiver} from '../typeguards';
  22. import {BatteryStatusService} from './battery';
  23. import {BrowserService} from './browser';
  24. import {FingerPrintService} from './fingerprint';
  25. import {TrustedKeyStoreService} from './keystore';
  26. import {MessageService} from './message';
  27. import {MimeService} from './mime';
  28. import {NotificationService} from './notification';
  29. import {PeerConnectionHelper} from './peerconnection';
  30. import {PushService} from './push';
  31. import {QrCodeService} from './qrcode';
  32. import {ReceiverService} from './receiver';
  33. import {StateService} from './state';
  34. import {TitleService} from './title';
  35. import {VersionService} from './version';
  36. // Aliases
  37. import InitializationStep = threema.InitializationStep;
  38. class WebClientDefault {
  39. private avatar: threema.AvatarRegistry = {
  40. group: {
  41. low: 'img/ic_group_t.png',
  42. high: 'img/ic_group_picture_big.png',
  43. },
  44. contact: {
  45. low: 'img/ic_contact_picture_t.png',
  46. high: 'img/ic_contact_picture_big.png',
  47. },
  48. distributionList: {
  49. low: 'img/ic_distribution_list_t.png',
  50. high: 'img/ic_distribution_list_t.png',
  51. },
  52. };
  53. /**
  54. * Return path to avatar.
  55. *
  56. * If the avatar type is invalid, return null.
  57. */
  58. public getAvatar(type: string, highResolution: boolean): string {
  59. const field: string = highResolution ? 'high' : 'low';
  60. if (typeof this.avatar[type] === 'undefined') {
  61. return null;
  62. }
  63. return this.avatar[type][field];
  64. }
  65. }
  66. /**
  67. * This service handles everything related to the communication with the peer.
  68. */
  69. export class WebClientService {
  70. private static AVATAR_LOW_MAX_SIZE = 48;
  71. private static MAX_TEXT_LENGTH = 3500;
  72. private static MAX_FILE_SIZE = 15 * 1024 * 1024;
  73. private static TYPE_REQUEST = 'request';
  74. private static TYPE_RESPONSE = 'response';
  75. private static TYPE_UPDATE = 'update';
  76. private static TYPE_CREATE = 'create';
  77. private static TYPE_DELETE = 'delete';
  78. private static SUB_TYPE_RECEIVER = 'receiver';
  79. private static SUB_TYPE_RECEIVERS = 'receivers';
  80. private static SUB_TYPE_CONVERSATIONS = 'conversations';
  81. private static SUB_TYPE_CONVERSATION = 'conversation';
  82. private static SUB_TYPE_MESSAGE = 'message';
  83. private static SUB_TYPE_TEXT_MESSAGE = 'textMessage';
  84. private static SUB_TYPE_FILE_MESSAGE = 'fileMessage';
  85. private static SUB_TYPE_AVATAR = 'avatar';
  86. private static SUB_TYPE_THUMBNAIL = 'thumbnail';
  87. private static SUB_TYPE_BLOB = 'blob';
  88. private static SUB_TYPE_TYPING = 'typing';
  89. private static SUB_TYPE_READ = 'read';
  90. private static SUB_TYPE_CLIENT_INFO = 'clientInfo';
  91. private static SUB_TYPE_KEY_PERSISTED = 'keyPersisted';
  92. private static SUB_TYPE_ACK = 'ack';
  93. private static SUB_TYPE_CONTACT_DETAIL = 'contactDetail';
  94. private static SUB_TYPE_CONTACT = 'contact';
  95. private static SUB_TYPE_GROUP = 'group';
  96. private static SUB_TYPE_DISTRIBUTION_LIST = 'distributionList';
  97. private static SUB_TYPE_ALERT = 'alert';
  98. private static SUB_TYPE_GROUP_SYNC = 'groupSync';
  99. private static SUB_TYPE_BATTERY_STATUS = 'batteryStatus';
  100. private static SUB_TYPE_CLEAN_RECEIVER_CONVERSATION = 'cleanReceiverConversation';
  101. private static SUB_TYPE_CONFIRM_ACTION = 'confirmAction';
  102. private static ARGUMENT_MODE = 'mode';
  103. private static ARGUMENT_MODE_NEW = 'new';
  104. private static ARGUMENT_MODE_MODIFIED = 'modified';
  105. private static ARGUMENT_MODE_REMOVED = 'removed';
  106. private static ARGUMENT_RECEIVER_TYPE = 'type';
  107. private static ARGUMENT_RECEIVER_ID = 'id';
  108. private static ARGUMENT_TEMPORARY_ID = 'temporaryId';
  109. private static ARGUMENT_REFERENCE_MSG_ID = 'refMsgId';
  110. private static ARGUMENT_AVATAR = 'avatar';
  111. private static ARGUMENT_AVATAR_HIGH_RESOLUTION = 'highResolution';
  112. private static ARGUMENT_CONTACT_IS_TYPING = 'isTyping';
  113. private static ARGUMENT_MESSAGE_ID = 'messageId';
  114. private static ARGUMENT_HAS_MORE = 'more';
  115. private static ARGUMENT_MESSAGE_ACKNOWLEDGED = 'acknowledged';
  116. private static ARGUMENT_IDENTITY = 'identity';
  117. private static ARGUMENT_SUCCESS = 'success';
  118. private static ARGUMENT_MESSAGE = 'message';
  119. private static ARGUMENT_SYSTEM_CONTACT = 'systemContact';
  120. private static ARGUMENT_NAME = 'name';
  121. private static ARGUMENT_MEMBERS = 'members';
  122. private static ARGUMENT_FIRST_NAME = 'firstName';
  123. private static ARGUMENT_LAST_NAME = 'lastName';
  124. private static ARGUMENT_DELETE_TYPE = 'deleteType';
  125. private static ARGUMENT_ERROR = 'error';
  126. private static ARGUMENT_MAX_SIZE = 'maxSize';
  127. private static DELETE_GROUP_TYPE_LEAVE = 'leave';
  128. private static DELETE_GROUP_TYPE_DELETE = 'delete';
  129. private static DATA_FIELD_BLOB_BLOB = 'blob';
  130. private static DC_LABEL = 'THREEMA';
  131. private logTag: string = '[WebClientService]';
  132. // Angular services
  133. private $state: ng.ui.IStateService;
  134. private $log: ng.ILogService;
  135. private $rootScope: any;
  136. private $q: ng.IQService;
  137. private $window: ng.IWindowService;
  138. private $translate: ng.translate.ITranslateService;
  139. private $filter: any;
  140. private $timeout: ng.ITimeoutService;
  141. // Custom services
  142. private batteryStatusService: BatteryStatusService;
  143. private browserService: BrowserService;
  144. private fingerPrintService: FingerPrintService;
  145. private messageService: MessageService;
  146. private mimeService: MimeService;
  147. private notificationService: NotificationService;
  148. private pushService: PushService;
  149. private qrCodeService: QrCodeService;
  150. private receiverService: ReceiverService;
  151. private titleService: TitleService;
  152. private versionService: VersionService;
  153. // State handling
  154. private startupPromise: ng.IDeferred<{}> = null; // TODO: deferred type
  155. private startupDone: boolean = false;
  156. private pendingInitializationStepRoutines: threema.InitializationStepRoutine[] = [];
  157. private initialized: Set<threema.InitializationStep> = new Set();
  158. private stateService: StateService;
  159. // SaltyRTC
  160. private saltyRtcHost: string = null;
  161. public salty: saltyrtc.SaltyRTC = null;
  162. private webrtcTask: saltyrtc.tasks.webrtc.WebRTCTask = null;
  163. private relayedDataTask: saltyrtc.tasks.relayed_data.RelayedDataTask = null;
  164. private secureDataChannel: saltyrtc.tasks.webrtc.SecureDataChannel = null;
  165. private chosenTask: threema.ChosenTask = threema.ChosenTask.None;
  166. // Messenger data
  167. public messages: threema.Container.Messages;
  168. public conversations: threema.Container.Conversations;
  169. public receivers: threema.Container.Receivers;
  170. public alerts: threema.Alert[] = [];
  171. public defaults: WebClientDefault;
  172. private myIdentity: threema.Identity;
  173. private pushToken: string = null;
  174. // Other
  175. private config: threema.Config;
  176. private container: threema.Container.Factory;
  177. private typingInstance: threema.Container.Typing;
  178. private drafts: threema.Container.Drafts;
  179. private pcHelper: PeerConnectionHelper = null;
  180. private clientInfo: threema.ClientInfo;
  181. private trustedKeyStore: TrustedKeyStoreService;
  182. public version = null;
  183. private blobCache = new Map<string, ArrayBuffer>();
  184. private loadingMessages = new Map<string, boolean>();
  185. public receiverListener: threema.ReceiverListener[] = [];
  186. // Msgpack
  187. private msgpackEncoderOptions: msgpack.EncoderOptions = {
  188. codec: msgpack.createCodec({binarraybuffer: true}),
  189. };
  190. private msgpackDecoderOptions: msgpack.DecoderOptions = {
  191. codec: msgpack.createCodec({binarraybuffer: true}),
  192. };
  193. // pending rtc promises
  194. private requestPromises: Map<string, threema.PromiseCallbacks> = new Map();
  195. public static $inject = [
  196. '$log', '$rootScope', '$q', '$state', '$window', '$translate', '$filter', '$timeout',
  197. 'Container', 'TrustedKeyStore',
  198. 'StateService', 'NotificationService', 'MessageService', 'PushService', 'BrowserService',
  199. 'TitleService', 'FingerPrintService', 'QrCodeService', 'MimeService', 'ReceiverService',
  200. 'VersionService', 'BatteryStatusService',
  201. 'CONFIG',
  202. ];
  203. constructor($log: ng.ILogService,
  204. $rootScope: any,
  205. $q: ng.IQService,
  206. $state: ng.ui.IStateService,
  207. $window: ng.IWindowService,
  208. $translate: ng.translate.ITranslateService,
  209. $filter: ng.IFilterService,
  210. $timeout: ng.ITimeoutService,
  211. container: threema.Container.Factory,
  212. trustedKeyStore: TrustedKeyStoreService,
  213. stateService: StateService,
  214. notificationService: NotificationService,
  215. messageService: MessageService,
  216. pushService: PushService,
  217. browserService: BrowserService,
  218. titleService: TitleService,
  219. fingerPrintService: FingerPrintService,
  220. qrCodeService: QrCodeService,
  221. mimeService: MimeService,
  222. receiverService: ReceiverService,
  223. versionService: VersionService,
  224. batteryStatusService: BatteryStatusService,
  225. CONFIG: threema.Config) {
  226. // Angular services
  227. this.$log = $log;
  228. this.$rootScope = $rootScope;
  229. this.$q = $q;
  230. this.$state = $state;
  231. this.$window = $window;
  232. this.$translate = $translate;
  233. this.$filter = $filter;
  234. this.$timeout = $timeout;
  235. // Own services
  236. this.batteryStatusService = batteryStatusService;
  237. this.browserService = browserService;
  238. this.fingerPrintService = fingerPrintService;
  239. this.messageService = messageService;
  240. this.mimeService = mimeService;
  241. this.notificationService = notificationService;
  242. this.pushService = pushService;
  243. this.qrCodeService = qrCodeService;
  244. this.receiverService = receiverService;
  245. this.titleService = titleService;
  246. this.versionService = versionService;
  247. // Configuration object
  248. this.config = CONFIG;
  249. // State
  250. this.stateService = stateService;
  251. // Other properties
  252. this.container = container;
  253. this.trustedKeyStore = trustedKeyStore;
  254. // Get default class
  255. this.defaults = new WebClientDefault();
  256. // Initialize drafts
  257. this.drafts = this.container.createDrafts();
  258. // Setup fields
  259. this._resetFields();
  260. // Register event handlers
  261. this.stateService.evtConnectionBuildupStateChange.attach(
  262. (stateChange: threema.ConnectionBuildupStateChange) => {
  263. if (this.startupPromise !== null) {
  264. this.startupPromise.notify(stateChange);
  265. }
  266. },
  267. );
  268. }
  269. get me(): threema.MeReceiver {
  270. return this.receivers.me;
  271. }
  272. get contacts(): Map<string, threema.ContactReceiver> {
  273. return this.receivers.contacts;
  274. }
  275. get groups(): Map<string, threema.GroupReceiver> {
  276. return this.receivers.groups;
  277. }
  278. get distributionLists(): Map<string, threema.DistributionListReceiver> {
  279. return this.receivers.distributionLists;
  280. }
  281. get typing(): threema.Container.Typing {
  282. return this.typingInstance;
  283. }
  284. /**
  285. * Return QR code payload.
  286. */
  287. public buildQrCodePayload(persistent: boolean): string {
  288. return this.qrCodeService.buildQrCodePayload(
  289. this.salty.permanentKeyBytes,
  290. this.salty.authTokenBytes,
  291. hexToU8a(this.config.SALTYRTC_SERVER_KEY),
  292. this.saltyRtcHost, this.config.SALTYRTC_PORT,
  293. persistent);
  294. }
  295. /**
  296. * Initialize the webclient service.
  297. */
  298. public init(keyStore?: saltyrtc.KeyStore, peerTrustedKey?: Uint8Array, resetFields = true): void {
  299. // Reset state
  300. this.stateService.reset();
  301. // Create WebRTC task instance
  302. const maxPacketSize = this.browserService.getBrowser().firefox ? 16384 : 65536;
  303. this.webrtcTask = new saltyrtcTaskWebrtc.WebRTCTask(true, maxPacketSize);
  304. // Create Relayed Data task instance
  305. this.relayedDataTask = new saltyrtcTaskRelayedData.RelayedDataTask(this.config.DEBUG);
  306. // Create new keystore if necessary
  307. if (!keyStore) {
  308. keyStore = new saltyrtcClient.KeyStore();
  309. }
  310. // Determine SaltyRTC host
  311. if (this.config.SALTYRTC_HOST !== null) {
  312. // Static URL
  313. this.saltyRtcHost = this.config.SALTYRTC_HOST;
  314. } else {
  315. // Construct URL using prefix and suffix
  316. this.saltyRtcHost = this.config.SALTYRTC_HOST_PREFIX
  317. + keyStore.publicKeyHex.substr(0, 2)
  318. + this.config.SALTYRTC_HOST_SUFFIX;
  319. }
  320. // Create SaltyRTC client
  321. let builder = new saltyrtcClient.SaltyRTCBuilder()
  322. .connectTo(this.saltyRtcHost, this.config.SALTYRTC_PORT)
  323. .withServerKey(this.config.SALTYRTC_SERVER_KEY)
  324. .withKeyStore(keyStore)
  325. .usingTasks([this.webrtcTask, this.relayedDataTask])
  326. .withPingInterval(30);
  327. if (keyStore !== undefined && peerTrustedKey !== undefined) {
  328. builder = builder.withTrustedPeerKey(peerTrustedKey);
  329. }
  330. this.salty = builder.asInitiator();
  331. if (this.config.DEBUG) {
  332. this.$log.debug('Public key:', this.salty.permanentKeyHex);
  333. this.$log.debug('Auth token:', this.salty.authTokenHex);
  334. }
  335. // We want to know about new responders.
  336. this.salty.on('new-responder', () => {
  337. if (!this.startupDone) {
  338. // Peer handshake
  339. this.stateService.updateConnectionBuildupState('peer_handshake');
  340. }
  341. });
  342. // We want to know about state changes
  343. this.salty.on('state-change', (ev: saltyrtc.SaltyRTCEvent) => {
  344. // Wrap this in a $timeout to execute at the end of the event loop.
  345. this.$timeout(() => {
  346. const state: saltyrtc.SignalingState = ev.data;
  347. if (!this.startupDone) {
  348. switch (state) {
  349. case 'new':
  350. case 'ws-connecting':
  351. case 'server-handshake':
  352. if (this.stateService.connectionBuildupState !== 'push'
  353. && this.stateService.connectionBuildupState !== 'manual_start') {
  354. this.stateService.updateConnectionBuildupState('connecting');
  355. }
  356. break;
  357. case 'peer-handshake':
  358. // Waiting for peer
  359. if (this.stateService.connectionBuildupState !== 'push'
  360. && this.stateService.connectionBuildupState !== 'manual_start') {
  361. this.stateService.updateConnectionBuildupState('waiting');
  362. }
  363. break;
  364. case 'task':
  365. // Do nothing, state will be updated once SecureDataChannel is open
  366. break;
  367. case 'closing':
  368. case 'closed':
  369. this.stateService.updateConnectionBuildupState('closed');
  370. break;
  371. default:
  372. this.$log.warn('Unknown signaling state:', state);
  373. }
  374. }
  375. this.stateService.updateSignalingConnectionState(state);
  376. }, 0);
  377. });
  378. // Once the connection is established, if this is a WebRTC connection,
  379. // initiate the peer connection and start the handover.
  380. this.salty.once('state-change:task', () => {
  381. // Determine chosen task
  382. const task = this.salty.getTask();
  383. if (task.getName().indexOf('webrtc.tasks.saltyrtc.org') !== -1) {
  384. this.chosenTask = threema.ChosenTask.WebRTC;
  385. } else if (task.getName().indexOf('relayed-data.tasks.saltyrtc.org') !== -1) {
  386. this.chosenTask = threema.ChosenTask.RelayedData;
  387. } else {
  388. throw new Error('Invalid or unknown task name: ' + task.getName());
  389. }
  390. // If the WebRTC task was chosen, initialize handover.
  391. if (this.chosenTask === threema.ChosenTask.WebRTC) {
  392. // Firefox <53 does not yet support TLS. Skip it, to save allocations.
  393. const browser = this.browserService.getBrowser();
  394. if (browser.firefox && parseFloat(browser.version) < 53) {
  395. this.skipIceTls();
  396. }
  397. this.pcHelper = new PeerConnectionHelper(this.$log, this.$q, this.$timeout,
  398. this.$rootScope, this.webrtcTask,
  399. this.config.ICE_SERVERS,
  400. !this.config.ICE_DEBUGGING);
  401. // On state changes in the PeerConnectionHelper class, let state service know about it
  402. this.pcHelper.onConnectionStateChange = (state: threema.RTCConnectionState) => {
  403. this.stateService.updateRtcConnectionState(state);
  404. };
  405. // Initiate handover
  406. this.webrtcTask.handover(this.pcHelper.peerConnection);
  407. // Otherwise, no handover is necessary.
  408. } else {
  409. this.onHandover(resetFields);
  410. return;
  411. }
  412. });
  413. // Handle a disconnect request
  414. this.salty.on('application', (applicationData: any) => {
  415. if (applicationData.data.type === 'disconnect') {
  416. this.$log.debug(this.logTag, 'Disconnecting requested');
  417. const deleteStoredData = applicationData.data.forget === true;
  418. const resetPush = true;
  419. const redirect = true;
  420. this.stop(false, deleteStoredData, resetPush, redirect);
  421. }
  422. });
  423. // Wait for handover to be finished
  424. this.salty.on('handover', () => {
  425. this.$log.debug(this.logTag, 'Handover done');
  426. this.onHandover(resetFields);
  427. });
  428. // Handle SaltyRTC errors
  429. this.salty.on('connection-error', (ev) => {
  430. this.$log.error('Connection error:', ev);
  431. });
  432. this.salty.on('connection-closed', (ev) => {
  433. this.$log.warn('Connection closed:', ev);
  434. });
  435. }
  436. /**
  437. * For the WebRTC task, this is called when the DataChannel is open.
  438. * For the relayed data task, this is called once the connection is established.
  439. */
  440. private onDataChannelOpen(resetFields: boolean) {
  441. // Reset fields if requested
  442. if (resetFields) {
  443. this._resetFields();
  444. }
  445. // Resolve startup promise once initialization is done
  446. if (this.startupPromise !== null) {
  447. this.runAfterInitializationSteps([
  448. InitializationStep.ClientInfo,
  449. InitializationStep.Conversations,
  450. InitializationStep.Receivers,
  451. ], () => {
  452. this.stateService.updateConnectionBuildupState('done');
  453. this.startupPromise.resolve();
  454. this.startupPromise = null;
  455. this.startupDone = true;
  456. this._resetInitializationSteps();
  457. });
  458. }
  459. // Request initial data
  460. this._requestInitialData();
  461. // Fetch current version
  462. // Delay it to prevent the dialog from being closed by the messenger constructor,
  463. // which closes all open dialogs.
  464. this.$timeout(() => this.versionService.checkForUpdate(), 7000);
  465. // Notify state service about data loading
  466. this.stateService.updateConnectionBuildupState('loading');
  467. }
  468. /**
  469. * Handover done.
  470. *
  471. * This can either be a real handover to WebRTC (Android), or simply
  472. * when the relayed data task takes over (iOS).
  473. */
  474. private onHandover(resetFields: boolean) {
  475. // Initialize NotificationService
  476. this.$log.debug(this.logTag, 'Initializing NotificationService...');
  477. this.notificationService.init();
  478. // If the WebRTC task was chosen, initialize the data channel
  479. if (this.chosenTask === threema.ChosenTask.WebRTC) {
  480. // Create secure data channel
  481. this.$log.debug(this.logTag, 'Create SecureDataChannel "' + WebClientService.DC_LABEL + '"...');
  482. this.secureDataChannel = this.pcHelper.createSecureDataChannel(
  483. WebClientService.DC_LABEL,
  484. (event: Event) => {
  485. this.$log.debug(this.logTag, 'SecureDataChannel open');
  486. this.onDataChannelOpen(resetFields);
  487. },
  488. );
  489. // Handle incoming messages
  490. this.secureDataChannel.onmessage = (ev: MessageEvent) => {
  491. const bytes = new Uint8Array(ev.data);
  492. this.handleIncomingMessageBytes(bytes);
  493. };
  494. this.secureDataChannel.onbufferedamountlow = (ev: Event) => {
  495. this.$log.debug('Secure data channel: Buffered amount low');
  496. };
  497. this.secureDataChannel.onerror = (e: ErrorEvent) => {
  498. this.$log.warn('Secure data channel: Error:', e.message);
  499. this.$log.debug(e);
  500. };
  501. this.secureDataChannel.onclose = (ev: Event) => {
  502. this.$log.warn('Secure data channel: Closed');
  503. };
  504. } else if (this.chosenTask === threema.ChosenTask.RelayedData) {
  505. // Handle messages directly
  506. this.relayedDataTask.on('data', (ev: saltyrtc.SaltyRTCEvent) => {
  507. this.handleIncomingMessage(ev.data, true);
  508. });
  509. // The communication channel is now open! Fetch initial data
  510. this.onDataChannelOpen(resetFields);
  511. }
  512. }
  513. /**
  514. * Start the webclient service.
  515. * Return a promise that resolves once connected.
  516. */
  517. public start(): ng.IPromise<any> {
  518. this.$log.debug('Starting WebClientService...');
  519. // Promise to track startup state
  520. this.startupPromise = this.$q.defer();
  521. this.startupDone = false;
  522. // Connect
  523. this.salty.connect();
  524. // If push service is available, notify app
  525. if (this.pushService.isAvailable()) {
  526. this.pushService.sendPush(this.salty.permanentKeyBytes)
  527. .catch(() => this.$log.warn('Could not notify app!'))
  528. .then(() => {
  529. this.$log.debug('Requested app wakeup');
  530. this.$rootScope.$apply(() => {
  531. this.stateService.updateConnectionBuildupState('push');
  532. });
  533. });
  534. } else if (this.trustedKeyStore.hasTrustedKey()) {
  535. this.$log.debug('Push service not available');
  536. this.stateService.updateConnectionBuildupState('manual_start');
  537. }
  538. return this.startupPromise.promise;
  539. }
  540. /**
  541. * Stop the webclient service.
  542. *
  543. * This is a forced stop, meaning that all channels are closed.
  544. *
  545. * Parameters:
  546. *
  547. * - `requestedByUs`: Set this to `false` if the app requested to close the session.
  548. * - `deleteStoredData`: Whether to clear any trusted key or push token from the keystore.
  549. * - `resetPush`: Whether to reset the push service.
  550. * - `redirect`: Whether to redirect to the welcome page.
  551. */
  552. public stop(requestedByUs: boolean,
  553. deleteStoredData: boolean = false,
  554. resetPush: boolean = true,
  555. redirect: boolean = false): void {
  556. this.$log.info(this.logTag, 'Disconnecting...');
  557. if (requestedByUs && this.stateService.rtcConnectionState === 'connected') {
  558. // Ask peer to disconnect too
  559. this.salty.sendApplicationMessage({type: 'disconnect', forget: deleteStoredData});
  560. }
  561. this.stateService.reset();
  562. // Reset the unread count
  563. this.resetUnreadCount();
  564. // Clear stored data (trusted key, push token, etc)
  565. if (deleteStoredData === true) {
  566. this.trustedKeyStore.clearTrustedKey();
  567. }
  568. // Clear push token
  569. if (resetPush === true) {
  570. this.pushService.reset();
  571. }
  572. // Close data channel
  573. if (this.secureDataChannel !== null) {
  574. this.$log.debug(this.logTag, 'Closing secure datachannel');
  575. this.secureDataChannel.close();
  576. }
  577. // Close SaltyRTC connection
  578. if (this.salty !== null) {
  579. this.$log.debug(this.logTag, 'Closing signaling');
  580. this.salty.disconnect();
  581. }
  582. // Function to redirect to welcome screen
  583. const redirectToWelcome = () => {
  584. if (redirect === true) {
  585. this.$timeout(() => {
  586. this.$state.go('welcome');
  587. }, 0);
  588. }
  589. };
  590. // Close peer connection
  591. if (this.pcHelper !== null) {
  592. this.$log.debug(this.logTag, 'Closing peer connection');
  593. this.pcHelper.close()
  594. .then(
  595. () => this.$log.debug(this.logTag, 'Peer connection was closed'),
  596. (reason: string) => this.$log.warn(this.logTag, 'Peer connection could not be closed:', reason),
  597. )
  598. .finally(() => redirectToWelcome());
  599. } else {
  600. this.$log.debug(this.logTag, 'Peer connection was null');
  601. redirectToWelcome();
  602. }
  603. }
  604. /**
  605. * Remove "turns:" servers from the ICE_SERVERS configuration
  606. * if at least one "turn:" server with tcp transport is in the list.
  607. */
  608. public skipIceTls(): void {
  609. this.$log.debug(this.logTag, 'Requested to remove TURNS server from ICE configuration');
  610. const allUrls = [].concat(...this.config.ICE_SERVERS.map((conf) => conf.urls));
  611. if (allUrls.some((url) => url.startsWith('turn:') && url.endsWith('=tcp'))) {
  612. // There's at least one TURN server with TCP transport in the list
  613. for (const server of this.config.ICE_SERVERS) {
  614. // Remove TLS entries
  615. server.urls = server.urls.filter((url) => !url.startsWith('turns:'));
  616. }
  617. } else {
  618. this.$log.debug(this.logTag, 'No fallback TURN TCP server present, keeping TURNS server');
  619. }
  620. }
  621. /**
  622. * Mark a component as initialized
  623. */
  624. public registerInitializationStep(name: threema.InitializationStep) {
  625. if (this.initialized.has(name) ) {
  626. this.$log.warn(this.logTag, 'Initialization step "' + name + '" already registered');
  627. return;
  628. }
  629. this.$log.debug(this.logTag, 'Initialization step "' + name + '" done');
  630. this.initialized.add(name);
  631. // Check pending routines
  632. this.pendingInitializationStepRoutines = this.pendingInitializationStepRoutines.filter((routine) => {
  633. let isValid = true;
  634. for (const requiredStep of routine.requiredSteps) {
  635. if (!this.initialized.has(requiredStep)) {
  636. isValid = false;
  637. break;
  638. }
  639. }
  640. if (isValid) {
  641. this.$log.debug(this.logTag, 'Running routine after initialization "' + name + '" completed');
  642. routine.callback();
  643. }
  644. return !isValid;
  645. });
  646. }
  647. public setReceiverListener(listener: threema.ReceiverListener): void {
  648. this.receiverListener.push(listener);
  649. }
  650. /**
  651. * Send a client info request.
  652. */
  653. public requestClientInfo(): void {
  654. this.$log.debug('Sending client info request');
  655. this._sendRequest(WebClientService.SUB_TYPE_CLIENT_INFO, {
  656. userAgent: navigator.userAgent,
  657. });
  658. }
  659. /**
  660. * Send a receivers request.
  661. */
  662. public requestReceivers(): void {
  663. this.$log.debug('Sending receivers request');
  664. this._sendRequest(WebClientService.SUB_TYPE_RECEIVERS);
  665. }
  666. /**
  667. * Send a conversation request.
  668. */
  669. public requestConversations(): void {
  670. this.$log.debug('Sending conversation request');
  671. this._sendRequest(WebClientService.SUB_TYPE_CONVERSATIONS, {
  672. [WebClientService.ARGUMENT_MAX_SIZE]: WebClientService.AVATAR_LOW_MAX_SIZE,
  673. });
  674. }
  675. /**
  676. * Send a battery status request.
  677. */
  678. public requestBatteryStatus(): void {
  679. this.$log.debug('Sending battery status request');
  680. this._sendRequest(WebClientService.SUB_TYPE_BATTERY_STATUS);
  681. }
  682. /**
  683. * Send a message request for the specified receiver.
  684. *
  685. * This method will only be called when initializing a conversation in the
  686. * webclient. It is used to download all existing messages.
  687. *
  688. * New messages are not requested this way, instead they are sent as a
  689. * message update.
  690. */
  691. public requestMessages(receiver: threema.Receiver): string {
  692. // If there are no more messages available, stop here.
  693. if (!this.messages.hasMore(receiver)) {
  694. this.messages.notify(receiver, this.$rootScope);
  695. return null;
  696. }
  697. this.loadingMessages.set(receiver.type + receiver.id, true);
  698. // Check if messages have already been requested
  699. if (this.messages.isRequested(receiver)) {
  700. return null;
  701. }
  702. // Get the reference msg id
  703. const refMsgId = this.messages.getReferenceMsgId(receiver);
  704. // Set requested
  705. // TODO: Add timeout to reset flag
  706. this.messages.setRequested(receiver);
  707. // Create arguments
  708. const args = {
  709. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  710. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  711. } as any;
  712. // If a reference msg id has been set, send it along
  713. const msgId = this.messages.getReferenceMsgId(receiver);
  714. if (msgId !== null) {
  715. args[WebClientService.ARGUMENT_REFERENCE_MSG_ID] = msgId;
  716. }
  717. // Send request
  718. this.$log.debug('Sending message request for', receiver.type, receiver.id,
  719. 'with message id', msgId);
  720. this._sendRequest(WebClientService.SUB_TYPE_MESSAGE, args);
  721. return refMsgId;
  722. }
  723. /**
  724. * Send an avatar request for the specified receiver.
  725. */
  726. public requestAvatar(receiver: threema.Receiver, highResolution: boolean): Promise<any> {
  727. // Check if the receiver has an avatar or the avatar already exists
  728. const resolution = highResolution ? 'high' : 'low';
  729. const receiverInfo = this.receivers.getData(receiver);
  730. if (receiverInfo && receiverInfo.avatar && receiverInfo.avatar[resolution]) {
  731. // Avatar already exists
  732. // TODO: Do we get avatar changes via update?
  733. return new Promise<any>((e) => {
  734. e(receiverInfo.avatar[resolution]);
  735. });
  736. }
  737. // Create arguments and send request
  738. const args = {
  739. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  740. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  741. [WebClientService.ARGUMENT_AVATAR_HIGH_RESOLUTION]: highResolution,
  742. } as any;
  743. if (!highResolution) {
  744. args[WebClientService.ARGUMENT_MAX_SIZE] = WebClientService.AVATAR_LOW_MAX_SIZE;
  745. }
  746. this.$log.debug('Sending', resolution, 'res avatar request for', receiver.type, receiver.id);
  747. return this._sendRequestPromise(WebClientService.SUB_TYPE_AVATAR, args, 10000);
  748. }
  749. /**
  750. * Send an thumbnail request for the specified receiver.
  751. */
  752. public requestThumbnail(receiver: threema.Receiver, message: threema.Message): Promise<any> {
  753. // Check if the receiver has an avatar or the avatar already exists
  754. if (message.thumbnail !== undefined && message.thumbnail.img !== undefined) {
  755. return new Promise<any>((e) => {
  756. e(message.thumbnail.img);
  757. });
  758. }
  759. // Create arguments and send request
  760. const args = {
  761. [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
  762. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  763. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  764. };
  765. this.$log.debug('Sending', 'thumbnail request for', receiver.type, message.id);
  766. return this._sendRequestPromise(WebClientService.SUB_TYPE_THUMBNAIL, args, 10000);
  767. }
  768. /**
  769. * Request a blob.
  770. */
  771. public requestBlob(msgId: string, receiver: threema.Receiver): Promise<ArrayBuffer> {
  772. const cached = this.blobCache.get(msgId + receiver.type);
  773. if (cached !== undefined) {
  774. this.$log.debug('Use cached blob');
  775. return new Promise((resolve) => {
  776. resolve(cached);
  777. });
  778. }
  779. const args = {
  780. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  781. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  782. [WebClientService.ARGUMENT_MESSAGE_ID]: msgId,
  783. };
  784. this.$log.debug('Sending blob request for message', msgId);
  785. return this._sendRequestPromise(WebClientService.SUB_TYPE_BLOB, args);
  786. }
  787. /**
  788. */
  789. public requestRead(receiver, newestMessage: threema.Message): void {
  790. // Check if the receiver has an avatar or the avatar already exists
  791. // let field: string = highResolution ? 'high' : 'low';
  792. // let data = this.receivers.getData(receiver);
  793. // if (data && data['avatar'] && data['avatar'][field]) {
  794. // return;
  795. // }
  796. // if (data && data.hasAvatar === false) {
  797. // return;
  798. // }
  799. // Create arguments and send request
  800. const args = {
  801. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  802. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  803. [WebClientService.ARGUMENT_MESSAGE_ID]: newestMessage.id.toString(),
  804. };
  805. this.$log.debug('Sending read request for', receiver.type, receiver.id, '(msg ' + newestMessage.id + ')');
  806. this._sendRequest(WebClientService.SUB_TYPE_READ, args);
  807. }
  808. public requestContactDetail(contactReceiver: threema.ContactReceiver): Promise<any> {
  809. return this._sendRequestPromise(WebClientService.SUB_TYPE_CONTACT_DETAIL, {
  810. [WebClientService.ARGUMENT_IDENTITY]: contactReceiver.id,
  811. });
  812. }
  813. /**
  814. * Send a message to the specified receiver.
  815. */
  816. public sendMessage(receiver,
  817. type: threema.MessageContentType,
  818. message: threema.MessageData): Promise<Promise<any>> {
  819. return new Promise<any> (
  820. (resolve, reject) => {
  821. // Try to load receiver object
  822. const receiverObject = this.receivers.getData(receiver);
  823. // Check blocked flag
  824. if (receiverObject.type === 'contact'
  825. && (receiverObject as threema.ContactReceiver).isBlocked) {
  826. return reject(this.$translate.instant('error.CONTACT_BLOCKED'));
  827. }
  828. // Decide on subtype
  829. let subType;
  830. switch (type) {
  831. case 'text':
  832. subType = WebClientService.SUB_TYPE_TEXT_MESSAGE;
  833. const textMessage = message as threema.TextMessageData;
  834. const msgLength = textMessage.text.length;
  835. // Ignore empty text messages
  836. if (msgLength === 0) {
  837. return reject();
  838. }
  839. // Ignore text messages that are too long.
  840. if (msgLength > WebClientService.MAX_TEXT_LENGTH) {
  841. return reject(this.$translate.instant('error.TEXT_TOO_LONG', {
  842. max: WebClientService.MAX_TEXT_LENGTH,
  843. }));
  844. }
  845. break;
  846. case 'file':
  847. // validate max file size
  848. if ((message as threema.FileMessageData).size > WebClientService.MAX_FILE_SIZE) {
  849. return reject(this.$translate.instant('error.FILE_TOO_LARGE'));
  850. }
  851. // Determine required feature level
  852. let requiredFeatureLevel = 3;
  853. let invalidFeatureLevelMessage = 'error.FILE_MESSAGES_NOT_SUPPORTED';
  854. if ((message as threema.FileMessageData).sendAsFile !== true) {
  855. // check mime type
  856. const mime = (message as threema.FileMessageData).fileType;
  857. if (this.mimeService.isAudio(mime)) {
  858. requiredFeatureLevel = 1;
  859. invalidFeatureLevelMessage = 'error.AUDIO_MESSAGES_NOT_SUPPORTED';
  860. } else if (this.mimeService.isImage(mime)
  861. || this.mimeService.isVideo(mime)) {
  862. requiredFeatureLevel = 0;
  863. invalidFeatureLevelMessage = 'error.MESSAGE_NOT_SUPPORTED';
  864. }
  865. }
  866. subType = WebClientService.SUB_TYPE_FILE_MESSAGE;
  867. // check receiver
  868. switch (receiver.type) {
  869. case 'distributionList':
  870. return reject(this.$translate.instant(invalidFeatureLevelMessage, {
  871. receiverName: receiver.displayName}));
  872. case 'group':
  873. const unsupportedMembers = [];
  874. const group = this.groups.get(receiver.id);
  875. if (group === undefined) {
  876. return reject();
  877. }
  878. group.members.forEach((identity: string) => {
  879. if (identity !== this.me.id) {
  880. // tslint:disable-next-line: no-shadowed-variable
  881. const contact = this.contacts.get(identity);
  882. if (contact !== undefined && contact.featureLevel < requiredFeatureLevel) {
  883. unsupportedMembers.push(contact.displayName);
  884. }
  885. }
  886. });
  887. if (unsupportedMembers.length > 0) {
  888. return reject(this.$translate.instant(invalidFeatureLevelMessage, {
  889. receiverName: unsupportedMembers.join(',')}));
  890. }
  891. break;
  892. case 'contact':
  893. const contact = this.contacts.get(receiver.id);
  894. if (contact === undefined) {
  895. this.$log.error('Cannot retrieve contact');
  896. return reject(this.$translate.instant('error.ERROR_OCCURRED'));
  897. } else if (contact.featureLevel < requiredFeatureLevel) {
  898. this.$log.debug('Cannot send message: Feature level mismatch:',
  899. contact.featureLevel, '<', requiredFeatureLevel);
  900. return reject(this.$translate.instant(invalidFeatureLevelMessage, {
  901. receiverName: contact.displayName}));
  902. }
  903. break;
  904. default:
  905. return reject();
  906. }
  907. break;
  908. default:
  909. this.$log.warn('Invalid message type:', type);
  910. return reject();
  911. }
  912. const temporaryMessage = this.messageService.createTemporary(receiver, type, message);
  913. this.messages.addNewer(receiver, [temporaryMessage]);
  914. const args = {
  915. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  916. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  917. [WebClientService.ARGUMENT_TEMPORARY_ID]: temporaryMessage.temporaryId,
  918. };
  919. // Send message and handling error promise
  920. this._sendCreatePromise(subType, args, message).catch((error) => {
  921. this.$log.error('Error sending message:', error);
  922. // Remove temporary message
  923. this.messages.removeTemporary(receiver, temporaryMessage.temporaryId);
  924. // Determine error message
  925. let errorMessage;
  926. switch (error) {
  927. case 'file_too_large':
  928. errorMessage = this.$translate.instant('error.FILE_TOO_LARGE');
  929. break;
  930. case 'blocked':
  931. errorMessage = this.$translate.instant('error.CONTACT_BLOCKED');
  932. break;
  933. default:
  934. errorMessage = this.$translate.instant('error.ERROR_OCCURRED');
  935. }
  936. // Show alert
  937. this.alerts.push({
  938. source: 'sendMessage',
  939. type: 'alert',
  940. message: errorMessage,
  941. } as threema.Alert);
  942. });
  943. resolve();
  944. });
  945. }
  946. /**
  947. * Send a message a ack/decline message
  948. */
  949. public ackMessage(receiver, message: threema.Message, acknowledged: boolean = true): void {
  950. // Ignore empty text messages
  951. // TODO check into a util class
  952. if (message === null
  953. || message === undefined
  954. || message.isOutbox) {
  955. return;
  956. }
  957. const args = {
  958. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  959. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  960. [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
  961. [WebClientService.ARGUMENT_MESSAGE_ACKNOWLEDGED]: acknowledged,
  962. };
  963. this._sendRequest(WebClientService.SUB_TYPE_ACK, args);
  964. }
  965. /**
  966. * Send a message a ack/decline message
  967. */
  968. public deleteMessage(receiver, message: threema.Message): void {
  969. // Ignore empty text messages
  970. if (message === null || message === undefined) {
  971. return;
  972. }
  973. const args = {
  974. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  975. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  976. [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
  977. };
  978. this._sendDelete(WebClientService.SUB_TYPE_MESSAGE, args);
  979. }
  980. public sendMeIsTyping(receiver, isTyping: boolean): void {
  981. // Create arguments and send create
  982. const args = {
  983. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  984. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  985. [WebClientService.ARGUMENT_CONTACT_IS_TYPING]: isTyping,
  986. };
  987. this._sendRequest(WebClientService.SUB_TYPE_TYPING, args);
  988. }
  989. public sendKeyPersisted(): void {
  990. this._sendRequest(WebClientService.SUB_TYPE_KEY_PERSISTED);
  991. }
  992. /**
  993. * Add a contact receiver.
  994. */
  995. public addContact(threemaId: string): Promise<threema.ContactReceiver> {
  996. const args = null;
  997. const data = {
  998. [WebClientService.ARGUMENT_IDENTITY]: threemaId,
  999. };
  1000. return this._sendCreatePromise(WebClientService.SUB_TYPE_CONTACT, args, data);
  1001. }
  1002. /**
  1003. * Modify a contact name or a avatar
  1004. */
  1005. public modifyContact(threemaId: string,
  1006. firstName?: string,
  1007. lastName?: string,
  1008. avatar?: ArrayBuffer): Promise<threema.ContactReceiver> {
  1009. // Prepare payload data
  1010. const data = {};
  1011. if (firstName !== undefined) {
  1012. data[WebClientService.ARGUMENT_FIRST_NAME] = firstName;
  1013. }
  1014. if (lastName !== undefined) {
  1015. data[WebClientService.ARGUMENT_LAST_NAME] = lastName;
  1016. }
  1017. if (avatar !== undefined) {
  1018. data[WebClientService.ARGUMENT_AVATAR] = avatar;
  1019. }
  1020. // If no changes happened, resolve the promise immediately.
  1021. if (Object.keys(data).length === 0) {
  1022. this.$log.warn(this.logTag, 'Trying to modify contact without any changes');
  1023. return Promise.resolve(this.contacts.get(threemaId));
  1024. }
  1025. // Send update
  1026. const args = {
  1027. [WebClientService.ARGUMENT_IDENTITY]: threemaId,
  1028. };
  1029. const promise = this._sendUpdatePromise(WebClientService.SUB_TYPE_CONTACT, args, data);
  1030. // If necessary, reset avatar to force a avatar reload
  1031. if (avatar !== undefined) {
  1032. this.contacts.get(threemaId).avatar = {};
  1033. }
  1034. return promise;
  1035. }
  1036. /**
  1037. * Create a group receiver.
  1038. */
  1039. public createGroup(
  1040. members: string[],
  1041. name: string = null,
  1042. avatar?: ArrayBuffer,
  1043. ): Promise<threema.GroupReceiver> {
  1044. const args = null;
  1045. const data = {
  1046. [WebClientService.ARGUMENT_MEMBERS]: members,
  1047. [WebClientService.ARGUMENT_NAME]: name,
  1048. } as object;
  1049. if (avatar !== undefined) {
  1050. data[WebClientService.ARGUMENT_AVATAR] = avatar;
  1051. }
  1052. return this._sendCreatePromise(WebClientService.SUB_TYPE_GROUP, args, data);
  1053. }
  1054. /**
  1055. * Modify a group receiver.
  1056. */
  1057. public modifyGroup(id: string,
  1058. members: string[],
  1059. name?: string,
  1060. avatar?: ArrayBuffer): Promise<threema.GroupReceiver> {
  1061. // Prepare payload data
  1062. const data = {
  1063. [WebClientService.ARGUMENT_MEMBERS]: members,
  1064. } as object;
  1065. if (name !== undefined) {
  1066. data[WebClientService.ARGUMENT_NAME] = name;
  1067. }
  1068. if (avatar !== undefined) {
  1069. data[WebClientService.ARGUMENT_AVATAR] = avatar;
  1070. }
  1071. // Send update
  1072. const args = {
  1073. [WebClientService.ARGUMENT_RECEIVER_ID]: id,
  1074. };
  1075. const promise = this._sendUpdatePromise(WebClientService.SUB_TYPE_GROUP, args, data);
  1076. // If necessary, reset avatar to force a avatar reload
  1077. if (avatar !== undefined) {
  1078. this.groups.get(id).avatar = {};
  1079. }
  1080. return promise;
  1081. }
  1082. public leaveGroup(group: threema.GroupReceiver): Promise<any> {
  1083. if (group === null || group === undefined || !group.access.canLeave) {
  1084. return new Promise((resolve, reject) => reject('not allowed'));
  1085. }
  1086. const args = {
  1087. [WebClientService.ARGUMENT_RECEIVER_ID]: group.id,
  1088. [WebClientService.ARGUMENT_DELETE_TYPE]: WebClientService.DELETE_GROUP_TYPE_LEAVE,
  1089. };
  1090. return this._sendDeletePromise(WebClientService.SUB_TYPE_GROUP, args);
  1091. }
  1092. public deleteGroup(group: threema.GroupReceiver): Promise<any> {
  1093. if (group === null || group === undefined || !group.access.canDelete) {
  1094. return new Promise<any> (
  1095. (resolve, reject) => {
  1096. reject('not allowed');
  1097. });
  1098. }
  1099. const args = {
  1100. [WebClientService.ARGUMENT_RECEIVER_ID]: group.id,
  1101. [WebClientService.ARGUMENT_DELETE_TYPE]: WebClientService.DELETE_GROUP_TYPE_DELETE,
  1102. };
  1103. return this._sendDeletePromise(WebClientService.SUB_TYPE_GROUP, args);
  1104. }
  1105. /**
  1106. * Force-sync a group.
  1107. */
  1108. public syncGroup(group: threema.GroupReceiver): Promise<any> {
  1109. if (group === null || group === undefined || !group.access.canSync) {
  1110. return Promise.reject('not allowed');
  1111. }
  1112. const args = {
  1113. [WebClientService.ARGUMENT_RECEIVER_ID]: group.id,
  1114. };
  1115. return this._sendRequestPromise(WebClientService.SUB_TYPE_GROUP_SYNC, args, 10000);
  1116. }
  1117. /**
  1118. * Create a new distribution list receiver.
  1119. */
  1120. public createDistributionList(
  1121. members: string[],
  1122. name: string = null,
  1123. ): Promise<threema.DistributionListReceiver> {
  1124. const args = null;
  1125. const data = {
  1126. [WebClientService.ARGUMENT_MEMBERS]: members,
  1127. [WebClientService.ARGUMENT_NAME]: name,
  1128. };
  1129. return this._sendCreatePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, args, data);
  1130. }
  1131. public modifyDistributionList(id: string,
  1132. members: string[],
  1133. name: string = null): Promise<threema.DistributionListReceiver> {
  1134. const data = {
  1135. [WebClientService.ARGUMENT_MEMBERS]: members,
  1136. [WebClientService.ARGUMENT_NAME]: name,
  1137. } as any;
  1138. return this._sendUpdatePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, {
  1139. [WebClientService.ARGUMENT_RECEIVER_ID]: id,
  1140. }, data);
  1141. }
  1142. public deleteDistributionList(distributionList: threema.DistributionListReceiver): Promise<any> {
  1143. if (distributionList === null || distributionList === undefined || !distributionList.access.canDelete) {
  1144. return new Promise((resolve, reject) => reject('not allowed'));
  1145. }
  1146. const args = {
  1147. [WebClientService.ARGUMENT_RECEIVER_ID]: distributionList.id,
  1148. };
  1149. return this._sendDeletePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, args);
  1150. }
  1151. /**
  1152. * Remove all messages of a receiver
  1153. * @param {threema.Receiver} receiver
  1154. * @returns {Promise<any>}
  1155. */
  1156. public cleanReceiverConversation(receiver: threema.Receiver): Promise<any> {
  1157. if (receiver === null || receiver === undefined) {
  1158. return new Promise((resolve, reject) => reject('invalid receiver'));
  1159. }
  1160. const args = {
  1161. [WebClientService.ARGUMENT_RECEIVER_TYPE]: receiver.type,
  1162. [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
  1163. };
  1164. return this._sendDeletePromise(WebClientService.SUB_TYPE_CLEAN_RECEIVER_CONVERSATION, args);
  1165. }
  1166. /**
  1167. * Return whether the specified contact is currently typing.
  1168. *
  1169. * This always returns false for groups and distribution lists.
  1170. */
  1171. public isTyping(receiver: threema.ContactReceiver): boolean {
  1172. return this.typing.isTyping(receiver);
  1173. }
  1174. /**
  1175. * Return own identity.
  1176. */
  1177. public getMyIdentity(): threema.Identity {
  1178. return this.myIdentity;
  1179. }
  1180. /**
  1181. * Return the curring quoted message model
  1182. */
  1183. public getQuote(receiver: threema.Receiver): threema.Quote {
  1184. return this.drafts.getQuote(receiver);
  1185. }
  1186. /**
  1187. * Set or remove (if message is null) a quoted message model.
  1188. */
  1189. public setQuote(receiver: threema.Receiver, message: threema.Message = null): void {
  1190. // Remove current quote
  1191. this.drafts.removeQuote(receiver);
  1192. if (message !== null) {
  1193. let quoteText;
  1194. switch (message.type) {
  1195. case 'text':
  1196. quoteText = message.body;
  1197. break;
  1198. case 'location':
  1199. quoteText = message.location.poi;
  1200. break;
  1201. case 'file':
  1202. case 'image':
  1203. quoteText = message.caption;
  1204. break;
  1205. default:
  1206. // Ignore (handled below)
  1207. }
  1208. if (quoteText !== undefined) {
  1209. const quote = {
  1210. identity: message.isOutbox ? this.me.id : message.partnerId,
  1211. text: quoteText,
  1212. } as threema.Quote;
  1213. this.drafts.setQuote(receiver, quote);
  1214. this.$rootScope.$broadcast('onQuoted', {
  1215. receiver: receiver,
  1216. quote: quote,
  1217. });
  1218. }
  1219. }
  1220. }
  1221. /**
  1222. * Set or remove (if string is null) a draft message
  1223. */
  1224. public setDraft(receiver: threema.Receiver, message: string = null): void {
  1225. if (message === null || message.trim().length === 0) {
  1226. this.drafts.removeText(receiver);
  1227. } else {
  1228. this.drafts.setText(receiver, message.trim());
  1229. }
  1230. }
  1231. /**
  1232. * return draft text
  1233. */
  1234. public getDraft(receiver: threema.Receiver): string {
  1235. return this.drafts.getText(receiver);
  1236. }
  1237. /**
  1238. * Reset data related to initialization.
  1239. */
  1240. private _resetInitializationSteps(): void {
  1241. this.$log.debug(this.logTag, 'Reset initialization steps');
  1242. this.initialized.clear();
  1243. this.pendingInitializationStepRoutines = [];
  1244. }
  1245. /**
  1246. * Reset data fields.
  1247. */
  1248. private _resetFields(): void {
  1249. // Reset initialization data
  1250. this._resetInitializationSteps();
  1251. // Create container instances
  1252. this.receivers = this.container.createReceivers();
  1253. this.conversations = this.container.createConversations();
  1254. this.messages = this.container.createMessages();
  1255. this.typingInstance = this.container.createTyping();
  1256. // Add converters (pre-processors)
  1257. this.messages.converter = this.container.Converter.unicodeToEmoji;
  1258. this.conversations.setConverter(this.container.Converter.addReceiverToConversation(this.receivers));
  1259. // Add filters
  1260. this.conversations.setFilter(this.container.Filters.hasData(this.receivers));
  1261. }
  1262. private _requestInitialData(): void {
  1263. // if all conversations are reloaded, clear the message cache
  1264. // to get in sync (we dont know if a message was removed, updated etc..)
  1265. this.messages.clear(this.$rootScope);
  1266. // Request initial data
  1267. this.requestClientInfo();
  1268. this.requestReceivers();
  1269. this.requestConversations();
  1270. this.requestBatteryStatus();
  1271. }
  1272. /**
  1273. * Return a PromiseRequestResult with success=false and the specified error code.
  1274. */
  1275. private promiseRequestError(error: string): threema.PromiseRequestResult<undefined> {
  1276. return {
  1277. success: false,
  1278. error: error,
  1279. };
  1280. }
  1281. private _receiveResponseConfirmAction(message: threema.WireMessage): threema.PromiseRequestResult<void> {
  1282. this.$log.debug('Received receiver response');
  1283. // Unpack and validate args
  1284. const args = message.args;
  1285. if (args === undefined) {
  1286. this.$log.error('Invalid confirmAction response, args missing');
  1287. return this.promiseRequestError('invalidResponse');
  1288. }
  1289. switch (args[WebClientService.ARGUMENT_SUCCESS]) {
  1290. case true:
  1291. return { success: true };
  1292. case false:
  1293. return this.promiseRequestError(args[WebClientService.ARGUMENT_ERROR]);
  1294. default:
  1295. this.$log.error('Invalid confirmAction response, success field is not a boolean');
  1296. return this.promiseRequestError('invalidResponse');
  1297. }
  1298. }
  1299. private _receiveResponseReceivers(message: threema.WireMessage): void {
  1300. this.$log.debug('Received receivers response');
  1301. // Unpack and validate data
  1302. const data = message.data;
  1303. if (data === undefined) {
  1304. this.$log.warn('Invalid receivers response, data missing');
  1305. return;
  1306. }
  1307. // Store receivers
  1308. this.receivers.set(data);
  1309. this.registerInitializationStep(InitializationStep.Receivers);
  1310. }
  1311. private _receiveResponseContactDetail(message: threema.WireMessage): threema.PromiseRequestResult<any> {
  1312. this.$log.debug('Received contact detail');
  1313. // Unpack and validate data
  1314. const args = message.args;
  1315. const data = message.data;
  1316. if (args === undefined || data === undefined) {
  1317. this.$log.error('Invalid contact response, args or data missing');
  1318. return this.promiseRequestError('invalidResponse');
  1319. }
  1320. switch (args[WebClientService.ARGUMENT_SUCCESS]) {
  1321. case true:
  1322. const contactReceiver = this.receivers.contacts
  1323. .get(args[WebClientService.ARGUMENT_IDENTITY]) as threema.ContactReceiver;
  1324. if (data[WebClientService.SUB_TYPE_RECEIVER]) {
  1325. contactReceiver.systemContact =
  1326. data[WebClientService.SUB_TYPE_RECEIVER][WebClientService.ARGUMENT_SYSTEM_CONTACT];
  1327. }
  1328. return {
  1329. success: true,
  1330. data: contactReceiver,
  1331. };
  1332. case false:
  1333. return {
  1334. success: false,
  1335. error: args[WebClientService.ARGUMENT_ERROR],
  1336. };
  1337. default:
  1338. this.$log.error('Invalid contact response, success field is not a boolean');
  1339. return this.promiseRequestError('invalidResponse');
  1340. }
  1341. }
  1342. private _receiveAlert(message: threema.WireMessage): void {
  1343. this.$log.debug('Received alert from device');
  1344. this.alerts.push({
  1345. source: message.args.source,
  1346. type: message.data.type,
  1347. message: message.data.message,
  1348. } as threema.Alert);
  1349. }
  1350. /**
  1351. * Process an incoming contact, group or distributionList response.
  1352. */
  1353. private _receiveResponseReceiver<T extends threema.Receiver>(
  1354. message: threema.WireMessage,
  1355. receiverType: threema.ReceiverType,
  1356. ): threema.PromiseRequestResult<T> {
  1357. this.$log.debug('Received ' + receiverType + ' response');
  1358. // Unpack and validate data
  1359. const args = message.args;
  1360. const data = message.data;
  1361. if (args === undefined || data === undefined) {
  1362. this.$log.error('Invalid ' + receiverType + ' response, args or data missing');
  1363. return this.promiseRequestError('invalidResponse');
  1364. }
  1365. switch (args[WebClientService.ARGUMENT_SUCCESS]) {
  1366. case true:
  1367. // Get receiver instance
  1368. const receiver = data[WebClientService.SUB_TYPE_RECEIVER] as T;
  1369. // Update receiver type if not set
  1370. if (receiver.type === undefined) {
  1371. receiver.type = receiverType;
  1372. }
  1373. // Extend models
  1374. if (isContactReceiver(receiver)) {
  1375. this.receivers.extendContact(receiver);
  1376. } else if (isGroupReceiver(receiver)) {
  1377. this.receivers.extendGroup(receiver);
  1378. } else if (isDistributionListReceiver(receiver)) {
  1379. this.receivers.extendDistributionList(receiver);
  1380. }
  1381. return {
  1382. success: true,
  1383. data: receiver,
  1384. };
  1385. case false:
  1386. return this.promiseRequestError(args[WebClientService.ARGUMENT_ERROR]);
  1387. default:
  1388. this.$log.error('Invalid ' + receiverType + ' response, success field is not a boolean');
  1389. return this.promiseRequestError('invalidResponse');
  1390. }
  1391. }
  1392. /**
  1393. * Handle new or modified contacts.
  1394. */
  1395. private _receiveResponseContact(message: threema.WireMessage):
  1396. threema.PromiseRequestResult<threema.ContactReceiver> {
  1397. return this._receiveResponseReceiver(message, 'contact');
  1398. }
  1399. /**
  1400. * Handle new or modified groups.
  1401. */
  1402. private _receiveResponseGroup(message: threema.WireMessage):
  1403. threema.PromiseRequestResult<threema.GroupReceiver> {
  1404. return this._receiveResponseReceiver(message, 'group');
  1405. }
  1406. /**
  1407. * Handle new or modified distribution lists.
  1408. */
  1409. private _receiveResponseDistributionList(message: threema.WireMessage):
  1410. threema.PromiseRequestResult<threema.DistributionListReceiver> {
  1411. return this._receiveResponseReceiver(message, 'distributionList');
  1412. }
  1413. private _receiveResponseCreateMessage(message: threema.WireMessage): threema.PromiseRequestResult<string> {
  1414. this.$log.debug('Received create message response');
  1415. // Unpack data and arguments
  1416. const args = message.args;
  1417. const data = message.data;
  1418. if (args === undefined || data === undefined) {
  1419. this.$log.warn('Invalid create message received, arguments or data missing');
  1420. return this.promiseRequestError('invalidResponse');
  1421. }
  1422. switch (args[WebClientService.ARGUMENT_SUCCESS]) {
  1423. case true:
  1424. const receiverType: threema.ReceiverType = args[WebClientService.ARGUMENT_RECEIVER_TYPE];
  1425. const receiverId: string = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1426. const temporaryId: string = args[WebClientService.ARGUMENT_TEMPORARY_ID];
  1427. const messageId: string = data[WebClientService.ARGUMENT_MESSAGE_ID];
  1428. if (receiverType === undefined || receiverId === undefined ||
  1429. temporaryId === undefined || messageId === undefined) {
  1430. this.$log.warn('Invalid create received [type, id, temporaryId arg ' +
  1431. 'or messageId in data missing]');
  1432. return this.promiseRequestError('invalidResponse');
  1433. }
  1434. this.messages.bindTemporaryToMessageId(
  1435. {
  1436. type: receiverType,
  1437. id: receiverId,
  1438. } as threema.Receiver,
  1439. temporaryId,
  1440. messageId,
  1441. );
  1442. return { success: true, data: messageId };
  1443. case false:
  1444. return this.promiseRequestError(args[WebClientService.ARGUMENT_ERROR]);
  1445. default:
  1446. this.$log.error('Invalid create message response, success field is not a boolean');
  1447. return this.promiseRequestError('invalidResponse');
  1448. }
  1449. }
  1450. private _receiveResponseConversations(message: threema.WireMessage) {
  1451. this.$log.debug('Received conversations response');
  1452. const data = message.data;
  1453. if (data === undefined) {
  1454. this.$log.warn('Invalid conversation response, data missing');
  1455. } else {
  1456. // if a avatar was set on a conversation
  1457. // convert and copy to the receiver
  1458. for (const conversation of data) {
  1459. if (conversation.avatar !== undefined && conversation.avatar !== null) {
  1460. const receiver = this.receivers.getData({
  1461. id: conversation.id,
  1462. type: conversation.type,
  1463. } as threema.Receiver);
  1464. if (receiver !== undefined
  1465. && receiver.avatar === undefined) {
  1466. receiver.avatar = {
  1467. low: this.$filter('bufferToUrl')(conversation.avatar, 'image/png'),
  1468. };
  1469. }
  1470. // reset avatar from object
  1471. delete conversation.avatar;
  1472. }
  1473. this.conversations.updateOrAdd(conversation);
  1474. }
  1475. }
  1476. this.updateUnreadCount();
  1477. this.registerInitializationStep(InitializationStep.Conversations);
  1478. }
  1479. private _receiveResponseConversation(message: threema.WireMessage) {
  1480. this.$log.debug('Received conversation response');
  1481. const args = message.args;
  1482. const data = message.data;
  1483. if (args === undefined || data === undefined) {
  1484. this.$log.warn('Invalid conversation response, data or arguments missing');
  1485. return;
  1486. }
  1487. // Unpack required argument fields
  1488. const type: string = args[WebClientService.ARGUMENT_MODE];
  1489. switch (type) {
  1490. case WebClientService.ARGUMENT_MODE_NEW:
  1491. this.conversations.add(data);
  1492. break;
  1493. case WebClientService.ARGUMENT_MODE_MODIFIED:
  1494. // A conversation update *can* mean that a new message arrived.
  1495. // To find out, we'll look at the unread count. If it has been
  1496. // incremented, it must be a new message.
  1497. if (data.unreadCount > 0) {
  1498. // Find the correct conversation in the conversation list
  1499. const conversation = this.conversations.find(data);
  1500. if (data === null) {
  1501. // Conversation not found, add it!
  1502. this.conversations.add(data);
  1503. this.onNewMessage(data.latestMessage, conversation);
  1504. } else {
  1505. // Check for unread count changes
  1506. const unreadCountIncreased = data.unreadCount > conversation.unreadCount;
  1507. const unreadCountDecreased = data.unreadCount < conversation.unreadCount;
  1508. // Update the conversation
  1509. this.conversations.updateOrAdd(data);
  1510. // If the unreadcount has increased, we received a new message.
  1511. // Otherwise, if it has decreased, hide the notification.
  1512. if (unreadCountIncreased) {
  1513. this.onNewMessage(data.latestMessage, conversation);
  1514. } else if (unreadCountDecreased) {
  1515. this.notificationService.hideNotification(data.type + '-' + data.id);
  1516. }
  1517. }
  1518. } else {
  1519. // Update the conversation and hide any notifications
  1520. this.conversations.updateOrAdd(data);
  1521. this.notificationService.hideNotification(data.type + '-' + data.id);
  1522. }
  1523. break;
  1524. case WebClientService.ARGUMENT_MODE_REMOVED:
  1525. this.conversations.remove(data);
  1526. // Remove all cached messages
  1527. this.messages.clearReceiverMessages((data as threema.Receiver));
  1528. this.receiverListener.forEach((listener: threema.ReceiverListener) => {
  1529. this.$log.debug('call on removed listener');
  1530. listener.onRemoved(data);
  1531. });
  1532. break;
  1533. default:
  1534. this.$log.warn('Received conversation without a mode');
  1535. return;
  1536. }
  1537. this.updateUnreadCount();
  1538. }
  1539. private _receiveResponseMessages(message: threema.WireMessage): void {
  1540. this.$log.debug('Received message response');
  1541. // Unpack data and arguments
  1542. const args = message.args;
  1543. const data = message.data as threema.Message[];
  1544. if (args === undefined || data === undefined) {
  1545. this.$log.warn('Invalid message response, data or arguments missing');
  1546. return;
  1547. }
  1548. // Unpack required argument fields
  1549. const type: string = args[WebClientService.ARGUMENT_RECEIVER_TYPE];
  1550. const id: string = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1551. let more: boolean = args[WebClientService.ARGUMENT_HAS_MORE];
  1552. if (type === undefined || id === undefined || more === undefined) {
  1553. this.$log.warn('Invalid message response, argument field missing');
  1554. return;
  1555. }
  1556. // If there's no data returned, override `more` field.
  1557. if (data.length === 0) {
  1558. more = false;
  1559. }
  1560. // Check if the page was requested
  1561. const receiver = {type: type, id: id} as threema.Receiver;
  1562. // Set as loaded
  1563. this.loadingMessages.delete(receiver.type + receiver.id);
  1564. if (this.messages.isRequested(receiver)) {
  1565. // Add messages
  1566. this.messages.addOlder(receiver, data);
  1567. // Clear pending request
  1568. this.messages.clearRequested(receiver);
  1569. // Set "more" flag to indicate that more (older) messages are available.
  1570. this.messages.setMore(receiver, more);
  1571. this.messages.notify(receiver, this.$rootScope);
  1572. } else {
  1573. this.$log.warn("Ignoring message response that hasn't been requested");
  1574. return;
  1575. }
  1576. }
  1577. private _receiveResponseAvatar(message: threema.WireMessage): threema.PromiseRequestResult<any> {
  1578. this.$log.debug('Received avatar response');
  1579. // Unpack data and arguments
  1580. const args = message.args;
  1581. if (args === undefined) {
  1582. this.$log.warn('Invalid message response: arguments missing');
  1583. return this.promiseRequestError('invalidResponse');
  1584. }
  1585. const data = message.data;
  1586. if (data === undefined) {
  1587. // It's ok, a receiver without a avatar
  1588. return { success: true, data: null };
  1589. }
  1590. // Unpack required argument fields
  1591. const type = args[WebClientService.ARGUMENT_RECEIVER_TYPE];
  1592. const id = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1593. const highResolution = args[WebClientService.ARGUMENT_AVATAR_HIGH_RESOLUTION];
  1594. if (type === undefined || id === undefined || highResolution === undefined) {
  1595. this.$log.warn('Invalid avatar response, argument field missing');
  1596. return this.promiseRequestError('invalidResponse');
  1597. }
  1598. // Set avatar for receiver according to resolution
  1599. const field: string = highResolution ? 'high' : 'low';
  1600. const receiverData = this.receivers.getData(args);
  1601. if (receiverData.avatar === null || receiverData.avatar === undefined) {
  1602. receiverData.avatar = {};
  1603. }
  1604. const avatar = this.$filter('bufferToUrl')(data, 'image/png');
  1605. receiverData.avatar[field] = avatar;
  1606. return { success: true, data: avatar };
  1607. }
  1608. private _receiveResponseThumbnail(message: threema.WireMessage): threema.PromiseRequestResult<any> {
  1609. this.$log.debug('Received thumbnail response');
  1610. // Unpack data and arguments
  1611. const args = message.args;
  1612. if (args === undefined) {
  1613. this.$log.warn('Invalid message response: arguments missing');
  1614. return {
  1615. success: false,
  1616. data: 'invalidResponse',
  1617. };
  1618. }
  1619. const data = message.data;
  1620. if ( data === undefined) {
  1621. // It's ok, a message without a thumbnail
  1622. return {
  1623. success: true,
  1624. data: null,
  1625. };
  1626. }
  1627. // Unpack required argument fields
  1628. const type = args[WebClientService.ARGUMENT_RECEIVER_TYPE];
  1629. const id = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1630. const messageId: string = args[WebClientService.ARGUMENT_MESSAGE_ID];
  1631. if (type === undefined || id === undefined || messageId === undefined ) {
  1632. this.$log.warn('Invalid thumbnail response, argument field missing');
  1633. return {
  1634. success: false,
  1635. data: 'invalidResponse',
  1636. };
  1637. }
  1638. this.messages.setThumbnail( this.receivers.getData(args), messageId, data);
  1639. return {
  1640. success: true,
  1641. data: data};
  1642. }
  1643. private _receiveResponseBlob(message: threema.WireMessage): threema.PromiseRequestResult<ArrayBuffer> {
  1644. this.$log.debug('Received blob response');
  1645. // Unpack data and arguments
  1646. const args = message.args;
  1647. const data = message.data;
  1648. if (args === undefined || data === undefined) {
  1649. this.$log.warn('Invalid message response, data or arguments missing');
  1650. return {
  1651. success: false,
  1652. };
  1653. }
  1654. // Unpack required argument fields
  1655. const receiverType = args[WebClientService.ARGUMENT_RECEIVER_TYPE];
  1656. const receiverId = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1657. const msgId: string = args[WebClientService.ARGUMENT_MESSAGE_ID];
  1658. if (receiverType === undefined || receiverId === undefined || msgId === undefined) {
  1659. this.$log.warn('Invalid blob response, argument field missing');
  1660. return {
  1661. success: false,
  1662. };
  1663. }
  1664. // Unpack data
  1665. const buffer: ArrayBuffer = data[WebClientService.DATA_FIELD_BLOB_BLOB];
  1666. if (buffer === undefined) {
  1667. this.$log.warn('Invalid blob response, data field missing');
  1668. return {
  1669. success: false,
  1670. };
  1671. }
  1672. this.blobCache.set(msgId + receiverType, buffer);
  1673. // Download to browser
  1674. return {
  1675. success: true,
  1676. data: buffer,
  1677. };
  1678. }
  1679. private _receiveUpdateMessage(message: threema.WireMessage): void {
  1680. this.$log.debug('Received message update');
  1681. // Unpack data and arguments
  1682. const args = message.args;
  1683. const data: threema.Message = message.data;
  1684. if (args === undefined || data === undefined) {
  1685. this.$log.warn('Invalid message update, data or arguments missing');
  1686. return;
  1687. }
  1688. // Unpack required argument fields
  1689. const type: string = args[WebClientService.ARGUMENT_RECEIVER_TYPE];
  1690. const id: string = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1691. const mode: string = args[WebClientService.ARGUMENT_MODE];
  1692. if (type === undefined || id === undefined || mode === undefined) {
  1693. this.$log.warn('Invalid message update, argument field missing');
  1694. return;
  1695. }
  1696. const receiver = {type: type, id: id} as threema.Receiver;
  1697. // React depending on mode
  1698. switch (mode) {
  1699. case WebClientService.ARGUMENT_MODE_NEW:
  1700. this.$log.debug('New message', data.id);
  1701. // It's possible that this message already exists (placeholder message on send).
  1702. // Try to update it first. If not, add it as a new msg.
  1703. if (!this.messages.update(receiver, data)) {
  1704. this.messages.addNewer(receiver, [data]);
  1705. }
  1706. break;
  1707. case WebClientService.ARGUMENT_MODE_MODIFIED:
  1708. this.$log.debug('Modified message', data.id);
  1709. this.messages.update(receiver, data);
  1710. break;
  1711. case WebClientService.ARGUMENT_MODE_REMOVED:
  1712. this.messages.remove(receiver, data.id);
  1713. break;
  1714. default:
  1715. this.$log.warn('Invalid message response, unknown mode:', mode);
  1716. }
  1717. }
  1718. private _receiveUpdateReceiver(message: threema.WireMessage): void {
  1719. this.$log.debug('Received receiver update or delete');
  1720. // Unpack data and arguments
  1721. const args = message.args;
  1722. const data = message.data;
  1723. if (args === undefined || data === undefined) {
  1724. this.$log.warn('Invalid receiver update, data or arguments missing');
  1725. return;
  1726. }
  1727. // Unpack required argument fields
  1728. const type = args[WebClientService.ARGUMENT_RECEIVER_TYPE] as threema.ReceiverType;
  1729. const id = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1730. const mode: 'new' | 'modified' | 'removed' = args[WebClientService.ARGUMENT_MODE];
  1731. if (type === undefined || mode === undefined || id === undefined) {
  1732. this.$log.warn('Invalid receiver update, argument field missing');
  1733. return;
  1734. }
  1735. // React depending on mode
  1736. switch (mode) {
  1737. case WebClientService.ARGUMENT_MODE_NEW:
  1738. case WebClientService.ARGUMENT_MODE_MODIFIED:
  1739. // Add or update a certain receiver
  1740. const updatedReceiver = this.receivers.extend(type, data);
  1741. // Remove all cached messages if the receiver was moved to "locked" state
  1742. if (updatedReceiver !== undefined && updatedReceiver.locked) {
  1743. this.messages.clearReceiverMessages(updatedReceiver);
  1744. }
  1745. break;
  1746. case WebClientService.ARGUMENT_MODE_REMOVED:
  1747. // Remove a certain receiver
  1748. (this.receivers.get(type) as Map<string, threema.Receiver>).delete(id);
  1749. break;
  1750. default:
  1751. this.$log.warn('Invalid receiver response, unknown mode:', mode);
  1752. }
  1753. }
  1754. private _receiveUpdateReceivers(message: threema.WireMessage): void {
  1755. this.$log.debug('Received receivers update');
  1756. // Unpack data and arguments
  1757. const args = message.args;
  1758. const data = message.data;
  1759. if (args === undefined || data === undefined) {
  1760. this.$log.warn('Invalid receiver update, data or arguments missing');
  1761. return;
  1762. }
  1763. // Unpack required argument fields
  1764. const type = args[WebClientService.ARGUMENT_RECEIVER_TYPE] as threema.ReceiverType;
  1765. if (type === undefined) {
  1766. this.$log.warn('Invalid receivers update, argument field missing');
  1767. return;
  1768. }
  1769. // Refresh lists of receivers
  1770. switch (type) {
  1771. case 'me':
  1772. this.receivers.setMe(data);
  1773. break;
  1774. case 'contact':
  1775. this.receivers.setContacts(data);
  1776. break;
  1777. case 'group':
  1778. this.receivers.setGroups(data);
  1779. break;
  1780. case 'distributionList':
  1781. this.receivers.setDistributionLists(data);
  1782. break;
  1783. default:
  1784. this.$log.warn('Unknown receiver type:', type);
  1785. }
  1786. }
  1787. private _receiveUpdateTyping(message: threema.WireMessage): void {
  1788. this.$log.debug('Received typing update');
  1789. // Unpack data and arguments
  1790. const args = message.args;
  1791. const data = message.data;
  1792. if (args === undefined || data === undefined) {
  1793. this.$log.warn('Invalid typing update, data or arguments missing');
  1794. return;
  1795. }
  1796. // Unpack required argument fields
  1797. const id: string = args[WebClientService.ARGUMENT_RECEIVER_ID];
  1798. const isTyping: boolean = args[WebClientService.ARGUMENT_CONTACT_IS_TYPING];
  1799. if (id === undefined || isTyping === undefined) {
  1800. this.$log.warn('Invalid typing update, argument field missing');
  1801. return;
  1802. }
  1803. // Store or remove typing notification.
  1804. // Note that we know that the receiver must be a contact, because
  1805. // groups and distribution lists can't type.
  1806. const receiver = {id: id, type: 'contact'} as threema.ContactReceiver;
  1807. if (isTyping === true) {
  1808. this.typing.setTyping(receiver);
  1809. } else {
  1810. this.typing.unsetTyping(receiver);
  1811. }
  1812. }
  1813. /**
  1814. * Process an incoming battery status message.
  1815. */
  1816. private _receiveUpdateBatteryStatus(message: threema.WireMessage): void {
  1817. this.$log.debug('Received battery status');
  1818. // Unpack data and arguments
  1819. const data = message.data as threema.BatteryStatus;
  1820. if (data === undefined) {
  1821. this.$log.warn('Invalid battery status message, data missing');
  1822. return;
  1823. }
  1824. // Set battery status
  1825. this.batteryStatusService.setStatus(data);
  1826. this.$log.debug('[BatteryStatusService]', this.batteryStatusService.toString());
  1827. }
  1828. /**
  1829. * The peer sends the device information string. This can be used to
  1830. * identify the active session.
  1831. */
  1832. private _receiveResponseClientInfo(message: threema.WireMessage): void {
  1833. this.$log.debug('Received client info');
  1834. const args = message.args;
  1835. if (args === undefined) {
  1836. this.$log.warn('Invalid client info, argument field missing');
  1837. return;
  1838. }
  1839. this.clientInfo = args as threema.ClientInfo;
  1840. this.$log.debug('Client device:', this.clientInfo.device);
  1841. // Store push token
  1842. if (this.clientInfo.myPushToken) {
  1843. this.pushToken = this.clientInfo.myPushToken;
  1844. this.pushService.init(this.pushToken);
  1845. }
  1846. // Set own identity
  1847. this.myIdentity = {
  1848. identity: this.clientInfo.myAccount.identity,
  1849. publicKey: this.clientInfo.myAccount.publicKey,
  1850. publicNickname: this.clientInfo.myAccount.publicNickname,
  1851. fingerprint: this.fingerPrintService.generate(this.clientInfo.myAccount.publicKey),
  1852. } as threema.Identity;
  1853. this.registerInitializationStep(InitializationStep.ClientInfo);
  1854. }
  1855. public setPassword(password: string) {
  1856. // If a password has been set, store trusted key and push token
  1857. if (this._maybeTrustKeys(password)) {
  1858. // Saved trusted key, send information to client
  1859. this.sendKeyPersisted();
  1860. }
  1861. }
  1862. /**
  1863. * Reset all Fields and clear the blob cache
  1864. */
  1865. public clearCache(): void {
  1866. this._resetFields();
  1867. this.blobCache.clear();
  1868. }
  1869. /**
  1870. * Return the max text length
  1871. * @returns {number}
  1872. */
  1873. public getMaxTextLength(): number {
  1874. return WebClientService.MAX_TEXT_LENGTH;
  1875. }
  1876. /**
  1877. * Returns the max group member size
  1878. * @returns {number}
  1879. */
  1880. public getMaxGroupMemberSize(): number {
  1881. return this.clientInfo && this.clientInfo.maxGroupSize ? this.clientInfo.maxGroupSize : 50;
  1882. }
  1883. /**
  1884. * Called when a new message arrives.
  1885. */
  1886. private onNewMessage(message: threema.Message, conversation: threema.Conversation): void {
  1887. // Ignore message from active receivers (and if the browser tab is visible)
  1888. if (this.browserService.isVisible()
  1889. && this.receiverService.compare(conversation, this.receiverService.getActive())) {
  1890. return;
  1891. }
  1892. const sender: threema.Receiver = conversation.receiver;
  1893. // Do not show any notifications on private chats
  1894. if (sender === undefined || sender.locked) {
  1895. return;
  1896. }
  1897. // Do not show any notifications on muted chats
  1898. if (conversation.isMuted === true) {
  1899. return;
  1900. }
  1901. // Determine sender and partner name (used for notification)
  1902. let senderName = sender.id;
  1903. if (sender.displayName) {
  1904. senderName = sender.displayName;
  1905. } else if (sender.type === 'contact') {
  1906. senderName = '~' + (sender as threema.ContactReceiver).publicNickname;
  1907. }
  1908. const partner = this.receivers.getData({
  1909. id: message.partnerId,
  1910. type: 'contact',
  1911. } as threema.Receiver) as threema.ContactReceiver;
  1912. const partnerName = partner.displayName || ('~' + partner.publicNickname);
  1913. // Show notification
  1914. this.$translate('messenger.MESSAGE_NOTIFICATION_SUBJECT', {messageCount: 1 + conversation.unreadCount})
  1915. .then((titlePrefix) => {
  1916. const title = `${titlePrefix} ${senderName}`;
  1917. let body = '';
  1918. const messageType = message.type;
  1919. const caption = message.caption;
  1920. let captionString = '';
  1921. if (caption !== undefined) {
  1922. captionString = captionString + ': ' + caption;
  1923. }
  1924. const messageTypeString = this.$translate.instant('messageTypes.' + messageType);
  1925. switch (messageType as threema.MessageType) {
  1926. case 'text':
  1927. body = message.body;
  1928. break;
  1929. case 'location':
  1930. body = messageTypeString + ': ' + message.location.poi;
  1931. break;
  1932. case 'file':
  1933. if (message.file.type === 'image/gif') {
  1934. body = this.$translate.instant('messageTypes.' + 'gif') + captionString;
  1935. break;
  1936. }
  1937. // Display caption, if available otherwise use filename
  1938. if (captionString.length > 0) {
  1939. body = messageTypeString + captionString;
  1940. } else {
  1941. body = messageTypeString + ': ' + message.file.name;
  1942. }
  1943. break;
  1944. case 'ballot':
  1945. // TODO Show ballot title if ballot messages are implemented in the web version
  1946. body = messageTypeString;
  1947. break;
  1948. case 'voipStatus':
  1949. let translationKey: string;
  1950. switch ((message as threema.Message).voip.status) {
  1951. case 1:
  1952. translationKey = 'CALL_MISSED';
  1953. break;
  1954. case 2:
  1955. translationKey = message.isOutbox ? 'CALL_FINISHED_IN' : 'CALL_FINISHED_OUT';
  1956. break;
  1957. case 3:
  1958. translationKey = 'CALL_REJECTED';
  1959. break;
  1960. case 4:
  1961. translationKey = 'CALL_ABORTED';
  1962. break;
  1963. default:
  1964. // No default
  1965. }
  1966. if (translationKey !== undefined) {
  1967. body = this.$translate.instant('voip.' + translationKey);
  1968. }
  1969. break;
  1970. default:
  1971. // Image, video and audio
  1972. body = messageTypeString + captionString;
  1973. }
  1974. if (conversation.type === 'group') {
  1975. body = partnerName + ': ' + body;
  1976. }
  1977. const tag = conversation.type + '-' + conversation.id;
  1978. const avatar = (sender.avatar && sender.avatar.low) ? sender.avatar.low : null;
  1979. this.notificationService.showNotification(tag, title, body, avatar, () => {
  1980. this.$state.go('messenger.home.conversation', {
  1981. type: conversation.type,
  1982. id: conversation.id,
  1983. initParams: null,
  1984. });
  1985. });
  1986. });
  1987. }
  1988. /**
  1989. * If a password has been set, store own private permanent key and public
  1990. * key of the peer in the trusted key store.
  1991. */
  1992. private _maybeTrustKeys(password: string): boolean {
  1993. if (password !== undefined && password !== null && password.length > 0) {
  1994. this.trustedKeyStore.storeTrustedKey(
  1995. this.salty.keyStore.publicKeyBytes,
  1996. this.salty.keyStore.secretKeyBytes,
  1997. this.salty.peerPermanentKeyBytes,
  1998. this.pushToken,
  1999. password,
  2000. );
  2001. this.$log.info('Stored trusted key');
  2002. return true;
  2003. }
  2004. return false;
  2005. }
  2006. private _sendRequest(type, args = null): void {
  2007. const message: threema.WireMessage = {
  2008. type: WebClientService.TYPE_REQUEST,
  2009. subType: type,
  2010. };
  2011. if (args) {
  2012. message.args = args;
  2013. }
  2014. this.send(message);
  2015. }
  2016. private _sendPromiseMessage(message: threema.WireMessage, timeout: number = null): Promise<any> {
  2017. // create arguments on wired message
  2018. if (message.args === undefined || message.args === null) {
  2019. message.args = {};
  2020. }
  2021. let promiseId = message.args[WebClientService.ARGUMENT_TEMPORARY_ID];
  2022. if (promiseId === undefined) {
  2023. // create a random id to identity the promise
  2024. promiseId = 'p' + Math.random().toString(36).substring(7);
  2025. message.args[WebClientService.ARGUMENT_TEMPORARY_ID] = promiseId;
  2026. }
  2027. return new Promise(
  2028. (resolve, reject) => {
  2029. const p = {
  2030. resolve: resolve,
  2031. reject: reject,
  2032. } as threema.PromiseCallbacks;
  2033. this.requestPromises.set(promiseId, p);
  2034. if (timeout !== null && timeout > 0) {
  2035. this.$timeout(() => {
  2036. p.reject('timeout');
  2037. this.requestPromises.delete(promiseId);
  2038. }, timeout);
  2039. }
  2040. this.send(message);
  2041. },
  2042. );
  2043. }
  2044. /**
  2045. * Send a request and return a promise.
  2046. *
  2047. * The promise will be resolved if a response arrives with the same temporary ID.
  2048. *
  2049. * @param timeout Optional request timeout in ms
  2050. */
  2051. private _sendRequestPromise(type, args = null, timeout: number = null): Promise<any> {
  2052. const message: threema.WireMessage = {
  2053. type: WebClientService.TYPE_REQUEST,
  2054. subType: type,
  2055. args: args,
  2056. };
  2057. return this._sendPromiseMessage(message, timeout);
  2058. }
  2059. private _sendCreatePromise(type, args = null, data: any = null, timeout: number = null): Promise<any> {
  2060. const message: threema.WireMessage = {
  2061. type: WebClientService.TYPE_CREATE,
  2062. subType: type,
  2063. args: args,
  2064. data: data,
  2065. };
  2066. return this._sendPromiseMessage(message, timeout);
  2067. }
  2068. private _sendUpdatePromise(type, args = null, data: any = null, timeout: number = null): Promise<any> {
  2069. const message: threema.WireMessage = {
  2070. type: WebClientService.TYPE_UPDATE,
  2071. subType: type,
  2072. data: data,
  2073. args: args,
  2074. };
  2075. return this._sendPromiseMessage(message, timeout);
  2076. }
  2077. private _sendCreate(type, data, args = null): void {
  2078. const message: threema.WireMessage = {
  2079. type: WebClientService.TYPE_CREATE,
  2080. subType: type,
  2081. data: data,
  2082. };
  2083. if (args) {
  2084. message.args = args;
  2085. }
  2086. this.send(message);
  2087. }
  2088. private _sendDelete(type, args, data = null): void {
  2089. const message: threema.WireMessage = {
  2090. type: WebClientService.TYPE_DELETE,
  2091. subType: type,
  2092. data: data,
  2093. args: args,
  2094. };
  2095. this.send(message);
  2096. }
  2097. private _sendDeletePromise(type, args, data: any = null, timeout: number = null): Promise<any> {
  2098. const message: threema.WireMessage = {
  2099. type: WebClientService.TYPE_DELETE,
  2100. subType: type,
  2101. data: data,
  2102. args: args,
  2103. };
  2104. return this._sendPromiseMessage(message, timeout);
  2105. }
  2106. private _receiveRequest(type, message): void {
  2107. this.$log.warn('Ignored request with type:', type);
  2108. }
  2109. private _receivePromise(message: any, receiveResult: threema.PromiseRequestResult<any>) {
  2110. if (
  2111. message !== undefined
  2112. && message.args !== undefined
  2113. && message.args[WebClientService.ARGUMENT_TEMPORARY_ID] !== undefined) {
  2114. // find pending promise
  2115. const promiseId = message.args[WebClientService.ARGUMENT_TEMPORARY_ID];
  2116. if (this.requestPromises.has(promiseId)) {
  2117. const promise = this.requestPromises.get(promiseId);
  2118. if (receiveResult === null || receiveResult === undefined) {
  2119. promise.reject('unknown');
  2120. } else if (receiveResult.success) {
  2121. promise.resolve(receiveResult.data);
  2122. } else {
  2123. promise.reject(receiveResult.error);
  2124. }
  2125. // remove from map
  2126. this.requestPromises.delete(promiseId);
  2127. }
  2128. }
  2129. }
  2130. private _receiveResponse(type, message): void {
  2131. let receiveResult: threema.PromiseRequestResult<any>;
  2132. switch (type) {
  2133. case WebClientService.SUB_TYPE_CONFIRM_ACTION:
  2134. receiveResult = this._receiveResponseConfirmAction(message);
  2135. break;
  2136. case WebClientService.SUB_TYPE_RECEIVERS:
  2137. this._receiveResponseReceivers(message);
  2138. break;
  2139. case WebClientService.SUB_TYPE_CONVERSATIONS:
  2140. this.runAfterInitializationSteps([
  2141. InitializationStep.Receivers,
  2142. ], () => {
  2143. this._receiveResponseConversations(message);
  2144. });
  2145. break;
  2146. case WebClientService.SUB_TYPE_CONVERSATION:
  2147. this._receiveResponseConversation(message);
  2148. break;
  2149. case WebClientService.SUB_TYPE_MESSAGE:
  2150. this._receiveResponseMessages(message);
  2151. break;
  2152. case WebClientService.SUB_TYPE_AVATAR:
  2153. receiveResult = this._receiveResponseAvatar(message);
  2154. break;
  2155. case WebClientService.SUB_TYPE_THUMBNAIL:
  2156. receiveResult = this._receiveResponseThumbnail(message);
  2157. break;
  2158. case WebClientService.SUB_TYPE_BLOB:
  2159. receiveResult = this._receiveResponseBlob(message);
  2160. break;
  2161. case WebClientService.SUB_TYPE_CLIENT_INFO:
  2162. this._receiveResponseClientInfo(message);
  2163. break;
  2164. case WebClientService.SUB_TYPE_CONTACT_DETAIL:
  2165. receiveResult = this._receiveResponseContactDetail(message);
  2166. break;
  2167. case WebClientService.SUB_TYPE_ALERT:
  2168. this._receiveAlert(message);
  2169. break;
  2170. case WebClientService.SUB_TYPE_BATTERY_STATUS:
  2171. this._receiveUpdateBatteryStatus(message);
  2172. break;
  2173. default:
  2174. this.$log.warn('Ignored response with type:', type);
  2175. return;
  2176. }
  2177. this._receivePromise(message, receiveResult);
  2178. }
  2179. private _receiveUpdate(type, message): void {
  2180. let receiveResult;
  2181. switch (type) {
  2182. case WebClientService.SUB_TYPE_RECEIVER:
  2183. this._receiveUpdateReceiver(message);
  2184. break;
  2185. case WebClientService.SUB_TYPE_RECEIVERS:
  2186. this._receiveUpdateReceivers(message);
  2187. break;
  2188. case WebClientService.SUB_TYPE_MESSAGE:
  2189. this._receiveUpdateMessage(message);
  2190. break;
  2191. case WebClientService.SUB_TYPE_TYPING:
  2192. this._receiveUpdateTyping(message);
  2193. break;
  2194. case WebClientService.SUB_TYPE_BATTERY_STATUS:
  2195. this._receiveUpdateBatteryStatus(message);
  2196. break;
  2197. case WebClientService.SUB_TYPE_CONTACT:
  2198. receiveResult = this._receiveResponseContact(message);
  2199. break;
  2200. case WebClientService.SUB_TYPE_GROUP:
  2201. receiveResult = this._receiveResponseGroup(message);
  2202. break;
  2203. case WebClientService.SUB_TYPE_DISTRIBUTION_LIST:
  2204. receiveResult = this._receiveResponseDistributionList(message);
  2205. break;
  2206. default:
  2207. this.$log.warn('Ignored update with type:', type);
  2208. return;
  2209. }
  2210. this._receivePromise(message, receiveResult);
  2211. }
  2212. private _receiveCreate(type, message): void {
  2213. let receiveResult: threema.PromiseRequestResult<any>;
  2214. switch (type) {
  2215. case WebClientService.SUB_TYPE_CONTACT:
  2216. receiveResult = this._receiveResponseContact(message);
  2217. break;
  2218. case WebClientService.SUB_TYPE_GROUP:
  2219. receiveResult = this._receiveResponseGroup(message);
  2220. break;
  2221. case WebClientService.SUB_TYPE_DISTRIBUTION_LIST:
  2222. receiveResult = this._receiveResponseDistributionList(message);
  2223. break;
  2224. case WebClientService.SUB_TYPE_TEXT_MESSAGE:
  2225. case WebClientService.SUB_TYPE_FILE_MESSAGE:
  2226. receiveResult = this._receiveResponseCreateMessage(message);
  2227. break;
  2228. default:
  2229. this.$log.warn('Ignored response with type:', type);
  2230. return;
  2231. }
  2232. this._receivePromise(message, receiveResult);
  2233. }
  2234. private _receiveDelete(type, message): void {
  2235. let receiveResult;
  2236. switch (type) {
  2237. case WebClientService.SUB_TYPE_CONTACT_DETAIL:
  2238. receiveResult = this._receiveUpdateReceiver(message);
  2239. break;
  2240. default:
  2241. this.$log.warn('Ignored delete with type:', type);
  2242. return;
  2243. }
  2244. this._receivePromise(message, receiveResult);
  2245. }
  2246. /**
  2247. * Encode an object using the msgpack format.
  2248. */
  2249. private msgpackEncode(data: any): Uint8Array {
  2250. return msgpack.encode(data, this.msgpackEncoderOptions);
  2251. }
  2252. /**
  2253. * Decode an object using the msgpack format.
  2254. */
  2255. private msgpackDecode(bytes: Uint8Array): any {
  2256. return msgpack.decode(bytes, this.msgpackDecoderOptions);
  2257. }
  2258. /**
  2259. * Send a message through the secure data channel.
  2260. */
  2261. private send(message: threema.WireMessage): void {
  2262. this.$log.debug('Sending', message.type + '/' + message.subType, 'message');
  2263. if (this.config.MSG_DEBUGGING) {
  2264. this.$log.debug('[Message] Outgoing:', message.type, '/', message.subType, message);
  2265. }
  2266. switch (this.chosenTask) {
  2267. case threema.ChosenTask.WebRTC:
  2268. // Send bytes through WebRTC DataChannel
  2269. const bytes: Uint8Array = this.msgpackEncode(message);
  2270. this.secureDataChannel.send(bytes);
  2271. break;
  2272. case threema.ChosenTask.RelayedData:
  2273. // Send bytes through e2e encrypted WebSocket
  2274. this.relayedDataTask.sendMessage(message);
  2275. break;
  2276. }
  2277. }
  2278. /**
  2279. * Handle incoming message bytes from the SecureDataChannel.
  2280. */
  2281. private handleIncomingMessageBytes(bytes: Uint8Array): void {
  2282. this.$log.debug('New incoming message (' + bytes.byteLength + ' bytes)');
  2283. if (this.config.MSGPACK_DEBUGGING) {
  2284. this.$log.debug('Incoming message payload: ' + msgpackVisualizer(bytes));
  2285. }
  2286. // Decode bytes
  2287. const message: threema.WireMessage = this.msgpackDecode(bytes);
  2288. return this.handleIncomingMessage(message, false);
  2289. }
  2290. /**
  2291. * Handle incoming incoming from the SecureDataChannel
  2292. * or from the relayed data WebSocket.
  2293. */
  2294. private handleIncomingMessage(message: threema.WireMessage, log: boolean): void {
  2295. if (log) {
  2296. this.$log.debug('New incoming message');
  2297. }
  2298. // Validate message to keep contract defined by `threema.WireMessage` type
  2299. if (message.type === undefined) {
  2300. this.$log.warn('Ignoring invalid message (no type attribute)');
  2301. return;
  2302. } else if (message.subType === undefined) {
  2303. this.$log.warn('Ignoring invalid message (no subType attribute)');
  2304. return;
  2305. }
  2306. // If desired, log message type / subtype
  2307. if (this.config.MSG_DEBUGGING) {
  2308. // Deep copy message to prevent issues with JS debugger
  2309. const deepcopy = JSON.parse(JSON.stringify(message));
  2310. this.$log.debug('[Message] Incoming:', message.type, '/', message.subType, deepcopy);
  2311. }
  2312. // Process data
  2313. this.$rootScope.$apply(() => {
  2314. this.receive(message);
  2315. });
  2316. }
  2317. /**
  2318. * Receive a new incoming decrypted message.
  2319. */
  2320. private receive(message: threema.WireMessage): void {
  2321. // Dispatch message
  2322. switch (message.type) {
  2323. case WebClientService.TYPE_REQUEST:
  2324. this._receiveRequest(message.subType, message);
  2325. break;
  2326. case WebClientService.TYPE_RESPONSE:
  2327. this._receiveResponse(message.subType, message);
  2328. break;
  2329. case WebClientService.TYPE_CREATE:
  2330. this._receiveCreate(message.subType, message);
  2331. break;
  2332. case WebClientService.TYPE_UPDATE:
  2333. this._receiveUpdate(message.subType, message);
  2334. break;
  2335. case WebClientService.TYPE_DELETE:
  2336. this._receiveDelete(message.subType, message);
  2337. break;
  2338. default:
  2339. this.$log.warn('Ignored message with type:', message.type);
  2340. }
  2341. }
  2342. private runAfterInitializationSteps(requiredSteps: threema.InitializationStep[], callback: any): void {
  2343. for (const requiredStep of requiredSteps) {
  2344. if (!this.initialized.has(requiredStep)) {
  2345. this.$log.debug(this.logTag,
  2346. 'Required initialization step', requiredStep, 'not completed, add pending routine');
  2347. this.pendingInitializationStepRoutines.push({
  2348. requiredSteps: requiredSteps,
  2349. callback: callback,
  2350. } as threema.InitializationStepRoutine);
  2351. return;
  2352. }
  2353. }
  2354. callback();
  2355. }
  2356. private currentController: string;
  2357. public setControllerName(name: string): void {
  2358. this.currentController = name;
  2359. }
  2360. public getControllerName(): string {
  2361. return this.currentController;
  2362. }
  2363. /**
  2364. * Update the unread count in the window title.
  2365. */
  2366. private updateUnreadCount(): void {
  2367. const totalUnreadCount = this.conversations
  2368. .get()
  2369. .reduce((a: number, b: threema.Conversation) => a + b.unreadCount, 0);
  2370. this.titleService.updateUnreadCount(totalUnreadCount);
  2371. }
  2372. /**
  2373. * Reset the unread count in the window title
  2374. */
  2375. private resetUnreadCount(): void {
  2376. this.titleService.updateUnreadCount(0);
  2377. }
  2378. }